blackbeard 0.0.4.0 → 0.0.5.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +44 -16
  3. data/TODO.md +3 -1
  4. data/dashboard/routes/cohorts.rb +25 -0
  5. data/dashboard/routes/features.rb +7 -4
  6. data/dashboard/routes/groups.rb +6 -3
  7. data/dashboard/routes/metrics.rb +25 -5
  8. data/dashboard/routes/tests.rb +6 -3
  9. data/dashboard/views/cohorts/index.erb +22 -0
  10. data/dashboard/views/cohorts/show.erb +41 -0
  11. data/dashboard/views/groups/show.erb +2 -2
  12. data/dashboard/views/layout.erb +1 -0
  13. data/dashboard/views/metrics/show.erb +37 -10
  14. data/dashboard/views/shared/_charts.erb +20 -0
  15. data/lib/blackbeard/chart.rb +65 -0
  16. data/lib/blackbeard/chartable.rb +47 -0
  17. data/lib/blackbeard/cohort.rb +48 -0
  18. data/lib/blackbeard/cohort_data.rb +72 -0
  19. data/lib/blackbeard/cohort_metric.rb +47 -0
  20. data/lib/blackbeard/context.rb +11 -2
  21. data/lib/blackbeard/dashboard.rb +2 -3
  22. data/lib/blackbeard/dashboard_helpers.rb +0 -22
  23. data/lib/blackbeard/errors.rb +2 -0
  24. data/lib/blackbeard/group.rb +3 -0
  25. data/lib/blackbeard/group_metric.rb +39 -0
  26. data/lib/blackbeard/metric.rb +30 -14
  27. data/lib/blackbeard/metric_data/base.rb +18 -35
  28. data/lib/blackbeard/metric_data/total.rb +2 -1
  29. data/lib/blackbeard/metric_data/uid_generator.rb +38 -0
  30. data/lib/blackbeard/metric_data/unique.rb +1 -1
  31. data/lib/blackbeard/metric_date.rb +10 -0
  32. data/lib/blackbeard/metric_hour.rb +8 -0
  33. data/lib/blackbeard/pirate.rb +18 -0
  34. data/lib/blackbeard/redis_store.rb +10 -1
  35. data/lib/blackbeard/storable.rb +1 -0
  36. data/lib/blackbeard/version.rb +1 -1
  37. data/spec/chart_spec.rb +38 -0
  38. data/spec/chartable_spec.rb +56 -0
  39. data/spec/cohort_data_spec.rb +142 -0
  40. data/spec/cohort_metric_spec.rb +26 -0
  41. data/spec/cohort_spec.rb +31 -0
  42. data/spec/context_spec.rb +9 -1
  43. data/spec/dashboard/cohorts_spec.rb +43 -0
  44. data/spec/dashboard/groups_spec.rb +0 -7
  45. data/spec/dashboard/metrics_spec.rb +35 -0
  46. data/spec/group_metric_spec.rb +26 -0
  47. data/spec/metric_data/base_spec.rb +0 -16
  48. data/spec/metric_data/uid_generator_spec.rb +40 -0
  49. data/spec/metric_spec.rb +23 -12
  50. data/spec/pirate_spec.rb +22 -1
  51. data/spec/redis_store_spec.rb +8 -2
  52. data/spec/storable_spec.rb +3 -0
  53. metadata +29 -3
  54. data/dashboard/views/metrics/_metric_data.erb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a8e907aad46889da2e0abfab0cd85fdc87c93a4e
4
- data.tar.gz: 125429d019e4e2e4767f18e2428faac8db1bff8b
3
+ metadata.gz: 07e955101ffdd9c1bfe155e1916ebfc0a9911f4d
4
+ data.tar.gz: eaab8a527217bc44eaadf13665a230cd46e8ae65
5
5
  SHA512:
