Skip to content

Commit ad65d9e

Browse files
yasirfolio3Michael Ng
authored andcommitted
feat(gherkin-UPS): Implemented UPS support for gherkin. (#190)
1 parent cd323d4 commit ad65d9e

File tree

11 files changed

+416
-43
lines changed

11 files changed

+416
-43
lines changed

pkg/decision/persisting_experiment_service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func (p PersistingExperimentService) GetDecision(decisionContext ExperimentDecis
6060
experimentDecision, err = p.experimentBucketedService.GetDecision(decisionContext, userContext)
6161
if experimentDecision.Variation != nil {
6262
// save decision if a user profile service is provided
63+
userProfile.ID = userContext.ID
6364
p.saveDecision(userProfile, decisionContext.Experiment, experimentDecision)
6465
}
6566

tests/integration/main_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ func FeatureContext(s *godog.Suite) {
5555
})
5656
s.Step(`^the datafile is "([^"]*)"$`, context.TheDatafileIs)
5757
s.Step(`^(\d+) "([^"]*)" listener is added$`, context.ListenerIsAdded)
58+
s.Step(`^the User Profile Service is "([^"]*)"$`, context.TheUserProfileServiceIs)
59+
s.Step(`^user "([^"]*)" has mapping "([^"]*)": "([^"]*)" in User Profile Service$`, context.UserHasMappingInUserProfileService)
5860
s.Step(`^([^\\\"]*) is called with arguments$`, context.IsCalledWithArguments)
5961
s.Step(`^the result should be (?:string )?"([^"]*)"$`, context.TheResultShouldBeString)
6062
s.Step(`^the result should be (?:integer )?(\d+)$`, context.TheResultShouldBeInteger)
@@ -70,4 +72,7 @@ func FeatureContext(s *godog.Suite) {
7072
s.Step(`^there are no dispatched events$`, context.ThereAreNoDispatchedEvents)
7173
s.Step(`^dispatched events payloads include$`, context.DispatchedEventsPayloadsInclude)
7274
s.Step(`^payloads of dispatched events don\'t include decisions$`, context.PayloadsOfDispatchedEventsDontIncludeDecisions)
75+
s.Step(`^the User Profile Service state should be$`, context.TheUserProfileServiceStateShouldBe)
76+
s.Step(`^there is no user profile state$`, context.ThereIsNoUserProfileState)
77+
7378
}

tests/integration/models/api_options.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package models
1818

1919
// APIOptions represents parameters for a scenario
2020
type APIOptions struct {
21-
DatafileName string
22-
APIName string
23-
Arguments string
24-
Listeners map[string]int
21+
DatafileName string
22+
APIName string
23+
Arguments string
24+
Listeners map[string]int
25+
UserProfileServiceType string
26+
UPSMapping map[string]map[string]string
2527
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package userprofileservice
18+
19+
import "github.com/optimizely/go-sdk/pkg/decision"
20+
21+
// LookupErrorUserProfileService represents a user profile service with lookup error
22+
type LookupErrorUserProfileService struct {
23+
NormalUserProfileService
24+
}
25+
26+
// Lookup is used to retrieve past bucketing decisions for users
27+
func (s *LookupErrorUserProfileService) Lookup(userID string) decision.UserProfile {
28+
return decision.UserProfile{}
29+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package userprofileservice
18+
19+
import "github.com/optimizely/go-sdk/pkg/decision"
20+
21+
// NoOpUserProfileService represents a user profile service with save and lookup error
22+
type NoOpUserProfileService struct {
23+
NormalUserProfileService
24+
}
25+
26+
// Lookup is used to retrieve past bucketing decisions for users
27+
func (s *NoOpUserProfileService) Lookup(userID string) decision.UserProfile {
28+
return decision.UserProfile{}
29+
}
30+
31+
// Save is used to save bucketing decisions for users
32+
func (s *NoOpUserProfileService) Save(userProfile decision.UserProfile) {
33+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package userprofileservice
18+
19+
import (
20+
"sync"
21+
22+
"github.com/optimizely/go-sdk/pkg/decision"
23+
)
24+
25+
// NormalUserProfileService represents the default implementation of UserProfileService interface
26+
type NormalUserProfileService struct {
27+
sync.RWMutex
28+
profiles map[string]decision.UserProfile
29+
}
30+
31+
// Lookup is used to retrieve past bucketing decisions for users
32+
func (s *NormalUserProfileService) Lookup(userID string) decision.UserProfile {
33+
s.RLock()
34+
profile := s.profiles[userID]
35+
s.RUnlock()
36+
return profile
37+
}
38+
39+
// Save is used to save bucketing decisions for users
40+
func (s *NormalUserProfileService) Save(userProfile decision.UserProfile) {
41+
if userProfile.ID == "" {
42+
return
43+
}
44+
s.Lock()
45+
if s.profiles == nil {
46+
s.profiles = make(map[string]decision.UserProfile)
47+
}
48+
if savedProfile, ok := s.profiles[userProfile.ID]; ok {
49+
for k, v := range userProfile.ExperimentBucketMap {
50+
savedProfile.ExperimentBucketMap[k] = v
51+
}
52+
s.profiles[userProfile.ID] = savedProfile
53+
} else {
54+
s.profiles[userProfile.ID] = userProfile
55+
}
56+
s.Unlock()
57+
}
58+
59+
// SaveUserProfiles saves multiple user profiles
60+
func (s *NormalUserProfileService) SaveUserProfiles(userProfiles []decision.UserProfile) {
61+
for _, profile := range userProfiles {
62+
s.Save(profile)
63+
}
64+
}
65+
66+
// GetUserProfiles returns currently saved user profiles
67+
func (s *NormalUserProfileService) GetUserProfiles() (savedProfiles []decision.UserProfile) {
68+
for _, v := range s.profiles {
69+
savedProfiles = append(savedProfiles, v)
70+
}
71+
return savedProfiles
72+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package userprofileservice
18+
19+
import (
20+
"github.com/optimizely/go-sdk/pkg/decision"
21+
)
22+
23+
// SaveErrorUserProfileService represents a user profile service with save error
24+
type SaveErrorUserProfileService struct {
25+
NormalUserProfileService
26+
}
27+
28+
// Save is used to save bucketing decisions for users
29+
func (s *SaveErrorUserProfileService) Save(userProfile decision.UserProfile) {
30+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package userprofileservice
18+
19+
import (
20+
"github.com/optimizely/go-sdk/pkg"
21+
"github.com/optimizely/go-sdk/pkg/decision"
22+
"github.com/optimizely/go-sdk/tests/integration/models"
23+
)
24+
25+
// UPSHelper defines Helper methods for UPS
26+
type UPSHelper interface {
27+
SaveUserProfiles(userProfiles []decision.UserProfile)
28+
GetUserProfiles() (savedProfiles []decision.UserProfile)
29+
}
30+
31+
// CreateUserProfileService creates a user profile service with the given parameters
32+
func CreateUserProfileService(config pkg.ProjectConfig, apiOptions models.APIOptions) decision.UserProfileService {
33+
var userProfileService decision.UserProfileService
34+
switch apiOptions.UserProfileServiceType {
35+
case "NormalService":
36+
userProfileService = new(NormalUserProfileService)
37+
break
38+
case "LookupErrorService":
39+
userProfileService = new(LookupErrorUserProfileService)
40+
break
41+
case "SaveErrorService":
42+
userProfileService = new(SaveErrorUserProfileService)
43+
break
44+
default:
45+
userProfileService = new(NoOpUserProfileService)
46+
break
47+
}
48+
49+
var profilesArray []decision.UserProfile
50+
for userID, bucketMap := range apiOptions.UPSMapping {
51+
var profile decision.UserProfile
52+
profile.ID = userID
53+
profile.ExperimentBucketMap = make(map[decision.UserDecisionKey]string)
54+
for experimentKey, variationKey := range bucketMap {
55+
if experiment, err := config.GetExperimentByKey(experimentKey); err == nil {
56+
decisionKey := decision.NewUserDecisionKey(experiment.ID)
57+
for _, variation := range experiment.Variations {
58+
if variation.Key == variationKey {
59+
profile.ExperimentBucketMap[decisionKey] = variation.ID
60+
break
61+
}
62+
}
63+
}
64+
}
65+
profilesArray = append(profilesArray, profile)
66+
}
67+
userProfileService.(UPSHelper).SaveUserProfiles(profilesArray)
68+
return userProfileService
69+
}
70+
71+
// ParseUserProfiles converts raw profiles into an array of user profiles
72+
func ParseUserProfiles(rawProfiles []map[string]interface{}) (parsedProfiles []decision.UserProfile) {
73+
for _, profile := range rawProfiles {
74+
userProfile := decision.UserProfile{}
75+
if userID, ok := profile["user_id"]; ok {
76+
userProfile.ID = userID.(string)
77+
}
78+
if experimentBucketMap, ok := profile["experiment_bucket_map"]; ok {
79+
userProfile.ExperimentBucketMap = make(map[decision.UserDecisionKey]string)
80+
for k, v := range experimentBucketMap.(map[string]interface{}) {
81+
decisionKey := decision.NewUserDecisionKey(k)
82+
if bucketMap, ok := v.(map[string]interface{}); ok {
83+
userProfile.ExperimentBucketMap[decisionKey] = bucketMap[decisionKey.Field].(string)
84+
}
85+
}
86+
}
87+
parsedProfiles = append(parsedProfiles, userProfile)
88+
}
89+
return parsedProfiles
90+
}

tests/integration/support/client_wrapper.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/optimizely/go-sdk/pkg/event"
3131
"github.com/optimizely/go-sdk/tests/integration/models"
3232
"github.com/optimizely/go-sdk/tests/integration/optlyplugins"
33+
"github.com/optimizely/go-sdk/tests/integration/optlyplugins/userprofileservice"
3334
"gopkg.in/yaml.v3"
3435
)
3536

@@ -38,9 +39,10 @@ var clientInstance *ClientWrapper
3839

3940
// ClientWrapper - wrapper around the optimizely client that keeps track of various custom components used with the client
4041
type ClientWrapper struct {
41-
Client *client.OptimizelyClient
42-
DecisionService decision.Service
43-
EventDispatcher event.Dispatcher
42+
Client *client.OptimizelyClient
43+
DecisionService decision.Service
44+
EventDispatcher event.Dispatcher
45+
UserProfileService decision.UserProfileService
4446
}
4547

4648
// DeleteInstance deletes cached instance of optly wrapper
@@ -49,14 +51,14 @@ func DeleteInstance() {
4951
}
5052

5153
// GetInstance returns a cached or new instance of the optly wrapper
52-
func GetInstance(datafileName string) *ClientWrapper {
54+
func GetInstance(apiOptions models.APIOptions) *ClientWrapper {
5355

5456
if clientInstance != nil {
5557
return clientInstance
5658
}
5759

5860
datafileDir := os.Getenv("DATAFILES_DIR")
59-
datafile, err := ioutil.ReadFile(filepath.Clean(path.Join(datafileDir, datafileName)))
61+
datafile, err := ioutil.ReadFile(filepath.Clean(path.Join(datafileDir, apiOptions.DatafileName)))
6062
if err != nil {
6163
log.Fatal(err)
6264
}
@@ -75,9 +77,21 @@ func GetInstance(datafileName string) *ClientWrapper {
7577
Datafile: datafile,
7678
}
7779

78-
decisionService := &optlyplugins.TestCompositeService{CompositeService: *decision.NewCompositeService("")}
7980
eventProcessor.EventDispatcher = &optlyplugins.ProxyEventDispatcher{}
8081

82+
config, err := configManager.GetConfig()
83+
if err != nil {
84+
log.Fatal(err)
85+
}
86+
87+
userProfileService := userprofileservice.CreateUserProfileService(config, apiOptions)
88+
compositeExperimentService := decision.NewCompositeExperimentService(
89+
decision.WithUserProfileService(userProfileService),
90+
)
91+
// @TODO: Add sdkKey dynamically once event-batching support is implemented
92+
compositeService := *decision.NewCompositeService("", decision.WithCompositeExperimentService(compositeExperimentService))
93+
decisionService := &optlyplugins.TestCompositeService{CompositeService: compositeService}
94+
8195
client, err := optimizelyFactory.Client(
8296
client.WithConfigManager(configManager),
8397
client.WithDecisionService(decisionService),
@@ -87,9 +101,10 @@ func GetInstance(datafileName string) *ClientWrapper {
87101
}
88102

89103
clientInstance = &ClientWrapper{
90-
Client: client,
91-
DecisionService: decisionService,
92-
EventDispatcher: eventProcessor.EventDispatcher,
104+
Client: client,
105+
DecisionService: decisionService,
106+
EventDispatcher: eventProcessor.EventDispatcher,
107+
UserProfileService: userProfileService,
93108
}
94109
return clientInstance
95110
}

0 commit comments

Comments
 (0)