i_wonder 0.0.1 → 0.0.3

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 (37) hide show
  1. data/README.rdoc +1 -24
  2. data/app/assets/javascripts/i_wonder/application.js +1 -0
  3. data/app/assets/javascripts/i_wonder/reports.js +11 -1
  4. data/app/assets/stylesheets/i_wonder/_shared.css.scss +6 -0
  5. data/app/assets/stylesheets/i_wonder/application.css.scss +2 -5
  6. data/app/assets/stylesheets/i_wonder/reports.css.scss +7 -0
  7. data/app/controllers/i_wonder/reports_controller.rb +36 -3
  8. data/app/models/i_wonder/event.rb +16 -6
  9. data/app/models/i_wonder/metric.rb +57 -14
  10. data/app/models/i_wonder/report.rb +2 -2
  11. data/app/models/i_wonder/snapshot.rb +2 -1
  12. data/app/views/i_wonder/events/index.html.erb +4 -0
  13. data/app/views/i_wonder/metrics/_options_for_custom.html.erb +2 -2
  14. data/app/views/i_wonder/metrics/_options_for_model_counter.html.erb +2 -2
  15. data/app/views/i_wonder/metrics/index.html.erb +3 -1
  16. data/app/views/i_wonder/reports/_form.html.erb +1 -1
  17. data/app/views/i_wonder/reports/_line_report.html.erb +10 -33
  18. data/app/views/i_wonder/reports/_line_report.js.erb +20 -0
  19. data/app/views/layouts/i_wonder.html.erb +4 -0
  20. data/db/migrate/20111022230720_i_wonder_migrations.rb +5 -0
  21. data/lib/i_wonder/configuration.rb +3 -3
  22. data/lib/i_wonder/logging/middleware.rb +5 -4
  23. data/lib/i_wonder/version.rb +1 -1
  24. data/lib/tasks/i_wonder_tasks.rake +4 -0
  25. data/test/dummy/app/assets/javascripts/application.js +1 -0
  26. data/test/dummy/db/schema.rb +5 -0
  27. data/test/dummy/log/development.log +1941 -0
  28. data/test/dummy/log/test.log +25698 -0
  29. data/test/dummy/tmp/cache/assets/D8E/250/sprockets%2Fefe57e932e1d26c3b6af42a5311f0a1c +0 -0
  30. data/test/dummy/tmp/cache/assets/DA7/BA0/sprockets%2F463a9dd811f2418d96fa72cfe05fb8cd +0 -0
  31. data/test/integration/i_wonder/reports_controller_test.rb +3 -3
  32. data/test/unit/i_wonder/event_test.rb +25 -0
  33. data/test/unit/i_wonder/metric_collection_test.rb +53 -29
  34. data/test/unit/i_wonder/metric_creation_test.rb +9 -7
  35. data/test/unit/i_wonder/report_test.rb +9 -9
  36. metadata +22 -23
  37. data/test/dummy/tmp/pids/server.pid +0 -1
@@ -54,29 +54,6 @@ Add the engine to your routes. This is where the dashboard will be hosted
54
54
  mount IWonder::Engine => "/super_analytics_engine"
55
55
  end
56
56
 
57
- This engine will use your standard application layout (unless you set a different layout in the configurations explained later). Whichever layout you use, you
58
- must include a yield in the HTML header so the CSS and Javascript can be loaded.
59
-
60
- <%= yield :extra_scripts_and_styles %>
61
-
62
- This engine also assumes that your layout will handle the flash notifications.
63
-
64
- ===== Example application layout:
65
-
66
- <html>
67
- <head>
68
-
69
- ... your javascript/css/meta/header stuff here ...
70
-
71
- <%= yield :extra_scripts_and_styles %>
72
- </head>
73
- <body>
74
- <%= notifications %>
75
- <%= yield %>
76
- </body>
77
- </html
78
-
79
-
80
57
  === Setting up the hourly cron task
81
58
 
82
59
  This gem hooks into the "<code>rake cron</code>" request. It can't take snapshots of your metrics any faster than this cron is run. We recommend every hour.
