Skip to content

Commit 60cb87f

Browse files
authored
Don't record assignment events for feature gates (#69)
* Don't record assignment events for feature gates Because feature gates are not used for analysis, there's no need to record anything but overrides for feature gates. This will vastly reduce the data that needs to change when deciding a feature gate split. It also will cause reweighting feature gates to affect clients in the field instantly unless they've been overridden. Also explain the difference between experiments and gates to admins. * more clarity on why specific variants are assigned * preposition * Let's do this. * 👮 * don't use the word population for gates because we use it in the admin to mean "recorded assignees of experiments."
1 parent d4fe28b commit 60cb87f

11 files changed

Lines changed: 109 additions & 16 deletions

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,44 @@ One the values that a given visitor will be assigned for a split, e.g. `true` or
154154
### Weighting
155155
Variants are assigned pseudo-randomly to visitors based on their visitor IDs and the weightings for the variants. Weightings describe the probability of a visitor being assigned to a given variant in integer percentages. All the variant weightings for a given split must sum to 100, though variants may have a weighting of 0.
156156

157+
### Experiment
158+
159+
Experiments are the standard flavor of splits in TestTrack. They are
160+
intended to be used for A/B testing, and the TestTrack server records
161+
visitors' experienced variants so that those visitors will continue to
162+
experience the same variant regardless of subsequent changes to the
163+
weightings of those variants via the admin interface.
164+
165+
Storing the variant a visitor experienced for an experiment also allows
166+
TestTrack to provide a consistent UX to a customer who experienced a
167+
new-to-them experiment before logging in on a new device, only to be
168+
recognized as an existing visitor upon sign-in. TestTrack will merge
169+
all variant assignments from the anonymous visitor into the
170+
authenticated visitor at sign-in as long as the authenticated visitor
171+
doesn't have conflicting assignments. In that case, the authenticated
172+
visitor's previous assignments win.
173+
174+
### Feature Gate
175+
176+
As of TestTrack version 1.2, splits with names ending in the `_enabled`
177+
suffix will be treated as feature gates. Feature gates differ from
178+
experiments in that they are not intended to be used for analysis, and
179+
therefore it is not important that the user remain in the same variant
180+
throughout the entire split lifecycle. Feature gates are meant to be
181+
slow-rolled (incrementally increasing the percentage of customers
182+
experiencing the new feature), released en masse, or instantly rolled
183+
back.
184+
185+
To facilitate these smooth transitions and rapid toggles, the TestTrack
186+
server will not record variant assignments when a visitor experiences a
187+
split. This means that every time a visitor experiences a split, they
188+
will be deterministically (pseudorandomly) assigned to a variant based
189+
on their visitor ID and the name of the split. This will provide the
190+
customer with a stable variant given a constant split weighting, but
191+
probablistically increase the percentage of visitors experiencing the
192+
the `true` variant as the split weightings are increased via the admin
193+
panel, giving an admin full control over the feature's release.
194+
157195
### IdentifierType
158196
A name for a customer identifier that is meaningful in your application, typically things that people sign up as, log in as. They should be expressed in `snake_case` and conventionally are prefixed with the application name that the identifier is for, e.g. `myapp_user_id`, `myapp_lead_id`.
159197

app/models/deterministic_assignment_creation.rb

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ def self.create!(params)
1414
end
1515

1616
def save!
17-
ArbitraryAssignmentCreation.create!(
18-
visitor_id: visitor_id,
19-
split_name: split_name,
20-
variant: variant,
21-
mixpanel_result: mixpanel_result,
22-
context: context
23-
)
17+
unless split.feature_gate?
18+
ArbitraryAssignmentCreation.create!(
19+
visitor_id: visitor_id,
20+
split_name: split_name,
21+
variant: variant,
22+
mixpanel_result: mixpanel_result,
23+
context: context
24+
)
25+
end
2426
end
2527

2628
def variant

app/models/split_creation.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ def weighting_registry=(registry)
2525
end
2626

2727
def split
28-
@split ||= app.splits.create_with(registry: merged_registry).find_or_initialize_by(name: name)
28+
@split ||= app.splits.create_with(registry: merged_registry, feature_gate: feature_gate?).find_or_initialize_by(name: name)
29+
end
30+
31+
def feature_gate?
32+
name.end_with?("_enabled")
2933
end
3034

3135
private

app/views/admin/split_configs/new.html.erb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@
55
<h1 class="TakeoverText-title sc-m-v--l">Split: <%= @split_creation.name %></h1>
66
</div>
77

