...

Source file src/github.com/growthbook/growthbook-golang/experiment.go

Documentation: github.com/growthbook/growthbook-golang

     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  // Experiment defines a single experiment.
    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  // NewExperiment creates an experiment with default settings: active,
    42  // but all other fields empty.
    43  func NewExperiment(key string) *Experiment {
    44  	return &Experiment{
    45  		Key:    key,
    46  		Active: true,
    47  	}
    48  }
    49  
    50  // WithVariations set the feature variations for an experiment.
    51  func (exp *Experiment) WithVariations(variations ...FeatureValue) *Experiment {
    52  	exp.Variations = variations
    53  	return exp
    54  }
    55  
    56  // WithRanges set the ranges for an experiment.
    57  func (exp *Experiment) WithRanges(ranges ...Range) *Experiment {
    58  	exp.Ranges = ranges
    59  	return exp
    60  }
    61  
    62  // WithMeta sets the meta information for an experiment.
    63  func (exp *Experiment) WithMeta(meta ...VariationMeta) *Experiment {
    64  	exp.Meta = meta
    65  	return exp
    66  }
    67  
    68  // WithFilters sets the filters for an experiment.
    69  func (exp *Experiment) WithFilters(filters ...Filter) *Experiment {
    70  	exp.Filters = filters
    71  	return exp
    72  }
    73  
    74  // WithSeed sets the hash seed for an experiment.
    75  func (exp *Experiment) WithSeed(seed string) *Experiment {
    76  	exp.Seed = seed
    77  	return exp
    78  }
    79  
    80  // WithName sets the name for an experiment.
    81  func (exp *Experiment) WithName(name string) *Experiment {
    82  	exp.Name = name
    83  	return exp
    84  }
    85  
    86  // WithPhase sets the phase for an experiment.
    87  func (exp *Experiment) WithPhase(phase string) *Experiment {
    88  	exp.Phase = phase
    89  	return exp
    90  }
    91  
    92  // WithWeights set the weights for an experiment.
    93  func (exp *Experiment) WithWeights(weights ...float64) *Experiment {
    94  	exp.Weights = weights
    95  	return exp
    96  }
    97  
    98  // WithCondition sets the condition for an experiment.
    99  func (exp *Experiment) WithCondition(condition Condition) *Experiment {
   100  	exp.Condition = condition
   101  	return exp
   102  }
   103  
   104  // WithCoverage sets the coverage for an experiment.
   105  func (exp *Experiment) WithCoverage(coverage float64) *Experiment {
   106  	exp.Coverage = &coverage
   107  	return exp
   108  }
   109  
   110  // WithInclude sets the inclusion function for an experiment.
   111  func (exp *Experiment) WithIncludeFunction(include func() bool) *Experiment {
   112  	exp.Include = include
   113  	return exp
   114  }
   115  
   116  // WithNamespace sets the namespace for an experiment.
   117  func (exp *Experiment) WithNamespace(namespace *Namespace) *Experiment {
   118  	exp.Namespace = namespace
   119  	return exp
   120  }
   121  
   122  // WithForce sets the forced value index for an experiment.
   123  func (exp *Experiment) WithForce(force int) *Experiment {
   124  	exp.Force = &force
   125  	return exp
   126  }
   127  
   128  // WithHashAttribute sets the hash attribute for an experiment.
   129  func (exp *Experiment) WithHashAttribute(hashAttribute string) *Experiment {
   130  	exp.HashAttribute = hashAttribute
   131  	return exp
   132  }
   133  
   134  // WithHashVersion sets the hash version for an experiment.
   135  func (exp *Experiment) WithHashVersion(hashVersion int) *Experiment {
   136  	exp.HashVersion = hashVersion
   137  	return exp
   138  }
   139  
   140  // WithActive sets the enabled flag for an experiment.
   141  func (exp *Experiment) WithActive(active bool) *Experiment {
   142  	exp.Active = active
   143  	return exp
   144  }
   145  
   146  // WithStatus sets the status for an experiment.
   147  func (exp *Experiment) WithStatus(status ExperimentStatus) *Experiment {
   148  	exp.Status = status
   149  	return exp
   150  }
   151  
   152  // WithGroups sets the groups for an experiment.
   153  func (exp *Experiment) WithGroups(groups ...string) *Experiment {
   154  	exp.Groups = groups
   155  	return exp
   156  }
   157  
   158  // WithURL sets the URL for an experiment.
   159  func (exp *Experiment) WithURL(url *regexp.Regexp) *Experiment {
   160  	exp.URL = url
   161  	return exp
   162  }
   163  
   164  // ParseExperiment creates an Experiment value from raw JSON input.
   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  // BuildExperiment creates an Experiment value from a JSON object
   176  // represented as a Go map.
   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