6
- metadata.gz: 94047af0590aa7109951b25870f1c46f1b12d9231c09af49f141c025c03dcca9468a1b48d75878e1154ed867cc9dc768175011af0d9d42725251757976478527
7
- data.tar.gz: 7c407ffcffb34c88a29552e85a44a488bf377a7b21bc3f89f3d8d5c7213e3b888fcc5dad59ad8e1293e07a88b088694a824970c8f74a004eb82f5d2660c7631b
6
+ metadata.gz: d16393da029f3e584a1b632332582b20645dd1857b7b845012546bd0b939a99251969c6ec5e658fb425e61cb5cdf4ad2588461883a5128541f0a18ea66ca54d8
7
+ data.tar.gz: 5f5154181ac58db415657199eddb3f6e19ff236e999bf21bd74fc597903ab63350f783face5647bd534b5dd1b6c8580ffbe439616a5b812fc77320af3325e838
data/README.md CHANGED
@@ -109,7 +109,7 @@ $pirate.context(user).add_metric(:referral)
109
109
 
110
110
  If a context does not exist, `$pirate` will silently ignore all actions. This is useful for dealing with bots.
111
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.
112
+ If the user is unidentified set user to nil or false. If your app can return a Guest object for unidentified users, see the guest configuration setting.
113
113
 
114
114
  ### Collecting Metrics
115
115
 
@@ -215,33 +215,61 @@ if $pirate.context(user).feature_active?(:friend_feed){ ... }
215
215
 
216
216
  ### Defining groups
217
217
 
218
+ Group definitions are part of the configuration and should be defined
219
+ in an initializer.
220
+
218
221
  ```ruby
219
- $pirate.define_group(:admin) do |user, context|
220
- user.admin? # true, false
221
- end
222
+ $pirate = Blackbeard.pirate do |config|
223
+ config.define_group(:admin) do |user, controller|
224
+ user && user.admin? # true, false
225
+ end
222
226
 
223
- $pirate.define_group(:seo_traffic) do |user, context|
224
- context.session.refer =~ /google.com/ # remember to store refer in sessions
225
- end
227
+ config.define_group(:seo_traffic) do |user, controller|
228
+ controller && controller.session.refer =~ /google.com/ # remember to store refer in sessions
229
+ end
226
230
 
227
- $pirate.define_group(:seo_traffic) do |user, context|
228
- context.session.refer =~ /google.com/ # remember to store refer in sessions
229
- end
231
+ config.define_group(:seo_traffic) do |user, controller|
232
+ controller && controller.session.refer =~ /google.com/ # remember to store refer in sessions
233
+ end
230
234
 
231
- $pirate.define_group(:purchasers) do |user, context|
232
- user.purchases.any?
235
+ config.define_group(:purchasers) do |user, controller|
236
+ users && user.purchases.any?
237
+ end
233
238
  end
234
239
  ```
240
+ The user will be nil for non-logged in visitors. The controller
241
+ will be nil if not defined by the context (e.g. outside a web context).
235
242
 
