Skip to content

Commit 209e330

Browse files
pawels-optimizelyyasirfolio3Mat001msohailhussain
authored
feat: adding new /decide endpoint (#292)
* feat: adding new /decide endpoint * addressing comments from PR review * Updating openapi-spec for decide-api. * nit fixed. * Implemented decide for keys with unit tests and updated openapi.yaml * Updated unit test names. * linter warnings fixed. * Updated go.mod to use latest go-sdk commit b9c14b4 from master. * Updated go.mod to use latest go-sdk commit 6964919 from master. * feat(decide_api) add decide api acceptance tests * cleanup * add docstring * put all in one line * updated go-sdk version to 1.6.0 * fix: bypass schema validation for feature parameter tests-as test fals in travis but passes locally * fix: fix bypassing validation * fix: fix bypassing validation * by pass response as well * feat(makefile) run acceptance tests using makefile * cleanup: add newline * fix: update command to run accept tests on travis * fix: change sh to bash command * fix: update function name * feat:separate bypass cschema validation per request and response * fix: set bypass validation for response for decide test test_decide__flag_key_parameter * fix: set bypass validation for request and response for decide test test_decide__flag_key_parameter * Updated go-sdk version to latest with decide-api fixes. * fixes. Co-authored-by: Yasir Ali <yali@folio3.com> Co-authored-by: Matjaz Pirnovar <matjaz.pirnovar@optimizely.com> Co-authored-by: Sohail Hussain <mirza.sohailhussain@gmail.com>
1 parent 31ea7e2 commit 209e330

File tree

18 files changed

+983
-222
lines changed

18 files changed

+983
-222
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ jobs:
7676
install:
7777
- pip install -r tests/acceptance/requirements.txt
7878
script:
79-
- pytest -vv --diff-type=split tests/acceptance/test_acceptance/ --host http://localhost:8080
79+
- MYHOST="http://localhost:8080" make test-acceptance
8080

8181
- stage: 'Trigger FSC Tests'
8282
if: (branch = master AND type = push) OR type = pull_request OR tag IS present

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ lint: check-go static ## runs `golangci-lint` linters defined in `.golangci.yml`
6363
run: $(TARGET) ## builds and executes the TARGET binary
6464
$(GOBIN)/$(TARGET)
6565

66+
stop: ## stops TARGET binary process
67+
pkill -f "$(GOBIN)/$(TARGET)"
68+
6669
static: check-go
6770
$(GOPATH)/bin/statik -src=web/static -f
6871

@@ -85,3 +88,12 @@ generate_secret: $(GEN_SECRET_TARGET) ## builds and executes the GEN_SECRET_TARG
8588

8689
help: ## help
8790
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
91+
92+
test-acceptance:
93+
export OPTIMIZELY_SERVER_BATCHREQUESTS_OPERATIONSLIMIT='3' && \
94+
make clean && \
95+
make setup && \
96+
make run & \
97+
bash scripts/wait_for_agent_to_start.sh && \
98+
pytest -vv -rA --diff-type=split tests/acceptance/test_acceptance/ -k "$(TEST)" --host "$(MYHOST)" && \
99+
make stop

api/openapi-spec/openapi.yaml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,37 @@ paths:
4545
description: Unauthorized, invalid JWT
4646
'403':
4747
$ref: '#/components/responses/Forbidden'
48+
/v1/decide:
49+
parameters:
50+
- $ref: '#/components/parameters/decideKeysParam'
51+
post:
52+
summary: Decide makes feature decisions for the selected query parameters.
53+
operationId: decide
54+
description: >-
55+
Returns decision results for flag keys for a user.
56+
The result for a single key is returned as an
57+
OptimizelyDecision object whereas the result for multiple keys is returned as an array of OptimizelyDecision objects. If no flag key is
58+
provided, decision is made for all flag keys. OptimizelyDecision object
59+
contains all data required to deliver the flag rule.
60+
requestBody:
61+
$ref: '#/components/requestBodies/DecideContext'
62+
responses:
63+
'200':
64+
description: Valid response
65+
content:
66+
application/json:
67+
schema:
68+
oneOf:
69+
- type: array
70+
items:
71+
$ref: '#/components/schemas/OptimizelyDecision'
72+
- $ref: '#/components/schemas/OptimizelyDecision'
73+
'400':
74+
description: Missing required parameters
75+
'401':
76+
description: Unauthorized, invalid JWT
77+
'403':
78+
$ref: '#/components/responses/Forbidden'
4879
/v1/track:
4980
parameters:
5081
- $ref: '#/components/parameters/eventKeyParam'
@@ -167,6 +198,13 @@ components:
167198
description: Key of the event we're tracking
168199
schema:
169200
type: string
201+
decideKeysParam:
202+
in: query
203+
name: keys
204+
required: false
205+
description: Flag keys for decision
206+
schema:
207+
type: string
170208
experimentKeyParam:
171209
in: query
172210
name: experimentKey
@@ -212,6 +250,12 @@ components:
212250
application/json:
213251
schema:
214252
$ref: '#/components/schemas/TrackContext'
253+
DecideContext:
254+
required: true
255+
content:
256+
application/json:
257+
schema:
258+
$ref: '#/components/schemas/DecideContext'
215259
TokenContext:
216260
required: true
217261
content:
@@ -305,6 +349,36 @@ components:
305349
additionalProperties: true
306350
error:
307351
type: string
352+
OptimizelyDecision:
353+
properties:
354+
variables:
355+
type: object
356+
variationKey:
357+
type: string
358+
enabled:
359+
type: boolean
360+
ruleKey:
361+
type: string
362+
flagKey:
363+
type: string
364+
userContext:
365+
type: object
366+
properties:
367+
userId:
368+
type: string
369+
attributes:
370+
type: object
371+
additionalProperties: true
372+
required:
373+
- userId
374+
reasons:
375+
type: array
376+
items:
377+
type: string
378+
required:
379+
- ruleKey
380+
- flagKey
381+
- userContext
308382
ActivateContext:
309383
properties:
310384
userId:
@@ -357,6 +431,25 @@ components:
357431
userAttributes:
358432
type: object
359433
additionalProperties: true
434+
DecideContext:
435+
properties:
436+
decideOptions:
437+
type: array
438+
items:
439+
type: string
440+
enum:
441+
- DISABLE_DECISION_EVENT
442+
- ENABLED_FLAGS_ONLY
443+
- IGNORE_USER_PROFILE_SERVICE
444+
- EXCLUDE_VARIABLES
445+
- INCLUDE_REASONS
446+
userId:
447+
type: string
448+
userAttributes:
449+
type: object
450+
additionalProperties: true
451+
required:
452+
- userId
360453
Variation:
361454
properties:
362455
id:

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/go-kit/kit v0.9.0
1313
github.com/google/uuid v1.1.1
1414
github.com/lestrrat-go/jwx v0.9.0
15-
github.com/optimizely/go-sdk v1.5.1
15+
github.com/optimizely/go-sdk v1.6.1-0.20210226222257-68aca7d10f77
1616
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
1717
github.com/rakyll/statik v0.1.7
1818
github.com/rs/zerolog v1.18.1-0.20200514152719-663cbb4c8469

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD
9191
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
9292
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
9393
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
94-
github.com/optimizely/go-sdk v1.5.1 h1:jut7lAO0jCrkwIQr4kBcsT4f7PKdh+AOlltLXPFEoxM=
95-
github.com/optimizely/go-sdk v1.5.1/go.mod h1:1uinGREH+AdijSRw3qitWkvIna1e/ZGN5eymNYPjw1A=
94+
github.com/optimizely/go-sdk v1.6.1-0.20210226222257-68aca7d10f77 h1:YGHuD2FA1qPkRqDSKJwuw5pGQmip8S6hOBcRPcYKntE=
95+
github.com/optimizely/go-sdk v1.6.1-0.20210226222257-68aca7d10f77/go.mod h1:1uinGREH+AdijSRw3qitWkvIna1e/ZGN5eymNYPjw1A=
9696
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
9797
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
9898
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=

pkg/handlers/decide.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/****************************************************************************
2+
* Copyright 2021, 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 handlers //
18+
package handlers
19+
20+
import (
21+
"errors"
22+
"net/http"
23+
24+
"github.com/optimizely/agent/pkg/middleware"
25+
26+
"github.com/optimizely/go-sdk/pkg/client"
27+
"github.com/optimizely/go-sdk/pkg/decide"
28+
29+
"github.com/go-chi/render"
30+
)
31+
32+
// DecideBody defines the request body for decide API
33+
type DecideBody struct {
34+
UserID string `json:"userId"`
35+
UserAttributes map[string]interface{} `json:"userAttributes"`
36+
DecideOptions []string `json:"decideOptions"`
37+
}
38+
39+
// DecideOut defines the response
40+
type DecideOut struct {
41+
client.OptimizelyDecision
42+
Variables map[string]interface{} `json:"variables,omitempty"`
43+
}
44+
45+
// Decide makes feature decisions for the selected query parameters
46+
func Decide(w http.ResponseWriter, r *http.Request) {
47+
optlyClient, err := middleware.GetOptlyClient(r)
48+
logger := middleware.GetLogger(r)
49+
if err != nil {
50+
RenderError(err, http.StatusInternalServerError, w, r)
51+
return
52+
}
53+
54+
db, err := getUserContextWithOptions(r)
55+
if err != nil {
56+
RenderError(err, http.StatusBadRequest, w, r)
57+
return
58+
}
59+
60+
decideOptions, err := translateOptions(db.DecideOptions)
61+
if err != nil {
62+
RenderError(err, http.StatusBadRequest, w, r)
63+
return
64+
}
65+
66+
optimizelyUserContext := optlyClient.CreateUserContext(db.UserID, db.UserAttributes)
67+
68+
keys := []string{}
69+
if err := r.ParseForm(); err == nil {
70+
keys = r.Form["keys"]
71+
}
72+
73+
var decides map[string]client.OptimizelyDecision
74+
switch len(keys) {
75+
case 0:
76+
// Decide All
77+
decides = optimizelyUserContext.DecideAll(decideOptions)
78+
case 1:
79+
// Decide
80+
key := keys[0]
81+
logger.Debug().Str("featureKey", key).Msg("fetching feature decision")
82+
d := optimizelyUserContext.Decide(key, decideOptions)
83+
decideOut := DecideOut{d, d.Variables.ToMap()}
84+
render.JSON(w, r, decideOut)
85+
return
86+
default:
87+
// Decide for Keys
88+
decides = optimizelyUserContext.DecideForKeys(keys, decideOptions)
89+
}
90+
91+
decideOuts := []DecideOut{}
92+
for _, d := range decides {
93+
decideOut := DecideOut{d, d.Variables.ToMap()}
94+
decideOuts = append(decideOuts, decideOut)
95+
}
96+
render.JSON(w, r, decideOuts)
97+
}
98+
99+
func getUserContextWithOptions(r *http.Request) (DecideBody, error) {
100+
var body DecideBody
101+
err := ParseRequestBody(r, &body)
102+
if err != nil {
103+
return DecideBody{}, err
104+
}
105+
106+
if body.UserID == "" {
107+
return DecideBody{}, ErrEmptyUserID
108+
}
109+
110+
return body, nil
111+
}
112+
113+
func translateOptions(options []string) ([]decide.OptimizelyDecideOptions, error) {
114+
decideOptions := []decide.OptimizelyDecideOptions{}
115+
for _, val := range options {
116+
switch val {
117+
case "DISABLE_DECISION_EVENT":
118+
decideOptions = append(decideOptions, decide.DisableDecisionEvent)
119+
case "ENABLED_FLAGS_ONLY":
120+
decideOptions = append(decideOptions, decide.EnabledFlagsOnly)
121+
case "IGNORE_USER_PROFILE_SERVICE":
122+
decideOptions = append(decideOptions, decide.IgnoreUserProfileService)
123+
case "EXCLUDE_VARIABLES":
124+
decideOptions = append(decideOptions, decide.ExcludeVariables)
125+
case "INCLUDE_REASONS":
126+
decideOptions = append(decideOptions, decide.IncludeReasons)
127+
default:
128+
return []decide.OptimizelyDecideOptions{}, errors.New("invalid option: " + val)
129+
}
130+
}
131+
return decideOptions, nil
132+
}

0 commit comments

Comments
 (0)