-
Notifications
You must be signed in to change notification settings - Fork 19
feat(ats): add ODP integration. #355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
fe7f328
to
6d0e7f2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
few minor comments, will review again.
pkg/client/client.go
Outdated
// A user context will be created successfully even when the SDK is not fully configured yet. | ||
func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[string]interface{}) OptimizelyUserContext { | ||
// Passing qualified segments as nil initially since they will be fetched later | ||
if o.identify { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the comment above is valid for this line?
pkg/client/client.go
Outdated
return o.decideForKeys(userContext, allFlagKeys, options) | ||
} | ||
|
||
func (o *OptimizelyClient) fetchQualifiedSegments(userContext *OptimizelyUserContext, options []pkgOdpSegment.OptimizelySegmentOption, callback func(segments []string, err error)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
documentation?
pkg/client/client.go
Outdated
userContext.SetQualifiedSegments(nil) | ||
|
||
_, err = o.getProjectConfig() | ||
if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't we combine these two conditions above & below.
pkg/client/client.go
Outdated
// Async | ||
if callback != nil { | ||
go func() { | ||
callback(fetchAndSetSegments()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there may be inconsistency / race condition issue when you call routine. thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All internal implementations are using locks where required so this should not be a problem.
pkg/client/factory.go
Outdated
userProfileService decision.UserProfileService | ||
overrideStore decision.ExperimentOverrideStore | ||
metricsRegistry metrics.Registry | ||
identify *bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
alphabetize please.
var hostForODP, publicKeyForODP string | ||
for _, integration := range datafile.Integrations { | ||
if integration.Key == "odp" { | ||
if integration.Key == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about if it's empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
empty key is allowed.
Key string `json:"key"` | ||
Host string `json:"host"` | ||
PublicKey string `json:"publicKey"` | ||
Key *string `json:"key"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
take it as type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you mean we should rename it to type? pointer to the string
is required here since string
type in golang cannot be nil by itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
discussed offline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks good overall. It's a quite big PR. A few top-level suggestions first.
pkg/client/client.go
Outdated
// A user context will be created successfully even when the SDK is not fully configured yet. | ||
func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[string]interface{}) OptimizelyUserContext { | ||
// Passing qualified segments as nil initially since they will be fetched later | ||
if o.identify { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we use odp.Manager == null
for identify event instead of this separate flag? Or we can call send event and odpManager can decide if to send or not.
// FetchQualifiedSegments fetches all qualified segments for the user context. | ||
func (o *OptimizelyUserContext) FetchQualifiedSegments(options []pkgOdpSegment.OptimizelySegmentOption, callback func(segments []string, err error)) { | ||
o.optimizely.fetchQualifiedSegments(o, options, callback) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we split into 2 apis (sync and async)? Sync api returns a boolean (success/failure) and async callback is called with the boolean. https://github.com/optimizely/java-sdk/blob/b16dff5f7d958ea36b94e1cfd8d0bdd0b2aae7f8/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java#L316
if (FetchQualifiedSegments()) {
Decide()
}
FetchQualifiedSegments(callback: (status) -> {
if(status) Decide()
})
Host string `json:"host"` | ||
PublicKey string `json:"publicKey"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering why Key is a pointer while Host/PublicKey are not. They are all optional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because in golang string
cannot be null, it is always empty if nothing is assigned to it. In our case we need to make sure that Key
is provided with every integration in the datafile, if not then optimizely-client should throw an error. This is a internal struct and user will not be able to access it.
// on failure, qualifiedSegments should be reset if a previous value exists. | ||
userContext.SetQualifiedSegments(nil) | ||
|
||
if _, err = o.getProjectConfig(); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This guard is good to have. Wondering if there is any case when a user context is created before ProjectConfig is not ready. If so, identify event will be discarded (ODP key is not ready yet) for those user contexts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case user provides an sdkKey, go-sdk allows user to create client as well as userContext while datafile is being downloaded in the background. As for discarding events, the check to ignore events if odp is not integrated is there in the odp manager.
pkg/client/factory.go
Outdated
} | ||
|
||
// WithOdpUserIdentification confirms whether to call IdentifyUser API on userContext initialization. | ||
func WithOdpUserIdentification(identify bool) OptionFunc { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's expected that identify event will be sent if odpManager is enabled and integration is set in datafile. Wondering what this extra control flag is for. We can remove it for now and consider if we need this configurability for all SDKs.
pkg/client/factory.go
Outdated
f.optimizelySDKSettings = &OptimizelySdkSettings{ | ||
SegmentsCacheSize: pkgOdpUtils.DefaultSegmentsCacheSize, | ||
SegmentsCacheTimeoutInSecs: pkgOdpUtils.DefaultSegmentsCacheTimeout, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this structuring for? I see optimizelySDKSettings
is used in this function only.
pkg/client/factory.go
Outdated
func WithOptimizelySdkSettings(optimizelySdkSettings *OptimizelySdkSettings) OptionFunc { | ||
return func(f *OptimizelyFactory) { | ||
f.optimizelySDKSettings = optimizelySdkSettings | ||
} | ||
} | ||
|
||
// WithOdpManager sets odp manager on a client. | ||
func WithOdpManager(odpManager odp.Manager) OptionFunc { | ||
return func(f *OptimizelyFactory) { | ||
f.odpManager = odpManager | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like we use combinations of config files (cacheSize, cacheTimeout, enabled) and builder method for ODP configuration. Can we consider removing config files and adding them ODP Builder (withSegmentCacheSize, withSegmentCacheTimeout) just like other ODP configuration?
pkg/client/factory.go
Outdated
// Create new odp manaager with latest config | ||
if f.odpManager == nil { | ||
options := []odp.OMOptionFunc{} | ||
if isProjectConfigAvailable { | ||
// Add odp Config with latest config | ||
options = append(options, odp.WithOdpConfig(pkgOdpConfig.NewConfig(projectConfig.GetPublicKeyForODP(), projectConfig.GetHostForODP(), projectConfig.GetSegmentList()))) | ||
} | ||
// Create ODP Manager | ||
appClient.OdpManager = odp.NewOdpManager(f.SDKKey, f.optimizelySDKSettings.DisableOdp, f.optimizelySDKSettings.SegmentsCacheSize, f.optimizelySDKSettings.SegmentsCacheTimeoutInSecs, options...) | ||
return | ||
} | ||
|
||
// Update user given odp manager with latest config | ||
if f.odpManager != nil { | ||
appClient.OdpManager = f.odpManager | ||
// Update odp config with latest config | ||
if isProjectConfigAvailable { | ||
appClient.OdpManager.Update(projectConfig.GetPublicKeyForODP(), projectConfig.GetHostForODP(), projectConfig.GetSegmentList()) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we drop OMOption for OdpConfig?
if odpManager == nil {
odpManager = odp.NewOdpManager(f.SDKKey, ...)
}
if isProjectConfigAvailable {
appClient.OdpManager.Update(projectConfig.GetPublicKeyForODP(), ...)
}
pkg/odp/odp_manager.go
Outdated
func WithOdpConfig(odpConfig config.Config) OMOptionFunc { | ||
return func(om *DefaultOdpManager) { | ||
om.OdpConfig = odpConfig | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may drop this?
om.segmentsCache = segmentsCache | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can add withSegmentCacheSize
and withSegmentCacheTimeout
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm. please address jae's comments.
|
||
projectConfig, err := appClient.ConfigManager.GetConfig() | ||
// For cases when project config is not fetched yet | ||
isProjectConfigAvailable := err == nil && projectConfig != nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think return in case projectConfig not available.
Key string `json:"key"` | ||
Host string `json:"host"` | ||
PublicKey string `json:"publicKey"` | ||
Key *string `json:"key"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
discussed offline.
b60957a
to
9c45e0b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with a nit.
} | ||
|
||
// FetchQualifiedSegmentsAsync fetches all qualified segments aysnchronously for the user context. | ||
func (o *OptimizelyUserContext) FetchQualifiedSegmentsAsync(callback func(success bool), options []pkgOdpSegment.OptimizelySegmentOption) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if this order is a common practice in golang -
func (o *OptimizelyUserContext) FetchQualifiedSegmentsAsync(callback func(success bool), options []pkgOdpSegment.OptimizelySegmentOption) { | |
func (o *OptimizelyUserContext) FetchQualifiedSegmentsAsync(options []pkgOdpSegment.OptimizelySegmentOption, callback func(success bool)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not, might've copied it while looking into java, thank's for pointing out 👍
Summary
This PR adds a support for Optimizely Data Platform (ODP) integration to Full Stack. With this extension, clients may not need to pre-determine and include user segments in attributes. SDK can fetch user segments from the ODP server for the current user.
Test plan
Issues