blackbeard 0.0.2.0 → 0.0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +9 -0
  3. data/Guardfile +8 -0
  4. data/README.md +162 -20
  5. data/Rakefile +6 -0
  6. data/TODO.md +13 -34
  7. data/blackbeard.gemspec +5 -1
  8. data/console.rb +3 -0
  9. data/dashboard/public/bootstrap3-editable/css/bootstrap-editable.css +663 -0
  10. data/dashboard/public/bootstrap3-editable/img/clear.png +0 -0
  11. data/dashboard/public/bootstrap3-editable/img/loading.gif +0 -0
  12. data/dashboard/public/bootstrap3-editable/js/bootstrap-editable.min.js +7 -0
  13. data/dashboard/public/fonts/glyphicons-halflings-regular.eot +0 -0
  14. data/dashboard/public/fonts/glyphicons-halflings-regular.svg +229 -0
  15. data/dashboard/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  16. data/dashboard/public/fonts/glyphicons-halflings-regular.woff +0 -0
  17. data/dashboard/public/javascripts/bootstrap.min.js +7 -0
  18. data/dashboard/public/javascripts/jquery-1.10.2.min.js +6 -0
  19. data/dashboard/public/stylesheets/application.css +28 -0
  20. data/dashboard/public/stylesheets/bootstrap-theme.css +7 -0
  21. data/dashboard/public/stylesheets/bootstrap.css +7 -0
  22. data/dashboard/routes/base.rb +19 -0
  23. data/dashboard/routes/groups.rb +22 -0
  24. data/dashboard/routes/home.rb +11 -0
  25. data/dashboard/routes/metrics.rb +30 -0
  26. data/dashboard/routes/tests.rb +23 -0
  27. data/dashboard/views/groups/index.erb +22 -0
  28. data/dashboard/views/groups/show.erb +58 -0
  29. data/dashboard/views/index.erb +4 -0
  30. data/dashboard/views/layout.erb +48 -0
  31. data/dashboard/views/metrics/_metric_data.erb +59 -0
  32. data/dashboard/views/metrics/index.erb +23 -0
  33. data/dashboard/views/metrics/show.erb +73 -0
  34. data/dashboard/views/tests/index.erb +21 -0
  35. data/dashboard/views/tests/show.erb +58 -0
  36. data/lib/blackbeard/configuration.rb +8 -1
  37. data/lib/blackbeard/configuration_methods.rb +24 -0
  38. data/lib/blackbeard/context.rb +33 -21
  39. data/lib/blackbeard/dashboard.rb +17 -21
  40. data/lib/blackbeard/dashboard_helpers.rb +29 -0
  41. data/lib/blackbeard/errors.rb +2 -2
  42. data/lib/blackbeard/group.rb +35 -0
  43. data/lib/blackbeard/metric.rb +34 -32
  44. data/lib/blackbeard/metric_data/base.rb +101 -0
  45. data/lib/blackbeard/metric_data/total.rb +39 -0
  46. data/lib/blackbeard/metric_data/unique.rb +58 -0
  47. data/lib/blackbeard/metric_date.rb +11 -0
  48. data/lib/blackbeard/metric_hour.rb +17 -0
  49. data/lib/blackbeard/pirate.rb +33 -22
  50. data/lib/blackbeard/redis_store.rb +46 -2
  51. data/lib/blackbeard/selected_variation.rb +13 -0
  52. data/lib/blackbeard/storable.rb +39 -27
  53. data/lib/blackbeard/storable_attributes.rb +54 -0
  54. data/lib/blackbeard/storable_has_many.rb +60 -0
  55. data/lib/blackbeard/storable_has_set.rb +59 -0
  56. data/lib/blackbeard/test.rb +21 -0
  57. data/lib/blackbeard/version.rb +1 -1
  58. data/lib/blackbeard.rb +0 -8
  59. data/spec/configuration_spec.rb +15 -0
  60. data/spec/context_spec.rb +94 -19
  61. data/spec/dashboard/groups_spec.rb +50 -0
  62. data/spec/dashboard/home_spec.rb +20 -0
  63. data/spec/dashboard/metrics_spec.rb +57 -0
  64. data/spec/dashboard/tests_spec.rb +43 -0
  65. data/spec/group_spec.rb +36 -0
  66. data/spec/metric_data/base_spec.rb +57 -0
  67. data/spec/metric_data/total_spec.rb +116 -0
  68. data/spec/metric_data/unique_spec.rb +91 -0
  69. data/spec/metric_spec.rb +52 -44
  70. data/spec/pirate_spec.rb +32 -15
  71. data/spec/redis_store_spec.rb +121 -0
  72. data/spec/spec_helper.rb +13 -1
  73. data/spec/storable_attributes_spec.rb +47 -0
  74. data/spec/storable_has_many_spec.rb +49 -0
  75. data/spec/storable_has_set_spec.rb +39 -0
  76. data/spec/storable_spec.rb +33 -0
  77. data/spec/test_spec.rb +25 -0
  78. metadata +133 -17
  79. data/lib/blackbeard/dashboard/helpers.rb +0 -8
  80. data/lib/blackbeard/dashboard/views/layout.erb +0 -15
  81. data/lib/blackbeard/dashboard/views/metrics/index.erb +0 -10
  82. data/lib/blackbeard/dashboard/views/metrics/show.erb +0 -16
  83. data/lib/blackbeard/feature.rb +0 -13
  84. data/lib/blackbeard/metric/total.rb +0 -17
  85. data/lib/blackbeard/metric/unique.rb +0 -18
  86. data/spec/dashboard_spec.rb +0 -38
  87. data/spec/total_metric_spec.rb +0 -65
  88. data/spec/unique_metric_spec.rb +0 -60
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 613ebf477638192eb2ab160882e277bb3b6d0f1a
4
- data.tar.gz: 55fea491870b73d48e81ff393a7ae2ba22f337d1
3
+ metadata.gz: be704d408f74d3fed4eceb91f873c087967ed947
4
+ data.tar.gz: 33e1a39fe2efeea751f99b9e769e00c72b19e99e
5
5
  SHA512:
6
- metadata.gz: 05a94bcd373c3c302a64179489a6b1efc3bd58980684f7afcd8200a690663fb9e7daed6ab01c9d5d4f3fae946d11e03326298d938ac94e24057a6da8e08d38dc
7
- data.tar.gz: 49f3612b4a6c53f0a5c8b6910ee585e17473e30071ec778318aa6a4715ea3a756ef025f8fb091f4a522a490cae6b87f0686828f72f7e1788f0202e7f79e30b7d
6
+ metadata.gz: 33d585c054f11b46334cfdbd98907d0b659decd5b4df3d06172b6c44c365ad97500461be00f7d10324ca9499f7620786271a5b22745fca72f39fb6758dbd55f5
7
+ data.tar.gz: d932724e9091f21dd1c16557cd821ac9a3c7b380391b362d0582a1cf80166381fae3cb07816dbe15f5e2fb5cff8169ede3087ae5b8d0ce9d8568dd5a56ea0271
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ services:
6
+ - redis-server
7
+ addons:
8
+ code_climate:
9
+ repo_token: c4f9d40da5073ae61d9bea245b5602eb70566eab3e47248b68dcccc25cb79b56
data/Guardfile ADDED
@@ -0,0 +1,8 @@
1
+ guard :rspec, cmd: 'bundle exec rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/blackbeard/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch(%r{^dashboard/routes/(.+)\.rb$}) { |m| "spec/dashboard/#{m[1]}_spec.rb" }
5
+ watch(%r{^dashboard/views/(.+)/.+\.erb$}) { |m| "spec/dashboard/#{m[1]}_spec.rb" }
6
+ watch('spec/spec_helper.rb') { "spec" }
7
+ end
8
+
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Blackbeard is a Redis backed metrics collection system with a Rack dashboard.
4
4
 
