1 package growthbook
2
3 import (
4 "encoding/json"
5 "regexp"
6 )
7
8 type ExperimentStatus string
9
10 const (
11 DraftStatus ExperimentStatus = "draft"
12 RunningStatus ExperimentStatus = "running"
13 StoppedStatus ExperimentStatus = "stopped"
14 )
15
16
17 type Experiment struct {
18 Key string
19 Variations []FeatureValue
20 Ranges []Range
21 Meta []VariationMeta
22 Filters []Filter
23 Seed string
24 Name string
25 Phase string
26 URLPatterns []URLTarget
27 Weights []float64
28 Condition Condition
29 Coverage *float64
30 Include func() bool
31 Namespace *Namespace
32 Force *int
33 HashAttribute string
34 HashVersion int
35 Active bool
36 Status ExperimentStatus
37 URL *regexp.Regexp
38 Groups []string
39 }
40
41
42
43 func NewExperiment(key string) *Experiment {
44 return &Experiment{
45 Key: key,
46 Active: true,
47 }
48 }
49
50
51 func (exp *Experiment) WithVariations(variations ...FeatureValue) *Experiment {
52 exp.Variations = variations
53 return exp
54 }
55
56
57 func (exp *Experiment) WithRanges(ranges ...Range) *Experiment {
58 exp.Ranges = ranges
59 return exp
60 }
61
62
63 func (exp *Experiment) WithMeta(meta ...VariationMeta) *Experiment {
64 exp.Meta = meta
65 return exp
66 }
67
68
69 func (exp *Experiment) WithFilters(filters ...Filter) *Experiment {
70 exp.Filters = filters
71 return exp
72 }
73
74
75 func (exp *Experiment) WithSeed(seed string) *Experiment {
76 exp.Seed = seed
77 return exp
78 }
79
80
81 func (exp *Experiment) WithName(name string) *Experiment {
82 exp.Name = name
83 return exp
84 }
85
86
87 func (exp *Experiment) WithPhase(phase string) *Experiment {
88 exp.Phase = phase
89 return exp
90 }
91
92
93 func (exp *Experiment) WithWeights(weights ...float64) *Experiment {
94 exp.Weights = weights
95 return exp
96 }
97
98
99 func (exp *Experiment) WithCondition(condition Condition) *Experiment {
100 exp.Condition = condition
101 return exp
102 }
103
104
105 func (exp *Experiment) WithCoverage(coverage float64) *Experiment {
106 exp.Coverage = &coverage
107 return exp
108 }
109
110
111 func (exp *Experiment) WithIncludeFunction(include func() bool) *Experiment {
112 exp.Include = include
113 return exp
114 }
115
116
117 func (exp *Experiment) WithNamespace(namespace *Namespace) *Experiment {
118 exp.Namespace = namespace
119 return exp
120 }
121
122
123 func (exp *Experiment) WithForce(force int) *Experiment {
124 exp.Force = &force
125 return exp
126 }
127
128
129 func (exp *Experiment) WithHashAttribute(hashAttribute string) *Experiment {
130 exp.HashAttribute = hashAttribute
131 return exp
132 }
133
134
135 func (exp *Experiment) WithHashVersion(hashVersion int) *Experiment {
136 exp.HashVersion = hashVersion
137 return exp
138 }
139
140
141 func (exp *Experiment) WithActive(active bool) *Experiment {
142 exp.Active = active
143 return exp
144 }
145
146
147 func (exp *Experiment) WithStatus(status ExperimentStatus) *Experiment {
148 exp.Status = status
149 return exp
150 }
151
152
153 func (exp *Experiment) WithGroups(groups ...string) *Experiment {
154 exp.Groups = groups
155 return exp
156 }
157
158
159 func (exp *Experiment) WithURL(url *regexp.Regexp) *Experiment {
160 exp.URL = url
161 return exp
162 }
163
164
165 func ParseExperiment(data []byte) *Experiment {
166 dict := make(map[string]interface{})
167 err := json.Unmarshal(data, &dict)
168 if err != nil {
169 logError("Failed parsing JSON input", "Experiment")
170 return nil
171 }
172 return BuildExperiment(dict)
173 }
174
175
176
177 func BuildExperiment(dict map[string]interface{}) *Experiment {
178 exp := NewExperiment("tmp")
179 gotKey := false
180 for k, v := range dict {
181 switch k {
182 case "key":
183 key, ok := jsonString(v, "Experiment", "key")
184 if !ok {
185 return nil
186 }
187 exp.Key = key
188 gotKey = true
189 case "variations":
190 exp = exp.WithVariations(BuildFeatureValues(v)...)
191 case "ranges":
192 ranges, ok := jsonRangeArray(v, "Experiment", "ranges")
193 if !ok {
194 return nil
195 }
196 exp = exp.WithRanges(ranges...)
197 case "meta":
198 meta, ok := jsonVariationMetaArray(v, "Experiment", "meta")
199 if !ok {
200 return nil
201 }
202 exp = exp.WithMeta(meta...)
203 case "filters":
204 filters, ok := jsonFilterArray(v, "Experiment", "filters")
205 if !ok {
206 return nil
207 }
208 exp = exp.WithFilters(filters...)
209 case "seed":
210 seed, ok := jsonString(v, "FeatureRule", "seed")
211 if !ok {
212 return nil
213 }
214 exp = exp.WithSeed(seed)
215 case "name":
216 name, ok := jsonString(v, "FeatureRule", "name")
217 if !ok {
218 return nil
219 }
220 exp = exp.WithName(name)
221 case "phase":
222 phase, ok := jsonString(v, "FeatureRule", "phase")
223 if !ok {
224 return nil
225 }
226 exp = exp.WithPhase(phase)
227 case "weights":
228 weights, ok := jsonFloatArray(v, "Experiment", "weights")
229 if !ok {
230 return nil
231 }
232 exp = exp.WithWeights(weights...)
233 case "active":
234 active, ok := jsonBool(v, "Experiment", "active")
235 if !ok {
236 return nil
237 }
238 exp = exp.WithActive(active)
239 case "coverage":
240 coverage, ok := jsonFloat(v, "Experiment", "coverage")
241 if !ok {
242 return nil
243 }
244 exp = exp.WithCoverage(coverage)
245 case "condition":
246 tmp, ok := v.(map[string]interface{})
247 if !ok {
248 logError("Invalid JSON data type", "Experiment", "condition")
249 continue
250 }
251 cond := BuildCondition(tmp)
252 if cond == nil {
253 logError("Invalid condition in JSON experiment data")
254 } else {
255 exp = exp.WithCondition(cond)
256 }
257 case "namespace":
258 namespace := BuildNamespace(v)
259 if namespace == nil {
260 return nil
261 }
262 exp = exp.WithNamespace(namespace)
263 case "force":
264 force, ok := jsonInt(v, "Experiment", "force")
265 if !ok {
266 return nil
267 }
268 exp = exp.WithForce(force)
269 case "hashAttribute":
270 hashAttribute, ok := jsonString(v, "Experiment", "hashAttribute")
271 if !ok {
272 return nil
273 }
274 exp = exp.WithHashAttribute(hashAttribute)
275 case "hashVersion":
276 hashVersion, ok := jsonInt(v, "Experiment", "hashVersion")
277 if !ok {
278 return nil
279 }
280 exp.HashVersion = hashVersion
281 default:
282 logWarn("Unknown key in JSON data", "Experiment", k)
283 }
284 }
285 if !gotKey {
286 logWarn("Key not set in JSON experiment data")
287 }
288 return exp
289 }
290
291 func (exp *Experiment) applyOverride(override *ExperimentOverride) *Experiment {
292 newExp := *exp
293 if override.Condition != nil {
294 newExp.Condition = override.Condition
295 }
296 if override.Weights != nil {
297 newExp.Weights = override.Weights
298 }
299 if override.Active != nil {
300 newExp.Active = *override.Active
301 }
302 if override.Status != nil {
303 newExp.Status = *override.Status
304 }
305 if override.Force != nil {
306 newExp.Force = override.Force
307 }
308 if override.Coverage != nil {
309 newExp.Coverage = override.Coverage
310 }
311 if override.Groups != nil {
312 newExp.Groups = override.Groups
313 }
314 if override.Namespace != nil {
315 newExp.Namespace = override.Namespace
316 }
317 if override.URL != nil {
318 newExp.URL = override.URL
319 }
320 return &newExp
321 }
322
323 func experimentFromFeatureRule(id string, rule *FeatureRule) *Experiment {
324 exp := NewExperiment(id).WithVariations(rule.Variations...)
325 if rule.Key != "" {
326 exp.Key = rule.Key
327 }
328 if rule.Coverage != nil {
329 exp = exp.WithCoverage(*rule.Coverage)
330 }
331 if rule.Weights != nil {
332 tmp := make([]float64, len(rule.Weights))
333 copy(tmp, rule.Weights)
334 exp = exp.WithWeights(tmp...)
335 }
336 if rule.HashAttribute != "" {
337 exp = exp.WithHashAttribute(rule.HashAttribute)
338 }
339 if rule.Namespace != nil {
340 val := Namespace{rule.Namespace.ID, rule.Namespace.Start, rule.Namespace.End}
341 exp = exp.WithNamespace(&val)
342 }
343 if rule.Meta != nil {
344 exp = exp.WithMeta(rule.Meta...)
345 }
346 if rule.Ranges != nil {
347 exp = exp.WithRanges(rule.Ranges...)
348 }
349 if rule.Name != "" {
350 exp = exp.WithName(rule.Name)
351 }
352 if rule.Phase != "" {
353 exp = exp.WithPhase(rule.Phase)
354 }
355 if rule.Seed != "" {
356 exp = exp.WithSeed(rule.Seed)
357 }
358 if rule.HashVersion != 0 {
359 exp = exp.WithHashVersion(rule.HashVersion)
360 }
361 if rule.Filters != nil {
362 exp = exp.WithFilters(rule.Filters...)
363 }
364 return exp
365 }
366
View as plain text