1
2
3 package growthbook
4
5 import (
6 "errors"
7 "fmt"
8 "net/url"
9 "reflect"
10 "regexp"
11 "runtime"
12 "strconv"
13 "strings"
14 "sync"
15 "time"
16 )
17
18 type subscriptionID uint
19
20
21 type Assignment struct {
22 Experiment *Experiment
23 Result *Result
24 }
25
26
27 type GrowthBook struct {
28 inner *growthBookData
29 }
30
31 type growthBookData struct {
32 sync.RWMutex
33 context *Context
34 forcedFeatureValues map[string]interface{}
35 attributeOverrides Attributes
36 trackedFeatures map[string]interface{}
37 trackedExperiments map[string]bool
38 nextSubscriptionID subscriptionID
39 subscriptions map[subscriptionID]ExperimentCallback
40 assigned map[string]*Assignment
41 ready bool
42 }
43
44
45 func New(context *Context) *GrowthBook {
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92 if context == nil {
93 context = NewContext()
94 }
95 inner := &growthBookData{
96 context: context,
97 forcedFeatureValues: nil,
98 attributeOverrides: nil,
99 trackedFeatures: make(map[string]interface{}),
100 trackedExperiments: make(map[string]bool),
101 nextSubscriptionID: 1,
102 subscriptions: make(map[subscriptionID]ExperimentCallback),
103 assigned: make(map[string]*Assignment),
104 }
105 gb := &GrowthBook{inner}
106 runtime.SetFinalizer(gb, func(gb *GrowthBook) { repoUnsubscribe(gb) })
107 if context.ClientKey != "" {
108 go gb.refresh(nil, true, false)
109 }
110 return gb
111 }
112
113
114
115 func (gb *GrowthBook) Ready() bool {
116 gb.inner.RLock()
117 defer gb.inner.RUnlock()
118
119 return gb.inner.ready
120 }
121
122
123 func (gb *GrowthBook) WithForcedFeatures(values map[string]interface{}) *GrowthBook {
124 gb.inner.Lock()
125 defer gb.inner.Unlock()
126
127 gb.inner.forcedFeatureValues = values
128 return gb
129 }
130
131
132 func (gb *GrowthBook) WithAttributeOverrides(overrides Attributes) *GrowthBook {
133 gb.inner.Lock()
134 defer gb.inner.Unlock()
135
136 gb.inner.attributeOverrides = overrides
137 return gb
138 }
139
140
141 func (gb *GrowthBook) WithEnabled(enabled bool) *GrowthBook {
142 gb.inner.Lock()
143 defer gb.inner.Unlock()
144
145 gb.inner.context.Enabled = enabled
146 return gb
147 }
148
149
150 func (gb *GrowthBook) WithAttributes(attrs Attributes) *GrowthBook {
151 gb.inner.Lock()
152 defer gb.inner.Unlock()
153
154 gb.inner.context.Attributes = attrs
155 return gb
156 }
157
158
159
160 func (gb *GrowthBook) Attributes() Attributes {
161 gb.inner.RLock()
162 defer gb.inner.RUnlock()
163
164 attrs := Attributes{}
165 for id, v := range gb.inner.context.Attributes {
166 attrs[id] = v
167 }
168 if gb.inner.attributeOverrides != nil {
169 for id, v := range gb.inner.attributeOverrides {
170 attrs[id] = v
171 }
172 }
173 return attrs
174 }
175
176
177 func (gb *GrowthBook) WithURL(url *url.URL) *GrowthBook {
178 gb.inner.Lock()
179 defer gb.inner.Unlock()
180
181 gb.inner.context.URL = url
182 return gb
183 }
184
185
186 func (gb *GrowthBook) WithFeatures(features FeatureMap) *GrowthBook {
187 gb.inner.withFeatures(features)
188 return gb
189 }
190
191 func (inner *growthBookData) withFeatures(features FeatureMap) {
192 inner.Lock()
193 defer inner.Unlock()
194
195 inner.context.Features = features
196 inner.ready = true
197 }
198
199
200 func (gb *GrowthBook) Features() FeatureMap {
201 return gb.inner.features()
202 }
203
204 func (inner *growthBookData) features() FeatureMap {
205 inner.RLock()
206 defer inner.RUnlock()
207
208 return inner.context.Features
209 }
210
211
212
213 func (gb *GrowthBook) WithEncryptedFeatures(encrypted string, key string) (*GrowthBook, error) {
214 err := gb.inner.withEncryptedFeatures(encrypted, key)
215 return gb, err
216 }
217
218 func (inner *growthBookData) withEncryptedFeatures(encrypted string, key string) error {
219 inner.Lock()
220 defer inner.Unlock()
221
222 if key == "" {
223 key = inner.context.DecryptionKey
224 }
225 featuresJson, err := decrypt(encrypted, key)
226
227 var features FeatureMap
228 if err == nil {
229 features = ParseFeatureMap([]byte(featuresJson))
230 if features != nil {
231 inner.context.Features = ParseFeatureMap([]byte(featuresJson))
232 }
233 }
234 if err != nil || features == nil {
235 err = errors.New("failed to decode encrypted features")
236 }
237 return err
238 }
239
240
241
242 func (gb *GrowthBook) WithForcedVariations(forcedVariations ForcedVariationsMap) *GrowthBook {
243 gb.inner.Lock()
244 defer gb.inner.Unlock()
245
246 gb.inner.context.ForcedVariations = forcedVariations
247 return gb
248 }
249
250 func (gb *GrowthBook) ForceVariation(key string, variation int) {
251 gb.inner.RLock()
252 defer gb.inner.RUnlock()
253
254 gb.inner.context.ForceVariation(key, variation)
255 }
256
257 func (gb *GrowthBook) UnforceVariation(key string) {
258 gb.inner.RLock()
259 defer gb.inner.RUnlock()
260
261 gb.inner.context.UnforceVariation(key)
262 }
263
264
265
266 func (gb *GrowthBook) WithQAMode(qaMode bool) *GrowthBook {
267 gb.inner.Lock()
268 defer gb.inner.Unlock()
269
270 gb.inner.context.QAMode = qaMode
271 return gb
272 }
273
274
275
276 func (gb *GrowthBook) WithDevMode(devMode bool) *GrowthBook {
277 gb.inner.Lock()
278 defer gb.inner.Unlock()
279
280 gb.inner.context.DevMode = devMode
281 return gb
282 }
283
284
285
286 func (gb *GrowthBook) WithTrackingCallback(callback ExperimentCallback) *GrowthBook {
287 gb.inner.Lock()
288 defer gb.inner.Unlock()
289
290 gb.inner.context.TrackingCallback = callback
291 return gb
292 }
293
294
295
296 func (gb *GrowthBook) WithFeatureUsageCallback(callback FeatureUsageCallback) *GrowthBook {
297 gb.inner.Lock()
298 defer gb.inner.Unlock()
299
300 gb.inner.context.OnFeatureUsage = callback
301 return gb
302 }
303
304
305 func (gb *GrowthBook) WithGroups(groups map[string]bool) *GrowthBook {
306 gb.inner.Lock()
307 defer gb.inner.Unlock()
308
309 gb.inner.context.Groups = groups
310 return gb
311 }
312
313
314 func (gb *GrowthBook) WithAPIHost(host string) *GrowthBook {
315 gb.inner.Lock()
316 defer gb.inner.Unlock()
317
318 gb.inner.context.APIHost = host
319 return gb
320 }
321
322
323 func (gb *GrowthBook) WithClientKey(key string) *GrowthBook {
324 gb.inner.Lock()
325 defer gb.inner.Unlock()
326
327 gb.inner.context.ClientKey = key
328 return gb
329 }
330
331
332 func (gb *GrowthBook) WithDecryptionKey(key string) *GrowthBook {
333 gb.inner.Lock()
334 defer gb.inner.Unlock()
335
336 gb.inner.context.DecryptionKey = key
337 return gb
338 }
339
340
341
342 func (fr *FeatureResult) GetValueWithDefault(def FeatureValue) FeatureValue {
343 if fr.Value == nil {
344 return def
345 }
346 return fr.Value
347 }
348
349
350 func (gb *GrowthBook) IsOn(key string) bool {
351 return gb.EvalFeature(key).On
352 }
353
354
355 func (gb *GrowthBook) IsOff(key string) bool {
356 return gb.EvalFeature(key).Off
357 }
358
359
360
361 func (gb *GrowthBook) GetFeatureValue(key string, defaultValue interface{}) interface{} {
362 featureValue := gb.EvalFeature(key).Value
363 if featureValue != nil {
364 return featureValue
365 }
366 return defaultValue
367 }
368
369
370
371 func (gb *GrowthBook) Feature(key string) *FeatureResult {
372 return gb.EvalFeature(key)
373 }
374
375
376
377 func (gb *GrowthBook) EvalFeature(id string) *FeatureResult {
378 gb.inner.RLock()
379 defer gb.inner.RUnlock()
380
381
382 if gb.inner.forcedFeatureValues != nil {
383 if override, ok := gb.inner.forcedFeatureValues[id]; ok {
384 logInfo("Global override", id, override)
385 return gb.getFeatureResult(id, override, OverrideResultSource, "", nil, nil)
386 }
387 }
388
389
390 feature, ok := gb.inner.context.Features[id]
391 if !ok {
392 logWarn("Unknown feature", id)
393 return gb.getFeatureResult(id, nil, UnknownResultSource, "", nil, nil)
394 }
395
396
397 for _, rule := range feature.Rules {
398
399
400 if rule.Condition != nil && !rule.Condition.Eval(gb.Attributes()) {
401 logInfo("Skip rule because of condition", id, rule)
402 continue
403 }
404
405
406 if rule.Filters != nil && gb.isFilteredOut(rule.Filters) {
407 logInfo("Skip rule because of filters", id, rule)
408 continue
409 }
410
411
412 if rule.Force != nil {
413
414 seed := id
415 if rule.Seed != "" {
416 seed = rule.Seed
417 }
418 if !gb.isIncludedInRollout(
419 seed,
420 rule.HashAttribute,
421 rule.Range,
422 rule.Coverage,
423 rule.HashVersion,
424 ) {
425 logInfo("Skip rule because user not included in rollout", id, rule)
426 continue
427 }
428
429
430 logInfo("Force value from rule", id, rule)
431 return gb.getFeatureResult(id, rule.Force, ForceResultSource, rule.ID, nil, nil)
432 }
433
434 if rule.Variations == nil {
435 logWarn("Skip invalid rule", id, rule)
436 continue
437 }
438
439
440
441 exp := experimentFromFeatureRule(id, rule)
442
443
444 result := gb.doRun(exp, id)
445 gb.fireSubscriptions(exp, result)
446
447
448
449 if result.InExperiment && !result.Passthrough {
450 return gb.getFeatureResult(id, result.Value, ExperimentResultSource, rule.ID, exp, result)
451 }
452 }
453
454
455 logInfo("Use default value", id, feature.DefaultValue)
456 return gb.getFeatureResult(id, feature.DefaultValue, DefaultValueResultSource, "", nil, nil)
457 }
458
459
460
461 func (gb *GrowthBook) Run(exp *Experiment) *Result {
462 gb.inner.RLock()
463 defer gb.inner.RUnlock()
464
465 result := gb.doRun(exp, "")
466 gb.fireSubscriptions(exp, result)
467 return result
468 }
469
470
471
472
473 func (gb *GrowthBook) Subscribe(callback ExperimentCallback) func() {
474 gb.inner.Lock()
475 defer gb.inner.Unlock()
476
477 id := gb.inner.nextSubscriptionID
478 gb.inner.subscriptions[id] = callback
479 gb.inner.nextSubscriptionID++
480 return func() {
481 delete(gb.inner.subscriptions, id)
482 }
483 }
484
485
486
487 func (gb *GrowthBook) GetAllResults() map[string]*Assignment {
488 gb.inner.RLock()
489 defer gb.inner.RUnlock()
490
491 return gb.inner.assigned
492 }
493
494
495
496
497 func (gb *GrowthBook) ClearSavedResults() {
498 gb.inner.Lock()
499 defer gb.inner.Unlock()
500
501 gb.inner.assigned = make(map[string]*Assignment)
502 }
503
504
505
506 func (gb *GrowthBook) ClearTrackingData() {
507 gb.inner.Lock()
508 defer gb.inner.Unlock()
509
510 gb.inner.trackedExperiments = make(map[string]bool)
511 }
512
513
514
515 func (gb *GrowthBook) GetAPIInfo() (string, string) {
516 gb.inner.RLock()
517 defer gb.inner.RUnlock()
518
519 apiHost := gb.inner.context.APIHost
520 if apiHost == "" {
521 apiHost = "https://cdn.growthbook.io"
522 }
523
524 return strings.TrimRight(apiHost, "/"), gb.inner.context.ClientKey
525 }
526
527 type FeatureRepoOptions struct {
528 AutoRefresh bool
529 Timeout time.Duration
530 SkipCache bool
531 }
532
533 func (gb *GrowthBook) LoadFeatures(options *FeatureRepoOptions) {
534 gb.refresh(options, true, true)
535 if options != nil && options.AutoRefresh {
536 repoSubscribe(gb)
537 }
538 }
539
540 func (gb *GrowthBook) LatestFeatureUpdate() *time.Time {
541 return repoLatestUpdate(gb)
542 }
543
544 func (gb *GrowthBook) RefreshFeatures(options *FeatureRepoOptions) {
545 gb.refresh(options, false, true)
546 }
547
548
549
550 func (gb *GrowthBook) refresh(
551 options *FeatureRepoOptions, allowStale bool, updateInstance bool) {
552
553 if gb.inner.context.ClientKey == "" {
554 logError("Missing clientKey")
555 return
556 }
557 var timeout time.Duration
558 skipCache := gb.inner.context.DevMode
559 if options != nil {
560 timeout = options.Timeout
561 skipCache = skipCache || options.SkipCache
562 }
563 configureCacheStaleTTL(gb.inner.context.CacheTTL)
564 repoRefreshFeatures(gb, timeout, skipCache, allowStale, updateInstance)
565 }
566
567 func (gb *GrowthBook) trackFeatureUsage(key string, res *FeatureResult) {
568
569 if res.Source == OverrideResultSource {
570 return
571 }
572
573
574 if saved, ok := gb.inner.trackedFeatures[key]; ok && reflect.DeepEqual(saved, res.Value) {
575 return
576 }
577 gb.inner.trackedFeatures[key] = res.Value
578
579
580 if gb.inner.context.OnFeatureUsage != nil {
581 gb.inner.context.OnFeatureUsage(key, res)
582 }
583 }
584
585 func (gb *GrowthBook) getFeatureResult(
586 key string,
587 value FeatureValue,
588 source FeatureResultSource,
589 ruleID string,
590 experiment *Experiment,
591 result *Result) *FeatureResult {
592 on := truthy(value)
593 off := !on
594 retval := FeatureResult{
595 Value: value,
596 On: on,
597 Off: off,
598 Source: source,
599 RuleID: ruleID,
600 Experiment: experiment,
601 ExperimentResult: result,
602 }
603
604 gb.trackFeatureUsage(key, &retval)
605
606 return &retval
607 }
608
609 func (gb *GrowthBook) getResult(
610 exp *Experiment, variationIndex int,
611 hashUsed bool, featureID string, bucket *float64) *Result {
612 inExperiment := true
613
614
615
616 if variationIndex < 0 || variationIndex >= len(exp.Variations) {
617 variationIndex = 0
618 inExperiment = false
619 }
620
621
622 hashAttribute, hashString := gb.getHashAttribute(exp.HashAttribute)
623
624 var meta *VariationMeta
625 if exp.Meta != nil {
626 if variationIndex < len(exp.Meta) {
627 meta = &exp.Meta[variationIndex]
628 }
629 }
630
631
632 var value FeatureValue
633 if variationIndex < len(exp.Variations) {
634 value = exp.Variations[variationIndex]
635 }
636 key := fmt.Sprint(variationIndex)
637 name := ""
638 passthrough := false
639 if meta != nil {
640 if meta.Key != "" {
641 key = meta.Key
642 }
643 if meta.Name != "" {
644 name = meta.Name
645 }
646 passthrough = meta.Passthrough
647 }
648 return &Result{
649 Key: key,
650 FeatureID: featureID,
651 InExperiment: inExperiment,
652 HashUsed: hashUsed,
653 VariationID: variationIndex,
654 Value: value,
655 HashAttribute: hashAttribute,
656 HashValue: hashString,
657 Bucket: bucket,
658 Name: name,
659 Passthrough: passthrough,
660 }
661 }
662
663 func (gb *GrowthBook) fireSubscriptions(exp *Experiment, result *Result) {
664
665
666 changed := false
667 storedResult, exists := gb.inner.assigned[exp.Key]
668 if exists {
669 if storedResult.Result.InExperiment != result.InExperiment ||
670 storedResult.Result.VariationID != result.VariationID {
671 changed = true
672 }
673 }
674
675
676 gb.inner.assigned[exp.Key] = &Assignment{exp, result}
677
678
679 if changed || !exists {
680 for _, sub := range gb.inner.subscriptions {
681 sub(exp, result)
682 }
683 }
684 }
685
686
687 func (gb *GrowthBook) doRun(exp *Experiment, featureID string) *Result {
688
689
690 if len(exp.Variations) < 2 {
691 logWarn("Invalid experiment", exp.Key)
692 return gb.getResult(exp, -1, false, featureID, nil)
693 }
694
695
696 if !gb.inner.context.Enabled {
697 logInfo("Context disabled", exp.Key)
698 return gb.getResult(exp, -1, false, featureID, nil)
699 }
700
701
702 exp = gb.mergeOverrides(exp)
703
704
705
706 if gb.inner.context.URL != nil {
707 qsOverride := getQueryStringOverride(exp.Key, gb.inner.context.URL, len(exp.Variations))
708 if qsOverride != nil {
709 logInfo("Force via querystring", exp.Key, qsOverride)
710 return gb.getResult(exp, *qsOverride, false, featureID, nil)
711 }
712 }
713
714
715
716 if gb.inner.context.ForcedVariations != nil {
717 force, forced := gb.inner.context.ForcedVariations[exp.Key]
718 if forced {
719 logInfo("Forced variation", exp.Key, force)
720 return gb.getResult(exp, force, false, featureID, nil)
721 }
722 }
723
724
725 if exp.Status == DraftStatus || !exp.Active {
726 logInfo("Skip because inactive", exp.Key)
727 return gb.getResult(exp, -1, false, featureID, nil)
728 }
729
730
731 _, hashString := gb.getHashAttribute(exp.HashAttribute)
732 if hashString == "" {
733 logInfo("Skip because of missing hash attribute", exp.Key)
734 return gb.getResult(exp, -1, false, featureID, nil)
735 }
736
737
738 if exp.Filters != nil {
739 if gb.isFilteredOut(exp.Filters) {
740 logInfo("Skip because of filters", exp.Key)
741 return gb.getResult(exp, -1, false, featureID, nil)
742 }
743 } else if exp.Namespace != nil {
744 if !exp.Namespace.inNamespace(hashString) {
745 logInfo("Skip because of namespace", exp.Key)
746 return gb.getResult(exp, -1, false, featureID, nil)
747 }
748 }
749
750
751 if exp.Include != nil && !exp.Include() {
752 logInfo("Skip because of include function", exp.Key)
753 return gb.getResult(exp, -1, false, featureID, nil)
754 }
755
756
757 if exp.Condition != nil {
758 if !exp.Condition.Eval(gb.inner.context.Attributes) {
759 logInfo("Skip because of condition", exp.Key)
760 return gb.getResult(exp, -1, false, featureID, nil)
761 }
762 }
763
764
765 if exp.Groups != nil && !gb.hasGroupOverlap(exp.Groups) {
766 logInfo("Skip because of groups", exp.Key)
767 return gb.getResult(exp, -1, false, featureID, nil)
768 }
769
770
771 if exp.URL != nil && !gb.urlIsValid(exp.URL) {
772 logInfo("Skip because of URL", exp.Key)
773 return gb.getResult(exp, -1, false, featureID, nil)
774 }
775
776
777 if exp.URLPatterns != nil && !isURLTargeted(gb.inner.context.URL, exp.URLPatterns) {
778 logInfo("Skip because of URL targeting", exp.Key)
779 return gb.getResult(exp, -1, false, featureID, nil)
780 }
781
782
783 seed := exp.Key
784 if exp.Seed != "" {
785 seed = exp.Seed
786 }
787 hv := 1
788 if exp.HashVersion != 0 {
789 hv = exp.HashVersion
790 }
791 n := hash(seed, hashString, hv)
792 if n == nil {
793 logWarn("Skip because of invalid hash version", exp.Key)
794 return gb.getResult(exp, -1, false, featureID, nil)
795 }
796 coverage := float64(1)
797 if exp.Coverage != nil {
798 coverage = *exp.Coverage
799 }
800 ranges := exp.Ranges
801 if ranges == nil {
802 ranges = getBucketRanges(len(exp.Variations), coverage, exp.Weights)
803 }
804 assigned := chooseVariation(*n, ranges)
805
806
807 if assigned == -1 {
808 logInfo("Skip because of coverage", exp.Key)
809 return gb.getResult(exp, -1, false, featureID, nil)
810 }
811
812
813 if exp.Force != nil {
814 return gb.getResult(exp, *exp.Force, false, featureID, nil)
815 }
816
817
818 if gb.inner.context.QAMode {
819 return gb.getResult(exp, -1, false, featureID, nil)
820 }
821
822
823 if exp.Status == StoppedStatus {
824 logInfo("Skip because stopped", exp.Key)
825 return gb.getResult(exp, -1, false, featureID, nil)
826 }
827
828
829 result := gb.getResult(exp, assigned, true, featureID, n)
830
831
832 gb.track(exp, result)
833
834 logInfo("In experiment", fmt.Sprintf("%s[%d]", exp.Key, result.VariationID))
835 return result
836 }
837
838 func (gb *GrowthBook) mergeOverrides(exp *Experiment) *Experiment {
839 if gb.inner.context.Overrides == nil {
840 return exp
841 }
842 if override, ok := gb.inner.context.Overrides[exp.Key]; ok {
843 exp = exp.applyOverride(override)
844 }
845 return exp
846 }
847
848
849
850
851 func (gb *GrowthBook) track(exp *Experiment, result *Result) {
852 if gb.inner.context.TrackingCallback == nil {
853 return
854 }
855
856
857
858 key := result.HashAttribute + result.HashValue +
859 exp.Key + strconv.Itoa(result.VariationID)
860 if _, exists := gb.inner.trackedExperiments[key]; exists {
861 return
862 }
863
864 gb.inner.trackedExperiments[key] = true
865 gb.inner.context.TrackingCallback(exp, result)
866 }
867
868 func (gb *GrowthBook) getHashAttribute(attr string) (string, string) {
869 hashAttribute := "id"
870 if attr != "" {
871 hashAttribute = attr
872 }
873
874 var hashValue interface{}
875 ok := false
876 if gb.inner.attributeOverrides != nil {
877 hashValue, ok = gb.inner.attributeOverrides[hashAttribute]
878 }
879 if !ok {
880 if gb.inner.context.Attributes != nil {
881 hashValue, ok = gb.inner.context.Attributes[hashAttribute]
882 } else if gb.inner.context.UserAttributes != nil {
883 hashValue, ok = gb.inner.context.UserAttributes[hashAttribute]
884 }
885 if !ok {
886 return "", ""
887 }
888 }
889 hashString, ok := convertHashValue(hashValue)
890 if !ok {
891 return "", ""
892 }
893
894 return hashAttribute, hashString
895 }
896
897 func (gb *GrowthBook) isIncludedInRollout(
898 seed string,
899 hashAttribute string,
900 rng *Range,
901 coverage *float64,
902 hashVersion int,
903 ) bool {
904 if rng == nil && coverage == nil {
905 return true
906 }
907
908 _, hashValue := gb.getHashAttribute(hashAttribute)
909 if hashValue == "" {
910 return false
911 }
912
913 hv := 1
914 if hashVersion != 0 {
915 hv = hashVersion
916 }
917 n := hash(seed, hashValue, hv)
918 if n == nil {
919 return false
920 }
921
922 if rng != nil {
923 return rng.InRange(*n)
924 }
925 if coverage != nil {
926 return *n <= *coverage
927 }
928 return true
929 }
930
931 func (gb *GrowthBook) isFilteredOut(filters []Filter) bool {
932 for _, filter := range filters {
933 _, hashValue := gb.getHashAttribute(filter.Attribute)
934 if hashValue == "" {
935 return true
936 }
937 hv := 2
938 if filter.HashVersion != 0 {
939 hv = filter.HashVersion
940 }
941 n := hash(filter.Seed, hashValue, hv)
942 if n == nil {
943 return true
944 }
945 if filter.Ranges != nil {
946 inRange := false
947 for _, rng := range filter.Ranges {
948 if rng.InRange(*n) {
949 inRange = true
950 break
951 }
952 }
953 if !inRange {
954 return true
955 }
956 }
957 }
958 return false
959 }
960
961 func (gb *GrowthBook) hasGroupOverlap(groups []string) bool {
962 for _, g := range groups {
963 if val, ok := gb.inner.context.Groups[g]; ok && val {
964 return true
965 }
966 }
967 return false
968 }
969
970 func (gb *GrowthBook) urlIsValid(urlRegexp *regexp.Regexp) bool {
971 url := gb.inner.context.URL
972 if url == nil {
973 return false
974 }
975
976 return urlRegexp.MatchString(url.String()) ||
977 urlRegexp.MatchString(url.Path)
978 }
979
View as plain text