8+
<% if @split_creation.feature_gate? %>
9+
<p>
10+
This split is a feature gate. Changing weights will immediately change
11+
behavior of visitors who do not have an explicit assignment, even if they've
12+
already experienced a specific variant of this split. This is usually
13+
desirable for slow-rolling features.
14+
</p>
15+
<% else %>
16+
<p>
17+
This split is an experiment. Changing weights will have no immediate effect
18+
on the behavior of visitors who have already experienced a variant of this
19+
split. Experiments rarely benefit from changing weightings unless you are
20+
performing analysis over a date range.
21+
</p>
22+
<% end %>
23+
824
<%= simple_form_for(@split_creation, url: admin_split_split_config_path) do |f| %>
925
<% f.simple_fields_for :weighting_registry do |ff| %>
1026

@@ -18,6 +34,6 @@
1834
<%= ff.input variant, as: :percent, input_html: { value: weight, class: "weight-input" } %>
1935
<% end %>
2036

21-
<%= render "shared/form_footer", f: f, submit_text: "Edit", submit_disable_with_text: "Changing..." %>
37+
<%= render "shared/form_footer", f: f, submit_text: "Save", submit_disable_with_text: "Changing..." %>
2238
<% end %>
2339
<% end %>

app/views/admin/splits/_details.html.erb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
<tr class="population-row">
1010
<td>Population Size</td>
1111
<td>
12-
<span class='population'><%= @split.assignments.count %></span>
12+
<span class='population'>
13+
<%= @split.assignments.count %><% if @split.feature_gate? %>* [<a href="#gate_population">feature gate</a>]<% end %>
14+
</span>
1315
</td>
1416
<td>
1517
<span><%= link_to "Edit", new_admin_split_bulk_assignment_path(@split), class: 'upload-new-assignments-link' %></span>
@@ -33,5 +35,11 @@
3335
<td>&nbsp;</td>
3436
</tr>
3537
</table>
38+
<% if @split.feature_gate? %>
39+
<p>
40+
<a id="gate_population"></a>
41+
* Feature gates no longer track assignment events and population reflects only visitors assigned to specific variants via the chrome extension or admin tool.
42+
</p>
43+
<% end %>
3644
</div>
3745
</article>

app/views/admin/splits/_variants.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<article class="InfoCard sc-m-v--l">
22
<div class="InfoCard-titleContainer">
33
<h4>Variant Details</h4>
4-
<%= link_to "Edit", new_admin_split_split_config_path(split), class: 'change-weights-link' %>
4+
<%= link_to "Change Weights", new_admin_split_split_config_path(split), class: 'change-weights-link' %>
55
</div>
66
<hr class="InfoCard-divider">
77
<div class="InfoCard-description">

app/views/admin/variant_details/edit.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
method: :post,
2323
data: { confirm: "You're redistributing #{@variant_detail.variant} assignees to the other variants according to their weights. Do you wish to proceed?" } %>
2424
<% end %>
25-
<%= f.submit 'Continue', data: { disable_with: 'Updating variant...' }, class: 'u-button ft-confirmButton' %>
25+
<%= f.submit 'Save', data: { disable_with: 'Updating variant...' }, class: 'u-button ft-confirmButton' %>
2626
</div>
2727
</div>
2828
<% end %>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class AddSplitFeatureGate < ActiveRecord::Migration[5.0]
2+
def change
3+
add_column :splits, :feature_gate, :boolean, default: false, null: false
4+
execute "update splits set feature_gate = true where name like '%_enabled'"
5+
end
6+
end

db/schema.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema.define(version: 20170501180350) do
13+
ActiveRecord::Schema.define(version: 20180412153251) do
1414

1515
# These are extensions that must be enabled in order to support this database
1616
enable_extension "plpgsql"
@@ -133,17 +133,18 @@
133133

134134
create_table "splits", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t|
135135
t.string "name"
136-
t.uuid "owner_app_id", null: false
136+
t.uuid "owner_app_id", null: false
137137
t.datetime "created_at"
138138
t.datetime "updated_at"
139139
t.datetime "finished_at"
140-
t.json "registry", null: false
140+
t.json "registry", null: false
141141
t.text "hypothesis"
142142
t.text "assignment_criteria"
143143
t.text "description"
144144
t.string "owner"
145145
t.string "location"
146146
t.integer "platform"
147+
t.boolean "feature_gate", default: false, null: false
147148
t.index ["name"], name: "index_splits_on_name", unique: true, using: :btree
148149
t.index ["owner_app_id"], name: "index_splits_on_owner_app_id", using: :btree
149150
end

spec/models/deterministic_assignment_creation_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@
7070
expect(ArbitraryAssignmentCreation).to have_received(:create!)
7171
.with(hash_including(context: "the_context"))
7272
end
73+
74+
context "with a feature gate" do
75+
let!(:split) do
76+
FactoryBot.create(:split, name: "split", registry: { variant1: 61, variant2: 1, variant3: 38 }, feature_gate: true)
77+
end
78+
79+
it "skips creating for feature gates" do
80+
subject.save!
81+
82+
expect(ArbitraryAssignmentCreation).not_to have_received(:create!)
83+
end
84+
end
7385
end
7486

7587
describe "#variant_calculator" do

0 commit comments

Comments
 (0)