@@ -103,7 +80,7 @@ You can choose to navigate directly to the dashboard. If you want to include a l
103
80
 
104
81
  If you want to configure this gem, add this to an initializer.
105
82
 
106
- IWonder.configuration do |c|
83
+ IWonder.configure do |c|
107
84
  c.controllers_to_ignore = [] # These are all the controllers which don't log any events (i_wonder is also ignored)
108
85
  c.only_log_hits_on_200 = true # By default hits and new visitors won't be logged on anything but a 200 or 304 (not modified). Your custom events will still be recorded.
109
86
  c.back_to_app_link = "/" # this is the link hitting the home button in IWonder will take you to.
@@ -5,5 +5,6 @@
5
5
  // the compiled file.
6
6
  //
7
7
  //= require jquery
8
+ //= require jquery-ui
8
9
  //= require jquery_ujs
9
10
  //= require_tree .
@@ -1,4 +1,14 @@
1
1
  // Place all the behaviors and hooks related to the matching controller here.
2
2
  // All this logic will automatically be available in application.js.
3
+ //= require highcharts
3
4
  //= require_self
4
- //= require highcharts
5
+
6
+
7
+ $(function(){
8
+ $("#chart_holder .calendar").datepicker({ dateFormat: 'yy/mm/dd' });
9
+ $("#ui-datepicker-div").hide(); // this shouldn't be needed, but there seems to be a bug
10
+
11
+ $("#chart_holder #chart_options input").change(function(){
12
+ $(this).closest("form").submit();
13
+ });
14
+ });
@@ -13,6 +13,12 @@ $pale_red: #ffd2d2;
13
13
  box-shadow:$x_offset $y_offset $size $color;
14
14
  }
15
15
 