5
+ [![Code Climate](https://codeclimate.com/repos/5300f44be30ba0790d01b5a7/badges/b912a89a38a56f61398a/gpa.png)](https://codeclimate.com/repos/5300f44be30ba0790d01b5a7/feed) [![Code Climate](https://codeclimate.com/repos/5300f44be30ba0790d01b5a7/badges/b912a89a38a56f61398a/coverage.png)](https://codeclimate.com/repos/5300f44be30ba0790d01b5a7/feed) [![Build Status](https://travis-ci.org/goldstar/blackbeard.png?branch=master)](https://travis-ci.org/goldstar/blackbeard)
6
+
5
7
  ## Installation
6
8
 
7
9
  Add this line to your application's Gemfile:
@@ -18,27 +20,97 @@ Or install it yourself as:
18
20
 
19
21
  ## Usage
20
22
 
21
- In an initializer create your global $pirate and pass in your configuration.
23
+ All Blackbeard operations are done via the global `$pirate` which you should initialize early in your app.
24
+
25
+ ### Rails
26
+
27
+ To configure Rails 3+ create an initializer in `config/initializers`
22
28
 
23
29
  ```ruby
24
30
  require 'blackbeard'
25
31
  require 'blackbeard/dashboard'
26
32
 
27
33
  $pirate = Blackbeard.pirate do |config|
28
- config.redis = Redis.new # required. Will automatically be namespaced.
29
- config.namespace = "Blackbeard" # optional
30
- config.timezone = "America/Los_Angeles" # optional
34
+ # see configuration options below
35
+ end
36
+ ```
37
+
38
+ And inside your ApplicationController:
39
+
40
+ ```ruby
41
+ before_filter { |c| $pirate.set_context(c.current_user, c.request) }, :unless => robot?
42
+ after_filter{ |c| $pirate.clear_context }
43
+ ```
44
+
45
+ ### Sinatra
46
+
47
+ To configure Sinatra
48
+
49
+ ```ruby
50
+ require 'blackbeard'
51
+ require 'blackbeard/dashboard'
52
+
53
+ class MySinatraApp < Sinatra::Base
54
+ enable :sessions
55
+
56
+ $pirate = Blackbead.pirate do |config|
57
+ # see configuration options below
58
+ end
59
+
60
+ before do
61
+ $pirate.set_context(current_user, self) unless robot?
62
+ end
63
+
64
+ after do
65
+ $pirate.clear_context
66
+ end
67
+
68
+ ...
31
69
  end
32
70
  ```
33
71
 
72
+ ### Configuration options
73
+
74
+ ```ruby
75
+ $pirate = Blackbeard.pirate do |config|
76
+ config.redis = Redis.new # required. Will automatically be namespaced.
77
+ config.namespace = "Blackbeard" # optional
78
+ config.timezone = "America/Los_Angeles" # optional
79
+ config.guest_method = :guest? # optional method to call on user objects if they are guests or visitors
80
+ end
81
+ ```
82
+
34
83
  Note that the configuration is shared by all pirates, so only create one pirate at a time.
35
84
 
85
+ ### Mounting the dashboard
86
+
36
87
  To get the rack dashboard on Rails, mount it in your `routes.rb`. Don't forget to protect it with some constraints.
37
88
 
38
89
  ```ruby
39
90
  mount Blackbeard::Dashboard => '/blackbeard', :constraints => ConstraintClassYouCreate.new
40
91
  ```
41
92
 
93
+ ### Setting Context
94
+
95
+ Most of Blackbeard's calls are done via a context.
96
+
97
+ In a web request, this is handled by a before filter:
98
+
99
+ ```ruby
100
+ before_filter { |controller| $pirate.set_context(controller.current_user, controller) }, :unless => robot?
101
+ after_filter { |controller| $pirate.clear_context }
102
+ ```
103
+
104
+ Outside of a web request--or if you want to reference a user other than the one in the current request (e..g referrals)--you set the context before each call to `$pirate`.
105
+
106
+ ```ruby
107
+ $pirate.context(user).add_metric(:referral)
108
+ ```
109
+
110
+ If a context does not exist, `$pirate` will silently ignore all actions. This is useful for dealing with bots.
111
+
112
+ If the user is unidentied set user to nil or false. If your app can return a Guest object for unidentied users, see the guest configuration setting.
113
+
42
114
  ### Collecting Metrics
43
115
 
44
116
  In your app, have your pirate collect the important metrics.
@@ -48,7 +120,7 @@ In your app, have your pirate collect the important metrics.
48
120
  Unique counts are for metrics wherein a user may trigger the same metric twice, but should only be counted once.
49
121
 
50
122
  ```ruby
51
- $pirate.context(:user_id => user_id, :cookies => cookies).add_unique(:logged_in_user)
123
+ $pirate.add_unique(:logged_in_user)
52
124
  ```
53
125
 
54
126
  #### Counting Non-Unique Metrics
@@ -56,38 +128,108 @@ $pirate.context(:user_id => user_id, :cookies => cookies).add_unique(:logged_in_
56
128
  Non-unique counts are for metrics wherein a user may trigger the same metric multiple times and the amounts are summed up.
57
129
 
58
130
  ```ruby
59
- $pirate.context(...).add_total(:like, +1) # increment a like
60
- $pirate.context(...).add_total(:like, -1) # de-increment a like
61
- $pirate.context(...).add_total(:revenue, +119.95) # can also accept floats
131
+ $pirate.add_total(:like, +1) # increment a like
132
+ $pirate.add_total(:like, -1) # de-increment a like
133
+ $pirate.add_total(:revenue, +119.95) # can also accept floats
62
134
  ```
63
135
 
64
- ### Setting Context
136
+ ### Chaining Metrics
65
137
 
66
- Most of Blackbeard's calls are done via a context.
138
+ Context methods that inrement metrics always return the context, so you can chain them together.
139
+
140
+ ```ruby
141
+ $pirate.set_context(user)
142
+ $pirate.add_total(:like, +1).add_unique(:likers)
143
+
144
+ $pirate.context(user).add_total(:like, +1).add_unique(:likers)
145
+ ```
146
+
147
+
148
+ ### Defining Tests (changes or features)
149
+
150
+ Features are defined in your views, controller or anywhere in your app via the global $pirate. There is no configuration necessary (but see the gotcha below).
151
+
152
+
153
+ In a view:
154
+
155
+ ```erb
156
+ <%= $pirate.ab_test(:new_onboarding, :control => '/onboarding', :welcome_flow => '/welcome') %>
157
+ ```
158
+
159
+ In a controller:
160
+
161
+ ```ruby
162
+ @onboarding_path = $pirate.ab_test(:new_onboarding, :control => '/onboarding', :welcome_flow => '/welcome') %>
163
+ ```
164
+
165
+ You can call the feature multiple times with different variations:
67
166
 
68
167
  ```ruby
69
- $pirate.context(:user_id => current_user.id, :bot => false, :cookies => app_cookie_jar)
168
+ @button_bg_color = $pirate.ab_test(:button_color, :control => "#FFF", :black => "#000")
169
+ @button_text_color = $pirate.ab_test(:button_color, :control => "#CCC", :black => "#FFF")
70
170
  ```
71
171
 
72
- To establish identity, a context must have a user_id or a cookie_jar where blackbeard will cookie a visitor with it's own uid. As cookie_jar is optional, you can collect metrics outside a web request. You can also increment metrics for users other than the one currently logged in. For example, if user A refers visitor B and vistor B joins, then you can increment User A's referrals.
172
+ The best things to test are the biggest things that don't fit into a hash of options:
73
173
 
74
- It gets pretty tedious for a web app to constantly set the context. To make that more paletable you can use a before filter in Rails (or your framework's equivalent) and the $pirate.set_context method.
174
+ ```erb
175
+ <% if $pirate.ab_test(:join_form) == :long_version do %>
176
+ <!-- extra field here -->
177
+ <% end %>
178
+ ```
75
179
 
76
180
  ```ruby
77
- before_action do |controller|
78
- $pirate.set_context(
79
- :user_id => controller.current_user.id,
80
- :bot => controller.bot?,
81
- :cookies => controller.cookies)
181
+ if $pirate.ab_test(:join_form) == :long_version do
182
+ # validate the extra fields
82
183
  end
83
184
  ```
84
185
 
85
- If you `set_context` you can now make calls directly on $pirate and they will be delegate to that context.
186
+ If you're simply rolling out a feature or want a feature flipper, you can:
187
+
188
+ ```ruby
189
+ if $pirate.active?(:friend_feed){ ... } # shorthand for test(:friend_feed) == :active
190
+ ```
191
+
192
+ GOTCHA #1: It's good to avoid elsif and case statements when testing for features. Blackbeard learns about the features and their variations dynamically. If you're not passing in your variations as a hash, but only using conditionals, you can ensure all your variations are available with:
193
+
194
+ ```ruby
195
+ $pirate.test(:friend_feed).add_variations(:variation_one, :variation_two, ...)
196
+ ```
197
+
198
+ Look at the dashboard to see which variations are registered.
199
+
200
+ Features do not turn on automatically. When you first deploy the feature will be set to `:inactive` or `:control`, `:off`, or `:default` if any of those variations are defined.
201
+
202
+ GOTCHA #2: If you do not define the :inactive, :control, :off or :default variations, the result will be nil. This is the desired behavior but it may be confusing.
86
203
 
87
204
  ```ruby
88
- $pirate.add_total(:like, +1)
205
+ $pirate.ab_test(:new_onboarding, :one => 'one', :two => 'two') # is the same as the next line
206
+ $pirate.ab_test(:new_onboarding, :inactive => nil, :one => 'one', :two => 'two') # nil when feature is inactive
207
+ $pirate.ab_test(:new_onboarding, :default => 'one', :two => 'two') # => 'one' when feature is inactive
89
208
  ```
90
209
 
210
+ ### Defining groups
211
+
212
+ ```ruby
213
+ $pirate.define_group(:admin) do |user, context|
214
+ user.admin? # true, false
215
+ end
216
+
217
+ $pirate.define_group(:medalist) do |user, context|
218
+ user.engagement_level # nil, :bronze, :silver, :gold
219
+ end
220
+
221
+ $pirate.define_group(:seo_traffic) do |user, context|
222
+ context.session.refer =~ /google.com/ # remember to store refer in sessions
223
+ end
224
+
225
+ $pirate.define_group(:seo_traffic) do |user, context|
226
+ context.session.refer =~ /google.com/ # remember to store refer in sessions
227
+ end
228
+
229
+ $pirate.define_group(:purchasers) do |user, context|
230
+ user.purchases.any?
231
+ end
232
+ ```
91
233
 
92
234
  ## Contributing
93
235
 
data/Rakefile CHANGED
@@ -1 +1,7 @@
1
+ #!/usr/bin/env rake
1
2
  require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new('spec')
6
+
7
+ task :default => :spec
data/TODO.md CHANGED
@@ -16,6 +16,16 @@
16
16
  * by defined segments (e.g. staff?)
17
17
  * by user_id (e.g. rollout like)
18
18
 
19
+ ### Documentation
20
+
21
+ ### API and javascript $pirate
22
+
23
+ ### Funnels - collection of metrics
24
+
25
+ hour: hash each "uid->from->to" => segment
26
+ day: hash each "segment->from->to" => count
27
+ week: hash each "segment->from->to" => count
28
+
19
29
 
20
30
  Create and manage multiple experiments in a browser/rack dashboard
21
31
 
@@ -37,41 +47,10 @@ $pirate.experiment(
37
47
  $pirate.funnel(:checkout, 'Confirm') # User reached step 3 of funnel (Confirm)
38
48
  ```
39
49
 
40
- ### Segments
41
-
42
- ```ruby
43
- $pirate.segment(:premium_member) do
44
- current_user.premium_member? # true or false
45
- end
46
-
47
- $pirate.segment(:organization_size) do
48
- current_user.organization_size # '0-5', '6-14', '15+'
49
- end
50
- ```
51
-
52
- ### Changes
53
-
54
- ```ruby
55
- $pirate.change(:link, :control => 'blue', :red => 'red')
56
- $pirate.change(:link,
57
- :control => nil, # noop
58
- :red => Proc.new("red")
59
- )
60
-
61
50
 
62
- $pirate.variation(:link, :control) do
63
- something.blue
51
+ $pirate.define_group(:achieve_pirate_style) do |user, context|
52
+ $pirate.metric(:pirate_style).achieved?
64
53
  end
65
54
 
66
- $pirate.variation(:link, :red) do
67
- ...
68
- end
69
- ```
70
-
71
- ### Outside a Web Session
72
55
 
73
- You can run them in crons or asynchronously.
74
-
75
- ```ruby
76
- $pirate.context().goal(...)
77
- $pirate.context()
56
+ http://blog.sourcing.io/structuring-sinatra?utm_content=buffer1955d&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer
data/blackbeard.gemspec CHANGED
@@ -20,10 +20,14 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
22
  spec.add_development_dependency "rake", "~> 10.0"
23
- spec.add_development_dependency "rspec", "~> 2.6"
23
+ spec.add_development_dependency "rspec", "~> 2.14.1"
24
24
  spec.add_development_dependency 'rack-test', '~> 0.6'
25
+ spec.add_development_dependency 'guard-rspec', '~> 4.2.5'
26
+ spec.add_development_dependency 'terminal-notifier-guard'
27
+ spec.add_development_dependency 'codeclimate-test-reporter'
25
28
 
26
29
  spec.add_runtime_dependency "sinatra-base", "~> 1.4"
30
+ spec.add_runtime_dependency "sinatra-partial", "~> 0.4.0"
27
31
  spec.add_runtime_dependency "tzinfo", "~> 0.3"
28
32
  spec.add_runtime_dependency 'redis', '~> 3.0', '>= 3.0.4'
29
33
  spec.add_runtime_dependency 'redis-namespace', '~> 1.4', '>= 1.4.1'
data/console.rb ADDED
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
2
+ require 'blackbeard'
3
+