Skip to content

Conversation

yasirfolio3
Copy link
Contributor

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.

  • Add a new public API to OptimizelyUserContext (fetchQualifiedSegments).
  • Add a new public API to OptimizelyClient (SendOdpEvent).

Test plan

  • Tests for OptimizelyUserContext new APIs.
  • Tests for OptimizelyClient new APIs.
  • Tests for odpManager.

Issues

  • FSSDK-8516

Copy link
Contributor

@msohailhussain msohailhussain left a 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.

// 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 {
Copy link
Contributor

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?

return o.decideForKeys(userContext, allFlagKeys, options)
}

func (o *OptimizelyClient) fetchQualifiedSegments(userContext *OptimizelyUserContext, options []pkgOdpSegment.OptimizelySegmentOption, callback func(segments []string, err error)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentation?

userContext.SetQualifiedSegments(nil)

_, err = o.getProjectConfig()
if err != nil {
Copy link
Contributor

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.

// Async
if callback != nil {
go func() {
callback(fetchAndSetSegments())
Copy link
Contributor

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?

Copy link
Contributor Author

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.

userProfileService decision.UserProfileService
overrideStore decision.ExperimentOverrideStore
metricsRegistry metrics.Registry
identify *bool
Copy link
Contributor

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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

take it as type

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed offline.

Copy link

@jaeopt jaeopt left a 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.

// 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 {
Copy link

@jaeopt jaeopt Nov 28, 2022

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.

Comment on lines 100 to 104
// 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)
}

Copy link

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()
})

Comment on lines +110 to +111
Host string `json:"host"`
PublicKey string `json:"publicKey"`
Copy link

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.

Copy link
Contributor Author

@yasirfolio3 yasirfolio3 Dec 1, 2022

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 {
Copy link

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.

Copy link
Contributor Author

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.

}

// WithOdpUserIdentification confirms whether to call IdentifyUser API on userContext initialization.
func WithOdpUserIdentification(identify bool) OptionFunc {
Copy link

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.

Comment on lines 299 to 302
f.optimizelySDKSettings = &OptimizelySdkSettings{
SegmentsCacheSize: pkgOdpUtils.DefaultSegmentsCacheSize,
SegmentsCacheTimeoutInSecs: pkgOdpUtils.DefaultSegmentsCacheTimeout,
}
Copy link

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.

Comment on lines 181 to 192
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
}
}
Copy link

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?

Comment on lines 309 to 329
// 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())
}
}
}
Copy link

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(), ...)
	}

Comment on lines 53 to 57
func WithOdpConfig(odpConfig config.Config) OMOptionFunc {
return func(om *DefaultOdpManager) {
om.OdpConfig = odpConfig
}
}
Copy link

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
}
}

Copy link

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.

Copy link
Contributor

@msohailhussain msohailhussain left a 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
Copy link
Contributor

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"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed offline.

@yasirfolio3 yasirfolio3 requested a review from jaeopt December 1, 2022 15:30
Copy link

@jaeopt jaeopt left a 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) {
Copy link

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 -

Suggested change
func (o *OptimizelyUserContext) FetchQualifiedSegmentsAsync(callback func(success bool), options []pkgOdpSegment.OptimizelySegmentOption) {
func (o *OptimizelyUserContext) FetchQualifiedSegmentsAsync(options []pkgOdpSegment.OptimizelySegmentOption, callback func(success bool)) {

Copy link
Contributor Author

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 👍

@msohailhussain msohailhussain merged commit a4246e6 into master Dec 19, 2022
@msohailhussain msohailhussain deleted the yasir/odp-manager branch December 19, 2022 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants