Skip to content

Commit f6b91bc

Browse files
authored
feat(integrations): Added support for integrations in datafile and evaluation. (#350)
Parse new integrations section of the datafile Evaluate() using UserContext
1 parent 1ee0e72 commit f6b91bc

22 files changed

+791
-86
lines changed

examples/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"fmt"
88
"time"
99

10-
"github.com/optimizely/go-sdk"
10+
optimizely "github.com/optimizely/go-sdk"
1111
"github.com/optimizely/go-sdk/pkg/client"
1212
"github.com/optimizely/go-sdk/pkg/config"
1313
"github.com/optimizely/go-sdk/pkg/event"

pkg/client/client.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/****************************************************************************
2-
* Copyright 2019-2021, Optimizely, Inc. and contributors *
2+
* Copyright 2019-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, *
@@ -53,7 +53,7 @@ type OptimizelyClient struct {
5353
// CreateUserContext creates a context of the user for which decision APIs will be called.
5454
// A user context will be created successfully even when the SDK is not fully configured yet.
5555
func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[string]interface{}) OptimizelyUserContext {
56-
return newOptimizelyUserContext(o, userID, attributes, nil)
56+
return newOptimizelyUserContext(o, userID, attributes, nil, nil)
5757
}
5858

5959
func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, options *decide.Options) OptimizelyDecision {
@@ -90,8 +90,9 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string,
9090
decisionContext.Feature = &feature
9191

9292
usrContext := entities.UserContext{
93-
ID: userContext.GetUserID(),
94-
Attributes: userContext.GetUserAttributes(),
93+
ID: userContext.GetUserID(),
94+
Attributes: userContext.GetUserAttributes(),
95+
QualifiedSegments: userContext.GetQualifiedSegments(),
9596
}
9697
var variationKey string
9798
var eventSent, flagEnabled bool

pkg/client/optimizely_user_context.go

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/****************************************************************************
2-
* Copyright 2020-2021, Optimizely, Inc. and contributors *
2+
* Copyright 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, *
@@ -31,21 +31,24 @@ type OptimizelyUserContext struct {
3131
UserID string `json:"userId"`
3232
Attributes map[string]interface{} `json:"attributes"`
3333

34+
qualifiedSegments []string
3435
optimizely *OptimizelyClient
3536
forcedDecisionService *pkgDecision.ForcedDecisionService
3637
mutex *sync.RWMutex
3738
}
3839

3940
// returns an instance of the optimizely user context.
40-
func newOptimizelyUserContext(optimizely *OptimizelyClient, userID string, attributes map[string]interface{}, forcedDecisionService *pkgDecision.ForcedDecisionService) OptimizelyUserContext {
41+
func newOptimizelyUserContext(optimizely *OptimizelyClient, userID string, attributes map[string]interface{}, qualifiedSegments []string, forcedDecisionService *pkgDecision.ForcedDecisionService) OptimizelyUserContext {
4142
// store a copy of the provided attributes so it isn't affected by changes made afterwards.
4243
if attributes == nil {
4344
attributes = map[string]interface{}{}
4445
}
4546
attributesCopy := copyUserAttributes(attributes)
47+
qualifiedSegmentsCopy := copyQualifiedSegments(qualifiedSegments)
4648
return OptimizelyUserContext{
4749
UserID: userID,
4850
Attributes: attributesCopy,
51+
qualifiedSegments: qualifiedSegmentsCopy,
4952
optimizely: optimizely,
5053
forcedDecisionService: forcedDecisionService,
5154
mutex: new(sync.RWMutex),
@@ -69,6 +72,13 @@ func (o OptimizelyUserContext) GetUserAttributes() map[string]interface{} {
6972
return copyUserAttributes(o.Attributes)
7073
}
7174

75+
// GetQualifiedSegments returns qualified segments for Optimizely user context
76+
func (o *OptimizelyUserContext) GetQualifiedSegments() []string {
77+
o.mutex.RLock()
78+
defer o.mutex.RUnlock()
79+
return copyQualifiedSegments(o.qualifiedSegments)
80+
}
81+
7282
func (o OptimizelyUserContext) getForcedDecisionService() *pkgDecision.ForcedDecisionService {
7383
if o.forcedDecisionService != nil {
7484
return o.forcedDecisionService.CreateCopy()
@@ -86,25 +96,40 @@ func (o *OptimizelyUserContext) SetAttribute(key string, value interface{}) {
8696
o.Attributes[key] = value
8797
}
8898

99+
// SetQualifiedSegments clears and adds qualified segments for Optimizely user context
100+
func (o *OptimizelyUserContext) SetQualifiedSegments(qualifiedSegments []string) {
101+
o.mutex.Lock()
102+
defer o.mutex.Unlock()
103+
o.qualifiedSegments = copyQualifiedSegments(qualifiedSegments)
104+
}
105+
106+
// IsQualifiedFor returns true if the user is qualified for the given segment name
107+
func (o *OptimizelyUserContext) IsQualifiedFor(segment string) bool {
108+
userContext := entities.UserContext{
109+
QualifiedSegments: o.GetQualifiedSegments(),
110+
}
111+
return userContext.IsQualifiedFor(segment)
112+
}
113+
89114
// Decide returns a decision result for a given flag key and a user context, which contains
90115
// all data required to deliver the flag or experiment.
91116
func (o *OptimizelyUserContext) Decide(key string, options []decide.OptimizelyDecideOptions) OptimizelyDecision {
92117
// use a copy of the user context so that any changes to the original context are not reflected inside the decision
93-
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService())
118+
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.GetQualifiedSegments(), o.getForcedDecisionService())
94119
return o.optimizely.decide(userContextCopy, key, convertDecideOptions(options))
95120
}
96121

97122
// DecideAll returns a key-map of decision results for all active flag keys with options.
98123
func (o *OptimizelyUserContext) DecideAll(options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision {
99124
// use a copy of the user context so that any changes to the original context are not reflected inside the decision
100-
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService())
125+
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.GetQualifiedSegments(), o.getForcedDecisionService())
101126
return o.optimizely.decideAll(userContextCopy, convertDecideOptions(options))
102127
}
103128

104129
// DecideForKeys returns a key-map of decision results for multiple flag keys and options.
105130
func (o *OptimizelyUserContext) DecideForKeys(keys []string, options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision {
106131
// use a copy of the user context so that any changes to the original context are not reflected inside the decision
107-
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService())
132+
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.GetQualifiedSegments(), o.getForcedDecisionService())
108133
return o.optimizely.decideForKeys(userContextCopy, keys, convertDecideOptions(options))
109134
}
110135

@@ -160,3 +185,12 @@ func copyUserAttributes(attributes map[string]interface{}) (attributesCopy map[s
160185
}
161186
return attributesCopy
162187
}
188+
189+
func copyQualifiedSegments(qualifiedSegments []string) (qualifiedSegmentsCopy []string) {
190+
if qualifiedSegments == nil {
191+
return nil
192+
}
193+
qualifiedSegmentsCopy = make([]string, len(qualifiedSegments))
194+
copy(qualifiedSegmentsCopy, qualifiedSegments)
195+
return qualifiedSegmentsCopy
196+
}

pkg/client/optimizely_user_context_test.go

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/****************************************************************************
2-
* Copyright 2020-2021, Optimizely, Inc. and contributors *
2+
* Copyright 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, *
@@ -55,65 +55,83 @@ func (s *OptimizelyUserContextTestSuite) SetupTest() {
5555
s.userID = "tester"
5656
}
5757

58-
func (s *OptimizelyUserContextTestSuite) TestOptimizelyUserContextWithAttributes() {
58+
func (s *OptimizelyUserContextTestSuite) TestOptimizelyUserContextWithAttributesAndSegments() {
5959
attributes := map[string]interface{}{"key1": 1212, "key2": 1213}
60-
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, s.userID, attributes, nil)
60+
segments := []string{"123"}
61+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, s.userID, attributes, segments, nil)
6162

6263
s.Equal(s.OptimizelyClient, optimizelyUserContext.GetOptimizely())
6364
s.Equal(s.userID, optimizelyUserContext.GetUserID())
6465
s.Equal(attributes, optimizelyUserContext.GetUserAttributes())
66+
s.Equal(segments, optimizelyUserContext.GetQualifiedSegments())
6567
s.Nil(optimizelyUserContext.forcedDecisionService)
6668
}
6769

68-
func (s *OptimizelyUserContextTestSuite) TestOptimizelyUserContextNoAttributes() {
70+
func (s *OptimizelyUserContextTestSuite) TestOptimizelyUserContextNoAttributesAndNilSegments() {
6971
attributes := map[string]interface{}{}
70-
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, s.userID, attributes, nil)
72+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, s.userID, attributes, nil, nil)
7173

7274
s.Equal(s.OptimizelyClient, optimizelyUserContext.GetOptimizely())
7375
s.Equal(s.userID, optimizelyUserContext.GetUserID())
7476
s.Equal(attributes, optimizelyUserContext.GetUserAttributes())
77+
s.Nil(optimizelyUserContext.GetQualifiedSegments())
7578
}
7679

7780
func (s *OptimizelyUserContextTestSuite) TestUpatingProvidedUserContextHasNoImpactOnOptimizelyUserContext() {
7881
attributes := map[string]interface{}{"k1": "v1", "k2": false}
79-
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, s.userID, attributes, nil)
82+
segments := []string{"123"}
83+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, s.userID, attributes, segments, nil)
8084

8185
s.Equal(s.OptimizelyClient, optimizelyUserContext.GetOptimizely())
8286
s.Equal(s.userID, optimizelyUserContext.GetUserID())
8387
s.Equal(attributes, optimizelyUserContext.GetUserAttributes())
88+
s.Equal(segments, optimizelyUserContext.GetQualifiedSegments())
8489

8590
attributes["k1"] = "v2"
8691
attributes["k2"] = true
92+
segments[0] = "456"
8793

8894
s.Equal("v1", optimizelyUserContext.GetUserAttributes()["k1"])
8995
s.Equal(false, optimizelyUserContext.GetUserAttributes()["k2"])
96+
s.Equal([]string{"123"}, optimizelyUserContext.GetQualifiedSegments())
9097

9198
attributes = optimizelyUserContext.GetUserAttributes()
99+
segments = optimizelyUserContext.GetQualifiedSegments()
92100
attributes["k1"] = "v2"
93101
attributes["k2"] = true
102+
segments[0] = "456"
94103

95104
s.Equal("v1", optimizelyUserContext.GetUserAttributes()["k1"])
96105
s.Equal(false, optimizelyUserContext.GetUserAttributes()["k2"])
106+
s.Equal([]string{"123"}, optimizelyUserContext.GetQualifiedSegments())
97107
}
98108

99-
func (s *OptimizelyUserContextTestSuite) TestSetAttribute() {
109+
func (s *OptimizelyUserContextTestSuite) TestSetAndGetUserAttributesRaceCondition() {
100110
userID := "1212121"
101111
var attributes map[string]interface{}
102112

103-
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, nil)
113+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, nil, nil)
104114
s.Equal(s.OptimizelyClient, optimizelyUserContext.GetOptimizely())
105115

106116
var wg sync.WaitGroup
107-
wg.Add(4)
117+
wg.Add(8)
108118
addInsideGoRoutine := func(key string, value interface{}, wg *sync.WaitGroup) {
109119
optimizelyUserContext.SetAttribute(key, value)
110120
wg.Done()
111121
}
122+
getInsideGoRoutine := func(wg *sync.WaitGroup) {
123+
optimizelyUserContext.GetUserAttributes()
124+
wg.Done()
125+
}
112126

113127
go addInsideGoRoutine("k1", "v1", &wg)
114128
go addInsideGoRoutine("k2", true, &wg)
115129
go addInsideGoRoutine("k3", 100, &wg)
116130
go addInsideGoRoutine("k4", 3.5, &wg)
131+
go getInsideGoRoutine(&wg)
132+
go getInsideGoRoutine(&wg)
133+
go getInsideGoRoutine(&wg)
134+
go getInsideGoRoutine(&wg)
117135
wg.Wait()
118136

119137
s.Equal(userID, optimizelyUserContext.GetUserID())
@@ -126,7 +144,7 @@ func (s *OptimizelyUserContextTestSuite) TestSetAttribute() {
126144
func (s *OptimizelyUserContextTestSuite) TestSetAttributeOverride() {
127145
userID := "1212121"
128146
attributes := map[string]interface{}{"k1": "v1", "k2": false}
129-
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, nil)
147+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, nil, nil)
130148

131149
s.Equal(s.OptimizelyClient, optimizelyUserContext.GetOptimizely())
132150
s.Equal(userID, optimizelyUserContext.GetUserID())
@@ -142,7 +160,7 @@ func (s *OptimizelyUserContextTestSuite) TestSetAttributeOverride() {
142160
func (s *OptimizelyUserContextTestSuite) TestSetAttributeNullValue() {
143161
userID := "1212121"
144162
attributes := map[string]interface{}{"k1": nil}
145-
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, nil)
163+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, nil, nil)
146164

147165
s.Equal(s.OptimizelyClient, optimizelyUserContext.GetOptimizely())
148166
s.Equal(userID, optimizelyUserContext.GetUserID())
@@ -155,6 +173,87 @@ func (s *OptimizelyUserContextTestSuite) TestSetAttributeNullValue() {
155173
s.Equal(nil, optimizelyUserContext.GetUserAttributes()["k1"])
156174
}
157175

176+
func (s *OptimizelyUserContextTestSuite) TestSetAndGetQualifiedSegments() {
177+
userID := "1212121"
178+
var attributes map[string]interface{}
179+
qualifiedSegments := []string{"1", "2", "3"}
180+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, []string{}, nil)
181+
s.Len(optimizelyUserContext.GetQualifiedSegments(), 0)
182+
183+
optimizelyUserContext.SetQualifiedSegments(nil)
184+
s.Nil(optimizelyUserContext.GetQualifiedSegments())
185+
186+
optimizelyUserContext.SetQualifiedSegments(qualifiedSegments)
187+
s.Equal(qualifiedSegments, optimizelyUserContext.GetQualifiedSegments())
188+
}
189+
190+
func (s *OptimizelyUserContextTestSuite) TestQualifiedSegmentsRaceCondition() {
191+
userID := "1212121"
192+
qualifiedSegments := []string{"1", "2", "3"}
193+
segment := "1"
194+
var attributes map[string]interface{}
195+
196+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, nil, nil)
197+
s.Nil(optimizelyUserContext.GetQualifiedSegments())
198+
var wg sync.WaitGroup
199+
wg.Add(9)
200+
201+
setQualifiedSegments := func(value []string, wg *sync.WaitGroup) {
202+
optimizelyUserContext.SetQualifiedSegments(value)
203+
wg.Done()
204+
}
205+
getQualifiedSegments := func(wg *sync.WaitGroup) {
206+
optimizelyUserContext.GetQualifiedSegments()
207+
wg.Done()
208+
}
209+
210+
IsQualifiedFor := func(segment string, wg *sync.WaitGroup) {
211+
optimizelyUserContext.IsQualifiedFor(segment)
212+
wg.Done()
213+
}
214+
215+
go setQualifiedSegments(qualifiedSegments, &wg)
216+
go setQualifiedSegments(qualifiedSegments, &wg)
217+
go setQualifiedSegments(qualifiedSegments, &wg)
218+
go getQualifiedSegments(&wg)
219+
go getQualifiedSegments(&wg)
220+
go getQualifiedSegments(&wg)
221+
go IsQualifiedFor(segment, &wg)
222+
go IsQualifiedFor(segment, &wg)
223+
go IsQualifiedFor(segment, &wg)
224+
225+
wg.Wait()
226+
227+
s.Equal(qualifiedSegments, optimizelyUserContext.GetQualifiedSegments())
228+
s.Equal(true, optimizelyUserContext.IsQualifiedFor(segment))
229+
}
230+
231+
func (s *OptimizelyUserContextTestSuite) TestIsQualifiedFor() {
232+
userID := "1212121"
233+
qualifiedSegments := []string{"1", "2", "3"}
234+
var attributes map[string]interface{}
235+
236+
optimizelyUserContext := newOptimizelyUserContext(s.OptimizelyClient, userID, attributes, nil, nil)
237+
s.False(optimizelyUserContext.IsQualifiedFor("1"))
238+
optimizelyUserContext.SetQualifiedSegments(qualifiedSegments)
239+
240+
var wg sync.WaitGroup
241+
wg.Add(6)
242+
testInsideGoRoutine := func(value string, result bool, wg *sync.WaitGroup) {
243+
s.Equal(result, optimizelyUserContext.IsQualifiedFor(value))
244+
wg.Done()
245+
}
246+
247+
go testInsideGoRoutine("1", true, &wg)
248+
go testInsideGoRoutine("2", true, &wg)
249+
go testInsideGoRoutine("3", true, &wg)
250+
go testInsideGoRoutine("4", false, &wg)
251+
go testInsideGoRoutine("5", false, &wg)
252+
go testInsideGoRoutine("6", false, &wg)
253+
254+
wg.Wait()
255+
}
256+
158257
func (s *OptimizelyUserContextTestSuite) TestDecideResponseContainsUserContextCopy() {
159258
flagKey := "feature_2"
160259
userContext := s.OptimizelyClient.CreateUserContext(s.userID, nil)
@@ -163,8 +262,11 @@ func (s *OptimizelyUserContextTestSuite) TestDecideResponseContainsUserContextCo
163262

164263
// Change attributes for user context
165264
userContext.SetAttribute("test", 123)
166-
// Attributes should not update for the userContext returned inside decision
265+
// Change qualifiedSegments for user context
266+
userContext.SetQualifiedSegments([]string{"123"})
267+
// Attributes and qualifiedSegments should not update for the userContext returned inside decision
167268
s.Nil(decisionUserContext.Attributes["test"])
269+
s.Len(decisionUserContext.qualifiedSegments, 0)
168270
}
169271

170272
func (s *OptimizelyUserContextTestSuite) TestDecideFeatureTest() {

0 commit comments

Comments
 (0)