16
+ @mixin inset_shadow($x_offset: 0px, $y_offset: 1px, $size: 8px, $color: rgba(0, 0, 0, 0.5)) {
17
+ -webkit-box-shadow:$x_offset $y_offset $size $color inset;
18
+ -moz-box-shadow:$x_offset $y_offset $size $color inset;
19
+ box-shadow:$x_offset $y_offset $size $color inset;
20
+ }
21
+
16
22
  @mixin gradiant ($color_a:#aaa, $color_b:#bbb) {
17
23
  position:relative;
18
24
  background-image: -webkit-linear-gradient(top, $color_a, $color_b); /* Chrome 10+, Saf5.1+ */
@@ -30,6 +30,8 @@ html, body { background-color: #e5e5e5; font-family: Verdana, Helvetica, Arial;
30
30
  }
31
31
  #container { padding: 20px 40px; }
32
32
 
33
+ p.description { font-size:90%; color:#555;}
34
+
33
35
  p.empty_list {color:#666; }
34
36
 
35
37
  ul.primary_list { padding:0px; margin:10px 0; border:1px solid $light_blue_box_border; border-bottom:none; background-color:$light_blue_box_background;
@@ -45,11 +47,6 @@ html, body { background-color: #e5e5e5; font-family: Verdana, Helvetica, Arial;
45
47
  }
46
48
  }
47
49
  }
48
-
49
- .show {
50
- p.description { font-size:90%; color:#555;}
51
- }
52
-
53
50
 
54
51
  form.main_form { background-color:$light_blue_box_background; border:1px solid $light_blue_box_border; padding:10px 15px; display:inline-block;
55
52
  h3 {margin:0 0 10px 0;}
@@ -16,4 +16,11 @@ form.report {
16
16
  .report_metric_ids { padding:0px; margin:10px 0;
17
17
  li {padding:0px; margin:0px; list-style:none;}
18
18
  }
19
+ }
20
+
21
+ #chart_holder {background-color:#f0f1fa; padding:8px; border:1px solid #ddd; @include rounded_corners(4px); @include inset_shadow(1px, 1px, 3px, rgba(0,0,0,0.3));
22
+ #chart_options {
23
+ label {color:#555; font-size:90%; font-weight:bold;}
24
+ }
25
+ #iw_chart_container {background-color:#fff; margin-top:5px; border:1px solid #ddd; @include shadow(1px, 1px, 3px, rgba(0,0,0,0.3));}
19
26
  }
@@ -9,9 +9,20 @@ module IWonder
9
9
  def show
10
10
  @report = Report.find(params[:id])
11
11
 
12
- @start_time = Time.zone.now - 30.hours #(params[:start_time] || @report.default_start_time)
13
- @end_time = (params[:end_time] || Time.zone.now)
14
- @interval_length = 1.hour
12
+ @start_time = (params[:start_time] ? Time.zone.parse(params[:start_time]) : Time.zone.now - 1.month)
13
+ @end_time = (params[:end_time] ? Time.zone.parse(params[:end_time]) : Time.zone.now)
14
+ @interval_length = default_interval_length
15
+
16
+ respond_to {|format|
17
+ format.html { }
18
+ format.js {
19
+ if @report.line?
20
+ render :partial => "line_report.js"
21
+ else
22
+ render :text => "?"
23
+ end
24
+ }
25
+ }
15
26
  end
16
27
 
17
28
  def new
@@ -49,5 +60,27 @@ module IWonder
49
60
  redirect_to reports_path, :notice => "Report has been destroyed"
50
61
  end
51
62
 
63
+ protected
64
+
65
+ def default_interval_length
66
+ length = @end_time - @start_time
67
+
68
+ if length > 2.months
69
+ interval_length = 1.week
70
+ elsif length > 1.week
71
+ interval_length = 1.days
72
+ else
73
+ interval_length = 1.hours
74
+ end
75
+
76
+ # can't show a smaller frequency than the snampshots get taken in.
77
+ longest_metric_frequency = @report.metrics.collect(&:frequency).max
78
+ if longest_metric_frequency > interval_length
79
+ interval_length = longest_metric_frequency
80
+ end
81
+
82
+ return interval_length
83
+ end
84
+
52
85
  end
53
86
  end
@@ -7,16 +7,28 @@ module IWonder
7
7
 
8
8
  class << self
9
9
  def merge_session_to_user(session_id, user_id)
10
- update_all("user_id = '#{user_id}'", "session_id = '#{session_id}' AND user_id IS NULL")
10
+ # grab the session_id off of the new_visitor attached to that user
11
+ new_visitor_event = Event.where(:user_id => user_id, :event_type => "new_visitor").first
12
+ original_session_id = (new_visitor_event ? new_visitor_event.session_id : session_id)
11
13
 
12
- # TODO: remove all, but the earliest new_visit event
14
+
15
+ # for all events on the current session, attach the user and session from that first event
16
+ update_all({:user_id => user_id, :session_id => original_session_id}, ["session_id = ? AND user_id IS NULL", session_id])
17
+
18
+ # clear our any new_visitor events other than the first one
19
+ if new_visitor_event and original_session_id != session_id
20
+ Event.where(:user_id => user_id, :event_type => "new_visitor").where("id <> ?", new_visitor_event.id).delete_all
21
+ end
22
+
23
+
24
+ return original_session_id
13
25
  end
14
26
  # handle_asynchronously :merge_session_to_user
15
27
 
16
28
 
17
29
  def fast_create(attr_hash)
18
- attr_hash[:created_at] = Time.now.utc
19
- attr_hash[:updated_at] = Time.now.utc
30
+ attr_hash[:created_at] = Time.zone.now.utc
31
+ attr_hash[:updated_at] = Time.zone.now.utc
20
32
 
21
33
  keys = [:event_type, :account_id, :user_id, :session_id, :controller, :action, :extra_details, :remote_ip, :user_agent, :referrer, :created_at, :updated_at]
22
34
  key_str = keys.collect(&:to_s).join(", ")
@@ -54,8 +66,6 @@ module IWonder
54
66
  }.first
55
67
  end
56
68
 
57
-
58
-
59
69
  end
60
70
  end
61
71
  end
@@ -1,7 +1,10 @@
1
1
  module IWonder
2
2
  class Metric < ActiveRecord::Base
3
- attr_accessible :name, :frequency, :archived, :collection_method, :back_date_30_snapshots
4
- attr_writer :back_date_30_snapshots
3
+ BACK_DATE_ITERATIONS = 30
4
+
5
+ attr_accessible :name, :frequency, :archived, :collection_method, :back_date_snapshots
6
+ attr_accessor :back_date_snapshots
7
+ attr_writer :back_date_snapshots
5
8
 
6
9
  serialize :options, Hash
7
10
  attr_accessible :collection_type, :combination_rule, :takes_snapshots
@@ -20,9 +23,10 @@ module IWonder
20
23
  has_many :reports, :through => :report_memberships
21
24
  has_many :snapshots do
22
25
  def most_recent
23
- order("created_at DESC").first
26
+ order("end_time DESC").first
24
27
  end
25
28
  end
29
+ accepts_nested_attributes_for :snapshots, :allow_destroy => true
26
30
 
27
31
  # ==============================================================================================================================
28
32
  # The following methods are for creating the metrics with the coorect values ===================================================
@@ -55,6 +59,14 @@ module IWonder
55
59
  true # this preventing it from thinking the validation failed
56
60
  end
57
61
 
62
+ before_validation :remove_existing_snapshots
63
+ def remove_existing_snapshots
64
+ if collection_method_changed? or frequency_changed?
65
+ self.earliest_measurement = nil
66
+ self.snapshots.each(&:mark_for_destruction)
67
+ end
68
+ end
69
+
58
70
  validate :avoid_dangerous_words
59
71
  def avoid_dangerous_words
60
72
  if collection_method.present? and collection_method.gsub(QUOTE_REMOVER, "") =~ DANGEROUS_WORDS
@@ -82,6 +94,11 @@ module IWonder
82
94
  }
83
95
  end
84
96
  end
97
+
98
+ if self.model_counter_method == "Creation Rate" and !self.model_counter_class.constantize.new.respond_to?("created_at")
99
+ errors.add(:base, "Can't calculate creation rate on models without a :created_at column")
100
+ end
101
+
85
102
  rescue Exception => e
86
103
  errors.add(:model_counter_class, "is not a valid class")
87
104
  end
@@ -117,10 +134,19 @@ module IWonder
117
134
  query += model_counter_scopes.dup
118
135
  end
119
136
 
137
+ #TODO: these queries should be scoped under the table name
120
138
  if model_counter_method == "Creation Rate"
139
+ self.combination_rule = "sum"
121
140
  self.collection_method = "#{query}.where(\"created_at >= ? AND created_at < ?\", start_time, end_time).count"
122
141
  else # total numbers
123
- self.collection_method = "#{query}.count"
142
+
143
+ self.combination_rule = "average"
144
+ if self.model_counter_class.constantize.new.respond_to?("created_at")
145
+ # this is more accurate for guessing backwards
146
+ self.collection_method = "#{query}.where(\"created_at < ?\", end_time).count"
147
+ else
148
+ self.collection_method = "#{query}.count"
149
+ end
124
150
  end
125
151
  end
126
152
 
@@ -145,7 +171,7 @@ module IWonder
145
171
  # returns a hash with all the key values between the two times. If it has been collecting integers, the key will be the name of the metric
146
172
  def value_from(start_time, end_time)
147
173
  if takes_snapshots?
148
- data = self.snapshots.where("created_at >= ? and created_at < ?", start_time, end_time).collect(&:data)
174
+ data = self.snapshots.where("start_time < ? AND end_time > ?", end_time, start_time).collect(&:data)
149
175
  else
150
176
  data = [run_collection_method_from(start_time, end_time)]
151
177
  end
@@ -177,16 +203,31 @@ module IWonder
177
203
  #TODO: some code to avoid overlap in snapshots needs to be added
178
204
  def take_snapshot
179
205
  start_time, end_time = timeframe_for_next_snapshot
180
- self.snapshots.create(:data => run_collection_method_from(start_time, end_time))
206
+ while end_time <= Time.zone.now do
207
+ self.snapshots.create(:data => run_collection_method_from(start_time, end_time), :start_time => start_time, :end_time => end_time)
181
208
 
182
- if self.earliest_measurement.nil?
183
- self.update_attribute(:earliest_measurement, start_time)
209
+ if self.earliest_measurement.nil? or self.earliest_measurement > start_time
210
+ self.update_attribute(:earliest_measurement, start_time)
211
+ end
212
+
213
+ start_time, end_time = timeframe_for_next_snapshot
184
214
  end
185
215
  end
186
216
 
187
217
  after_save :back_date_if_chosen
188
218
  def back_date_if_chosen
189
- if @back_date_30_snapshots
219
+ if @back_date_snapshots and @back_date_snapshots.to_s =~ /1|true|on/i and takes_snapshots? and self.earliest_measurement.nil?
220
+
221
+ self.reload # this keeps bad variables and change states from sneaking in
222
+
223
+ start_time = Time.zone.now - BACK_DATE_ITERATIONS * frequency
224
+ end_time = start_time + frequency
225
+ start_time += 1.second
226
+ self.snapshots.create(:data => run_collection_method_from(start_time, end_time), :start_time => start_time, :end_time => end_time)
227
+ self.update_attribute(:earliest_measurement, start_time)
228
+
229
+ # not that the first snapshot is taken, running the :take_snapshot command will fill in the rest
230
+ take_snapshot
190
231
  end
191
232
  end
192
233
 
@@ -210,13 +251,15 @@ module IWonder
210
251
  def timeframe_for_next_snapshot
211
252
  recent = self.snapshots.most_recent
212
253
  if recent.present?
213
- start_at = recent.created_at
214
- end_at = start_at + frequency
254
+ start_time = recent.end_time
255
+ end_time = start_time + frequency
256
+ start_time += 1.second # this avoids overlap with the previous snapshot
215
257
  else
216
- start_at = Time.zone.now - frequency
217
- end_at = Time.zone.now
258
+ start_time = Time.zone.now - frequency
259
+ end_time = Time.zone.now
218
260
  end
219
- [start_at, end_at]
261
+
262
+ [start_time, end_time]
220
263
  end
221
264
 
222
265
  end
@@ -67,7 +67,7 @@ module IWonder
67
67
  master_hashes_array << master_hash_for_time_slice
68
68
 
69
69
  time_iterator += interval_length
70
- end while time_iterator <= end_time
70
+ end while (time_iterator + interval_length) <= end_time
71
71
 
72
72
  # set the value to zero for any
73
73
  master_hashes_array.each{|hash_for_slice_of_time|
@@ -77,7 +77,7 @@ module IWonder
77
77
  }
78
78
 
79
79
  keys_list.collect{|key|
80
- {:name => key, :pointInterval => interval_length*1000, :data => master_hashes_array.collect{|mha| mha[key] }}
80
+ {:name => key, :pointStart => start_time.to_i * 1000, :pointInterval => interval_length*1000, :data => master_hashes_array.collect{|mha| mha[key] }}
81
81
  }
82
82
  end
83
83
 
@@ -1,6 +1,6 @@
1
1
  module IWonder
2
2
  class Snapshot < ActiveRecord::Base
3
- attr_accessible :data
3
+ attr_accessible :data, :start_time, :end_time
4
4
  serialize :complex_data, Hash
5
5
 
6
6
  belongs_to :metric
@@ -28,5 +28,6 @@ module IWonder
28
28
  def complex?
29
29
  self.complex_data.present?
30
30
  end
31
+
31
32
  end
32
33
  end
@@ -1,5 +1,9 @@
1
1
  <h1>Events</h1>
2
2
 
3
+ <p class="description">
4
+ These are raw event details. To see them in a graph, create a metric to track them and a report to display the metric.
5
+ </p>
6
+
3
7
  <ul id="event_list" class="primary_list">
4
8
  <% for type_group in @groups do %>
5
9
  <li>
@@ -28,8 +28,8 @@
28
28
  </div>
29
29
 
30
30
  <div class="field">
31
- <%= f.check_box :back_date_30_snapshots%>
32
- <%= f.label :back_date_30_snapshots, "Try and guess values for the last 30 snapshots?" %>
31
+ <%= f.check_box :back_date_snapshots%>
32
+ <%= f.label :back_date_snapshots, "Try and guess values for the last 30 snapshots?" %>
33
33
  </div>
34
34
 
35
35
  </div>
@@ -26,8 +26,8 @@
26
26
  </div>
27
27
 
28
28
  <div class="field">
29
- <%= f.check_box :back_date_30_snapshots%>
30
- <%= f.label :back_date_30_snapshots, "Try and guess values for the last 30 snapshots?" %>
29
+ <%= f.check_box :back_date_snapshots%>
30
+ <%= f.label :back_date_snapshots, "Try and guess values for the last 30 snapshots?" %>
31
31
  </div>
32
32
 
33
33
  </div>
@@ -1,5 +1,7 @@
1
1
  <h1>Metrics</h1>
2
- <h3>What are we tracking?</h3>
2
+ <p class="description">
3
+ Metrics are anything you want to track. They can be set up to monitor pretty much anything. To view the data collected, try creating a report for the metrics you wish to display.
4
+ </p>
3
5
 
4
6
  <% if @metrics.empty? %>
5
7
  <p class="empty_list">You don't have any metrics</p>
@@ -22,7 +22,7 @@
22
22
 
23
23
  <div class="field">
24
24
  <%= f.label :report_type %>
25
- <%= f.select :report_type, {"Line" => {:value => "Line"}, "Bar" => {:disabled => true}, "AB Test" => {:disabled => true}} %>
25
+ <%= f.select :report_type, {"Line" => {:value => "Line"}, "Bar" => {:disabled => true}, "Pie" => {:disabled => true}} %>
26
26
  </div>
27
27
 
28
28
  <hr/>
@@ -1,38 +1,15 @@
1
- <div id="iw_chart_container"></div>
1
+ <div id="chart_holder">
2
+ <%= form_tag report_path(@report), :method => :get, :remote => true, :id => "chart_options" do %>
3
+ <%= label_tag :start_time, "Range" %>
4
+ <%= text_field_tag :start_time, @start_time.strftime("%Y/%m/%d"), :size => 12, :class => "calendar" %>
5
+ -
6
+ <%= text_field_tag :end_time, @end_time.strftime("%Y/%m/%d"), :size => 12, :class => "calendar" %>
7
+ <% end %>
8
+ <div id="iw_chart_container"></div>
9
+ </div>
2
10
 
3
11
  <script type="text/javascript" charset="utf-8">
4
12
  $(function() {
5
- <%
6
- options ||= {}
7
- options[:chart] ||= {}
8
- options[:chart][:renderTo] = "iw_chart_container";
9
-
10
- options[:title] ||= { :text => "I wonder "+@report.name }
11
-
12
- options[:series] = @report.collect_series_data(@start_time, @end_time, @interval_length);
13
-
14
- # options[:chart][:defaultSeriesType] =
15
- #
16
- options[:credits] = false
17
- #
18
- options[:xAxis] = {}
19
- # new_options[:xAxis][:categories] = options[:x_axis_categories]
20
- options[:xAxis][:dateTimeLabelFormats] ||= { :day => '%b %e', :hour => '%b %e' }
21
- options[:xAxis][:type] ||= 'datetime'
22
- #
23
- # new_options[:series] = options[:series]
24
- #
25
- # new_options[:yAxis] = (options[:yAxis] || {})
26
- # new_options[:yAxis][:allow_decimals] = (options[:y_axis_allow_decimals] ? 'true' : 'false')
27
- # new_options[:yAxis][:min] = (options[:y_axis_min] || 0)
28
- # new_options[:yAxis][:title] = { :text => options[:y_axis_title] } if options[:y_axis_title].present?
29
- #
30
- # new_options[:legend] = (options[:legend] || { :enabled => false })
31
- #
32
-
33
- #
34
- # new_options[:tooltip] = options[:tooltip] if options[:tooltip].present?
35
- %>
36
- var chart = new Highcharts.Chart(<%= raw options.to_json %>);
13
+ <%= render :partial => "line_report.js" %>
37
14
  });
38
15
  </script>