-
Notifications
You must be signed in to change notification settings - Fork 17
/
alephbet.coffee
113 lines (89 loc) · 3.51 KB
/
alephbet.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
utils = require('./utils')
adapters = require('./adapters')
options = require('./options')
class AlephBet
@options = options
@utils = utils
@GimelAdapter = adapters.GimelAdapter
@PersistentQueueGoogleAnalyticsAdapter = adapters.PersistentQueueGoogleAnalyticsAdapter
@PersistentQueueKeenAdapter = adapters.PersistentQueueKeenAdapter
class @Experiment
@_options:
name: null
variants: null
sample: 1.0
trigger: -> true
tracking_adapter: adapters.GoogleUniversalAnalyticsAdapter
storage_adapter: adapters.LocalStorageAdapter
constructor: (@options={}) ->
utils.defaults(@options, Experiment._options)
_validate.call(this)
@name = @options.name
@variants = @options.variants
@variant_names = utils.keys(@variants)
_run.call(this)
run: ->
utils.log("running with options: #{JSON.stringify(@options)}")
if variant = @get_stored_variant()
# a variant was already chosen. activate it
utils.log("#{variant} active")
@activate_variant(variant)
else
@conditionally_activate_variant()
_run = -> @run()
activate_variant: (variant) ->
@variants[variant]?.activate(this)
@storage().set("#{@options.name}:variant", variant)
# if experiment conditions match, pick and activate a variant, track experiment start
conditionally_activate_variant: ->
return unless @options.trigger()
utils.log('trigger set')
return unless @in_sample()
utils.log('in sample')
variant = @pick_variant()
@tracking().experiment_start(@options.name, variant)
@activate_variant(variant)
goal_complete: (goal_name, props={}) ->
utils.defaults(props, {unique: true})
return if props.unique && @storage().get("#{@options.name}:#{goal_name}")
variant = @get_stored_variant()
return unless variant
@storage().set("#{@options.name}:#{goal_name}", true) if props.unique
utils.log("experiment: #{@options.name} variant:#{variant} goal:#{goal_name} complete")
@tracking().goal_complete(@options.name, variant, goal_name)
get_stored_variant: ->
@storage().get("#{@options.name}:variant")
pick_variant: ->
partitions = 1.0 / @variant_names.length
chosen_partition = Math.floor(Math.random() / partitions)
variant = @variant_names[chosen_partition]
utils.log("#{variant} picked")
variant
in_sample: ->
active = @storage().get("#{@options.name}:in_sample")
return active unless typeof active is 'undefined'
active = Math.random() <= @options.sample
@storage().set("#{@options.name}:in_sample", active)
active
add_goal: (goal) =>
goal.add_experiment(this)
add_goals: (goals) =>
@add_goal(goal) for goal in goals
storage: -> @options.storage_adapter
tracking: -> @options.tracking_adapter
_validate = ->
throw 'an experiment name must be specified' if @options.name is null
throw 'variants must be provided' if @options.variants is null
throw 'trigger must be a function' if typeof @options.trigger isnt 'function'
class @Goal
constructor: (@name, @props={}) ->
utils.defaults(@props, {unique: true})
@experiments = []
add_experiment: (experiment) ->
@experiments.push(experiment)
add_experiments: (experiments) ->
@add_experiment(experiment) for experiment in experiments
complete: ->
for experiment in @experiments
experiment.goal_complete(@name, @props)
module.exports = AlephBet