Skip to content

Commit a4246e6

Browse files
authored
feat(ats): add ODP integration. (#355)
## 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
1 parent 74474c9 commit a4246e6

35 files changed

+2125
-581
lines changed

pkg/client/client.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import (
3333
"github.com/optimizely/go-sdk/pkg/event"
3434
"github.com/optimizely/go-sdk/pkg/logging"
3535
"github.com/optimizely/go-sdk/pkg/notification"
36+
"github.com/optimizely/go-sdk/pkg/odp"
37+
pkgOdpSegment "github.com/optimizely/go-sdk/pkg/odp/segment"
38+
pkgOdpUtils "github.com/optimizely/go-sdk/pkg/odp/utils"
3639
"github.com/optimizely/go-sdk/pkg/optimizelyjson"
3740
"github.com/optimizely/go-sdk/pkg/utils"
3841

@@ -44,6 +47,7 @@ type OptimizelyClient struct {
4447
ConfigManager config.ProjectConfigManager
4548
DecisionService decision.Service
4649
EventProcessor event.Processor
50+
OdpManager odp.Manager
4751
notificationCenter notification.Center
4852
execGroup *utils.ExecGroup
4953
logger logging.OptimizelyLogProducer
@@ -53,6 +57,10 @@ type OptimizelyClient struct {
5357
// CreateUserContext creates a context of the user for which decision APIs will be called.
5458
// A user context will be created successfully even when the SDK is not fully configured yet.
5559
func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[string]interface{}) OptimizelyUserContext {
60+
if o.OdpManager != nil {
61+
// Identify user to odp server
62+
o.OdpManager.IdentifyUser(userID)
63+
}
5664
// Passing qualified segments as nil initially since they will be fetched later
5765
return newOptimizelyUserContext(o, userID, attributes, nil, nil)
5866
}
@@ -234,6 +242,77 @@ func (o *OptimizelyClient) decideAll(userContext OptimizelyUserContext, options
234242
return o.decideForKeys(userContext, allFlagKeys, options)
235243
}
236244

245+
// fetchQualifiedSegments fetches all qualified segments for the user context.
246+
// request is performed asynchronously only when callback is provided
247+
func (o *OptimizelyClient) fetchQualifiedSegments(userContext *OptimizelyUserContext, options []pkgOdpSegment.OptimizelySegmentOption, callback func(success bool)) {
248+
var err error
249+
defer func() {
250+
if r := recover(); r != nil {
251+
switch t := r.(type) {
252+
case error:
253+
err = t
254+
case string:
255+
err = errors.New(t)
256+
default:
257+
err = errors.New("unexpected error")
258+
}
259+
o.logger.Error("fetchQualifiedSegments call, optimizely SDK is panicking with the error:", err)
260+
o.logger.Debug(string(debug.Stack()))
261+
}
262+
}()
263+
264+
// on failure, qualifiedSegments should be reset if a previous value exists.
265+
userContext.SetQualifiedSegments(nil)
266+
267+
if _, err = o.getProjectConfig(); err != nil {
268+
o.logger.Error("fetchQualifiedSegments failed with error:", decide.GetDecideError(decide.SDKNotReady))
269+
if callback != nil {
270+
callback(false)
271+
}
272+
return
273+
}
274+
275+
qualifiedSegments, segmentsError := o.OdpManager.FetchQualifiedSegments(userContext.GetUserID(), options)
276+
success := segmentsError == nil
277+
278+
if success {
279+
userContext.SetQualifiedSegments(qualifiedSegments)
280+
} else {
281+
o.logger.Error("fetchQualifiedSegments failed with error:", segmentsError)
282+
}
283+
284+
if callback != nil {
285+
callback(success)
286+
}
287+
}
288+
289+
// SendOdpEvent sends an event to the ODP server.
290+
func (o *OptimizelyClient) SendOdpEvent(eventType, action string, identifiers map[string]string, data map[string]interface{}) bool {
291+
292+
var err error
293+
defer func() {
294+
if r := recover(); r != nil {
295+
switch t := r.(type) {
296+
case error:
297+
err = t
298+
case string:
299+
err = errors.New(t)
300+
default:
301+
err = errors.New("unexpected error")
302+
}
303+
errorMessage := fmt.Sprintf("SendOdpEvent call, optimizely SDK is panicking with the error:")
304+
o.logger.Error(errorMessage, err)
305+
o.logger.Debug(string(debug.Stack()))
306+
}
307+
}()
308+
309+
// the event type (default = "fullstack").
310+
if eventType == "" {
311+
eventType = pkgOdpUtils.OdpEventType
312+
}
313+
return o.OdpManager.SendOdpEvent(eventType, action, identifiers, data)
314+
}
315+
237316
// Activate returns the key of the variation the user is bucketed into and queues up an impression event to be sent to
238317
// the Optimizely log endpoint for results processing.
239318
func (o *OptimizelyClient) Activate(experimentKey string, userContext entities.UserContext) (result string, err error) {

pkg/client/client_test.go

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/****************************************************************************
2-
* Copyright 2019-2020, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2020,2022 Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
66
* You may obtain a copy of the License at *
77
* *
8-
* http://www.apache.org/licenses/LICENSE-2.0 *
8+
* https://www.apache.org/licenses/LICENSE-2.0 *
99
* *
1010
* Unless required by applicable law or agreed to in writing, software *
1111
* distributed under the License is distributed on an "AS IS" BASIS, *
@@ -23,6 +23,7 @@ import (
2323
"strconv"
2424
"sync"
2525
"testing"
26+
"time"
2627

2728
"github.com/optimizely/go-sdk/pkg/config"
2829
"github.com/optimizely/go-sdk/pkg/decide"
@@ -31,6 +32,9 @@ import (
3132
"github.com/optimizely/go-sdk/pkg/event"
3233
"github.com/optimizely/go-sdk/pkg/logging"
3334
"github.com/optimizely/go-sdk/pkg/notification"
35+
"github.com/optimizely/go-sdk/pkg/odp"
36+
"github.com/optimizely/go-sdk/pkg/odp/segment"
37+
pkgOdpUtils "github.com/optimizely/go-sdk/pkg/odp/utils"
3438
"github.com/optimizely/go-sdk/pkg/utils"
3539

3640
"github.com/stretchr/testify/assert"
@@ -172,6 +176,84 @@ func (TestConfig) GetClientVersion() string {
172176
return "1.0.0"
173177
}
174178

179+
type MockODPManager struct {
180+
odp.Manager
181+
mock.Mock
182+
}
183+
184+
func (m *MockODPManager) FetchQualifiedSegments(userID string, options []segment.OptimizelySegmentOption) (segments []string, err error) {
185+
args := m.Called(userID, options)
186+
if segArray, ok := args.Get(0).([]string); ok {
187+
segments = segArray
188+
}
189+
return segments, args.Error(1)
190+
}
191+
192+
func (m *MockODPManager) IdentifyUser(userID string) {
193+
m.Called(userID)
194+
}
195+
196+
func (m *MockODPManager) SendOdpEvent(eventType, action string, identifiers map[string]string, data map[string]interface{}) bool {
197+
return m.Called(eventType, action, identifiers, data).Get(0).(bool)
198+
}
199+
200+
func (m *MockODPManager) Update(apiKey, apiHost string, segmentsToCheck []string) {
201+
m.Called(apiKey, apiHost, segmentsToCheck)
202+
}
203+
204+
func TestSendODPEventWhenODPDisabled(t *testing.T) {
205+
factory := OptimizelyFactory{SDKKey: "1212"}
206+
var segmentsCacheSize = 1
207+
var segmentsCacheTimeout = 1 * time.Second
208+
var disableOdp = true
209+
optimizelyClient, err := factory.Client(WithSegmentsCacheSize(segmentsCacheSize), WithSegmentsCacheTimeout(segmentsCacheTimeout), WithOdpDisabled(disableOdp))
210+
assert.NoError(t, err)
211+
success := optimizelyClient.SendOdpEvent("123", "456", map[string]string{
212+
"abc": "123",
213+
}, map[string]interface{}{
214+
"abc": nil,
215+
"idempotence_id": 234,
216+
"data_source_type": "456",
217+
"data_source": true,
218+
"data_source_version": 6.78,
219+
})
220+
assert.False(t, success)
221+
}
222+
223+
func TestSendODPEventEmptyType(t *testing.T) {
224+
eventType := pkgOdpUtils.OdpEventType
225+
action := "456"
226+
identifiers := map[string]string{
227+
"abc": "123",
228+
}
229+
data := map[string]interface{}{
230+
"abc": nil,
231+
"idempotence_id": 234,
232+
"data_source_type": "456",
233+
"data_source": true,
234+
"data_source_version": 6.78,
235+
}
236+
mockOdpManager := &MockODPManager{}
237+
mockOdpManager.On("SendOdpEvent", eventType, action, identifiers, data).Return(true)
238+
optimizelyClient := OptimizelyClient{
239+
OdpManager: mockOdpManager,
240+
}
241+
success := optimizelyClient.SendOdpEvent("", action, identifiers, data)
242+
assert.True(t, success)
243+
mockOdpManager.AssertExpectations(t)
244+
}
245+
246+
func TestSendODPEvent(t *testing.T) {
247+
mockOdpManager := &MockODPManager{}
248+
mockOdpManager.On("SendOdpEvent", "123", "", mock.Anything, mock.Anything).Return(true)
249+
optimizelyClient := OptimizelyClient{
250+
OdpManager: mockOdpManager,
251+
}
252+
success := optimizelyClient.SendOdpEvent("123", "", nil, nil)
253+
assert.True(t, success)
254+
mockOdpManager.AssertExpectations(t)
255+
}
256+
175257
func TestTrack(t *testing.T) {
176258
mockProcessor := new(MockProcessor)
177259
mockDecisionService := new(MockDecisionService)
@@ -2459,6 +2541,35 @@ func TestCreateUserContext(t *testing.T) {
24592541
assert.Equal(t, userAttributes, optimizelyUserContext.GetUserAttributes())
24602542
}
24612543

2544+
func TestCreateUserContextIdentifiesUser(t *testing.T) {
2545+
userID := "1212121"
2546+
userAttributes := map[string]interface{}{"key": 1212}
2547+
factory := OptimizelyFactory{SDKKey: "1212"}
2548+
mockOdpManager := &MockODPManager{}
2549+
mockOdpManager.On("IdentifyUser", userID)
2550+
client, err := factory.Client(WithOdpManager(mockOdpManager))
2551+
assert.NoError(t, err)
2552+
optimizelyUserContext := client.CreateUserContext(userID, userAttributes)
2553+
mockOdpManager.AssertExpectations(t)
2554+
assert.NotNil(t, optimizelyUserContext.optimizely.OdpManager)
2555+
assert.Equal(t, userID, optimizelyUserContext.GetUserID())
2556+
assert.Equal(t, userAttributes, optimizelyUserContext.GetUserAttributes())
2557+
}
2558+
2559+
func TestCreateUserContextWithNilODPManager(t *testing.T) {
2560+
userID := "1212121"
2561+
userAttributes := map[string]interface{}{"key": 1212}
2562+
factory := OptimizelyFactory{SDKKey: "1212"}
2563+
mockOdpManager := &MockODPManager{}
2564+
client, err := factory.Client(WithOdpManager(mockOdpManager))
2565+
assert.NoError(t, err)
2566+
client.OdpManager = nil
2567+
optimizelyUserContext := client.CreateUserContext(userID, userAttributes)
2568+
mockOdpManager.AssertNotCalled(t, "IdentifyUser", userID)
2569+
assert.Equal(t, userID, optimizelyUserContext.GetUserID())
2570+
assert.Equal(t, userAttributes, optimizelyUserContext.GetUserAttributes())
2571+
}
2572+
24622573
func TestChangingAttributesDoesntEffectUserContext(t *testing.T) {
24632574
client := OptimizelyClient{}
24642575
userID := "1"

0 commit comments

Comments
 (0)