cohortly 0.0.5 → 0.0.6

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.
data/app/#assets# ADDED
File without changes
@@ -4,11 +4,18 @@ class Cohortly::MetricsController < Cohortly::CohortlyController
4
4
 
5
5
  scope = Cohortly::Metric.sort(:created_at.desc)
6
6
  if params[:cohortly_metric] && params[:cohortly_metric][:tags]
7
- scope = scope.where(:tags => { :$all => @metric_search.tags })
7
+ scope = scope.where(:tags => { :$in => @metric_search.tags })
8
8
  end
9
- if @metric_search.user_id
9
+ if params[:cohortly_metric] && params[:cohortly_metric][:groups]
10
+ groups = params[:cohortly_metric][:groups]
11
+ scope = scope.where(:$where => "function() { return #{ groups.collect {|x| 'this.tags.indexOf("' + x + '") >= 0' }.join(' || ') }; }" )
12
+ end
13
+ if !@metric_search.user_id.blank?
10
14
  scope = scope.where(:user_id => @metric_search.user_id)
11
15
  end
16
+ if !@metric_search.username.blank?
17
+ scope = scope.where(:username => @metric_search.username)
18
+ end
12
19
  @metrics = scope.paginate(:per_page => 200, :page => params[:page])
13
20
  end
14
21
  end
@@ -1,12 +1,16 @@
1
1
  class Cohortly::ReportsController < Cohortly::CohortlyController
2
2
  def index
3
+ Cohortly::Metric.send :attr_accessor, :groups
3
4
  @metric_search = Cohortly::Metric.new(params[:cohortly_metric])
4
5
  tags = @metric_search.tags.any? ? @metric_search.tags : nil
5
- report_name = Cohortly::Metric.report_table_name(tags)
6
- unless Cohortly::Metric.database.collections.collect(&:name).include?( report_name )
7
- Cohortly::Metric.cohort_chart_for_tag(tags)
8
- end
9
- @report = Cohortly::Report.new( report_name )
6
+ groups = @metric_search.groups
7
+ @report_name = Cohortly::Metric.report_table_name(tags, groups, true)
8
+ # run this in background would be better
9
+ Cohortly::Metric.weekly_cohort_chart_for_tag(tags, groups)
10
+ @report = Cohortly::Report.new( tags, groups, true )
11
+ respond_to do |format|
12
+ format.html
13
+ format.js { render :json => @report }
14
+ end
10
15
  end
11
-
12
16
  end
@@ -10,7 +10,7 @@ module Cohortly
10
10
  key :controller, String
11
11
  key :action, String
12
12
  timestamps!
13
-
13
+
14
14
  ensure_index :tags
15
15
 
16
16
  def self.store!(args)
@@ -24,21 +24,62 @@ module Cohortly
24
24
  data[:tags] += data[:add_tags] if data[:add_tags]
25
25
  create(data)
26
26
  end
27
-
28
- def self.cohort_chart_for_tag(tags = nil)
29
- query = {}
30
- query = { :tags => { :$all => tags } } if tags
31
- self.collection.map_reduce(self.month_map,
27
+
28
+ def self.cohort_chart(tags = nil, groups = nil, weekly = false)
29
+ query = { }
30
+ query = {:tags => { :$in => tags} } if tags
31
+ if groups
32
+ query[:$where] = "function() { return #{ groups.collect {|x| 'this.tags.indexOf("' + x + '") >= 0' }.join(' || ') }; }"
33
+ end
34
+ collection_name = self.report_table_name(tags, groups, weekly)
35
+ # incremental map_reduce pattern
36
+ meta = Cohortly::ReportMeta.find_or_create_by_collection_name(collection_name)
37
+ query[:created_at] = { :$gt => meta.last_update_on.utc } if meta.last_update_on
38
+ self.collection.map_reduce(weekly ? self.week_map : self.month_map,
32
39
  self.reduce,
33
- { :out => self.report_table_name(tags),
40
+ { :out => meta.last_update_on ? { :reduce => collection_name } : collection_name,
34
41
  :raw => true,
35
- :query => query})
42
+ :query => query})
43
+ meta.last_update_on = Time.now.utc
44
+ meta.save
36
45
  end
