i_wonder 0.0.1 → 0.0.3

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