test_track_rails_client 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/LICENSE +19 -0
  2. data/README.md +246 -0
  3. data/Rakefile +30 -0
  4. data/app/assets/javascripts/testTrack.bundle.min.js +1 -0
  5. data/app/controllers/concerns/test_track/controller.rb +26 -0
  6. data/app/controllers/tt/api/v1/application_controller.rb +9 -0
  7. data/app/controllers/tt/api/v1/assignments_controller.rb +5 -0
  8. data/app/controllers/tt/api/v1/identifier_types_controller.rb +5 -0
  9. data/app/controllers/tt/api/v1/identifier_visitors_controller.rb +5 -0
  10. data/app/controllers/tt/api/v1/identifiers_controller.rb +5 -0
  11. data/app/controllers/tt/api/v1/resets_controller.rb +12 -0
  12. data/app/controllers/tt/api/v1/split_configs_controller.rb +9 -0
  13. data/app/controllers/tt/api/v1/split_registries_controller.rb +5 -0
  14. data/app/controllers/tt/api/v1/visitors_controller.rb +5 -0
  15. data/app/helpers/test_track/application_helper.rb +6 -0
  16. data/app/models/concerns/test_track/identity.rb +53 -0
  17. data/app/models/concerns/test_track/remote_model.rb +14 -0
  18. data/app/models/concerns/test_track/required_options.rb +11 -0
  19. data/app/models/test_track/ab_configuration.rb +53 -0
  20. data/app/models/test_track/analytics/mixpanel_client.rb +25 -0
  21. data/app/models/test_track/analytics/safe_wrapper.rb +43 -0
  22. data/app/models/test_track/assignment.rb +29 -0
  23. data/app/models/test_track/config_updater.rb +99 -0
  24. data/app/models/test_track/create_alias_job.rb +18 -0
  25. data/app/models/test_track/fake/split_registry.rb +36 -0
  26. data/app/models/test_track/fake/visitor.rb +41 -0
  27. data/app/models/test_track/fake_server.rb +24 -0
  28. data/app/models/test_track/identity_session_discriminator.rb +34 -0
  29. data/app/models/test_track/notify_assignment_job.rb +31 -0
  30. data/app/models/test_track/offline_session.rb +46 -0
  31. data/app/models/test_track/remote/assignment.rb +20 -0
  32. data/app/models/test_track/remote/assignment_event.rb +15 -0
  33. data/app/models/test_track/remote/fake_server.rb +8 -0
  34. data/app/models/test_track/remote/identifier.rb +26 -0
  35. data/app/models/test_track/remote/identifier_type.rb +13 -0
  36. data/app/models/test_track/remote/split_config.rb +13 -0
  37. data/app/models/test_track/remote/split_registry.rb +36 -0
  38. data/app/models/test_track/remote/visitor.rb +29 -0
  39. data/app/models/test_track/session.rb +167 -0
  40. data/app/models/test_track/unsynced_assignments_notifier.rb +36 -0
  41. data/app/models/test_track/variant_calculator.rb +48 -0
  42. data/app/models/test_track/vary_dsl.rb +88 -0
  43. data/app/models/test_track/visitor.rb +129 -0
  44. data/app/models/test_track/visitor_dsl.rb +11 -0
  45. data/app/views/tt/api/v1/identifier_visitors/show.json.jbuilder +1 -0
  46. data/app/views/tt/api/v1/identifiers/create.json.jbuilder +3 -0
  47. data/app/views/tt/api/v1/split_registries/show.json.jbuilder +3 -0
  48. data/app/views/tt/api/v1/visitors/_show.json.jbuilder +2 -0
  49. data/app/views/tt/api/v1/visitors/show.json.jbuilder +1 -0
  50. data/config/initializers/test_track_api.rb +15 -0
  51. data/config/routes.rb +28 -0
  52. data/lib/generators/test_track/migration_generator.rb +39 -0
  53. data/lib/tasks/pull_in_js_client.rake +7 -0
  54. data/lib/tasks/test_track_rails_client_tasks.rake +15 -0
  55. data/lib/tasks/vendor_deps.rake +36 -0
  56. data/lib/test_track.rb +64 -0
  57. data/lib/test_track_rails_client.rb +5 -0
  58. data/lib/test_track_rails_client/assignment_helper.rb +19 -0
  59. data/lib/test_track_rails_client/engine.rb +14 -0
  60. data/lib/test_track_rails_client/rspec_helpers.rb +5 -0
  61. data/lib/test_track_rails_client/version.rb +3 -0
  62. metadata +345 -0
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2016 Betterment
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,246 @@
1
+ # test_track_rails_client
2
+ Rails client for the test_track service
3
+
4
+ [![Build Status](https://travis-ci.com/Betterment/test_track_rails_client.svg?token=6b6DErRMUHX47kEoBZ3t&branch=master)](https://travis-ci.com/Betterment/test_track_rails_client)
5
+
6
+ ## Installation
7
+
8
+ Install the gem:
9
+
10
+ ```ruby
11
+ # Gemfile
12
+
13
+ gem 'test_track_rails_client', git: 'https://[GITHUB AUTH CREDS GO HERE]@github.com/Betterment/test_track_rails_client'
14
+ ```
15
+
16
+ In every environment (local included) cut an App record via the TestTrack rails console:
17
+
18
+ ```ruby
19
+ > App.create!(name: "[myapp]", auth_secret: SecureRandom.urlsafe_base64(32)).auth_secret
20
+ => "[your new app password]"
21
+ ```
22
+
23
+ Set up ENV vars in every environment:
24
+
25
+ * `MIXPANEL_TOKEN` - By default, TestTrack reports to Mixpanel. If you're using a [custom analytics provider](#custom-analytics) you can omit this.
26
+ * `TEST_TRACK_API_URL` - Set this to the URL of your TestTrack instance with your app credentials, e.g. `http://[myapp]:[your new app password]@testtrack.dev/`
27
+
28
+ Add find or create to `seeds.rb` in the test_track app
29
+
30
+ ```ruby
31
+ App.find_or_create_by!(name: "[myapp]", auth_secret: "[your LOCAL app password]")
32
+ ```
33
+
34
+ Mix `TestTrack::Controller` into any controllers needing access to TestTrack:
35
+
36
+ ```ruby
37
+ class MyController < ApplicationController
38
+ include TestTrack::Controller
39
+ end
40
+ ```
41
+
42
+ And, if you'd like to be able to use the TestTrack Chrome Extension, include `testTrack.bundle.min` in your `application.js` file after your reference to jQuery:
43
+
44
+ ```js
45
+ //= require jquery
46
+ //= require testTrack.bundle.min
47
+ ```
48
+
49
+ # Concepts
50
+
51
+ * **Visitor** - a person using your application. `test_track_rails_client` manages visitors for you and ensures that `test_track_visitor` is available in any controller that mixes in `TestTrack::Controller`
52
+ * **Split** - A feature for which TestTrack will be assigning different behavior for different visitors. Split names must be strings and should be expressed in `snake_case`. E.g. `homepage_redesign_late_2015` or `signup_button_color`.
53
+ * **Variant** - one the values that a given visitor will be assigned for a split, e.g. `true` or `false` for a classic A/B test or e.g. `red`, `blue`, and `green` for a multi-way split. Variants may be strings or booleans, and they should be expressed in `snake_case`.
54
+ * **Weighting** - 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.
55
+ * **IdentifierType** - 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`.
56
+
57
+ ## Configuring the TestTrack server from your app
58
+
59
+ TestTrack leans on ActiveRecord migrations to run idempotent configuration changes. There are two things an app can configure about TestTrack. It can define `identifier_type`s and configure `split`s.
60
+
61
+ ### Defining identifier types:
62
+
63
+ ```ruby
64
+ class AddIdentifierType < ActiveRecord::Migration
65
+ def change
66
+ TestTrack.update_config do |c|
67
+ c.identifier_type :myapp_user_id
68
+ end
69
+ end
70
+ end
71
+ ```
72
+
73
+ ### Configuring splits
74
+
75
+ Splits can be created or reconfigured using the config DSL. Variants can be added to an existing split, and weightings can be reassigned, but note that once a variant is added to a split, it doesn't ever completely disappear. Attempts to remove it will simply result in it having a `0` weighting moving forward. People who were already assigned to a given variant will continue to see the experience associated with that split.
76
+
77
+ ```ruby
78
+ class ConfigureMySplit < ActiveRecord::Migration
79
+ def change
80
+ TestTrack.update_config do |c|
81
+ c.split :signup_button_color, red: 34, green: 33, blue: 33, indigo: 0
82
+ end
83
+ end
84
+ end
85
+ ```
86
+
87
+ ### Cleaning Up Old Splits
88
+
89
+ In order to avoid clutter in the Test Track server's split registry as well as the Test Track Chrome Extension, a split can be dropped. This will remove the split from the split registry, dropping it from Test Track clients' perspectives. Thus, like a non-additive DDL migration (e.g. `DROP COLUMN`, `RENAME COLUMN`), it should be released in a subsequent deployment, after all code paths referencing the split have been removed. Otherwise those code paths will raise and potentially break the user experience.
90
+
91
+ ```ruby
92
+ class RemoveMyOldSplit < ActiveRecord::Migration
93
+ def change
94
+ TestTrack.update_config do |c|
95
+ c.drop_split :signup_button_color
96
+ end
97
+ end
98
+ end
99
+ ```
100
+
101
+ _Note: `drop_split` (a.k.a. `finish_split`) does not physically delete split data from mixpanel or Test Track's database._
102
+
103
+ ## Varying app behavior based on assigned variant
104
+
105
+ ### Varying app behavior in a web context
106
+
107
+ The `test_track_visitor`, which is accessible from all controllers and views that mix in `TestTrack::Controller` provides a `vary` DSL.
108
+
109
+ You must provide at least one call to `when` and only one call to `default`. `when` can take multiple variant names if you'd like to map multiple variants to one user experience.
110
+
111
+ If the user is assigned to a variant that is not represented in your vary configuration, Test Track will execute the `default` handler and re-assign the user to the variant specified in the `default` call. You should not rely on this defaulting behavior, it is merely provided to ensure we don't break the customer experience. You should instead make sure that you represent all variants of the split and if variants are added to the split on the backend, update your code to reflect the new variants. Because `default` re-assigns the user to the default variant, no data will be recorded for the variant that is not represented. This will impede our abiltiy to collect meaningful data for the split.
112
+
113
+ You must also provide a `context` at each `vary` and `ab` call. Context is a string value which represents the high-level user action in which the assignment takes place. For example, if a split can be assigned when viewing the home page and when going through sign up, the assignment calls in each of those paths should tagged with 'home_page' and 'signup' respectively. This will allow the test results to be filtered by what the user was doing when the split was assigned.
114
+
115
+ ```ruby
116
+ test_track_visitor.vary :name_of_split, context: 'home_page' do |v|
117
+ v.when :variant_1, :variant_2 do
118
+ # Do something
119
+ end
120
+ v.when :variant_3 do
121
+ # Do another thing
122
+ end
123
+ v.default :variant_4 do
124
+ # Do something else
125
+ end
126
+ end
127
+ ```
128
+
129
+ The `test_track_visitor`'s `ab` method provides a convenient way to do two-way splits. The optional second argument is used to tell `ab` which variant is the "true" variant. If no second argument is provided, the "true" variant is assumed to be `true`, which is convient for splits that have variants of `true` and `false`. `ab` can be easily used in an if statement.
130
+
131
+ ```ruby
132
+ # "button_color" split with "blue" and "red" variants
133
+ if test_track_visitor.ab :button_color, true_variant: :blue, context: 'signup'
134
+ # Color the button blue
135
+ else
136
+ # Color the button red
137
+ end
138
+ ```
139
+
140
+ ```ruby
141
+ # "dark_deployed_feature" split with "true" and "false" variants
142
+ if test_track_visitor.ab :dark_deployed_feature, context: 'signup'
143
+ # Show the dark deployed feature
144
+ end
145
+ ```
146
+
147
+ ### Varying app behavior in an offline context
148
+
149
+ The `OfflineSession` class can be used to load a test track visitor when there is no access to browser cookies. It is perfect for use in a process being run from either a job queue or a scheduler. The visitor object that is yielded to the block is the same as the visitor in a controller context; it has both the `vary` and `ab` methods.
150
+
151
+ ```ruby
152
+ OfflineSession.with_visitor_for(:myapp_user_id, 1234) do |test_track_visitor|
153
+ test_track_visitor.vary :name_of_split, context: 'background_job' do |v|
154
+ v.when :variant_1, :variant_2 do
155
+ # Do something
156
+ end
157
+ v.when :variant_3 do
158
+ # Do another thing
159
+ end
160
+ v.default :variant_4 do
161
+ # Do something else
162
+ end
163
+ end
164
+ end
165
+ ```
166
+
167
+ ### Varying app behavior from within a model
168
+
169
+ The `TestTrack::Identity` concern can be included in a model and it will add two methods to the model: `test_track_vary` and `test_track_ab`. Behind the scenes, these methods check to see if they are being used within a web context of a controller that includes `TestTrack::Controller` or not. If called in a web context they will use the `test_track_visitor` that the controller has and participate in the existing session, if not, they will standup an `OfflineSession`.
170
+
171
+ Because these methods may need to stand up an `OfflineSession` the consuming model needs to provide both the identifier type and which column should be used as the identifier value via the `test_track_identifier` method so that the `OfflineSession` can grab the correct visitor.
172
+
173
+ ```ruby
174
+ class User
175
+ include TestTrack::Identity
176
+
177
+ test_track_identifier :myapp_user_id, :id # `id` is a column on User model which is what we're using as the identifier value in this example.
178
+ end
179
+ ```
180
+
181
+ N.B. If you call `test_track_vary` and `test_track_ab` on a model in a web context, but that model is not the currently authenticated model, an `OfflineSession` will be created instead of participating in the existing session.
182
+
183
+ ## Tracking visitor logins
184
+
185
+ The `test_track_visitor.log_in!` is used to ensure a consistent experience across devices. For instance, when a user logs in to your app on their mobile device we can log in to Test Track in order to grab their existing split assignments instead of treating them like a new visitor.
186
+
187
+ ```ruby
188
+ test_track_visitor.log_in!(:myapp_user_id, 1234)
189
+ ```
190
+
191
+ When we call `log_in!` we merge assignments between the visitor prior to login (i.e. the current visitor) and the visitor we retrieve from the test track server (i.e. the canonical visitor). This means that any assignments for splits that the current visitor has which the canonical visitor does not have are copied from the prior visitor to the canonical visitor. While this merging behavior is preferrable there may be a case where we do not want to merge. In that case, we can pass the `forget_current_visitor` option to forget the current visitor before retrieving the canonical visitor.
192
+
193
+ ```ruby
194
+ test_track_visitor.log_in!(:myapp_user_id, 1234, forget_current_visitor: true)
195
+ ```
196
+
197
+ ## Tracking signups
198
+
199
+ The `test_track_visitor.sign_up!` method tells TestTrack when a new identifier has been created and assigned to a visitor. It works a lot like the `log_in!` method, but should only be used once per customer signup.
200
+
201
+ ```ruby
202
+ test_track_visitor.sign_up!(:myapp_user_id, 2345)
203
+ ```
204
+
205
+ ## Testing splits
206
+
207
+ Add this line to your `rails_helper.rb`:
208
+
209
+ ```ruby
210
+ # spec/rails_helper.rb
211
+ require 'test_track_rails_client/rspec_helpers'
212
+ ```
213
+
214
+ Force TestTrack to return a specific set of splits during a spec:
215
+
216
+ ```ruby
217
+ it "shows the right info" do
218
+ stub_test_track_assignments(button_color: :red)
219
+ # All `vary` calls for `button_color` will run the `red` codepath until the mocks are reset (after each `it` block)
220
+ end
221
+ ```
222
+
223
+ ## Custom Analytics
224
+ By default, TestTrack will use Mixpanel as an analytics backend. If you wish to use another provider, you can set the `analytics` attribute on `TestTrack` with your custom client. You should do this in a Rails initializer.
225
+
226
+ ```ruby
227
+ # config/initializers/test_track.rb
228
+ TestTrack.analytics = MyCustomAnalyticsClient.new
229
+ ```
230
+
231
+ Your client must implement the following methods:
232
+
233
+ ```ruby
234
+ # Called when a new Split has been Assigned
235
+ #
236
+ # @param visitor_id [String] TestTrack's unique visitor identification key
237
+ # @param assignment [TestTrack::Assignment] The assignment model itself
238
+ # @param properties [String] Any additional properties, currently only utilized for Mixpanel's UniqueId
239
+ def track_assignment(visitor_id, assignment, properties)
240
+
241
+ # Called after TestTrack.sign_up!
242
+ #
243
+ # @param visitor_id [String] TestTrack's unique visitor identification key
244
+ # @param existing_id [String] Any existing identifier for the visitor(defaults to Mixpanel's UniqueId)
245
+ def alias(visitor_id, existing_id)
246
+ ```
@@ -0,0 +1,30 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'TestTrackRailsClient'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require 'rspec/core'
23
+ require 'rspec/core/rake_task'
24
+ require 'rubocop/rake_task'
25
+
26
+ RSpec::Core::RakeTask.new
27
+ RuboCop::RakeTask.new
28
+
29
+ task(:default).clear
30
+ task default: [:rubocop, :spec]
@@ -0,0 +1 @@
1
+ !function(a){var b="object"==typeof exports&&exports,c="object"==typeof module&&module&&module.exports==b&&module,d="object"==typeof global&&global;d.global!==d&&d.window!==d||(a=d);var e=function(a){this.message=a};e.prototype=new Error,e.prototype.name="InvalidCharacterError";var f=function(a){throw new e(a)},g="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",h=/[\t\n\f\r ]/g,i=function(a){a=String(a).replace(h,"");var b=a.length;b%4==0&&(a=a.replace(/==?$/,""),b=a.length),(b%4==1||/[^+a-zA-Z0-9/]/.test(a))&&f("Invalid character: the string to be decoded is not correctly encoded.");for(var c,d,e=0,i="",j=-1;++j<b;)d=g.indexOf(a.charAt(j)),c=e%4?64*c+d:d,e++%4&&(i+=String.fromCharCode(255&c>>(-2*e&6)));return i},j=function(a){a=String(a),/[^\0-\xFF]/.test(a)&&f("The string to be encoded contains characters outside of the Latin1 range.");for(var b,c,d,e,h=a.length%3,i="",j=-1,k=a.length-h;++j<k;)b=a.charCodeAt(j)<<16,c=a.charCodeAt(++j)<<8,d=a.charCodeAt(++j),e=b+c+d,i+=g.charAt(e>>18&63)+g.charAt(e>>12&63)+g.charAt(e>>6&63)+g.charAt(63&e);return 2==h?(b=a.charCodeAt(j)<<8,c=a.charCodeAt(++j),e=b+c,i+=g.charAt(e>>10)+g.charAt(e>>4&63)+g.charAt(e<<2&63)+"="):1==h&&(e=a.charCodeAt(j),i+=g.charAt(e>>2)+g.charAt(e<<4&63)+"=="),i},k={encode:j,decode:i,version:"0.1.0"};if("function"==typeof define&&"object"==typeof define.amd&&define.amd)define(function(){return k});else if(b&&!b.nodeType)if(c)c.exports=k;else for(var l in k)k.hasOwnProperty(l)&&(b[l]=k[l]);else a.base64=k}(this),function(a){"use strict";function b(a,b){var c=(65535&a)+(65535&b),d=(a>>16)+(b>>16)+(c>>16);return d<<16|65535&c}function c(a,b){return a<<b|a>>>32-b}function d(a,d,e,f,g,h){return b(c(b(b(d,a),b(f,h)),g),e)}function e(a,b,c,e,f,g,h){return d(b&c|~b&e,a,b,f,g,h)}function f(a,b,c,e,f,g,h){return d(b&e|c&~e,a,b,f,g,h)}function g(a,b,c,e,f,g,h){return d(b^c^e,a,b,f,g,h)}function h(a,b,c,e,f,g,h){return d(c^(b|~e),a,b,f,g,h)}function i(a,c){a[c>>5]|=128<<c%32,a[(c+64>>>9<<4)+14]=c;var d,i,j,k,l,m=1732584193,n=-271733879,o=-1732584194,p=271733878;for(d=0;d<a.length;d+=16)i=m,j=n,k=o,l=p,m=e(m,n,o,p,a[d],7,-680876936),p=e(p,m,n,o,a[d+1],12,-389564586),o=e(o,p,m,n,a[d+2],17,606105819),n=e(n,o,p,m,a[d+3],22,-1044525330),m=e(m,n,o,p,a[d+4],7,-176418897),p=e(p,m,n,o,a[d+5],12,1200080426),o=e(o,p,m,n,a[d+6],17,-1473231341),n=e(n,o,p,m,a[d+7],22,-45705983),m=e(m,n,o,p,a[d+8],7,1770035416),p=e(p,m,n,o,a[d+9],12,-1958414417),o=e(o,p,m,n,a[d+10],17,-42063),n=e(n,o,p,m,a[d+11],22,-1990404162),m=e(m,n,o,p,a[d+12],7,1804603682),p=e(p,m,n,o,a[d+13],12,-40341101),o=e(o,p,m,n,a[d+14],17,-1502002290),n=e(n,o,p,m,a[d+15],22,1236535329),m=f(m,n,o,p,a[d+1],5,-165796510),p=f(p,m,n,o,a[d+6],9,-1069501632),o=f(o,p,m,n,a[d+11],14,643717713),n=f(n,o,p,m,a[d],20,-373897302),m=f(m,n,o,p,a[d+5],5,-701558691),p=f(p,m,n,o,a[d+10],9,38016083),o=f(o,p,m,n,a[d+15],14,-660478335),n=f(n,o,p,m,a[d+4],20,-405537848),m=f(m,n,o,p,a[d+9],5,568446438),p=f(p,m,n,o,a[d+14],9,-1019803690),o=f(o,p,m,n,a[d+3],14,-187363961),n=f(n,o,p,m,a[d+8],20,1163531501),m=f(m,n,o,p,a[d+13],5,-1444681467),p=f(p,m,n,o,a[d+2],9,-51403784),o=f(o,p,m,n,a[d+7],14,1735328473),n=f(n,o,p,m,a[d+12],20,-1926607734),m=g(m,n,o,p,a[d+5],4,-378558),p=g(p,m,n,o,a[d+8],11,-2022574463),o=g(o,p,m,n,a[d+11],16,1839030562),n=g(n,o,p,m,a[d+14],23,-35309556),m=g(m,n,o,p,a[d+1],4,-1530992060),p=g(p,m,n,o,a[d+4],11,1272893353),o=g(o,p,m,n,a[d+7],16,-155497632),n=g(n,o,p,m,a[d+10],23,-1094730640),m=g(m,n,o,p,a[d+13],4,681279174),p=g(p,m,n,o,a[d],11,-358537222),o=g(o,p,m,n,a[d+3],16,-722521979),n=g(n,o,p,m,a[d+6],23,76029189),m=g(m,n,o,p,a[d+9],4,-640364487),p=g(p,m,n,o,a[d+12],11,-421815835),o=g(o,p,m,n,a[d+15],16,530742520),n=g(n,o,p,m,a[d+2],23,-995338651),m=h(m,n,o,p,a[d],6,-198630844),p=h(p,m,n,o,a[d+7],10,1126891415),o=h(o,p,m,n,a[d+14],15,-1416354905),n=h(n,o,p,m,a[d+5],21,-57434055),m=h(m,n,o,p,a[d+12],6,1700485571),p=h(p,m,n,o,a[d+3],10,-1894986606),o=h(o,p,m,n,a[d+10],15,-1051523),n=h(n,o,p,m,a[d+1],21,-2054922799),m=h(m,n,o,p,a[d+8],6,1873313359),p=h(p,m,n,o,a[d+15],10,-30611744),o=h(o,p,m,n,a[d+6],15,-1560198380),n=h(n,o,p,m,a[d+13],21,1309151649),m=h(m,n,o,p,a[d+4],6,-145523070),p=h(p,m,n,o,a[d+11],10,-1120210379),o=h(o,p,m,n,a[d+2],15,718787259),n=h(n,o,p,m,a[d+9],21,-343485551),m=b(m,i),n=b(n,j),o=b(o,k),p=b(p,l);return[m,n,o,p]}function j(a){var b,c="";for(b=0;b<32*a.length;b+=8)c+=String.fromCharCode(a[b>>5]>>>b%32&255);return c}function k(a){var b,c=[];for(c[(a.length>>2)-1]=void 0,b=0;b<c.length;b+=1)c[b]=0;for(b=0;b<8*a.length;b+=8)c[b>>5]|=(255&a.charCodeAt(b/8))<<b%32;return c}function l(a){return j(i(k(a),8*a.length))}function m(a,b){var c,d,e=k(a),f=[],g=[];for(f[15]=g[15]=void 0,e.length>16&&(e=i(e,8*a.length)),c=0;c<16;c+=1)f[c]=909522486^e[c],g[c]=1549556828^e[c];return d=i(f.concat(k(b)),512+8*b.length),j(i(g.concat(d),640))}function n(a){var b,c,d="0123456789abcdef",e="";for(c=0;c<a.length;c+=1)b=a.charCodeAt(c),e+=d.charAt(b>>>4&15)+d.charAt(15&b);return e}function o(a){return unescape(encodeURIComponent(a))}function p(a){return l(o(a))}function q(a){return n(p(a))}function r(a,b){return m(o(a),o(b))}function s(a,b){return n(r(a,b))}function t(a,b,c){return b?c?r(b,a):s(b,a):c?p(a):q(a)}"function"==typeof define&&define.amd?define(function(){return t}):a.md5=t}(this),function(){function a(a,b,c){var d=b&&c||0,e=0;for(b=b||[],a.toLowerCase().replace(/[0-9a-f]{2}/g,function(a){e<16&&(b[d+e++]=m[a])});e<16;)b[d+e++]=0;return b}function b(a,b){var c=b||0,d=l;return d[a[c++]]+d[a[c++]]+d[a[c++]]+d[a[c++]]+"-"+d[a[c++]]+d[a[c++]]+"-"+d[a[c++]]+d[a[c++]]+"-"+d[a[c++]]+d[a[c++]]+"-"+d[a[c++]]+d[a[c++]]+d[a[c++]]+d[a[c++]]+d[a[c++]]+d[a[c++]]}function c(a,c,d){var e=c&&d||0,f=c||[];a=a||{};var g=null!=a.clockseq?a.clockseq:q,h=null!=a.msecs?a.msecs:(new Date).getTime(),i=null!=a.nsecs?a.nsecs:s+1,j=h-r+(i-s)/1e4;if(j<0&&null==a.clockseq&&(g=g+1&16383),(j<0||h>r)&&null==a.nsecs&&(i=0),i>=1e4)throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");r=h,s=i,q=g,h+=122192928e5;var k=(1e4*(268435455&h)+i)%4294967296;f[e++]=k>>>24&255,f[e++]=k>>>16&255,f[e++]=k>>>8&255,f[e++]=255&k;var l=h/4294967296*1e4&268435455;f[e++]=l>>>8&255,f[e++]=255&l,f[e++]=l>>>24&15|16,f[e++]=l>>>16&255,f[e++]=g>>>8|128,f[e++]=255&g;for(var m=a.node||p,n=0;n<6;n++)f[e+n]=m[n];return c?c:b(f)}function d(a,c,d){var f=c&&d||0;"string"==typeof a&&(c="binary"==a?new k(16):null,a=null),a=a||{};var g=a.random||(a.rng||e)();if(g[6]=15&g[6]|64,g[8]=63&g[8]|128,c)for(var h=0;h<16;h++)c[f+h]=g[h];return c||b(g)}var e,f=this;if("function"==typeof f.require)try{var g=f.require("crypto").randomBytes;e=g&&function(){return g(16)}}catch(h){}if(!e&&f.crypto&&crypto.getRandomValues){var i=new Uint8Array(16);e=function(){return crypto.getRandomValues(i),i}}if(!e){var j=new Array(16);e=function(){for(var a,b=0;b<16;b++)0===(3&b)&&(a=4294967296*Math.random()),j[b]=a>>>((3&b)<<3)&255;return j}}for(var k="function"==typeof f.Buffer?f.Buffer:Array,l=[],m={},n=0;n<256;n++)l[n]=(n+256).toString(16).substr(1),m[l[n]]=n;var o=e(),p=[1|o[0],o[1],o[2],o[3],o[4],o[5]],q=16383&(o[6]<<8|o[7]),r=0,s=0,t=d;if(t.v1=c,t.v4=d,t.parse=a,t.unparse=b,t.BufferClass=k,"function"==typeof define&&define.amd)define(function(){return t});else if("undefined"!=typeof module&&module.exports)module.exports=t;else{var u=f.uuid;t.noConflict=function(){return f.uuid=u,t},f.uuid=t}}.call(this),function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;n<o;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0!==a.cookie(b)&&(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}}),function(a,b){"function"==typeof define&&define.amd?define(["node-uuid","blueimp-md5","jquery","base-64","jquery.cookie"],b):a.TestTrack=b(a.uuid,a.md5,a.jQuery,a.base64)}(this,function(a,b,c,d){"use strict";if("undefined"==typeof a)throw new Error('TestTrack depends on node-uuid. Make sure you are including "bower_components/node-uuid/uuid.js"');if("undefined"==typeof b)throw new Error('TestTrack depends on blueimp-md5. Make sure you are including "bower_components/blueimp-md5/js/md5.js"');if("undefined"==typeof c)throw new Error('TestTrack depends on jquery. You can use your own copy of jquery or the one in "bower_components/jquery/dist/jquery.js"');if("function"!=typeof c.cookie)throw new Error("TestTrack depends on jquery.cookie. You can user your own copy of jquery.cooke or the one in bower_components/jquery.cookie/jquery.cookie.js");if("undefined"==typeof d)throw new Error('TestTrack depends on base-64. Make sure you are including "bower_components/base-64/base64.js"');!function(){Function.prototype.bind||(Function.prototype.bind=function(a){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var b=Array.prototype.slice.call(arguments,1),c=this,d=function(){},e=function(){return c.apply(this instanceof d?this:a,b.concat(Array.prototype.slice.call(arguments)))};return this.prototype&&(d.prototype=this.prototype),e.prototype=new d,e})}();var e=function(){var a=function(){};return a.prototype.track_assignment=function(a,b,c){var d={TTVisitorID:a,SplitName:b.getSplitName(),SplitVariant:b.getVariant(),SplitContext:b.getContext()};window.mixpanel&&window.mixpanel.track("SplitAssigned",d,c)},a.prototype.identify=function(a){window.mixpanel&&window.mixpanel.identify(a)},a.prototype.alias=function(a){window.mixpanel&&window.mixpanel.alias(a)},a}(),f=function(){var a=function(a){if(!a.splitName)throw new Error("must provide splitName");if(!a.hasOwnProperty("variant"))throw new Error("must provide variant");if(!a.hasOwnProperty("isUnsynced"))throw new Error("must provide isUnsynced");this._splitName=a.splitName,this._variant=a.variant,this._context=a.context,this._isUnsynced=a.isUnsynced};return a.fromJsonArray=function(a){for(var b=[],c=0;c<a.length;c++)b.push(new f({splitName:a[c].split_name,variant:a[c].variant,context:a[c].context,isUnsynced:a[c].unsynced}));return b},a.prototype.getSplitName=function(){return this._splitName},a.prototype.getVariant=function(){return this._variant},a.prototype.setVariant=function(a){this._variant=a},a.prototype.getContext=function(){return this._context},a.prototype.setContext=function(a){this._context=a},a.prototype.isUnsynced=function(){return this._isUnsynced},a.prototype.setUnsynced=function(a){this._isUnsynced=a},a}(),g=function(){var a=function(){};return a.prototype.getConfig=function(){return"function"==typeof window.atob?JSON.parse(window.atob(window.TT)):JSON.parse(d.decode(window.TT))},a}(),h=function(){var a,b,c=function(){if(!a){var b=new g;a=b.getConfig()}return a};return{getUrl:function(){return c().url},getCookieDomain:function(){return c().cookieDomain},getSplitRegistry:function(){return c().registry},getAssignments:function(){var a=c().assignments;if(!a)return null;if(!b){b=[];for(var d in a)b.push(new f({splitName:d,variant:a[d],isUnsynced:!1}))}return b}}}(),i=function(){var a=function(a){if(this.visitor=a.visitor,this.splitName=a.splitName,!this.visitor)throw new Error("must provide visitor");if(!this.splitName)throw new Error("must provide splitName")};return a.prototype.getVariant=function(){if(!h.getSplitRegistry())return null;for(var a=0,b=this.getAssignmentBucket(),c=this.getWeighting(),d=this.getSortedVariants(),e=0;e<d.length;e++){var f=d[e];if(a+=c[f],a>b)return f}throw new Error("Assignment bucket out of range. "+b+" unmatched in "+this.splitName+": "+JSON.stringify(c))},a.prototype.getSplitVisitorHash=function(){return b(this.splitName+this.visitor.getId())},a.prototype.getHashFixnum=function(){return parseInt(this.getSplitVisitorHash().substr(0,8),16)},a.prototype.getAssignmentBucket=function(){return this.getHashFixnum()%100},a.prototype.getSortedVariants=function(){return this.getVariants().sort()},a.prototype.getVariants=function(){return Object.getOwnPropertyNames(this.getWeighting())},a.prototype.getWeighting=function(){var a=h.getSplitRegistry()[this.splitName];if(!a){var b='Unknown split: "'+this.splitName+'"';throw this.visitor.logError(b),new Error(b)}return a},a}(),j=function(){var a=function(a){if(a=a||{},this._visitor=a.visitor,this._assignment=a.assignment,!this._visitor)throw new Error("must provide visitor");if(!this._assignment)throw new Error("must provide assignment")};return a.prototype.send=function(){this.persistAssignment(),this._visitor.analytics.track_assignment(this._visitor.getId(),this._assignment,function(a){this.persistAssignment(a?"success":"failure")}.bind(this))},a.prototype.persistAssignment=function(a){return c.ajax(h.getUrl()+"/api/v1/assignment",{method:"POST",dataType:"json",crossDomain:!0,data:{visitor_id:this._visitor.getId(),split_name:this._assignment.getSplitName(),variant:this._assignment.getVariant(),context:this._assignment.getContext(),mixpanel_result:a}}).fail(function(a,b,c){var d=a&&a.status,e=a&&a.responseText;this._visitor.logError("test_track persistAssignment error: "+[a,d,e,b,c].join(", "))}.bind(this))},a}(),k=function(){var b=function(a){if(a=a||{},this._id=a.id,this._assignments=a.assignments,this._ttOffline=a.ttOffline,!this._id)throw new Error("must provide id");if(!this._assignments)throw new Error("must provide assignments");this._errorLogger=function(a){window.console.error(a)},this.analytics=new e};return b.loadVisitor=function(b){var d=c.Deferred(),e=function(a){d.resolve(new k(a))};return b?h.getAssignments()?e({id:b,assignments:h.getAssignments(),ttOffline:!1}):c.ajax(h.getUrl()+"/api/v1/visitors/"+b,{method:"GET",timeout:5e3}).done(function(a){e({id:a.id,assignments:f.fromJsonArray(a.assignments),ttOffline:!1})}).fail(function(){e({id:b,assignments:[],ttOffline:!0})}):e({id:a.v4(),assignments:[],ttOffline:!1}),d.promise()},b.prototype.getId=function(){return this._id},b.prototype.getAssignmentRegistry=function(){if(!this._assignmentRegistry){for(var a={},b=0;b<this._assignments.length;b++){var c=this._assignments[b];a[c.getSplitName()]=c}this._assignmentRegistry=a}return this._assignmentRegistry},b.prototype.vary=function(a,b){if("object"!=typeof b.variants)throw new Error("must provide variants object to `vary` for "+a);if(!b.context)throw new Error("must provide context to `vary` for "+a);if(!b.defaultVariant&&b.defaultVariant!==!1)throw new Error("must provide defaultVariant to `vary` for "+a);var c=b.defaultVariant.toString(),d=b.variants,e=b.context;if(!d.hasOwnProperty(c))throw new Error("defaultVariant: "+c+" must be represented in variants object");var f=this._getAssignmentFor(a,e),g=new n({assignment:f,visitor:this});for(var h in d)d.hasOwnProperty(h)&&(h===c?g.default(h,d[h]):g.when(h,d[h]));g.run(),g.isDefaulted()&&(f.setVariant(g.getDefaultVariant()),f.setUnsynced(!0),f.setContext(e)),this.notifyUnsyncedAssignments()},b.prototype.ab=function(a,b){var c=new o({splitName:a,trueVariant:b.trueVariant,visitor:this}),d=c.getVariants(),e={};e[d.true]=function(){b.callback(!0)},e[d.false]=function(){b.callback(!1)},this.vary(a,{context:b.context,variants:e,defaultVariant:d.false})},b.prototype.setErrorLogger=function(a){if("function"!=typeof a)throw new Error("must provide function for errorLogger");this._errorLogger=a},b.prototype.logError=function(a){this._errorLogger.call(null,a)},b.prototype.linkIdentifier=function(a,b){var d=c.Deferred(),e=new m({visitorId:this.getId(),identifierType:a,value:b});return e.save().then(function(a){this._merge(a),this.notifyUnsyncedAssignments(),d.resolve()}.bind(this)),d.promise()},b.prototype.setAnalytics=function(a){if("object"!=typeof a)throw new Error("must provide object for setAnalytics");this.analytics=a},b.prototype.notifyUnsyncedAssignments=function(){for(var a=this._getUnsyncedAssignments(),b=0;b<a.length;b++)this._notify(a[b])},b.prototype._getUnsyncedAssignments=function(){var a=[],b=this.getAssignmentRegistry();return Object.keys(b).forEach(function(c){var d=b[c];d.isUnsynced()&&a.push(d)}),a},b.prototype._merge=function(a){var b=this.getAssignmentRegistry(),c=a.getAssignmentRegistry();this._id=a.getId();for(var d in c)c.hasOwnProperty(d)&&(b[d]=c[d])},b.prototype._getAssignmentFor=function(a,b){return this.getAssignmentRegistry()[a]||this._generateAssignmentFor(a,b)},b.prototype._generateAssignmentFor=function(a,b){var c=new i({visitor:this,splitName:a}).getVariant();c||(this._ttOffline=!0);var d=new f({splitName:a,variant:c,context:b,isUnsynced:!0});return this._assignments.push(d),this._assignmentRegistry=null,d},b.prototype._notify=function(a){try{if(this._ttOffline)return;var b=new j({visitor:this,assignment:a});b.send(),a.setUnsynced(!1)}catch(c){this.logError("test_track notify error: "+c)}},b}(),l=function(){var a="tt_visitor_id",b=function(){this._visitorDeferred=c.Deferred()};return b.prototype.initialize=function(b){var d=c.cookie(a);this._visitorDeferred.then(function(a){a.notifyUnsyncedAssignments()}),k.loadVisitor(d).then(function(a){b&&b.analytics&&a.setAnalytics(b.analytics),b&&b.errorLogger&&a.setErrorLogger(b.errorLogger),b&&"function"==typeof b.onVisitorLoaded&&b.onVisitorLoaded.call(null,a),this._visitorDeferred.resolve(a)}.bind(this)),this._setCookie()},b.prototype.vary=function(a,b){this._visitorDeferred.then(function(c){c.vary(a,b)})},b.prototype.ab=function(a,b){this._visitorDeferred.then(function(c){c.ab(a,b)})},b.prototype.logIn=function(a,b){var d=c.Deferred();return this._visitorDeferred.then(function(c){c.linkIdentifier(a,b).then(function(){this._setCookie(),c.analytics.identify(c.getId()),d.resolve()}.bind(this))}.bind(this)),d.promise()},b.prototype.signUp=function(a,b){var d=c.Deferred();return this._visitorDeferred.then(function(c){c.linkIdentifier(a,b).then(function(){this._setCookie(),c.analytics.alias(c.getId()),d.resolve()}.bind(this))}.bind(this)),d.promise()},b.prototype._setCookie=function(){this._visitorDeferred.then(function(b){c.cookie(a,b.getId(),{expires:365,path:"/",domain:h.getCookieDomain()})})},b.prototype.getPublicAPI=function(){return{vary:this.vary.bind(this),ab:this.ab.bind(this),logIn:this.logIn.bind(this),signUp:this.signUp.bind(this),initialize:this.initialize.bind(this),_crx:{loadInfo:function(){var a=c.Deferred();return this._visitorDeferred.then(function(b){var c={};for(var d in b.getAssignmentRegistry())c[d]=b.getAssignmentRegistry()[d].getVariant();a.resolve({visitorId:b.getId(),splitRegistry:h.getSplitRegistry(),assignmentRegistry:c})}),a.promise()}.bind(this),persistAssignment:function(a,b){var d=c.Deferred();return this._visitorDeferred.then(function(c){var e=new j({visitor:c,assignment:new f({splitName:a,variant:b,context:"chrome_extension",isUnsynced:!0})});e.persistAssignment().then(function(){d.resolve()})}),d.promise()}.bind(this)}}},b}(),m=function(){var a=function(a){if(this.visitorId=a.visitorId,this.identifierType=a.identifierType,this.value=a.value,!this.visitorId)throw new Error("must provide visitorId");if(!this.identifierType)throw new Error("must provide identifierType");if(!this.value)throw new Error("must provide value")};return a.prototype.save=function(a,b){var d=c.Deferred();return c.ajax(h.getUrl()+"/api/v1/identifier",{method:"POST",dataType:"json",crossDomain:!0,data:{identifier_type:this.identifierType,value:this.value,visitor_id:this.visitorId}}).then(function(a){var b=new k({id:a.visitor.id,assignments:f.fromJsonArray(a.visitor.assignments)});d.resolve(b)}),d.promise()},a}(),n=function(){var a=function(a){if(!a.assignment)throw new Error("must provide assignment");if(!a.visitor)throw new Error("must provide visitor");this._assignment=a.assignment,this._visitor=a.visitor,this._splitRegistry=h.getSplitRegistry(),this._variantHandlers={}};return a.prototype.when=function(){var a=Array.prototype.slice.call(arguments,0),b=a.length-1,c="function"!=typeof a[0]&&a.length>0,d=c?a.slice(0,Math.max(1,b)):[],e=a[b];if(0===d.length)throw new Error("must provide at least one variant");for(var f=0;f<d.length;f++)this._assignHandlerToVariant(d[f],e)},a.prototype.default=function(a,b){if(this._defaultVariant)throw new Error("must provide exactly one `default`");this._defaultVariant=this._assignHandlerToVariant(a,b)},a.prototype.run=function(){this._validate();var a;this._variantHandlers[this._assignment.getVariant()]?a=this._variantHandlers[this._assignment.getVariant()]:(a=this._variantHandlers[this.getDefaultVariant()],this._defaulted=!0),a()},a.prototype.isDefaulted=function(){return this._defaulted||!1},a.prototype.getDefaultVariant=function(){return this._defaultVariant},a.prototype._assignHandlerToVariant=function(a,b){if("function"!=typeof b)throw new Error("must provide handler for "+a);return a=a.toString(),this._getSplit()&&!this._getSplit().hasOwnProperty(a)&&this._visitor.logError("configures unknown variant "+a),this._variantHandlers[a]=b,a},a.prototype._validate=function(){if(!this.getDefaultVariant())throw new Error("must provide exactly one `default`");if(this._getVariants().length<2)throw new Error("must provide at least one `when`");if(this._getSplit()){var a=this._getMissingVariants();if(a.length>0){var b=a.join(", ").replace(/, (.+)$/," and $1");this._visitor.logError("does not configure variants "+b)}}},a.prototype._getSplit=function(){return this._splitRegistry?this._splitRegistry[this._assignment.getSplitName()]:null},a.prototype._getVariants=function(){return Object.getOwnPropertyNames(this._variantHandlers)},a.prototype._getMissingVariants=function(){for(var a=this._getVariants(),b=this._getSplit(),c=Object.getOwnPropertyNames(b),d=[],e=0;e<c.length;e++){var f=c[e];a.indexOf(f)===-1&&d.push(f)}return d},a}(),o=function(){var a=function(a){if(!a.splitName)throw new Error("must provide splitName");if(!a.hasOwnProperty("trueVariant"))throw new Error("must provide trueVariant");if(!a.visitor)throw new Error("must provide visitor");this._splitName=a.splitName,this._trueVariant=a.trueVariant,this._visitor=a.visitor,this._splitRegistry=h.getSplitRegistry()};return a.prototype.getVariants=function(){var a=this._getSplitVariants();return a&&a.length>2&&this._visitor.logError("A/B for "+this._splitName+" configures split with more than 2 variants"),{true:this._getTrueVariant(),false:this._getFalseVariant()}},a.prototype._getTrueVariant=function(){return this._trueVariant||!0},a.prototype._getFalseVariant=function(){var a=this._getNonTrueVariants();return!!a&&a.sort()[0]},a.prototype._getNonTrueVariants=function(){var a=this._getSplitVariants();if(a){var b=this._getTrueVariant(),c=a.indexOf(b);return c!==-1&&a.splice(c,1),a}return null},a.prototype._getSplit=function(){return this._splitRegistry?this._splitRegistry[this._splitName]:null},a.prototype._getSplitVariants=function(){return this._getSplit()&&Object.getOwnPropertyNames(this._getSplit())},a}(),p=(new l).getPublicAPI(),q=function(){window.dispatchEvent(new CustomEvent("tt:lib:loaded",{detail:{TestTrack:p}}))};try{c(document).ready(function(){c(document.body).addClass("_tt");try{window.dispatchEvent(new CustomEvent("tt:class:added"))}catch(a){}}),q(),window.addEventListener("tt:listener:ready",q)}catch(r){}return p});
@@ -0,0 +1,26 @@
1
+ module TestTrack::Controller
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ helper_method :test_track_session, :test_track_visitor
6
+ helper TestTrack::ApplicationHelper
7
+ around_action :manage_test_track_session
8
+ end
9
+
10
+ private
11
+
12
+ def test_track_session
13
+ @test_track_session ||= TestTrack::Session.new(self)
14
+ end
15
+
16
+ def test_track_visitor
17
+ test_track_session.visitor_dsl
18
+ end
19
+
20
+ def manage_test_track_session
21
+ RequestStore[:test_track_controller] = self
22
+ test_track_session.manage do
23
+ yield
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ class Tt::Api::V1::ApplicationController < ActionController::Base
2
+ before_action :return_json
3
+
4
+ private
5
+
6
+ def return_json
7
+ request.format = :json
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class Tt::Api::V1::AssignmentsController < Tt::Api::V1::ApplicationController
2
+ def create
3
+ head :no_content
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Tt::Api::V1::IdentifierTypesController < Tt::Api::V1::ApplicationController
2
+ def create
3
+ head :no_content
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Tt::Api::V1::IdentifierVisitorsController < Tt::Api::V1::ApplicationController
2
+ def show
3
+ @visitor = TestTrack::FakeServer.visitor
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Tt::Api::V1::IdentifiersController < Tt::Api::V1::ApplicationController
2
+ def create
3
+ @visitor = TestTrack::FakeServer.visitor
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ class Tt::Api::V1::ResetsController < Tt::Api::V1::ApplicationController
2
+ def update
3
+ TestTrack::FakeServer.reset!(seed)
4
+ head :no_content
5
+ end
6
+
7
+ private
8
+
9
+ def seed
10
+ params.permit(:seed)[:seed]
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ class Tt::Api::V1::SplitConfigsController < Tt::Api::V1::ApplicationController
2
+ def create
3
+ head :no_content
4
+ end
5
+
6
+ def destroy
7
+ head :no_content
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class Tt::Api::V1::SplitRegistriesController < Tt::Api::V1::ApplicationController
2
+ def show
3
+ @active_splits = TestTrack::FakeServer.split_registry
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Tt::Api::V1::VisitorsController < Tt::Api::V1::ApplicationController
2
+ def show
3
+ @visitor = TestTrack::FakeServer.visitor
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module TestTrack::ApplicationHelper
2
+ def test_track_setup_tag
3
+ state_json_base64 = Base64.strict_encode64(test_track_session.state_hash.to_json)
4
+ javascript_tag("window.TT = '#{state_json_base64}';")
5
+ end
6
+ end
@@ -0,0 +1,53 @@
1
+ module TestTrack::Identity
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
6
+ def test_track_identifier(identifier_type, identifier_value_method)
7
+ instance_methods = Module.new
8
+ include instance_methods
9
+
10
+ instance_methods.module_eval do
11
+ define_method :test_track_ab do |*args|
12
+ discriminator = TestTrack::IdentitySessionDiscriminator.new(self)
13
+
14
+ if discriminator.participate_in_online_session?
15
+ discriminator.controller.send(:test_track_visitor).ab(*args)
16
+ else
17
+ identifier_value = send(identifier_value_method)
18
+ TestTrack::OfflineSession.with_visitor_for(identifier_type, identifier_value) do |v|
19
+ v.ab(*args)
20
+ end
21
+ end
22
+ end
23
+
24
+ define_method :test_track_vary do |*args, &block|
25
+ discriminator = TestTrack::IdentitySessionDiscriminator.new(self)
26
+
27
+ if discriminator.participate_in_online_session?
28
+ discriminator.controller.send(:test_track_visitor).vary(*args, &block)
29
+ else
30
+ identifier_value = send(identifier_value_method)
31
+ TestTrack::OfflineSession.with_visitor_for(identifier_type, identifier_value) do |v|
32
+ v.vary(*args, &block)
33
+ end
34
+ end
35
+ end
36
+
37
+ define_method :test_track_visitor_id do
38
+ discriminator = TestTrack::IdentitySessionDiscriminator.new(self)
39
+
40
+ if discriminator.participate_in_online_session?
41
+ discriminator.controller.send(:test_track_visitor).id
42
+ else
43
+ identifier_value = send(identifier_value_method)
44
+ TestTrack::OfflineSession.with_visitor_for(identifier_type, identifier_value) do |v|
45
+ v.id
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
52
+ end
53
+ end