37
-
38
- def self.report_table_name(tags = nil)
39
- "cohort_report#{ tags ? "_#{ tags.sort.join('_') }" : '' }_#{ Time.now.strftime("%m-%d-%Y") }"
46
+
47
+ def self.cohort_chart_for_tag(tags = nil, groups = nil)
48
+ self.cohort_chart(tags, groups, false)
40
49
  end
41
50
 
51
+ def self.weekly_cohort_chart_for_tag(tags = nil, groups = nil)
52
+ self.cohort_chart(tags, groups, true)
53
+ end
54
+
55
+ def self.report_table_name(tags = nil, groups = nil, weekly = true)
56
+ "cohortly_report#{ tags ? "-tags=#{ tags.sort.join(':') }" : '' }#{ groups ? "-groups=#{ groups.sort.join(':') }" : '' }#{ weekly ? '-weekly' : '-monthly'}"
57
+ end
58
+
59
+ def self.report_name_to_args(name)
60
+ name = name.gsub(/^cohortly_report/, '')
61
+ if name =~ /-weekly$/
62
+ weekly = true
63
+ name = name.gsub(/-weekly$/, '')
64
+ else
65
+ weekly = false
66
+ name = name.gsub(/-monthly$/, '')
67
+ end
68
+ tags = nil
69
+ groups = nil
70
+ if name.length > 0
71
+ name = name.gsub(/-tags/, 'tags')
72
+ tags, groups = name.split('-')
73
+ tags = tags.gsub(/tags=/, '').split(':')
74
+ tags = tags.any? ? tags : nil
75
+ if groups
76
+ groups = groups.gsub(/groups=/,'').split(':')
77
+ groups = groups.any? ? groups : nil
78
+ end
79
+ end
80
+ [tags, groups, weekly]
81
+ end
82
+
42
83
  def self.month_map
43
84
  <<-JS
