blackbeard 0.0.4.0 → 0.0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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 %>