236
- If your group is segments, include a list possible segments.
243
+ If your group is segmented (doesn't return true or false), include a list possible segments.
237
244
 
238
245
  ```ruby
239
- $pirate.define_group(:medalist, [:bronze, :silver, :gold]) do |user, context|
240
- user.engagement_level # nil, :bronze, :silver, :gold
246
+ $pirate = Blackbeard.pirate do |config|
247
+ ...
248
+ config.define_group(:medalist, [:bronze, :silver, :gold]) do |user, context|
249
+ user.engagement_level # nil, :bronze, :silver, :gold
250
+ end
241
251
  end
242
252
  ```
243
253
 
244
- If your group definition block returns an uninitialized segment, it wil be initialized automatically.
254
+ If your group definition block returns an uninitialized segment, it wil
255
+ be initialized automatically.
256
+
257
+ ### Defining Cohorts
258
+
259
+ Groups and cohorts in the dictionary sense are identical. For our purposes,
260
+ cohorts are participants that experienced something at the same time. A participant
261
+ can only be in one cohort at a time, but can switch cohorts.
262
+
263
+ This will add the current user/visitor and current timestamp to the cohort.
264
+
265
+ ```ruby
266
+ $pirate.add_to_cohort(:joined_at) # returns false if already in cohort, otherwise true
267
+ $pirate.add_to_cohort!(:bought_at) # always returns true. If user is already in a cohort, the timestamp is updated.
268
+ $pirate.add_to_cohort(:joined_at, user.created_at) # You can optional pass in the timestamp
269
+ ```
270
+
271
+ You'll can add cohorts to metrics compare how members who joined one day against
272
+ members who joined another day.
245
273
 
246
274
  ## Contributing
247
275
 
data/TODO.md CHANGED
@@ -48,9 +48,11 @@ $pirate.funnel(:checkout, 'Confirm') # User reached step 3 of funnel (C
48
48
  ```
49
49
 
50
50
 
51
+ ```ruby
51
52
  $pirate.define_group(:achieve_pirate_style) do |user, context|
52
53
  $pirate.metric(:pirate_style).achieved?
53
54
  end
55
+ ```
54
56
 
55
57
 
56
- http://blog.sourcing.io/structuring-sinatra?utm_content=buffer1955d&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer
58
+ [Structuring Sinatra applications](http://blog.sourcing.io/structuring-sinatra)
@@ -0,0 +1,25 @@
1
+ module Blackbeard
2
+ module DashboardRoutes
3
+ class Cohorts < Base
4
+ get '/cohorts' do
5
+ @cohorts = Cohort.all
6
+ erb 'cohorts/index'.to_sym
7
+ end
8
+
9
+ get '/cohorts/:id' do
10
+ ensure_cohort
11
+ erb 'cohorts/show'.to_sym
12
+ end
13
+
14
+ post "/cohorts/:id" do
15
+ ensure_cohort.update_attributes(params)
16
+ "OK"
17
+ end
18
+
19
+ def ensure_cohort
20
+ @cohort = Cohort.find(params[:id]) or pass
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -8,24 +8,27 @@ module Blackbeard
8
8
  end
9
9
 
10
10
  get "/features/:id" do
11
- @feature = Feature.find(params[:id]) or pass
11
+ ensure_feature
12
12
  @groups = Group.all
13
13
  erb 'features/show'.to_sym
14
14
  end
15
15
 
16
16
  post "/features/:id" do
17
- @feature = Feature.find(params[:id]) or pass
18
- @feature.update_attributes(params)
17
+ ensure_feature.update_attributes(params)
19
18
  "OK"
20
19
  end
21
20
 
22
21
  post "/features/:id/groups/:group_id" do
23
- @feature = Feature.find(params[:id]) or pass
22
+ ensure_feature
24
23
  @feature.set_segments_for(params[:group_id], params[:segments])
25
24
  @feature.save
26
25
  "OK"
27
26
  end
28
27
 
28
+ def ensure_feature
29
+ @feature = Feature.find(params[:id]) or pass
30
+ end
31
+
29
32
  end
30
33
  end
31
34
  end
@@ -7,16 +7,19 @@ module Blackbeard
7
7
  end
8
8
 
9
9
  get '/groups/:id' do
10
- @group = Group.find(params[:id]) or pass
10
+ ensure_group
11
11
  erb 'groups/show'.to_sym
12
12
  end
13
13
 
14
14
  post "/groups/:id" do
15
- @group = Group.find(params[:id]) or pass
16
- @group.update_attributes(params)
15
+ ensure_group.update_attributes(params)
17
16
  "OK"
18
17
  end
19
18
 
19
+ def ensure_group
20
+ @group = Group.find(params[:id]) or pass
21
+ end
22
+
20
23
  end
21
24
  end
22
25
  end
@@ -7,24 +7,44 @@ module Blackbeard
7
7
  end
8
8
 
9
9
  get "/metrics/:type/:type_id" do
10
- @metric = Metric.find(params[:type], params[:type_id]) or pass
11
- @group = Group.find(params[:group_id]) if params[:group_id]
10
+ ensure_metric; find_group; find_cohort; ensure_charts
12
11
  erb 'metrics/show'.to_sym
13
12
  end
14
13
 
15
14
  post "/metrics/:type/:type_id" do
16
- @metric = Metric.find(params[:type], params[:type_id]) or pass
15
+ ensure_metric
17
16
  @metric.update_attributes(params)
18
17
  "OK"
19
18
  end
20
19
 
21
20
  post "/metrics/:type/:type_id/groups" do
22
- @metric = Metric.find(params[:type], params[:type_id]) or pass
23
- @group = Group.find(params[:group_id])
21
+ ensure_metric; find_group
24
22
  @metric.add_group(@group) if @group
25
23
  redirect url("metrics/#{@metric.type}/#{@metric.type_id}?group_id=#{@group.id}")
26
24
  end
27
25
 
26
+ post "/metrics/:type/:type_id/cohorts" do
27
+ ensure_metric; find_cohort
28
+ @metric.add_cohort(@cohort) if @cohort
29
+ redirect url("metrics/#{@metric.type}/#{@metric.type_id}?cohort_id=#{@cohort.id}")
30
+ end
31
+
32
+ def ensure_metric
33
+ @metric = Metric.find(params[:type], params[:type_id]) or pass
34
+ end
35
+
36
+ def find_group
37
+ @group = Group.find(params[:group_id]) if params[:group_id]
38
+ end
39
+
40
+ def find_cohort
41
+ @cohort = Cohort.find(params[:cohort_id]) if params[:cohort_id]
42
+ end
43
+
44
+ def ensure_charts
45
+ @charts = []
46
+ end
47
+
28
48
  end
29
49
  end
30
50
  end
@@ -8,16 +8,19 @@ module Blackbeard
8
8
  end
9
9
 
10
10
  get "/tests/:id" do
11
- @test = Test.find(params[:id]) or pass
11
+ ensure_test
12
12
  erb 'tests/show'.to_sym
13
13
  end
14
14
 
15
15
  post "/tests/:id" do
16
- @test = Test.find(params[:id]) or pass
17
- @test.update_attributes(params)
16
+ ensure_test.update_attributes(params)
18
17
  "OK"
19
18
  end
20
19
 
20
+ def ensure_test
21
+ @test = Test.find(params[:id]) or pass
22
+ end
23
+
21
24
  end
22
25
  end
23
26
  end
@@ -0,0 +1,22 @@
1
+ <div class="page-header">
2
+ <h1>Cohorts</h1>
3
+ <p>Are groupings of visitors that experience a certain event at the same time.</p>
4
+ </div>
5
+
6
+ <% if @cohorts.any? %>
7
+ <div class="panel panel-primary">
8
+ <div class="panel-heading">
9
+ <h3 class="panel-title">Active Cohorts</h3>
10
+ </div>
11
+ <ul class="list-group">
12
+ <% @cohorts.each do |cohort| %>
13
+ <li class="list-group-item"><a href="<%= url("cohorts/#{cohort.id}") %>"><%= cohort.name %></a></li>
14
+ <% end %>
15
+ </ul>
16
+ </div>
17
+
18
+ <% else %>
19
+ <div class="alert alert-danger">
20
+ <strong>No cohorts!</strong> Something may be wrong or you may just need to define some cohorts.
21
+ </div>
22
+ <% end %>
@@ -0,0 +1,41 @@
1
+ <div class="page-header">
2
+ <h1><span id="editable-name"><%= @cohort.name %></span> <small>Group</small></h1>
3
+ <p id="editable-description"><%= @cohort.description %></p>
4
+ </div>
5
+
6
+ <!--Load the AJAX API-->
7
+ <script src="<%= url('javascripts/jquery-1.10.2.min.js') %>"></script>
8
+ <script src="<%= url('javascripts/bootstrap.min.js') %>"></script>
9
+ <script src="<%= url('bootstrap3-editable/js/bootstrap-editable.min.js') %>"></script>
10
+ <script type="text/javascript">
11
+ //turn to inline mode
12
+ $.fn.editable.defaults.send = 'always';
13
+ $.fn.editable.defaults.params = function(params) {
14
+ var obj = {};
15
+ obj[params.name] = params.value;
16
+ return obj;
17
+ }
18
+
19
+ $(document).ready(function() {
20
+ $('#editable-name').editable({
21
+ placement: 'right',
22
+ type: 'text',
23
+ url: '<%= url("groups/#{@cohort.id}") %>',
24
+ name: 'name',
25
+ title: 'Edit cohort name',
26
+ validate: function(value) {
27
+ if($.trim(value) == '') {
28
+ return 'This field is required';
29
+ }
30
+ }
31
+ });
32
+ $('#editable-description').editable({
33
+ placement: 'bottom',
34
+ type: 'textarea',
35
+ url: '<%= url("metrics/#{@cohort.id}") %>',
36
+ name: 'description',
37
+ title: 'Edit cohort description'
38
+ });
39
+ });
40
+ </script>
41
+ <%= partial :'shared/charts', :locals => {:charts => [@cohort.recent_hours_chart, @cohort.recent_days_chart]} %>
@@ -50,9 +50,9 @@
50
50
  $('#editable-description').editable({
51
51
  placement: 'bottom',
52
52
  type: 'textarea',
53
- url: '<%= url("metrics/#{@group.id}") %>',
53
+ url: '<%= url("groups/#{@group.id}") %>',
54
54
  name: 'description',
55
- title: 'Edit metric description'
55
+ title: 'Edit group description'
56
56
  });
57
57
  });
58
58
  </script>
@@ -33,6 +33,7 @@
33
33
  </div>
34
34
  <div class="navbar-collapse collapse">
35
35
  <ul class="nav navbar-nav">
36
+ <li><a href="<%= url('cohorts') %>">Cohorts</a></li>
36
37
  <li><a href="<%= url('features') %>">Features</a></li>
37
38
  <li><a href="<%= url('groups') %>">Groups</a></li>
38
39
  <li><a href="<%= url('metrics') %>">Metrics</a></li>
@@ -3,21 +3,38 @@
3
3
  <p id="editable-description"><%= @metric.description %></p>
4
4
  </div>
5
5
  <form id="add_group" action="<%= url("/metrics/#{@metric.type}/#{@metric.type_id}/groups") %>" method="post"><input id="add_group_input" type="hidden" name="group_id"></form>
6
+ <form id="add_cohort" action="<%= url("/metrics/#{@metric.type}/#{@metric.type_id}/cohorts") %>" method="post"><input id="add_cohort_input" type="hidden" name="cohort_id"></form>
6
7
  <ul class="nav nav-tabs">
7
- <% if @group.nil? %><li class="active"><% else %><li><% end %><a href="?">All</a></li>
8
+ <% if @group.nil? && @cohort.nil? %><li class="active"><% else %><li><% end %><a href="?">All</a></li>
8
9
  <% @metric.groups.each do |group| %>
9
10
  <% if @group == group %><li class="active"><% else %><li><% end %><a href="?group_id=<%= group.id %>"><%= group.name %></a></li>
10
11
  <% end %>
11
- <% if @metric.addable_groups.any? %>
12
+ <% @metric.cohorts.each do |cohort| %>
13
+ <% if @cohort == cohort %><li class="active"><% else %><li><% end %><a href="?cohort_id=<%= cohort.id %>"><%= cohort.name %></a></li>
14
+ <% end %>
15
+
12
16
  <li class="dropdown">
13
- <a id="drop5" role="button" data-toggle="dropdown" href="#">Segment by Group <b class="caret"></b></a>
17
+ <a id="drop5" role="button" data-toggle="dropdown" href="#">Add <b class="caret"></b></a>
14
18
  <ul id="menu2" class="dropdown-menu" role="menu" aria-labelledby="drop5">
15
- <% @metric.addable_groups.each do |group| %>
16
- <li role="presentation" data-group-id="<%= group.id %>" data-group-name="<%= group.name %>" class="add-group"><a role="menuitem" tabindex="-1" href="#"><%= group.name %></a></li>
19
+ <% if @metric.addable_groups.any? %>
20
+ <li role="presentation" class="dropdown-header">Group Metrics</li>
21
+ <% @metric.addable_groups.each do |group| %>
22
+ <li role="presentation" data-group-id="<%= group.id %>" data-group-name="<%= group.name %>" class="add-group"><a role="menuitem" tabindex="-1" href="#"><%= group.name %></a></li>
23
+ <% end %>
24
+ <% else %>
25
+ <li role="presentation" class="dropdown-header">No Group Metrics</li>
26
+ <% end %>
27
+ <li role="presentation" class="divider"></li>
28
+ <% if @metric.addable_cohorts.any? %>
29
+ <li role="presentation" class="dropdown-header">Cohort Metrics</li>
30
+ <% @metric.addable_cohorts.each do |cohort| %>
31
+ <li role="presentation" data-cohort-id="<%= cohort.id %>" data-cohort-name="<%= cohort.name %>" class="add-cohort"><a role="menuitem" tabindex="-1" href="#"><%= cohort.name %></a></li>
32
+ <% end %>
33
+ <% else %>
34
+ <li role="presentation" class="dropdown-header">No Cohort Metrics</li>
17
35
  <% end %>
18
36
  </ul>
19
37
  </li>
20
- <% end %>
21
38
  </ul>
22
39
 
23
40
  <!--Load the AJAX API-->
@@ -37,6 +54,19 @@
37
54
  }
38
55
  });
39
56
 
57
+ $('ul.nav-tabs').on("click", ".add-cohort", function (e) {
58
+ e.preventDefault();
59
+ var cohortId = $(this).data("cohort-id");
60
+ var cohortName = $(this).data("cohort-name");
61
+
62
+ if(confirm("Start collecting data by cohort "+cohortName+"?")) {
63
+ $('#add_cohort_input').val(cohortId);
64
+ $('#add_cohort').submit();
65
+ }
66
+ });
67
+
68
+
69
+
40
70
  //turn to inline mode
41
71
  $.fn.editable.defaults.send = 'always';
42
72
  $.fn.editable.defaults.params = function(params) {
@@ -67,7 +97,4 @@
67
97
  });
68
98
  });
69
99
  </script>
70
- <%= partial :'metrics/metric_data', :locals => {:metric_data => @metric.metric_data(@group) } %>
71
-
72
-
73
-
100
+ <%= partial :'shared/charts', :locals => {:charts => @charts } %>
@@ -0,0 +1,20 @@
1
+ <script type="text/javascript" src="https://www.google.com/jsapi"></script>
2
+ <script type="text/javascript">
3
+ // Load the Visualization API and the piechart package.
4
+ google.load('visualization', '1.0', {'packages':['corechart']});
5
+ </script>
6
+
7
+ <% charts.each do |chart| %>
8
+ <div id="<%= chart.dom_id %>"></div>
9
+ <script type="text/javascript">
10
+ // Set a callback to run when the Google Visualization API is loaded.
11
+ google.setOnLoadCallback(
12
+ function(){
13
+ var data = new google.visualization.DataTable(jQuery.parseJSON('<%= chart.data.to_json %>'));
14
+ var options = jQuery.parseJSON('<%= chart.options.to_json %>');
15
+ var div = document.getElementById('<%= chart.dom_id %>');
16
+ new google.visualization.LineChart(div).draw(data, options);
17
+ });
18
+
19
+ </script>
20
+ <% end %>