...
1 package growthbook
2
3 import (
4 "encoding/json"
5 "net/url"
6 "regexp"
7 "time"
8 )
9
10
11
12 type ExperimentOverride struct {
13 Condition Condition
14 Weights []float64
15 Active *bool
16 Status *ExperimentStatus
17 Force *int
18 Coverage *float64
19 Groups []string
20 Namespace *Namespace
21 URL *regexp.Regexp
22 }
23
24 type ExperimentOverrides map[string]*ExperimentOverride
25
26
27
28 type Context struct {
29 Enabled bool
30 Attributes Attributes
31 URL *url.URL
32 Features FeatureMap
33 ForcedVariations ForcedVariationsMap
34 QAMode bool
35 DevMode bool
36 TrackingCallback ExperimentCallback
37 OnFeatureUsage FeatureUsageCallback
38 UserAttributes Attributes
39 Groups map[string]bool
40 APIHost string
41 ClientKey string
42 DecryptionKey string
43 Overrides ExperimentOverrides
44 CacheTTL time.Duration
45 }
46
47
48
49
50
51
52
53 type ExperimentCallback func(experiment *Experiment, result *Result)
54
55
56
57 type FeatureUsageCallback func(key string, result *FeatureResult)
58
59
60
61 func NewContext() *Context {
62 return &Context{
63 Enabled: true,
64 CacheTTL: 60 * time.Second,
65 }
66 }
67
68
69 func (ctx *Context) WithEnabled(enabled bool) *Context {
70 ctx.Enabled = enabled
71 return ctx
72 }
73
74
75 func (ctx *Context) WithAttributes(attributes Attributes) *Context {
76 savedAttributes := Attributes{}
77 for k, v := range attributes {
78 savedAttributes[k] = fixSliceTypes(v)
79 }
80 ctx.Attributes = savedAttributes
81 return ctx
82 }
83
84
85 func (ctx *Context) WithUserAttributes(attributes Attributes) *Context {
86 savedAttributes := Attributes{}
87 for k, v := range attributes {
88 savedAttributes[k] = fixSliceTypes(v)
89 }
90 ctx.UserAttributes = savedAttributes
91 return ctx
92 }
93
94
95 func (ctx *Context) WithURL(url *url.URL) *Context {
96 ctx.URL = url
97 return ctx
98 }
99
100
101
102 func (ctx *Context) WithFeatures(features FeatureMap) *Context {
103 ctx.Features = features
104 return ctx
105 }
106
107
108
109
110 func (ctx *Context) WithForcedVariations(forcedVariations ForcedVariationsMap) *Context {
111 ctx.ForcedVariations = forcedVariations
112 return ctx
113 }
114
115 func (ctx *Context) ForceVariation(key string, variation int) {
116 if ctx.ForcedVariations == nil {
117 ctx.ForcedVariations = ForcedVariationsMap{}
118 }
119 ctx.ForcedVariations[key] = variation
120 }
121
122 func (ctx *Context) UnforceVariation(key string) {
123 delete(ctx.ForcedVariations, key)
124 }
125
126
127
128 func (ctx *Context) WithQAMode(qaMode bool) *Context {
129 ctx.QAMode = qaMode
130 return ctx
131 }
132
133
134
135 func (ctx *Context) WithDevMode(devMode bool) *Context {
136 ctx.DevMode = devMode
137 return ctx
138 }
139
140
141
142 func (ctx *Context) WithTrackingCallback(callback ExperimentCallback) *Context {
143 ctx.TrackingCallback = callback
144 return ctx
145 }
146
147
148
149 func (ctx *Context) WithFeatureUsageCallback(callback FeatureUsageCallback) *Context {
150 ctx.OnFeatureUsage = callback
151 return ctx
152 }
153
154
155 func (ctx *Context) WithGroups(groups map[string]bool) *Context {
156 ctx.Groups = groups
157 return ctx
158 }
159
160
161 func (ctx *Context) WithAPIHost(host string) *Context {
162 ctx.APIHost = host
163 return ctx
164 }
165
166
167 func (ctx *Context) WithClientKey(key string) *Context {
168 ctx.ClientKey = key
169 return ctx
170 }
171
172
173 func (ctx *Context) WithDecryptionKey(key string) *Context {
174 ctx.DecryptionKey = key
175 return ctx
176 }
177
178
179 func (ctx *Context) WithOverrides(overrides ExperimentOverrides) *Context {
180 ctx.Overrides = overrides
181 return ctx
182 }
183
184
185 func (ctx *Context) WithCacheTTL(ttl time.Duration) *Context {
186 ctx.CacheTTL = ttl
187 return ctx
188 }
189
190
191 func ParseContext(data []byte) *Context {
192 dict := make(map[string]interface{})
193 err := json.Unmarshal(data, &dict)
194 if err != nil {
195 logError("Failed parsing JSON input", "Context")
196 return NewContext()
197 }
198 return BuildContext(dict)
199 }
200
201
202
203 func BuildContext(dict map[string]interface{}) *Context {
204 context := NewContext()
205 for k, v := range dict {
206 switch k {
207 case "enabled":
208 enabled, ok := v.(bool)
209 if ok {
210 context = context.WithEnabled(enabled)
211 } else {
212 logWarn("Invalid 'enabled' field in JSON context data")
213 }
214 case "attributes":
215 attrs, ok := v.(map[string]interface{})
216 if ok {
217 context = context.WithAttributes(attrs)
218 } else {
219 logWarn("Invalid 'attributes' field in JSON context data")
220 }
221 case "url":
222 urlString, ok := v.(string)
223 if ok {
224 url, err := url.Parse(urlString)
225 if err != nil {
226 logError("Invalid URL in JSON context data", urlString)
227 } else {
228 context = context.WithURL(url)
229 }
230 } else {
231 logWarn("Invalid 'url' field in JSON context data")
232 }
233 case "features":
234 features, ok := v.(map[string]interface{})
235 if ok {
236 context.Features = BuildFeatureMap(features)
237 } else {
238 logWarn("Invalid 'features' field in JSON context data")
239 }
240 case "forcedVariations":
241 forcedVariations, ok := v.(map[string]interface{})
242 if ok {
243 vars := make(map[string]int)
244 allVOK := true
245 for k, vr := range forcedVariations {
246 v, vok := vr.(float64)
247 if !vok {
248 allVOK = false
249 break
250 }
251 vars[k] = int(v)
252 }
253 if allVOK {
254 context = context.WithForcedVariations(vars)
255 } else {
256 ok = false
257 }
258 }
259 if !ok {
260 logWarn("Invalid 'forcedVariations' field in JSON context data")
261 }
262 case "qaMode":
263 qaMode, ok := v.(bool)
264 if ok {
265 context = context.WithQAMode(qaMode)
266 } else {
267 logWarn("Invalid 'qaMode' field in JSON context data")
268 }
269 case "groups":
270 groups, ok := v.(map[string]bool)
271 if ok {
272 context = context.WithGroups(groups)
273 } else {
274 logWarn("Invalid 'groups' field in JSON context data")
275 }
276 case "apiHost":
277 apiHost, ok := v.(string)
278 if ok {
279 context = context.WithAPIHost(apiHost)
280 } else {
281 logWarn("Invalid 'apiHost' field in JSON context data")
282 }
283 case "clientKey":
284 clientKey, ok := v.(string)
285 if ok {
286 context = context.WithClientKey(clientKey)
287 } else {
288 logWarn("Invalid 'clientKey' field in JSON context data")
289 }
290 case "decryptionKey":
291 decryptionKey, ok := v.(string)
292 if ok {
293 context = context.WithDecryptionKey(decryptionKey)
294 } else {
295 logWarn("Invalid 'decryptionKey' field in JSON context data")
296 }
297 default:
298 logWarn("Unknown key in JSON data", "Context", k)
299 }
300 }
301 return context
302 }
303
View as plain text