cohortly 0.0.5 → 0.0.6

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