44
85
  function() {
@@ -58,6 +99,19 @@ module Cohortly
58
99
  JS
59
100
  end
60
101
 
102
+ def self.week_map
103
+ <<-JS
104
+ function() {
105
+ var start_date = week_key(this.user_start_date);
106
+ var happened_on = week_key(this.created_at);
107
+ var payload = {};
108
+ payload[happened_on] = {};
109
+ payload[happened_on][this.user_id] = 1;
110
+ emit( start_date, payload );
111
+ }
112
+ JS
113
+ end
114
+
61
115
  def self.reduce
62
116
  <<-JS
63
117
  function(key, values) {
@@ -77,6 +131,5 @@ module Cohortly
77
131
  }
78
132
  JS
79
133
  end
80
-
81
134
  end
82
135
  end
@@ -1,60 +1,78 @@
1
1
  module Cohortly
2
2
  class Report
3
3
  # this is the reduced collection
4
- attr_accessor :collection
5
- def initialize(collection)
6
- self.collection = collection
4
+ attr_accessor :collection, :weekly, :key_pattern, :groups, :tags
5
+ def initialize( tags = nil, groups = nil, weekly = true )
6
+ self.collection = Cohortly::Metric.report_table_name(tags, groups, weekly)
7
+ self.groups = groups
8
+ self.tags = tags
9
+ self.weekly = weekly
10
+ self.key_pattern = self.weekly ? "%Y-%W" : "%Y-%m"
7
11
  end
8
12
 
9
13
  def data
10
14
  @data ||= (Cohortly::Metric.database[self.collection].find().collect {|x| x}).sort_by {|x| x['_id'] }
11
15
  end
12
16
 
13
- def start_month
17
+ def fix_data_lines
18
+ data.each do |line|
19
+ period_cohorts_from(line['_id']).collect do |key|
20
+ if line["value"][key].nil?
21
+ line["value"][key] = { }
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def offset
28
+ self.weekly ? 1.week : 1.month
29
+ end
30
+
31
+ def start_key
14
32
  data.first['_id']
15
33
  end
16
34
 
17
- def end_month
18
- time_to_month(Time.now)
35
+ def end_key
36
+ time_to_key(Time.now.utc)
19
37
  end
20
38
 
21
- def time_to_month(time)
22
- time.strftime('%Y-%m')
39
+ def time_to_key(time)
40
+ time.strftime(self.key_pattern)
23
41
  end
24
42
 
25
- def month_to_time(str_month)
26
- year, month = str_month.split('-')
27
- Time.utc(year.to_i, month.to_i)
43
+ def key_to_time(report_key)
44
+ DateTime.strptime(report_key, self.key_pattern).to_time.utc
28
45
  end
29
46
 
30
- def user_count_in_cohort(str_month)
31
- Cohortly::Metric.collection.distinct(:user_id,
32
- { :user_start_date => { :$gt => month_to_time(str_month),
33
- :$lt => (month_to_time(str_month) + 1.month)}}).length
47
+ def user_count_in_cohort(report_key)
48
+ params = { :user_start_date => { :$gt => key_to_time(report_key),
49
+ :$lt => (key_to_time(report_key) + self.offset)}}
50
+ params[:tags] = { :$in => groups } if self.groups
51
+ Cohortly::Metric.collection.distinct(:user_id, params).length
34
52
  end
35
53
 
36
- def month_cohorts
54
+ def period_cohorts
37
55
  return [] unless data.first
38
- start_time = month_to_time(start_month)
39
- end_time = month_to_time(end_month)
56
+ start_time = key_to_time(start_key)
57
+ end_time = key_to_time(end_key)
40
58
  cur_time = start_time
41
59
  res = []
42
60
  while(cur_time <= end_time) do
43
- res << time_to_month(cur_time)
44
- cur_time += 1.month
61
+ res << time_to_key(cur_time)
62
+ cur_time += self.offset
45
63
  end
46
64
  res
47
65
  end
48
-
49
- def month_cohorts_from(cohort_key)
50
- index = month_cohorts.index(cohort_key)
51
- month_cohorts[index..-1]
66
+
67
+ def period_cohorts_from(cohort_key)
68
+ index = period_cohorts.index(cohort_key)
69
+ period_cohorts[index..-1]
52
70
  end
53
71
 
54
72
  def report_line(cohort_key)
55
73
  line = data.find {|x| x['_id'] == cohort_key}
56
74
  return [] unless line
57
- month_cohorts_from(cohort_key).collect do |key|
75
+ period_cohorts_from(cohort_key).collect do |key|
58
76
  if line["value"][key]
59
77
  line["value"][key].keys.length
60
78
  else
@@ -70,9 +88,20 @@ module Cohortly
70
88
  end
71
89
 
72
90
  def report_totals
73
- month_cohorts.collect do |cohort_key|
91
+ period_cohorts.collect do |cohort_key|
74
92
  report_line(cohort_key)
75
93
  end
76
94
  end
95
+
96
+ def base_n
97
+ @base_n ||= self.period_cohorts.inject({ }) { |accum, key| accum[key] = user_count_in_cohort(key); accum }
98
+ end
99
+
100
+ def as_json(*args)
101
+ fix_data_lines
102
+ { :data => data,
103
+ :base_n => base_n
104
+ }
105
+ end
77
106
  end
78
107
  end
@@ -0,0 +1,8 @@
1
+ module Cohortly
2
+ class ReportMeta
3
+ include MongoMapper::Document
4
+
5
+ key :collection_name, String
6
+ key :last_update_on, Time
7
+ end
8
+ end
@@ -0,0 +1,54 @@
1
+ module Cohortly
2
+ class StoredProcedures
3
+ PROCS = [:day_of_year,
4
+ :week_of_year,
5
+ :week_key]
6
+
7
+ def self.eval_js(javascript)
8
+ Cohortly::Metric.database.eval(javascript)
9
+ end
10
+
11
+ def self.day_of_year
12
+ <<-JS
13
+ function(date) {
14
+ var ms = date - new Date('' + date.getUTCFullYear() + '/1/1 UTC');
15
+ return parseInt(ms / 60000 / 60 / 24, 10) + 1;
16
+ }
17
+ JS
18
+ end
19
+
20
+ def self.week_of_year
21
+ <<-JS
22
+ function(date) {
23
+ var doy = day_of_year(date);
24
+ var dow = date.getUTCDay();
25
+ dow = ((dow === 0) ? 7 : dow);
26
+ var rdow = 7 - dow;
27
+ var woy = parseInt((doy + rdow) / 7, 10);
28
+ return woy;
29
+ }
30
+ JS
31
+ end
32
+
33
+ def self.week_key
34
+ <<-JS
35
+ function(date) {
36
+ var year = date.getYear() + 1900;
37
+ var week = week_of_year(date);
38
+ if(week < 10) { week = '0' + week }
39
+ return year + '-' + week;
40
+ }
41
+ JS
42
+ end
43
+
44
+ def self.store_procedures()
45
+ PROCS.each do |proc_name|
46
+ Cohortly::Metric.database.add_stored_function(proc_name.to_s, self.send(proc_name))
47
+ end
48
+ end
49
+
50
+ def self.execute(proc_name, *args)
51
+ Cohortly::Metric.database.eval(self.send(proc_name), *args)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,9 @@
1
+ <div class="groups">
2
+ <% Cohortly::TagConfig.all_groups.each do |group| %>
3
+ <span class"group">
4
+ <%= check_box_tag 'cohortly_metric[groups][]', group,
5
+ params[:cohortly_metric] && params[:cohortly_metric][:groups] &&
6
+ params[:cohortly_metric][:groups].include?(group) %><label><%= group %></label>
7
+ </span>
8
+ <% end %>
9
+ </div>
@@ -1,9 +1,14 @@
1
1
  <div class="tags">
2
- <% Cohortly::Metric.collection.distinct(:tags).each do |tag| %>
3
- <span class"tag">
4
- <%= check_box_tag 'cohortly_metric[tags][]', tag,
5
- params[:cohortly_metric] && params[:cohortly_metric][:tags] &&
6
- params[:cohortly_metric][:tags].include?(tag) %><label><%= tag %></label>
7
- </span>
2
+ <% (Cohortly::Metric.collection.distinct(:tags) - Cohortly::TagConfig.all_groups).each_slice(8) do |tags| %>
3
+ <div class="tag_group" style="float:left; padding: 10px;">
4
+ <% tags.each do |tag| %>
5
+ <div class"tag">
6
+ <%= check_box_tag 'cohortly_metric[tags][]', tag,
7
+ params[:cohortly_metric] && params[:cohortly_metric][:tags] &&
8
+ params[:cohortly_metric][:tags].include?(tag) %><label><%= tag %></label>
9
+ </div>
10
+ <% end %>
11
+ </div>
8
12
  <% end %>
13
+ <div style="clear:both;"></div>
9
14
  </div>
@@ -3,9 +3,16 @@
3
3
  <%= form_for(@metric_search, :url => cohortly_metrics_path, :html => {
4
4
  :method =>:get }) do |f| %>
5
5
  <div class="field">
6
- <%= f.label :user_id %>
6
+ <%= f.label 'User id' %>
7
7
  <%= f.text_field :user_id %>
8
8
  </div>
9
+ <div class="field">
10
+ <%= f.label :username %>
11
+ <%= f.text_field :username %>
12
+ </div>
13
+ <h3>Groups</h3>
14
+ <%= render :partial => 'cohortly/metrics/groups' %>
15
+ <h3>Tags</h3>
9
16
  <%= render :partial => 'cohortly/metrics/tags' %>
10
17
  <%= f.submit "filter" %>
11
18
  <% end %>
@@ -1,32 +1,12 @@
1
1
  <h2>Cohort Reports</h2>
2
2
 
3
3
  <%= form_for @metric_search, :url => cohortly_reports_path, :html =>
4
- {:method => :get} do |f| %>
4
+ {:method => :get, :class => 'cohortly_report_form'} do |f| %>
5
+ <h3>Groups</h3>
6
+ <%= render :partial => 'cohortly/metrics/groups' %>
7
+ <h3>Tags</h3>
5
8
  <%= render :partial => 'cohortly/metrics/tags' %>
6
9
  <%= f.submit 'Get Report'%>
10
+ <div class="result_table">
11
+ </div>
7
12
  <% end %>
8
-
9
-
10
- <table class="one-column-emphasis">
11
- <colgroup>
12
- <col class="oce-first">
13
- </colgroup>
14
- <thead>
15
- <tr>
16
- <th>Month</th>
17
- <th>N</th>
18
- <% 12.times do |month| %>
19
- <th>M<%= month + 1 %></th>
20
- <% end %>
21
- </tr>
22
- </thead>
23
- <% @report.month_cohorts.each do |cohort_key| %>
24
- <tr class="<%= cycle('odd', 'even') %>">
25
- <td><%= cohort_key %></td>
26
- <% @report.percent_line(cohort_key)[0,13].each do |val| %>
27
- <td style="text-align: right;"><%= val %></td>
28
- <% end %>
29
- </tr>
30
- <% end %>
31
- </table>
32
-
@@ -0,0 +1,236 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Cohortly</title>
5
+ <%= csrf_meta_tag %>
6
+
7
+ <%= javascript_include_tag 'https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js' %>
8
+ <%= javascript_include_tag 'http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.1.7/underscore-min.js' %>
9
+ <%= javascript_include_tag 'http://cdnjs.cloudflare.com/ajax/libs/backbone.js/0.5.3/backbone-min.js' %>
10
+
11
+ <script type="text/javascript">
12
+ var Cohortly = {};
13
+ Cohortly.Report = Backbone.Model.extend({
14
+
15
+ });
16
+ Cohortly.ReportQuery = Backbone.Model.extend({});
17
+ Cohortly.ReportView = Backbone.View.extend({
18
+ initialize: function() {
19
+ _.bindAll(this, 'submitQuery', 'render', 'render_rows', 'render_row', 'render_cells');
20
+ this.model = new Cohortly.Report();
21
+ this.model.bind('change', this.render)
22
+ },
23
+ events: {
24
+ 'click input[type=submit]': 'submitQuery'
25
+ },
26
+ submitQuery: function(e) {
27
+ $.get( '/cohortly/reports.js?' + $(this.el).serialize(), _(function(data) {
28
+ this.model.set(data);
29
+ }).bind(this), 'json')
30
+ e.preventDefault();
31
+ },
32
+ render: function() {
33
+ $('.result_table', this.el).html(
34
+ ['<table class="one-column-emphasis">',
35
+ '<colgroup><col class="oce-first"></colgroup>',
36
+ this.render_header(),
37
+ this.render_rows(),
38
+ '</table>'
39
+ ].join(''));
40
+ },
41
+ render_header: function() {
42
+ return [
43
+ '<thead><tr>',
44
+ '<th>Month</th>',
45
+ '<th>N</th>',
46
+ _.range(1,13).map(function(x){ return '<th>M' + x + '</th>'; }).join(''),
47
+ '</tr></thead>'
48
+ ].join('');
49
+ },
50
+ render_rows: function() {
51
+ return _(this.model.get('data')).map(this.render_row).join('');
52
+ },
53
+ render_row: function(row) {
54
+ var row_key = row['_id'];
55
+ var base_n = this.model.get('base_n')[row_key];
56
+ return [
57
+ '<tr>',
58
+ '<td>' + row['_id']+ '</td>',
59
+ '<td>' + base_n + '</td>',
60
+ this.render_cells(row, base_n),
61
+ '</tr>'
62
+ ].join('');
63
+ },
64
+ render_cells: function(row, base_n) {
65
+ return _(_(row.value).keys()).sortBy(function(x){return x}).slice(0,12).map(function(key) {
66
+ var num_users = _(row.value[key]).keys().length;
67
+ var percent = (base_n > 0) ? (num_users / base_n) : 0;
68
+ return [
69
+ '<td style="text-align:right;">',
70
+ Math.floor(percent * 100),
71
+ '</td>'
72
+ ].join('');
73
+ }).join('');
74
+ }
75
+
76
+ });
77
+
78
+ $(function() {
79
+ Cohortly.report_view = new Cohortly.ReportView({ el: $('form') });
80
+ });
81
+ </script>
82
+
83
+ <style>
84
+
85
+ body {
86
+ font-family: "Lucida Sans Unicode","Lucida Grande",Sans-Serif;
87
+ font-size: 12px;
88
+ margin: 0;
89
+ color: #666699;
90
+ }
91
+ form {
92
+
93
+ }
94
+ .field, .tags {
95
+ margin-bottom: 1em;
96
+ }
97
+ .field label {
98
+ display:block;
99
+ }
100
+ .header {
101
+ background-color: #E8EDFF;
102
+ height: 50px;
103
+ }
104
+ .header h1 {
105
+ padding-top: 12px;
106
+ margin: 0;
107
+ color: #669;
108
+ }
109
+ .container {
110
+ position: relative;
111
+ margin:0 auto;
112
+ width: 960px;
113
+ }
114
+ .hor-zebra {
115
+ border-collapse: collapse;
116
+ font-family: "Lucida Sans Unicode","Lucida Grande",Sans-Serif;
117
+ font-size: 12px;
118
+ text-align: left;
119
+ width: 960px;
120
+ }
121
+ .hor-zebra th {
122
+ color: #003399;
123
+ font-size: 14px;
124
+ font-weight: normal;
125
+ padding: 10px 8px;
126
+ }
127
+ .hor-zebra td {
128
+ color: #666699;
129
+ padding: 8px;
130
+ }
131
+ .hor-zebra .odd {
132
+ background: none repeat scroll 0 0 #E8EDFF;
133
+ }
134
+
135
+ .one-column-emphasis {
136
+ border-collapse: collapse;
137
+ font-family: "Lucida Sans Unicode","Lucida Grande",Sans-Serif;
138
+ font-size: 12px;
139
+ text-align: left;
140
+ position: relative;
141
+ left: -4px;
142
+ margin-top: 20px;
143
+ }
144
+ .one-column-emphasis th {
145
+ color: #003399;
146
+ font-size: 14px;
147
+ font-weight: normal;
148
+ padding: 12px 15px;
149
+ }
150
+ .one-column-emphasis td {
151
+ border-top: 1px solid #E8EDFF;
152
+ color: #666699;
153
+ padding: 10px 15px;
154
+ }
155
+ .oce-first {
156
+ background: none repeat scroll 0 0 #D0DAFD;
157
+ border-left: 10px solid transparent;
158
+ border-right: 10px solid transparent;
159
+ }
160
+ .one-column-emphasis tr:hover td {
161
+ background: none repeat scroll 0 0 #EFF2FF;
162
+ color: #333399;
163
+ }
164
+ .navigation {
165
+ position: absolute;
166
+ bottom: -3px;
167
+ right: 0;
168
+ }
169
+
170
+
171
+ .navigation ul {
172
+ list-style: none;
173
+ margin: 0;
174
+ padding: 0;
175
+ }
176
+
177
+
178
+ .navigation ul li {
179
+ display: inline;
180
+ margin-right: .75em;
181
+ background-color: #FFF;
182
+ color: #669;
183
+ border-radius: 4px;
184
+ }
185
+
186
+ .navigation ul li.last {
187
+ margin-right: 0;
188
+ }
189
+ .navigation li {
190
+ padding: 5px 1em;
191
+ }
192
+ .navigation li a {
193
+ color: #669;
194
+ text-decoration: none;
195
+ }
196
+ .paginate {
197
+ margin-top: 1em;
198
+ margin-bottom: 1em;
199
+ text-align: right;
200
+ }
201
+
202
+ .paginate a, span {
203
+
204
+ text-decoration: none;
205
+ padding-left: 5px;
206
+ color: #666699;
207
+ }
208
+ .paginate span {
209
+ color: red;
210
+ }
211
+ </style>
212
+
213
+ </head>
214
+ <body>
215
+ <div class="header">
216
+ <div class="container">
217
+ <h1>Cohortly</h1>
218
+ <div class="navigation">
219
+ <ul>
220
+ <li><span><%= link_to "Metrics", cohortly_metrics_path %></span></li>
221
+ <li><%= link_to "Cohort Reports", cohortly_reports_path %></span></li>
222
+ </ul>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ <div class="content">
227
+ <div class="container">
228
+ <%= yield %>
229
+ </div>
230
+ </div>
231
+ <div class="footer">
232
+ <div class="container">
233
+ </div>
234
+ </div>
235
+ </body>
236
+ </html>