activeexperiment 0.1.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/MIT-LICENSE +20 -0
- data/README.md +373 -0
- data/lib/active_experiment/base.rb +76 -0
- data/lib/active_experiment/cache/active_record_cache_store.rb +116 -0
- data/lib/active_experiment/cache/redis_hash_cache_store.rb +68 -0
- data/lib/active_experiment/cache.rb +28 -0
- data/lib/active_experiment/caching.rb +172 -0
- data/lib/active_experiment/callbacks.rb +111 -0
- data/lib/active_experiment/capturable.rb +117 -0
- data/lib/active_experiment/configured_experiment.rb +74 -0
- data/lib/active_experiment/core.rb +177 -0
- data/lib/active_experiment/executed.rb +86 -0
- data/lib/active_experiment/execution.rb +156 -0
- data/lib/active_experiment/gem_version.rb +18 -0
- data/lib/active_experiment/instrumentation.rb +45 -0
- data/lib/active_experiment/log_subscriber.rb +178 -0
- data/lib/active_experiment/logging.rb +62 -0
- data/lib/active_experiment/railtie.rb +69 -0
- data/lib/active_experiment/rollout.rb +106 -0
- data/lib/active_experiment/rollouts/inactive_rollout.rb +24 -0
- data/lib/active_experiment/rollouts/percent_rollout.rb +84 -0
- data/lib/active_experiment/rollouts/random_rollout.rb +46 -0
- data/lib/active_experiment/rollouts.rb +127 -0
- data/lib/active_experiment/rspec.rb +12 -0
- data/lib/active_experiment/run_key.rb +55 -0
- data/lib/active_experiment/segments.rb +69 -0
- data/lib/active_experiment/test_case.rb +11 -0
- data/lib/active_experiment/test_helper.rb +267 -0
- data/lib/active_experiment/variants.rb +145 -0
- data/lib/active_experiment/version.rb +11 -0
- data/lib/active_experiment.rb +27 -0
- data/lib/activeexperiment.rb +8 -0
- data/lib/rails/generators/experiment/USAGE +12 -0
- data/lib/rails/generators/experiment/experiment_generator.rb +53 -0
- data/lib/rails/generators/experiment/templates/application_experiment.rb.tt +4 -0
- data/lib/rails/generators/experiment/templates/experiment.rb.tt +35 -0
- data/lib/rails/generators/rspec/experiment/experiment_generator.rb +20 -0
- data/lib/rails/generators/rspec/experiment/templates/experiment_spec.rb.tt +7 -0
- data/lib/rails/generators/test_unit/experiment/experiment_generator.rb +21 -0
- data/lib/rails/generators/test_unit/experiment/templates/experiment_test.rb.tt +9 -0
- metadata +118 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/current_attributes"
|
4
|
+
|
5
|
+
module ActiveExperiment
|
6
|
+
# == Executed Experiments
|
7
|
+
#
|
8
|
+
# This is useful for surfacing experiments to the client layer. For example,
|
9
|
+
# if you have a client only experiment, an empty experiment can be defined
|
10
|
+
# and then run in the controller layer. The client layer can then use the
|
11
|
+
# experiment information to render the appropriate client code based on the
|
12
|
+
# variant that's been assigned, or other information available the experiment
|
13
|
+
# can provide in its +serialize+ method.
|
14
|
+
#
|
15
|
+
# An example of an empty experiment might be as simple as:
|
16
|
+
#
|
17
|
+
# class MyClientExperiment < ActiveExperiment::Base
|
18
|
+
# variant(:red) { }
|
19
|
+
# variant(:blue) { }
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# In the controller, this experiment can be run in a before action, or
|
23
|
+
# anywhere else that makes sense for your application:
|
24
|
+
#
|
25
|
+
# class MyController < ApplicationController
|
26
|
+
# before_action :run_my_client_experiment
|
27
|
+
# # ... controller code
|
28
|
+
#
|
29
|
+
# private
|
30
|
+
#
|
31
|
+
# def run_my_client_experiment
|
32
|
+
# MyClientExperiment.run(current_user)
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# Then in the layout, or appropriate view, all experiments that have been run
|
37
|
+
# during the request can be surfaced:
|
38
|
+
#
|
39
|
+
# <title>My App</title>
|
40
|
+
# <script>
|
41
|
+
# window.experiments = <%= ActiveExperiment::Executed.to_json %>
|
42
|
+
# </script>
|
43
|
+
#
|
44
|
+
# Or the experiments that have been run can be iterated:
|
45
|
+
#
|
46
|
+
# <% ActiveExperiment::Executed.experiments.each do |experiment| %>
|
47
|
+
# <meta name="<%= experiment.name %>" content="<%= experiment.variant %>">
|
48
|
+
# <% end %>
|
49
|
+
class Executed < ActiveSupport::CurrentAttributes
|
50
|
+
attribute :experiments
|
51
|
+
|
52
|
+
# Interface to add an experiment to the executed experiments. This is
|
53
|
+
# intended to be used by the +run+ method of the experiment class.
|
54
|
+
#
|
55
|
+
# Experiments are added to the executed experiments if they have been
|
56
|
+
# assigned a variant.
|
57
|
+
def self.<<(experiment)
|
58
|
+
self.experiments ||= []
|
59
|
+
experiments << experiment
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns an array of experiments that have been run.
|
63
|
+
#
|
64
|
+
# Used by several other methods to return serialized experiments.
|
65
|
+
def self.as_array
|
66
|
+
self.experiments || []
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a json of the experiments that have been run, with the experiment
|
70
|
+
# name as the key, and the serialized experiment as the value. This assumes
|
71
|
+
# that if an experiment is run multiple times, the same variant has been
|
72
|
+
# assigned for all runs, which may not be true, like when using the random
|
73
|
+
# rollout.
|
74
|
+
#
|
75
|
+
# When needed, the executed experiments can be accessed and/or iterated
|
76
|
+
# directly, or the +to_json_array+ method can be used.
|
77
|
+
def self.to_json
|
78
|
+
as_array.each_with_object({}) { |e, hash| hash[e.name] = e.serialize }.to_json
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns an array of experiments that have been run.
|
82
|
+
def self.to_json_array
|
83
|
+
as_array.map(&:serialize).to_json
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveExperiment
|
4
|
+
# == Execution Module
|
5
|
+
#
|
6
|
+
# This module provides most of the logic for running experiments. Running an
|
7
|
+
# experiment can be performed in a few ways, some of which are provided as
|
8
|
+
# convenience.
|
9
|
+
#
|
10
|
+
# 1. Calling +run+ on the class, passing the context and a block:
|
11
|
+
#
|
12
|
+
# MyExperiment.run(context) do |experiment|
|
13
|
+
# experiment.on(:treatment) { "treatment" }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# 2. Instantiating the experiment with the context, and calling +run+:
|
17
|
+
#
|
18
|
+
# MyExperiment.new(context).run do |experiment|
|
19
|
+
# experiment.on(:treatment) { "treatment" }
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# 3. Using the +ConfiguredExperiment+ API to +set+ and then +run+:
|
23
|
+
#
|
24
|
+
# MyExperiment.set(variant: :treatment).run(id: 1) do |experiment|
|
25
|
+
# experiment.on(:treatment) { "treatment" }
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# In all cases, a block can be provided to the +run+ method. The block will
|
29
|
+
# be called with the experiment, which allows overriding the variant
|
30
|
+
# behaviors using the scope of where the experiment is being run.
|
31
|
+
#
|
32
|
+
# When the experiment is run, the variant will be determined and the variant
|
33
|
+
# steps will be executed. The result of the variant execution will be
|
34
|
+
# returned unless the experiment is aborted in a +before_run+ or
|
35
|
+
# +before_variant+ callback.
|
36
|
+
#
|
37
|
+
# In general, the following decision tree diagram helps illustrate the order
|
38
|
+
# that things will be executed in running an experiment, utilizing caching
|
39
|
+
# when possible:
|
40
|
+
# run
|
41
|
+
# |
|
42
|
+
# _ skipped? _
|
43
|
+
# | |
|
44
|
+
# no yes
|
45
|
+
# | |
|
46
|
+
# | assigned/default_variant
|
47
|
+
# |
|
48
|
+
# _ cached_variant? _
|
49
|
+
# | |
|
50
|
+
# no yes
|
51
|
+
# | |
|
52
|
+
# _ segmented? _ (cached value)
|
53
|
+
# | |
|
54
|
+
# yes no
|
55
|
+
# | |
|
56
|
+
# | ___ rollout.variant_for __
|
57
|
+
# | | | |
|
58
|
+
# (cache) (cache) (cache)
|
59
|
+
# variant_a variant_b variant_c
|
60
|
+
#
|
61
|
+
module Execution
|
62
|
+
extend ActiveSupport::Concern
|
63
|
+
|
64
|
+
# These methods will be included into any Active Experiment object and
|
65
|
+
# expose the class level run method, and the ability to get a configured
|
66
|
+
# experiment instance using the set method.
|
67
|
+
module ClassMethods
|
68
|
+
# Instantiates and runs an experiment with the provided context and
|
69
|
+
# block. This is a convenience method.
|
70
|
+
#
|
71
|
+
# An example of using this method to run an experiment:
|
72
|
+
#
|
73
|
+
# MyExperiment.run(id: 1) do |experiment|
|
74
|
+
# experiment.on(:treatment) { "red" }
|
75
|
+
# end
|
76
|
+
def run(*args, **kws, &block)
|
77
|
+
new(*args, **kws).run(&block)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Creates a configured experiment with the provided options. Configured
|
81
|
+
# experiments expose a few helpful methods for running and caching
|
82
|
+
# experiment details.
|
83
|
+
#
|
84
|
+
# The following options can be provided to configure an experiment:
|
85
|
+
#
|
86
|
+
# * +:variant+ - The variant to assign.
|
87
|
+
#
|
88
|
+
# An example of using this method to set a variant and run an experiment:
|
89
|
+
#
|
90
|
+
# MyExperiment.set(variant: :red).run(id: 1) do |experiment|
|
91
|
+
# experiment.on(:red) { "red" }
|
92
|
+
# end
|
93
|
+
def set(**options)
|
94
|
+
ConfiguredExperiment.new(self, **options)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Runs the experiment. Calling +run+ returns the value of the assigned
|
99
|
+
# variant block or method.
|
100
|
+
#
|
101
|
+
# When running an experiment, a block can be provided and it will be called
|
102
|
+
# with the experiment, which provides the ability to override variant
|
103
|
+
# behaviors when running the experiment.
|
104
|
+
#
|
105
|
+
# MyExperiment.new(id: 1).run do |experiment|
|
106
|
+
# experiment.on(:treatment) { "treatment" }
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# Raises an ActiveExperiment::ExecutionError if there are no variants
|
110
|
+
# registered, or if the experiment is already running, in the case of
|
111
|
+
# accidentally calling run again within a run or variant block.
|
112
|
+
def run(&block)
|
113
|
+
return @results if defined?(@results)
|
114
|
+
raise ExecutionError, "No variants registered" if variant_names.empty?
|
115
|
+
|
116
|
+
@results = nil
|
117
|
+
instrument(:start_experiment)
|
118
|
+
instrument(:process_run) do
|
119
|
+
run_callbacks(:run, :process_run_callbacks) do
|
120
|
+
call_run_block(&block) if block.present?
|
121
|
+
@variant = resolve_variant
|
122
|
+
@results = resolve_results
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
@results
|
127
|
+
ensure
|
128
|
+
Executed << self
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
def resolve_variant
|
133
|
+
return variant || default_variant if skipped?
|
134
|
+
|
135
|
+
resolved = cached_variant(variant) do
|
136
|
+
run_callbacks(:segment, :process_segment_callbacks)
|
137
|
+
variant || rollout.variant_for(self)
|
138
|
+
end
|
139
|
+
|
140
|
+
variant || resolved || default_variant
|
141
|
+
end
|
142
|
+
|
143
|
+
def resolve_results
|
144
|
+
resolved = nil
|
145
|
+
run_callbacks(variants[variant], :process_variant_callbacks) do
|
146
|
+
resolved = variant_step_chains[variant]&.call
|
147
|
+
end
|
148
|
+
|
149
|
+
resolved || @results
|
150
|
+
end
|
151
|
+
|
152
|
+
def call_run_block(&block)
|
153
|
+
block.call(self)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveExperiment
|
4
|
+
# Returns the currently loaded version of Active Experiment as a
|
5
|
+
# +Gem::Version+.
|
6
|
+
def self.gem_version
|
7
|
+
Gem::Version.new(VERSION::STRING)
|
8
|
+
end
|
9
|
+
|
10
|
+
module VERSION
|
11
|
+
MAJOR = 0
|
12
|
+
MINOR = 1
|
13
|
+
TINY = 0
|
14
|
+
PRE = "alpha"
|
15
|
+
|
16
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveExperiment
|
4
|
+
# == Instrumentation Module
|
5
|
+
#
|
6
|
+
# Instrumentation is provided through +ActiveSupport::Notifications+. The
|
7
|
+
# best example of how to utilize the instrumentation within Active Experiment
|
8
|
+
# is look at how +ActiveExperiment::LogSubscriber+ has been implemented.
|
9
|
+
module Instrumentation
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
private
|
13
|
+
def run_callbacks(kind, event_name, **payload, &block)
|
14
|
+
if kind.present? && __callbacks[kind.to_sym].any?
|
15
|
+
instrument(event_name, **payload) do
|
16
|
+
super(kind, &block)
|
17
|
+
end
|
18
|
+
else
|
19
|
+
yield if block_given?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def instrument(operation, **payload, &block)
|
24
|
+
operation = "#{operation}.active_experiment"
|
25
|
+
payload = payload.merge(experiment: self)
|
26
|
+
return ActiveSupport::Notifications.instrument(operation, payload) unless block.present?
|
27
|
+
|
28
|
+
ActiveSupport::Notifications.instrument(operation, payload) do |event_payload|
|
29
|
+
variant = @variant
|
30
|
+
results = block.call
|
31
|
+
|
32
|
+
event_payload[:variant] = @variant if variant != @variant
|
33
|
+
event_payload[:aborted] = @halted_callback if @halted_callback.present?
|
34
|
+
@halted_callback = nil if @halted_callback == :segment
|
35
|
+
|
36
|
+
results
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def halted_callback_hook(filter, name)
|
41
|
+
super
|
42
|
+
@halted_callback = name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/log_subscriber"
|
4
|
+
|
5
|
+
module ActiveExperiment
|
6
|
+
# == Log Subscriber
|
7
|
+
#
|
8
|
+
# TODO: finish documenting.
|
9
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
10
|
+
def start_experiment(event)
|
11
|
+
warn_of_nested_experiment(event) if execution_stack.any?
|
12
|
+
experiment_logger(event) do |experiment|
|
13
|
+
execution_stack.push(experiment)
|
14
|
+
|
15
|
+
info = []
|
16
|
+
info << "Run ID: #{experiment.run_id}"
|
17
|
+
info << "Variant: #{experiment.variant}" if experiment.variant.present?
|
18
|
+
|
19
|
+
build_message(:info, "Running #{experiment.name} (#{info.join(", ")})", context: experiment.log_context?)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_run(event)
|
24
|
+
errored = event.payload[:exception_object]
|
25
|
+
aborted = !errored && event.payload[:aborted]
|
26
|
+
|
27
|
+
experiment_logger(event) do |experiment|
|
28
|
+
execution_stack.pop
|
29
|
+
|
30
|
+
if errored
|
31
|
+
build_message(:error, "Run failed: #{errored.class} (#{errored.message})")
|
32
|
+
elsif aborted
|
33
|
+
build_message(:info, "Run aborted in #{aborted} callbacks", details: true)
|
34
|
+
else
|
35
|
+
variant_name = experiment.variant
|
36
|
+
if experiment.variant_names.include?(variant_name)
|
37
|
+
build_message(:info, "Completed running #{experiment.variant} variant", details: true)
|
38
|
+
elsif variant_name.present?
|
39
|
+
build_message(:error, "Run errored: unknown `#{variant_name}` variant resolved", details: true)
|
40
|
+
else
|
41
|
+
build_message(:error, "Run errored: no variant resolved", details: true)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def process_segment_callbacks(event)
|
48
|
+
return if event.payload[:exception_object].present?
|
49
|
+
|
50
|
+
experiment_logger(event) do |experiment|
|
51
|
+
if event.payload[:aborted] == :segment
|
52
|
+
build_message(:info, "Segmented into the `#{experiment.variant}` variant", duration: true)
|
53
|
+
else
|
54
|
+
build_callback_message(event)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_run_callbacks(event)
|
60
|
+
experiment_logger(event) { build_callback_message(event) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def process_variant_callbacks(event)
|
64
|
+
experiment_logger(event) { build_callback_message(event) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def process_variant_steps(event)
|
68
|
+
experiment_logger(event) { build_callback_message(event) }
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def warn_of_nested_experiment(event)
|
73
|
+
experiment_logger(event) do
|
74
|
+
build_message(:warn, "Nesting experiment in #{experiment_identifier(execution_stack.last)}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def experiment_logger(event, &block)
|
79
|
+
return unless logger.present?
|
80
|
+
|
81
|
+
experiment = event.payload[:experiment]
|
82
|
+
result = block.call(experiment)
|
83
|
+
|
84
|
+
return unless result.present?
|
85
|
+
|
86
|
+
logger.send(result[:level]) do
|
87
|
+
log = +colorized_prefix(experiment)
|
88
|
+
log << colorized_message(result[:message], level: result[:level])
|
89
|
+
log << colorized_duration(event, parens: true) if result[:duration]
|
90
|
+
log << colorized_details(event) if result[:details]
|
91
|
+
log << colorized_context(experiment) if result[:context]
|
92
|
+
log
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_message(level, message, **kws)
|
97
|
+
{ level: level, message: message, **kws }
|
98
|
+
end
|
99
|
+
|
100
|
+
def build_callback_message(event)
|
101
|
+
return if event.payload[:exception_object].present?
|
102
|
+
|
103
|
+
variant = event.payload[:variant]
|
104
|
+
process = event.name.split(".").first.gsub("process_", "").tr("_", " ")
|
105
|
+
|
106
|
+
if variant.present? && process != "run callbacks"
|
107
|
+
build_message(:info, "Resolved `#{variant}` variant in #{process}", duration: true)
|
108
|
+
elsif process != "variant steps"
|
109
|
+
build_message(:debug, "Completed #{process}", duration: true)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def colorized_prefix(experiment)
|
114
|
+
color(" #{experiment_identifier(experiment)} ", GREEN)
|
115
|
+
end
|
116
|
+
|
117
|
+
def colorized_message(message, level: :info)
|
118
|
+
case level
|
119
|
+
when :error
|
120
|
+
color(message, RED, bold: true)
|
121
|
+
when :warn
|
122
|
+
color(message, YELLOW, bold: true)
|
123
|
+
else
|
124
|
+
message
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def colorized_details(event)
|
129
|
+
" (Duration:#{colorized_duration(event, parens: false)} | Allocations: #{event.allocations})"
|
130
|
+
end
|
131
|
+
|
132
|
+
def colorized_duration(event, parens: true)
|
133
|
+
duration = event.duration.round(1)
|
134
|
+
if duration > 1000
|
135
|
+
ret = color("#{duration}ms", RED, bold: true)
|
136
|
+
elsif duration > 500
|
137
|
+
ret = color("#{duration}ms", YELLOW, bold: true)
|
138
|
+
else
|
139
|
+
ret = "#{duration}ms"
|
140
|
+
end
|
141
|
+
|
142
|
+
parens ? " (#{ret})" : " #{ret}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def colorized_context(experiment)
|
146
|
+
return "" unless experiment.log_context?
|
147
|
+
|
148
|
+
" with context: #{format_context(experiment.context).inspect}"
|
149
|
+
end
|
150
|
+
|
151
|
+
def format_context(arg)
|
152
|
+
case arg
|
153
|
+
when Hash
|
154
|
+
arg.transform_values { |value| format_context(value) }
|
155
|
+
when Array
|
156
|
+
arg.map { |value| format_context(value) }
|
157
|
+
when GlobalID::Identification
|
158
|
+
arg.to_global_id.to_s rescue arg
|
159
|
+
else
|
160
|
+
arg
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def experiment_identifier(experiment)
|
165
|
+
"#{experiment.class.name}[#{experiment.run_key.slice(0, 8)}]"
|
166
|
+
end
|
167
|
+
|
168
|
+
def execution_stack
|
169
|
+
ActiveSupport::IsolatedExecutionState[:active_experiment_log_subscriber_execution_stack] ||= []
|
170
|
+
end
|
171
|
+
|
172
|
+
def logger
|
173
|
+
ActiveExperiment.logger
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
ActiveExperiment::LogSubscriber.attach_to(:active_experiment)
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveExperiment
|
4
|
+
# == Logging Module
|
5
|
+
#
|
6
|
+
# Within experiments, you can log information about the experiment. This can
|
7
|
+
# be done by simply calling +logger+ within the experiment definition. By
|
8
|
+
# default the logger will be tagged with "ActiveExperiment" to help identify
|
9
|
+
# where the log message is coming from.
|
10
|
+
#
|
11
|
+
# The logger can also be tagged, and then used from within a block. The tag
|
12
|
+
# will be removed after the block has been executed.
|
13
|
+
#
|
14
|
+
# For instance, an experiment could implement tag the logger like this when
|
15
|
+
# run:
|
16
|
+
#
|
17
|
+
# class MyExperiment < ActiveExperiment::Base
|
18
|
+
# around_run(prepend: true) do |_, block|
|
19
|
+
# tag_logger("MyExperiment", "run", &block)
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# Any logging that occurs within this experiment, while it's being run, will
|
24
|
+
# be tagged with "MyExperiment" and "run". Those log lines might look like:
|
25
|
+
#
|
26
|
+
# "[ActiveExperiment] [MyExperiment] [run] Experiment started..."
|
27
|
+
#
|
28
|
+
# The +log_context+ class attribute is used as configuration within the
|
29
|
+
# +ActiveExperiment::LogSubscriber+. For some experiments that contain
|
30
|
+
# sensitive information, it might be useful to not log the context. This can
|
31
|
+
# be done by setting that experiment class's +log_context+ to false.
|
32
|
+
#
|
33
|
+
# More logging details can be found in the +ActiveExperiment::LogSubscriber+.
|
34
|
+
module Logging # :nodoc:
|
35
|
+
extend ActiveSupport::Concern
|
36
|
+
|
37
|
+
TAG_NAME = "ActiveExperiment"
|
38
|
+
private_constant :TAG_NAME
|
39
|
+
|
40
|
+
included do
|
41
|
+
class_attribute :log_context, instance_predicate: true, default: false
|
42
|
+
private :log_context=, :log_context
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns logger that can be used within the experiment.
|
46
|
+
#
|
47
|
+
# The logger will be tagged with "ActiveExperiment" if possible, to help
|
48
|
+
# identify where the log messages are coming from.
|
49
|
+
def logger
|
50
|
+
@logger ||= ActiveExperiment.logger.try(:tagged, TAG_NAME) || ActiveExperiment.logger
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def tag_logger(*tags, &block)
|
55
|
+
if logger.respond_to?(:tagged)
|
56
|
+
logger.tagged(*tags, &block)
|
57
|
+
else
|
58
|
+
yield
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "global_id/railtie"
|
4
|
+
require "active_experiment"
|
5
|
+
|
6
|
+
module ActiveExperiment
|
7
|
+
class Railtie < Rails::Railtie # :nodoc:
|
8
|
+
config.active_experiment = ActiveSupport::OrderedOptions.new
|
9
|
+
config.active_experiment.custom_rollouts = {}
|
10
|
+
config.active_experiment.log_query_tags_around_run = true
|
11
|
+
|
12
|
+
initializer "active_experiment.logger" do
|
13
|
+
ActiveSupport.on_load(:active_experiment) { ActiveExperiment.logger = ::Rails.logger }
|
14
|
+
end
|
15
|
+
|
16
|
+
initializer "active_experiment.custom_rollouts" do |app|
|
17
|
+
config.after_initialize do
|
18
|
+
app.config.active_experiment.custom_rollouts.each do |name, rollout|
|
19
|
+
ActiveExperiment::Rollouts.register(name, rollout)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
initializer "active_experiment.set_configs" do |app|
|
25
|
+
options = app.config.active_experiment
|
26
|
+
config.after_initialize do
|
27
|
+
options.digest_secret_key ||= app.secrets.secret_key_base
|
28
|
+
|
29
|
+
options.each do |k, v|
|
30
|
+
k = "#{k}="
|
31
|
+
if ActiveExperiment.respond_to?(k)
|
32
|
+
ActiveExperiment.send(k, v)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
ActiveSupport.on_load(:active_experiment) do
|
38
|
+
options.each do |k, v|
|
39
|
+
k = "#{k}="
|
40
|
+
if ActiveExperiment.respond_to?(k)
|
41
|
+
ActiveExperiment.send(k, v)
|
42
|
+
elsif respond_to?(k)
|
43
|
+
send(k, v)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
ActiveSupport.on_load(:action_dispatch_integration_test) do
|
49
|
+
include ActiveExperiment::TestHelper
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
initializer "active_experiment.query_log_tags" do |app|
|
54
|
+
query_logs_tags_enabled = app.config.respond_to?(:active_record) &&
|
55
|
+
app.config.active_record.query_log_tags_enabled &&
|
56
|
+
app.config.active_experiment.log_query_tags_around_run
|
57
|
+
|
58
|
+
if query_logs_tags_enabled
|
59
|
+
app.config.active_record.query_log_tags |= [:experiment]
|
60
|
+
|
61
|
+
ActiveSupport.on_load(:active_record) do
|
62
|
+
ActiveRecord::QueryLogs.taggings[:experiment] = lambda do |context|
|
63
|
+
context[:experiment].class.name if context[:experiment]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|