duly_noted 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/DOCS.md CHANGED
@@ -1,58 +1,74 @@
1
1
  ##Dependency
2
2
  * Redis
3
3
 
4
- The **DulyNoted** module contains four main methods:
4
+ The **DulyNoted** module contains five main methods:
5
5
 
6
6
  * `track`
7
7
  * `update`
8
8
  * `query`
9
9
  * `count`
10
+ * `chart`
10
11
 
11
- ##Track
12
+ ##Parameter Descriptions
13
+ `metric_name`: The name of the metric to track, ex: `page_views`, `downloads`
12
14
 
13
- _parameters: `metric_name`, `for`(optional), `generated_at`(optional), `meta`(optional), `ref_id`(optional)_
15
+ `for`: A name space for your metric, ex: `home_page`
16
+ *New in v1.0.0*: `for` can be an array of nested contexts. For example, say you had users, and users had videos, and you wanted to track plays. Your `for` might look like `["user_1", "video_6"]`. Now when you're doing `count`s or `quer`ies, you can specify just `for: "user_1"` to get all of the plays for user_1's videos, or you can specify `for: ["user_1", "video_6"]` to get just that video's plays. It is important to note that `for`s are nested, so you cannot ask for a count `for: "video_6"`, it must always be referenced through `user_1`.
14
17
 
15
- `metric_name`: The name of the metric to track, ex: `page_views`, `downloads`
18
+ `generated_at`: If the metric was generated in the past but is just now being logged, you can set the time it was generated at
16
19
 
17
- `for`_(optional)_: A name space for your metric, ex: `home_page`
20
+ `meta`: A hash with whatever meta data fields you might want to store, ex: `ip_address`, `file_type`
18
21
 
19
- `generated_at`_(optional)_: If the metric was generated in the past but is just now being logged, you can set the time it was generated at
22
+ `meta_fields`: An array of fields to retrieve from the meta hash. If not specified, the entire hash will be grabbed. Fields will be converted to strings, because redis converts all hash keys and values to strings.
20
23
 
21
- `meta`_(optional)_: A hash with whatever meta data fields you might want to store, ex: `ip_address`, `file_type`
24
+ `ref_id`: If you need to reference the metric later, perhaps to add more metadata later on, you can set a reference id that you can use to update the metric. The `ref_id` must be unique across `metric_name`s.
22
25
 
23
- `ref_id`_(optional)_: If you need to reference the metric later, perhaps to add more metadata later on, you can set a reference id that you can use to update the metric
26
+ `editable_for`: If you want to clear `ref_id`s out, you can set the metric to only be editable for a certain amount of time. `editable_for` is defined in seconds. After that amount of time, you will no longer be able to edit the meta data, and you may use that `ref_id` again. By default, `ref_id`s never expire.
24
27
 
25
- ##Update
28
+ `time_start`: The start of the time range to grab the data from. **Important:** `time_start` should always be the time farthest in the past.
29
+
30
+ `time_end`: The end of the time range to grab the data from. **Important:** `time_end` should always be the time closest to the present.
26
31
 
27
- _parameters: `metric_name`, `ref_id`, `for`(required if set when created), `meta`(optional)_
32
+ `time_range`: A Range object made up of two Time objects. The beginning of the Range should be farthest in the past, and the end of the range should be closest to the present. If `time_range` is defined, `time_end` and `time_start` do not need to be.
28
33
 
29
- The meta hash will not overwrite the old meta hash but be merged with it, with the new one overwriting conflicts.
30
34
 
31
- `metric_name`: The name of the metric to update, ex: `page_views`, `downloads`
32
35
 
33
- `ref_id`: The reference ID that you set when you called `track`
36
+ ##Track
34
37
 
35
- `for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
38
+ _parameters: `metric_name`, `for`(optional), `generated_at`(optional), `meta`(optional), `ref_id`(optional), `editable_for`(optional)_
36
39
 
37
- `meta`_(optional)_: A hash with whatever meta data fields you might want to store, or update ex: `ip_address`, `file_type`, `time_left`
40
+ Use track to track an event, like a page view, or a download. Use the `for` option to give an event a context. For instance, for page views, you might set `for` to `home_page`, so that you know which page was viewed. You can also store metadata along with your metric with the `meta` hash. If you need to update that `meta` hash at a later time, you can set a `ref_id`, which can be used to tell `#update` exactly which metric you want to update. `ref_id`s have to be unique across `metric_name`s, and if you want to free up your `ref_id`s, you can set them to expire after a certain number of seconds with `editable_for`.
38
41
 
39
42
  ###Usage
40
43
 
41
- DulyNoted.update("page_views", "a_unique_id", for: "home_page", meta: { time_on_page: 30 })
44
+ DulyNoted.track("page_views", for: "home", meta: {browser: "chrome"})
45
+
46
+ DulyNoted.track("video_plays", for: ["user_7261", "video_917216"], meta: {amount_watched: 0})
47
+
48
+ DulyNoted.track("purchases", for: "user_281", generated_at: 1.day.ago, ref_id: "pid_28172", editable_for: 30)
42
49
 
43
- ##Query
44
50
 
45
- _parameters: `metric_name`, `for`(required if set when created), `time_start`(optional), `time_end`(optional)_
46
51
 
47
- Query will return an array of all the metadata in chronological order from a time range, or for the whole data set.
52
+ ##Update
48
53
 
49
- `metric_name`: The name of the metric to query, ex: `page_views`, `downloads`
54
+ _parameters: `metric_name`, `ref_id`, `meta`(optional), `editable_for`(optional)_
50
55
 
51
- `for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
56
+ Use update to add, or edit the metadata stored with a metric. You can optionally set the `editable_for` option which will override any setting set by track. So if it was set to expire in 30 seconds, and in 20 seconds you called update with `editable_for` set to `30`, it would be editable for 30 seconds from the moment you updated it.
57
+
58
+ ###Usage
52
59
 
53
- `time_start`_(optional)_: The start of the time range to grab the data from.
60
+ DulyNoted.track("page_views", ref_id: "a_unique_id", meta: {time_on_page: 0, browser: "chrome"})
61
+
62
+ DulyNoted.update("page_views", "a_unique_id", meta: { time_on_page: 30 }, editable_for: 30)
63
+
64
+
65
+
66
+ ##Query
67
+
68
+ _parameters: `metric_name`, `for`(optional), `meta_fields`(optional), `time_start`(optional), `time_end`(optional), `time_range`(optional)_
69
+
70
+ Query will return an array of all the metadata in chronological order from a time range, or for the whole data set. If for is specified, it will limit it by that context. For instance, if you have `track`ed several page views with `for` set to the name of the page that was viewed, you could query with `for` set to `home_page` to get all of the metadata from the page views from the home page, or you could leave off the `for`, and return all of the metadata for all of the page views, across all pages.
54
71
 
55
- `time_end`_(optional)_: The end of the time range to grab the data from.
56
72
 
57
73
  ###Usage
58
74
 
@@ -63,17 +79,9 @@ Query will return an array of all the metadata in chronological order from a tim
63
79
 
64
80
  ##Count
65
81
 
66
- _parameters: `metric_name`, `for`(required if set when created), `time_start`(optional), `time_end`(optional)_
67
-
68
- Count will return the number of events logged in a given time range, or if no time range is given, the total count.
69
-
70
- `metric_name`: The name of the metric to query, ex: `page_views`, `downloads`
71
-
72
- `for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
82
+ _parameters: `metric_name`, `for`(optional), `time_start`(optional), `time_end`(optional), `time_range`(optional)_
73
83
 
74
- `time_start`_(optional)_: The start of the time range to grab the data from.
75
-
76
- `time_end`_(optional)_: The end of the time range to grab the data from.
84
+ Count will return the number of events logged in a given time range, or if no time range is given, the total count. As with `#query`, you can specify `for` to return a subset of counts, or you can leave it off to get the count across the whole `metric_name`.
77
85
 
78
86
  ###Usage
79
87
 
@@ -81,6 +89,62 @@ Count will return the number of events logged in a given time range, or if no ti
81
89
  for: "home_page",
82
90
  time_start: 1.day.ago,
83
91
  time_end: Time.now)
92
+
93
+
94
+ ##Chart
95
+
96
+ _parameters: `metric_name`, `data_points`(required),`for`(optional), `time_start`(optional), `time_end`(optional), `time_range`(optional)_
97
+
98
+ Chart is a little complex, but I'll try to explain all of the possibilities. It's main purpose is to pull out your data and prepare it in a way that makes it easy to chart. The smallest amount of input it will take is just a `metric_name` and an amount of `data_points` to capture. This will check the time of the earliest known data point, and the time of the last known data point, and run chart with those values as the `time_start` and `time_end` respectively. It will take the amount of time that that spans, and divide it by the number of data points you asked for, and will split the time up evenly, and return a hash of times, and counts. If you specify both a `time_start` and a `time_end`, and a number of `data_points`, then it will divide the amount of time that that spans and will return a hash of times and counts. The other option is that you can specify either a `time_start` OR a `time_end` and a `step` and a number of `data_points`. This will start at whatever time you specified, and (if it's `time_end`) count down by the step (if you specified `time_start`, it would count up), as many times as the number of data points you requested.
99
+
100
+ ###Usage
101
+
102
+ DulyNoted.chart("page_views",
103
+ :time_range => 1.month.ago..Time.now,
104
+ :step => 1.day)
105
+
106
+ DulyNoted.chart("page_views",
107
+ :time_range => 1.day.ago..Time.now,
108
+ :data_points => 12)
109
+
110
+ DulyNoted.chart("page_views",
111
+ :time_start => 1.day.ago,
112
+ :step => 1.hour,
113
+ :data_points => 12)
114
+
115
+ DulyNoted.chart("downloads",
116
+ :time_end => Time.now,
117
+ :step => 1.month,
118
+ :data_points => 12)
119
+
120
+ DulyNoted.chart("page_views",
121
+ :data_points => 100)
122
+
123
+
124
+ Chart can be a little confusing but it's pretty powerful, so play around with it.
125
+
126
+ ##Magic
127
+
128
+ ###count_x_by_y
129
+
130
+ If you want to count a number of events by a meta field, you can use this magic command. So imagine this scenario:
131
+
132
+ DulyNoted.track("page_views", meta: {browser: "chrome"})
133
+
134
+ And you wanted to see a break down of page views by various browsers, you can call `DulyNoted.count_page_views_by_browser` and you'd get a hash that looked something like this:
135
+
136
+ {"chrome" => 2913, "firefox" => 5281, "IE" => 7182, "safari" => 3213}
137
+
138
+ So that method will work as soon as you've tracked something with that metric name. If you try to call the method on a metric that you haven't yet tracked you will get a `DulyNoted::NotValidMetric`. But if you reference a meta field that didn't exist, you'd just get a hash that looks like
139
+
140
+ {nil => 1}
141
+
142
+ ##Behind the curtain (metaprogramming)
143
+
144
+ ###method_missing
145
+
146
+ As I'm sure you're aware, method_missing is the magic tool for ruby developers to define dynamic methods like the above `count_x_by_y`, which is exactly what we use it for.
147
+
84
148
 
85
149
  ##Redis
86
150
 
data/Guardfile CHANGED
@@ -9,5 +9,6 @@ end
9
9
 
10
10
  guard :bundler do
11
11
  watch('Gemfile')
12
+ watch('*.gemspec')
12
13
  end
13
14
 
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ _Currently being updated for v1.0.0..._
2
+
1
3
  #Duly Noted
2
4
  [![Build Status](https://secure.travis-ci.org/willcosgrove/duly_noted.png?branch=master)](http://travis-ci.org/willcosgrove/duly_noted)
3
5
 
@@ -27,6 +29,18 @@ You can also just specify a `time_range` like so:
27
29
 
28
30
  This will return the page view count for the home page for the past day.
29
31
 
32
+ ##Install
33
+
34
+ You probably already guessed it, but to install just
35
+
36
+ gem install duly_noted
37
+
38
+ or add
39
+
40
+ gem 'duly_noted'
41
+
42
+ to your `gemfile` and run `bundle install`
43
+
30
44
  ##What's New
31
45
 
32
46
  ### 0.1.0
data/duly_noted.gemspec CHANGED
@@ -4,8 +4,8 @@ require File.expand_path('../lib/duly_noted/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Will Cosgrove"]
6
6
  gem.email = ["will@willcosgrove.com"]
7
- gem.description = %q{keep detailed metrics on your project with a speedy, powerful redis backend.}
8
- gem.summary = %q{a simple redis based stat-tracker}
7
+ gem.description = %q{a simple redis based stat-tracker}
8
+ gem.summary = %q{keep detailed metrics on your project with a speedy, powerful redis backend.}
9
9
  gem.homepage = "http://github.com/willcosgrove/duly_noted"
10
10
 
11
11
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -19,7 +19,9 @@ Gem::Specification.new do |gem|
19
19
  gem.add_development_dependency("rake")
20
20
  gem.add_development_dependency("rb-fsevent")
21
21
  gem.add_development_dependency("guard-rspec")
22
- gem.add_development_dependency("growl")
22
+ # gem.add_development_dependency("growl")
23
23
  gem.add_development_dependency("guard-bundler")
24
24
  gem.add_development_dependency("chronic")
25
+ gem.add_development_dependency("timecop")
26
+ gem.add_development_dependency("ruby_gntp")
25
27
  end
@@ -1,14 +1,60 @@
1
1
  module DulyNoted
2
2
  module Helpers
3
+
4
+ def build_key(str, validity_test=true)
5
+ if validity_test
6
+ raise NotValidMetric if !valid_metric?(str) && !(caller[0] =~ /track/)
7
+ end
8
+ return "dn:" + normalize(str)
9
+ end
10
+
3
11
  def normalize(str)
4
12
  str.downcase.gsub(/[^a-z0-9 ]/i, '').strip
5
13
  end
6
14
 
15
+ def assemble_for(options)
16
+ case
17
+ when options[:for].is_a?(String)
18
+ ":#{normalize(options[:for])}"
19
+ when options[:for].is_a?(Array)
20
+ ":" << options[:for].collect{ |x| normalize(x) }.join(":")
21
+ else
22
+ ""
23
+ end
24
+ end
25
+
26
+ def find_keys(key)
27
+ keys = []
28
+ keys += DulyNoted.redis.keys("#{key}*")
29
+ keys -= DulyNoted.redis.keys("#{key}:*:meta")
30
+ keys -= DulyNoted.redis.keys("#{key}:ref:*")
31
+ keys -= DulyNoted.redis.keys("#{key}*fields")
32
+ end
33
+
7
34
  def parse_time_range(options)
8
35
  if options[:time_range]
9
36
  options[:time_start] = options[:time_range].first
10
37
  options[:time_end] = options[:time_range].last
11
38
  end
12
39
  end
40
+
41
+ def metrics
42
+ DulyNoted.redis.smembers build_key("metrics", false)
43
+ end
44
+
45
+ def valid_metric?(metric_name)
46
+ DulyNoted.redis.sismember build_key("metrics", false), build_key(metric_name, false)
47
+ end
48
+
49
+ def fields_for(metric_name, options={})
50
+ key = build_key(metric_name)
51
+ key << assemble_for(options)
52
+ keys = find_keys(key)
53
+ fields = []
54
+ keys.each do |key|
55
+ fields += DulyNoted.redis.smembers("#{key}:fields")
56
+ end
57
+ fields
58
+ end
13
59
  end
14
60
  end
@@ -0,0 +1,30 @@
1
+ require "duly_noted/version"
2
+
3
+ module DulyNoted
4
+ module Updater
5
+ def update_schema(schema_version, redis)
6
+ # magic updating!
7
+ redis.set "dn:version", VERSION
8
+ puts "All up to date"
9
+ return true
10
+ end
11
+
12
+ def check_schema(redis)
13
+ schema_version = redis.get "dn:version"
14
+ if !schema_version.nil?
15
+ schema_version = schema_version.split(".")
16
+ current_version = VERSION.split(".")
17
+ if schema_version[0] != current_version[0]
18
+ puts "Your duly_noted schema needs to be updated"
19
+ if update_schema(schema_version.join("."), redis)
20
+ check_schema(redis)
21
+ else
22
+ raise UpdateError
23
+ end
24
+ end
25
+ end
26
+ redis.set "dn:version", VERSION
27
+ return redis
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module DulyNoted
2
- VERSION = "0.1.0"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/duly_noted.rb CHANGED
@@ -45,89 +45,120 @@ require "redis"
45
45
  require "uri"
46
46
  require "duly_noted/helpers"
47
47
  require "duly_noted/version"
48
+ require "duly_noted/updater"
49
+
50
+ # The **DulyNoted** module contains five main methods:
48
51
 
49
- # The **DulyNoted** module contains four main methods:
50
- #
51
52
  # * `track`
52
53
  # * `update`
53
54
  # * `query`
54
55
  # * `count`
56
+ # * `chart`
55
57
 
56
58
  module DulyNoted
57
59
  include Helpers
60
+ include Updater
58
61
  extend self # the following are class methods
59
-
60
- # ##Track
61
- #
62
- # _parameters: `metric_name`, `for`(optional), `generated_at`(optional), `meta`(optional), `ref_id`(optional)_
63
- #
64
- # `metric_name`: The name of the metric to track, ex: `page_views`, `downloads`
65
- #
66
- # `for`_(optional)_: A name space for your metric, ex: `home_page`
67
- #
68
- # `generated_at`_(optional)_: If the metric was generated in the past but is just now being logged, you can set the time it was generated at
69
- #
70
- # `meta`_(optional)_: A hash with whatever meta data fields you might want to store, ex: `ip_address`, `file_type`
62
+
63
+ # ##Parameter Descriptions
64
+ # `metric_name`: The name of the metric to track, ex: `page_views`, `downloads`
65
+ #
66
+ # `for`: A name space for your metric, ex: `home_page`
67
+ # *New in v1.0.0*: `for` can be an array of nested contexts. For example, say you had users, and users had videos, and you wanted to track plays. Your `for` might look like `["user_1", "video_6"]`. Now when you're doing `count`s or `quer`ies, you can specify just `for: "user_1"` to get all of the plays for user_1's videos, or you can specify `for: ["user_1", "video_6"]` to get just that video's plays. It is important to note that `for`s are nested, so you cannot ask for a count `for: "video_6"`, it must always be referenced through `user_1`.
68
+ #
69
+ # `generated_at`: If the metric was generated in the past but is just now being logged, you can set the time it was generated at
70
+ #
71
+ # `meta`: A hash with whatever meta data fields you might want to store, ex: `ip_address`, `file_type`
72
+ #
73
+ # `meta_fields`: An array of fields to retrieve from the meta hash. If not specified, the entire hash will be grabbed. Fields will be converted to strings, because redis converts all hash keys and values to strings.
74
+ #
75
+ # `ref_id`: If you need to reference the metric later, perhaps to add more metadata later on, you can set a reference id that you can use to update the metric. The `ref_id` must be unique across `metric_name`s.
76
+ #
77
+ # `editable_for`: If you want to clear `ref_id`s out, you can set the metric to only be editable for a certain amount of time. `editable_for` is defined in seconds. After that amount of time, you will no longer be able to edit the meta data, and you may use that `ref_id` again. By default, `ref_id`s never expire.
78
+ #
79
+ # `time_start`: The start of the time range to grab the data from. **Important:** `time_start` should always be the time farthest in the past.
80
+ #
81
+ # `time_end`: The end of the time range to grab the data from. **Important:** `time_end` should always be the time closest to the present.
82
+ #
83
+ # `time_range`: A Range object made up of two Time objects. The beginning of the Range should be farthest in the past, and the end of the range should be closest to the present. If `time_range` is defined, `time_end` and `time_start` do not need to be.
84
+
85
+ # ##Track
86
+
87
+ # _parameters: `metric_name`, `for`(optional), `generated_at`(optional), `meta`(optional), `ref_id`(optional), `editable_for`(optional)_
88
+
89
+ # Use track to track an event, like a page view, or a download. Use the `for` option
90
+ # to give an event a context. For instance, for page views, you might set `for` to
91
+ # `home_page`, so that you know which page was viewed. You can also store metadata
92
+ # along with your metric with the `meta` hash. If you need to update that `meta` hash
93
+ # at a later time, you can set a `ref_id`, which can be used to tell `#update`
94
+ # exactly which metric you want to update. `ref_id`s have to be unique across
95
+ # `metric_name`s, and if you want to free up your `ref_id`s, you can set them to expire
96
+ # after a certain number of seconds with `editable_for`.
71
97
  #
72
- # `ref_id`_(optional)_: If you need to reference the metric later, perhaps to add more metadata later on, you can set a reference id that you can use to update the metric
98
+ # ###Usage
99
+
100
+ # DulyNoted.track("page_views",
101
+ # for: "home",
102
+ # meta: {browser: "chrome"})
103
+ #
104
+ # DulyNoted.track("video_plays",
105
+ # for: ["user_7261", "video_917216"],
106
+ # meta: {amount_watched: 0})
107
+ #
108
+ # DulyNoted.track("purchases",
109
+ # for: "user_281",
110
+ # generated_at: 1.day.ago,
111
+ # ref_id: "pid_28172",
112
+ # editable_for: 30)
73
113
 
74
114
  def track(metric_name, options={})
75
115
  options = {:generated_at => Time.now}.merge(options)
76
- key = normalize(metric_name)
77
- key << ":#{options[:for]}" if options[:for]
78
- DulyNoted.redis.zadd key, options[:generated_at].to_f, "#{key}:#{options[:generated_at].to_f}:meta"
79
- DulyNoted.redis.set "#{key}:#{options[:ref_id]}", "#{key}:#{options[:generated_at].to_f}:meta" if options[:ref_id] # set alias key
80
- DulyNoted.redis.mapped_hmset "#{key}:#{options[:generated_at].to_f}:meta", options[:meta] if options[:meta] # set meta data
116
+ key = build_key(metric_name)
117
+ key << assemble_for(options)
118
+ DulyNoted.redis.pipelined do
119
+ DulyNoted.redis.sadd build_key("metrics"), build_key(metric_name)
120
+ DulyNoted.redis.zadd key, options[:generated_at].to_f, "#{key}:#{options[:generated_at].to_f}:meta"
121
+ DulyNoted.redis.set "#{build_key(metric_name)}:ref:#{options[:ref_id]}", "#{key}:#{options[:generated_at].to_f}:meta" if options[:ref_id] # set alias key
122
+ DulyNoted.redis.expire "#{build_key(metric_name)}:ref:#{options[:ref_id]}", options[:editable_for] if options[:editable_for]
123
+ if options[:meta] # set meta data
124
+ DulyNoted.redis.mapped_hmset "#{key}:#{options[:generated_at].to_f}:meta", options[:meta]
125
+ options[:meta].keys.each do |field|
126
+ DulyNoted.redis.sadd "#{key}:fields", field
127
+ end
128
+ end
129
+ end
81
130
  end
82
131
 
83
- # ##Update
84
- #
85
- # _parameters: `metric_name`, `ref_id`, `for`(required if set when created), `meta`(optional)_
86
- #
87
- # The meta hash will not overwrite the old meta hash but be merged with it, with the new one overwriting conflicts.
88
- #
89
- # `metric_name`: The name of the metric to track, ex: `page_views`, `downloads`
90
- #
91
- # `ref_id`: The reference ID that you set when you called `track`
132
+ # ##Update
92
133
  #
93
- # `for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
134
+ # _parameters: `metric_name`, `ref_id`, `meta`(optional), `editable_for`(optional)_
94
135
  #
95
- # `meta`_(optional)_: A hash with whatever meta data fields you might want to store, or update ex: `ip_address`, `file_type`, `time_left`
136
+ # Use update to add, or edit the metadata stored with a metric. You can optionally set the `editable_for` option which will override any setting set by track. So if it was set to expire in 30 seconds, and in 20 seconds you called update with `editable_for` set to `30`, it would be editable for 30 seconds from the moment you updated it.
96
137
  #
97
138
  # ###Usage
98
139
  #
140
+ # DulyNoted.track("page_views",
141
+ # ref_id: "a_unique_id",
142
+ # meta: {time_on_page: 0, browser: "chrome"})
99
143
  # DulyNoted.update("page_views",
100
144
  # "a_unique_id",
101
- # for: "home_page",
102
- # meta: { time_on_page: 30 })
145
+ # meta: { time_on_page: 30 },
146
+ # editable_for: 30)
103
147
 
104
148
  def update(metric_name, ref_id, options={})
105
- key = normalize(metric_name)
106
- key << ":#{options[:for]}" if options[:for]
107
- key << ":#{ref_id}"
149
+ key = build_key(metric_name)
150
+ key << ":ref:#{ref_id}"
108
151
  real_key = DulyNoted.redis.get key
152
+ raise InvalidRefId if real_key == nil
109
153
  DulyNoted.redis.mapped_hmset real_key, options[:meta] if options[:meta]
110
154
  end
111
155
 
112
156
  # ##Query
113
157
  #
114
- # _parameters: `metric_name`, `for`(required if set when created), `time_start`(optional), `time_end`(optional)_
115
- #
116
- # Query will return an array of all the metadata in chronological order from a time range, or for the whole data set.
117
- #
118
- # `metric_name`: The name of the metric to query, ex: `page_views`, `downloads`
158
+ # _parameters: `metric_name`, `for`(optional), `meta_fields`(optional), `time_start`(optional), `time_end`(optional), `time_range`(optional)_
119
159
  #
120
- # `for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
121
- #
122
- # `ref_id`: _(optional)_: The reference ID that you set when you called `track` (if you set this, the time restraints is ignored)
123
- #
124
- # `meta_fields` _(optional)_: An array of fields to retrieve from the meta hash. If not specified, the entire hash will be grabbed. Fields will be converted to strings, because redis converts all hash keys and values to strings.
160
+ # Query will return an array of all the metadata in chronological order from a time range, or for the whole data set. If for is specified, it will limit it by that context. For instance, if you have `track`ed several page views with `for` set to the name of the page that was viewed, you could query with `for` set to `home_page` to get all of the metadata from the page views from the home page, or you could leave off the `for`, and return all of the metadata for all of the page views, across all pages.
125
161
  #
126
- # `time_start`_(optional)_: The start of the time range to grab the data from.
127
- #
128
- # `time_end`_(optional)_: The end of the time range to grab the data from.
129
- #
130
- # `time_range _(optional)_: Alternatively you can specify a time range, instead of `time_start` and `time_end`.
131
162
  #
132
163
  # ###Usage
133
164
  #
@@ -135,18 +166,13 @@ module DulyNoted
135
166
  # for: "home_page",
136
167
  # time_start: 1.day.ago,
137
168
  # time_end: Time.now)
138
- #
139
- #
140
- # DulyNoted.query("page_views",
141
- # for: "home_page",
142
- # time_range: 1.day.ago..Time.now)
143
169
 
144
170
  def query(metric_name, options={})
145
- key = normalize(metric_name)
171
+ key = build_key(metric_name)
146
172
  parse_time_range(options)
147
- key << ":#{options[:for]}" if options[:for]
173
+ key << assemble_for(options)
148
174
  if options[:ref_id]
149
- key << ":#{options[:ref_id]}"
175
+ key << ":ref:#{options[:ref_id]}"
150
176
  real_key = DulyNoted.redis.get key
151
177
  if options[:meta_fields]
152
178
  options[:meta_fields].collect! { |x| x.to_s }
@@ -159,6 +185,7 @@ module DulyNoted
159
185
  results = [DulyNoted.redis.hgetall(real_key)]
160
186
  end
161
187
  else
188
+ keys = find_keys(key)
162
189
  grab_results = Proc.new do |metric|
163
190
  if options[:meta_fields]
164
191
  options[:meta_fields].collect! { |x| x.to_s }
@@ -171,10 +198,15 @@ module DulyNoted
171
198
  DulyNoted.redis.hgetall metric
172
199
  end
173
200
  end
201
+ results = []
174
202
  if options[:time_start] && options[:time_end]
175
- results = DulyNoted.redis.zrangebyscore(key, options[:time_start].to_f, options[:time_end].to_f).collect(&grab_results)
203
+ keys.each do |key|
204
+ results += DulyNoted.redis.zrangebyscore(key, options[:time_start].to_f, options[:time_end].to_f).collect(&grab_results)
205
+ end
176
206
  else
177
- results = DulyNoted.redis.zrange(key, 0, -1).collect(&grab_results)
207
+ keys.each do |key|
208
+ results += DulyNoted.redis.zrange(key, 0, -1).collect(&grab_results)
209
+ end
178
210
  end
179
211
  end
180
212
  return results
@@ -182,63 +214,144 @@ module DulyNoted
182
214
 
183
215
  # ##Count
184
216
  #
185
- # _parameters: `metric_name`, `for`(required if set when created), `time_start`(optional), `time_end`(optional)_
186
- #
187
- # Count will return the number of events logged in a given time range, or if no time range is given, the total count.
188
- #
189
- # `metric_name`: The name of the metric to query, ex: `page_views`, `downloads`
217
+ # _parameters: `metric_name`, `for`(optional), `time_start`(optional), `time_end`(optional), `time_range`(optional)_
190
218
  #
191
- # `for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
192
- #
193
- # `time_start`_(optional)_: The start of the time range to grab the data from.
194
- #
195
- # `time_end`_(optional)_: The end of the time range to grab the data from.
196
- #
197
- # `time_range _(optional)_: Alternatively you can specify a time range, instead of `time_start` and `time_end`.
219
+ # Count will return the number of events logged in a given time range, or if no time range is given, the total count. As with `#query`, you can specify `for` to return a subset of counts, or you can leave it off to get the count across the whole `metric_name`.
198
220
  #
199
221
  # ###Usage
200
222
  #
201
223
  # DulyNoted.count("page_views",
202
224
  # for: "home_page",
203
- # time_start: Time.now,
204
- # time_end: 1.day.ago)
205
- #
206
- #
207
- # DulyNoted.count("page_views",
208
- # for: "home_page",
209
- # time_range: Time.now..1.day.ago)
225
+ # time_start: 1.day.ago,
226
+ # time_end: Time.now)
210
227
 
211
228
  def count(metric_name, options={})
212
229
  parse_time_range(options)
213
- key = normalize(metric_name)
214
- keys = []
215
- if options[:for]
216
- key << ":#{options[:for]}"
230
+ key = build_key(metric_name)
231
+ key << assemble_for(options)
232
+ keys = find_keys(key)
233
+ sum = 0
234
+ if options[:time_start] && options[:time_end]
235
+ keys.each do |key|
236
+ sum += DulyNoted.redis.zcount(key, options[:time_start].to_f, options[:time_end].to_f)
237
+ end
238
+ return sum
217
239
  else
218
- keys << DulyNoted.redis.keys("#{key}*")
219
- keys - DulyNoted.redis.keys("#{key}*:meta")
220
- keys - DulyNoted.redis.keys("#{key}:*:")
221
- keys.flatten!
240
+ keys.each do |key|
241
+ sum += DulyNoted.redis.zcard(key)
242
+ end
243
+ return sum
222
244
  end
223
- if keys.empty?
224
- if options[:time_start] && options[:time_end]
225
- return DulyNoted.redis.zcount(key, options[:time_start].to_f, options[:time_end].to_f)
226
- else
227
- return DulyNoted.redis.zcard(key)
245
+ end
246
+
247
+ # ##Chart
248
+ #
249
+ # _parameters: `metric_name`, `data_points`(required),`for`(optional), `time_start`(optional), `time_end`(optional), `time_range`(optional)_
250
+ #
251
+ # Chart is a little complex, but I'll try to explain all of the possibilities. It's main purpose is to pull out your data and prepare it in a way that makes it easy to chart. The smallest amount of input it will take is just a `metric_name` and an amount of `data_points` to capture. This will check the time of the earliest known data point, and the time of the last known data point, and run chart with those values as the `time_start` and `time_end` respectively. It will take the amount of time that that spans, and divide it by the number of data points you asked for, and will split the time up evenly, and return a hash of times, and counts. If you specify both a `time_start` and a `time_end`, and a number of `data_points`, then it will divide the amount of time that that spans and will return a hash of times and counts. The other option is that you can specify either a `time_start` OR a `time_end` and a `step` and a number of `data_points`. This will start at whatever time you specified, and (if it's `time_end`) count down by the step (if you specified `time_start`, it would count up), as many times as the number of data points you requested.
252
+ #
253
+ # ###Usage
254
+ #
255
+ # DulyNoted.chart("page_views",
256
+ # :time_range => 1.month.ago..Time.now,
257
+ # :step => 1.day)
258
+ #
259
+ # DulyNoted.chart("page_views",
260
+ # :time_range => 1.day.ago..Time.now,
261
+ # :data_points => 12)
262
+ #
263
+ # DulyNoted.chart("page_views",
264
+ # :time_start => 1.day.ago,
265
+ # :step => 1.hour,
266
+ # :data_points => 12)
267
+ #
268
+ # DulyNoted.chart("downloads",
269
+ # :time_end => Time.now,
270
+ # :step => 1.month,
271
+ # :data_points => 12)
272
+ #
273
+ # DulyNoted.chart("page_views",
274
+ # :data_points => 100)
275
+ #
276
+ #
277
+ # Chart can be a little confusing but it's pretty powerful, so play around with it.
278
+
279
+ def chart(metric_name, options={})
280
+ parse_time_range(options)
281
+ chart = Hash.new(0)
282
+ if options[:time_start] && options[:time_end]
283
+ time = options[:time_start]
284
+ if options[:data_points]
285
+ total_time = options[:time_end] - options[:time_start]
286
+ options[:step] = total_time.to_i / options[:data_points]
228
287
  end
229
- else
230
- sum = 0
231
- if options[:time_start] && options[:time_end]
232
- keys.each do |key|
233
- sum += DulyNoted.redis.zcount(key, options[:time_start].to_f, options[:time_end].to_f)
234
- end
235
- return sum
236
- else
237
- keys.each do |key|
238
- sum += DulyNoted.redis.zcard(key)
239
- end
240
- return sum
288
+ while time < options[:time_end]
289
+ chart[time.to_i] = DulyNoted.count(metric_name, :time_start => time, :time_end => time+options[:step], :for => options[:for])
290
+ time += options[:step]
291
+ end
292
+ elsif options[:step] && options[:data_points] && (options[:time_end] || options[:time_start])
293
+ raise InvalidStep if options[:step] == 0
294
+ options[:step] *= -1 if (options[:step] > 0 && options[:time_end]) || (options[:step] < 0 && options[:time_start])
295
+ time = options[:time_start] || options[:time_end]
296
+ step = options[:step]
297
+ options[:data_points].times do
298
+ options[:time_start] = time
299
+ options[:time_start] += step if step < 0
300
+ options[:time_end] = time
301
+ options[:time_end] += step if step > 0
302
+ chart[time.to_i] = DulyNoted.count(metric_name, options)
303
+ time += step
241
304
  end
305
+ elsif options[:data_points]
306
+ key = build_key(metric_name)
307
+ key << assemble_for(options)
308
+ options[:time_start] = Time.at(DulyNoted.redis.zrange(key, 0, 0, :withscores => true)[1].to_f)
309
+ options[:time_end] = Time.at(DulyNoted.redis.zrevrange(key, 0, 0, :withscores => true)[1].to_f)
310
+ chart = DulyNoted.chart(metric_name, options)
311
+ else
312
+ raise InvalidOptions
313
+ end
314
+ return chart
315
+ end
316
+
317
+ # ##Magic
318
+ #
319
+ # ###count_x_by_y
320
+ #
321
+ # If you want to count a number of events by a meta field, you can use this magic command. So imagine this scenario:
322
+ #
323
+ # DulyNoted.track("page_views", meta: {browser: "chrome"})
324
+ #
325
+ # And you wanted to see a break down of page views by various browsers, you can call `DulyNoted.count_page_views_by_browser` and you'd get a hash that looked something like this:
326
+ #
327
+ # {"chrome" => 2913, "firefox" => 5281, "IE" => 7182, "safari" => 3213}
328
+ #
329
+ # So that method will work as soon as you've tracked something with that metric name. If you try to call the method on a metric that you haven't yet tracked you will get a `DulyNoted::NotValidMetric`. But if you reference a meta field that didn't exist, you'd just get a hash that looks like
330
+ #
331
+ # {nil => 1}
332
+
333
+ def count_x_by_y(metric_name, meta_field, options)
334
+ options ||= {}
335
+ options = {:meta_fields => [meta_field]}.merge(options)
336
+ meta_hashes = query(metric_name, options)
337
+ result = Hash.new(0)
338
+ meta_hashes.each do |meta_hash|
339
+ result[meta_hash[meta_field]] += 1
340
+ end
341
+ result
342
+ end
343
+
344
+ # ##Behind the curtain (metaprogramming)
345
+ #
346
+ # ###method_missing
347
+ #
348
+ # As I'm sure you're aware, method_missing is the magic tool for ruby developers to define dynamic methods like the above `count_x_by_y`, which is exactly what we use it for.
349
+
350
+ def method_missing(method, *args, &block)
351
+ if method.to_s =~ /^count_(.+)_by_(.+)$/
352
+ count_x_by_y($1, $2, args[0])
353
+ else
354
+ super
242
355
  end
243
356
  end
244
357
 
@@ -258,13 +371,17 @@ module DulyNoted
258
371
  @redis ||= (
259
372
  url = URI(@redis_url || "redis://127.0.0.1:6379/0")
260
373
 
261
- Redis.new({
374
+ check_schema(Redis.new({
262
375
  :host => url.host,
263
376
  :port => url.port,
264
377
  :db => url.path[1..-1],
265
378
  :password => url.password
266
- })
379
+ }))
267
380
  )
268
381
  end
269
-
382
+ class NotValidMetric < StandardError; end
383
+ class InvalidOptions < StandardError; end
384
+ class InvalidStep < StandardError; end
385
+ class InvalidRefId < StandardError; end
386
+ class UpdateError < StandardError; end
270
387
  end
@@ -3,6 +3,7 @@ require 'spec_helper'
3
3
  describe DulyNoted do
4
4
  before :each do
5
5
  DulyNoted.redis.flushall
6
+ Timecop.return
6
7
  end
7
8
 
8
9
  describe "#track" do
@@ -16,14 +17,25 @@ describe DulyNoted do
16
17
  DulyNoted.count("page_views", :for => "home").should eq(2)
17
18
  DulyNoted.count("page_views", :for => "contact_us").should eq(5)
18
19
  end
20
+ it "can nest context" do
21
+ 2.times { DulyNoted.track "views", :for => ["user_123", "video_8172"] }
22
+ 2.times { DulyNoted.track "views", :for => ["user_123", "video_8173"] }
23
+ DulyNoted.count("views", :for => "user_123").should eq(4)
24
+ DulyNoted.count("views", :for => ["user_123", "video_8173"]).should eq(2)
25
+ end
19
26
  it "stores metadata" do
20
27
  DulyNoted.track "page_views", :meta => {:open => true}
21
28
  DulyNoted.query("page_views").should include({"open" => "true"})
22
29
  end
23
30
  it "can track past events" do
31
+ Timecop.freeze
24
32
  DulyNoted.track "page_views", :generated_at => Time.now-10
25
33
  DulyNoted.count "page_views", :time_range => Time.now-11..Time.now-9
26
34
  end
35
+ it "will allow you to set expirations on `ref_id`s" do
36
+ DulyNoted.track "page_views", :meta => {:open => true}, :ref_id => "unique", :editable_for => 0
37
+ expect { DulyNoted.update("page_views", "unique") }.to raise_error(DulyNoted::InvalidRefId)
38
+ end
27
39
  end
28
40
 
29
41
  describe "#update" do
@@ -38,6 +50,11 @@ describe DulyNoted do
38
50
  DulyNoted.update "page_views", "unique", :meta => {:ip_address => "19.27.182.32"}
39
51
  DulyNoted.query("page_views").should include({"seconds_open" => "0", "ip_address" => "19.27.182.32"})
40
52
  end
53
+ it "does not require that `for:` be set to update" do
54
+ DulyNoted.track "page_views", :for => "home", :meta => {:seconds_open => 0}, :ref_id => "unique"
55
+ DulyNoted.update "page_views", "unique", :meta => {:seconds_open => 5}
56
+ DulyNoted.query("page_views").should include({"seconds_open" => "5"})
57
+ end
41
58
  end
42
59
 
43
60
  describe "#query" do
@@ -59,9 +76,13 @@ describe DulyNoted do
59
76
  DulyNoted.query("downloads", :ref_id => "unique", :meta_fields => [:browser]).should include({"browser" => "chrome"})
60
77
  DulyNoted.query("downloads", :ref_id => "unique", :meta_fields => [:browser]).should_not include({"file_name" => "rules.pdf"})
61
78
  end
79
+ it "can grab only specific fields from a certain context from the hash" do
80
+ DulyNoted.track "page_views", :for => "home", :meta => {:seconds_open => 0, :browser => "chrome"}
81
+ DulyNoted.query("page_views", :for => "home", :meta_fields => [:browser]).should include({"browser" => "chrome"})
82
+ end
62
83
  it "can get only meta hashes from a certain time range" do
63
- 5.times { DulyNoted.track "page_views", :meta => {:seconds_open => 5, :browser => "chrome"} }
64
- sleep 0.5
84
+ Timecop.freeze
85
+ 5.times { DulyNoted.track "page_views", :meta => {:seconds_open => 5, :browser => "chrome"}, :generated_at => Time.now-1 }
65
86
  5.times { DulyNoted.track "page_views", :meta => {:seconds_open => 0, :browser => "firefox"} }
66
87
  DulyNoted.query("page_views", :time_start => Time.now-0.5, :time_end => Time.now).should include({"seconds_open" => "0", "browser" => "firefox"})
67
88
  DulyNoted.query("page_views", :time_range => Time.now-0.5..Time.now).should include({"seconds_open" => "0", "browser" => "firefox"})
@@ -72,8 +93,7 @@ describe DulyNoted do
72
93
 
73
94
  describe "#count" do
74
95
  it "can count events in between a time range" do
75
- 5.times { DulyNoted.track "page_views" }
76
- sleep 0.2
96
+ 5.times { DulyNoted.track "page_views", :generated_at => Time.now-1 }
77
97
  5.times { DulyNoted.track "page_views" }
78
98
  DulyNoted.count("page_views", :time_start => Time.now-0.2, :time_end => Time.now).should eq(5)
79
99
  DulyNoted.count("page_views", :time_range => Time.now-0.2..Time.now).should eq(5)
@@ -84,13 +104,95 @@ describe DulyNoted do
84
104
  DulyNoted.count("page_views").should eq(10)
85
105
  end
86
106
  it "can count all of one type between a time range" do
87
- 5.times { DulyNoted.track "page_views", :for => "home" }
88
- 5.times { DulyNoted.track "page_views", :for => "contact_us" }
89
- sleep 0.2
107
+ 5.times { DulyNoted.track "page_views", :for => "home", :generated_at => Time.now-1 }
108
+ 5.times { DulyNoted.track "page_views", :for => "contact_us", :generated_at => Time.now-1 }
90
109
  5.times { DulyNoted.track "page_views", :for => "home" }
91
110
  5.times { DulyNoted.track "page_views", :for => "contact_us" }
92
111
  DulyNoted.count("page_views", :time_start => Time.now-0.2, :time_end => Time.now).should eq(10)
93
112
  DulyNoted.count("page_views", :time_range => Time.now-0.2..Time.now).should eq(10)
94
113
  end
95
114
  end
115
+
116
+ describe "#chart" do
117
+ it "should count by a specified time step and store the results in a hash" do
118
+ # Timecop.freeze
119
+ 1.times { DulyNoted.track "page_views", :generated_at => Time.now-(2.9) }
120
+ 2.times { DulyNoted.track "page_views", :generated_at => Time.now-(1.9) }
121
+ 3.times { DulyNoted.track "page_views", :generated_at => Time.now-(0.9) }
122
+ DulyNoted.chart("page_views", :time_range => Time.now-(3)..Time.now-(1), :step => (1)).should have_at_least(3).items
123
+ DulyNoted.chart("page_views", :time_range => Time.now-(3)..Time.now-(1), :step => (1)).should eq({(Time.now-3).to_i => 1, (Time.now-2).to_i => 2, (Time.now-1).to_i => 3})
124
+ end
125
+ it "can count events between a time range, without a step set" do
126
+ DulyNoted.track "page_views", :generated_at => Chronic.parse("yesterday at 12:30am")
127
+ DulyNoted.track "page_views", :generated_at => Chronic.parse("yesterday at 1:20am")
128
+ DulyNoted.chart("page_views", :time_range => Chronic.parse("yesterday at 12am")...Chronic.parse("yesterday at 2am"), :data_points => 2).should eq({Chronic.parse("yesterday at 12am").to_i => 1, Chronic.parse("yesterday at 1am").to_i => 1})
129
+ end
130
+ it "will take time_start, step, and data_points options to build a chart" do
131
+ DulyNoted.track "page_views", :generated_at => Chronic.parse("yesterday at 12:30am")
132
+ DulyNoted.track "page_views", :generated_at => Chronic.parse("yesterday at 1:20am")
133
+ DulyNoted.chart("page_views", :time_start => Chronic.parse("yesterday at 12am"), :step => (3600), :data_points => 2).should eq({Chronic.parse("yesterday at 12am").to_i => 1, Chronic.parse("yesterday at 1am").to_i => 1})
134
+ end
135
+ it "will take time_end, step, and data_points options to build a chart" do
136
+ DulyNoted.track "page_views", :generated_at => Chronic.parse("yesterday at 12:30am")
137
+ DulyNoted.track "page_views", :generated_at => Chronic.parse("yesterday at 1:20am")
138
+ DulyNoted.chart("page_views", :time_end => Chronic.parse("yesterday at 2am"), :step => (3600), :data_points => 2).should eq({Chronic.parse("yesterday at 2am").to_i => 1, Chronic.parse("yesterday at 1am").to_i => 1})
139
+ end
140
+ it "should raise InvalidStep if you give it a step of zero" do
141
+ DulyNoted.track "page_views"
142
+ expect { DulyNoted.chart("page_views", :time_end => Time.now, :step => 0, :data_points => 2) }.to raise_error(DulyNoted::InvalidStep)
143
+ end
144
+ it "should raise InvalidOptions if you give it invalid options" do
145
+ DulyNoted.track "page_views"
146
+ expect { DulyNoted.chart("page_views", :time_end => Time.now, :step => 60*60) }.to raise_error(DulyNoted::InvalidOptions)
147
+ end
148
+ it "should chart everything if no time range is specified" do
149
+ DulyNoted.track "page_views", :generated_at => Chronic.parse("yesterday at 12:30am")
150
+ DulyNoted.track "page_views", :generated_at => Chronic.parse("yesterday at 1:20am")
151
+ DulyNoted.chart("page_views", :data_points => 2).should eq({Chronic.parse("yesterday at 12:30am").to_i => 1, Chronic.parse("yesterday at 12:55am").to_i => 1})
152
+ end
153
+ end
154
+
155
+ describe "#metrics" do
156
+ it "should list all currently tracking metrics" do
157
+ DulyNoted.track "page_views"
158
+ DulyNoted.metrics.should include("dn:pageviews")
159
+ end
160
+ end
161
+
162
+ describe "#valid_metric?" do
163
+ it "should determine if metric is valid" do
164
+ DulyNoted.track "page_views"
165
+ DulyNoted.valid_metric?("page_views").should be_true
166
+ DulyNoted.valid_metric?("kdfsjhfs").should be_false
167
+ end
168
+ end
169
+
170
+ describe "#fields_for" do
171
+ it "should list the stored meta fields for a given metric" do
172
+ DulyNoted.track "page_views", :for => "home", :meta => {:seconds_open => 5, :browser => "chrome"}
173
+ DulyNoted.fields_for("page_views").should include("seconds_open", "browser")
174
+ end
175
+ end
176
+
177
+ describe "#count_x_by_y" do
178
+ it "should count x by y" do
179
+ 5.times { DulyNoted.track "page_views", :for => "home", :meta => {:browser => "chrome"} }
180
+ 5.times { DulyNoted.track "page_views", :for => "contact_us", :meta => {:browser => "firefox"} }
181
+ DulyNoted.count_page_views_by_browser.should eq({"chrome" => 5, "firefox" => 5})
182
+ DulyNoted.count_page_views_by_browser(:for => "home").should eq({"chrome" => 5})
183
+ end
184
+ it "should raise NotValidMetric if the metric is not valid" do
185
+ DulyNoted.track "page_views", :meta => {:browser => "chrome"}
186
+ expect { DulyNoted.count_downloads_by_browser }.to raise_error(DulyNoted::NotValidMetric)
187
+ end
188
+ end
189
+
190
+ describe "#check_schema" do
191
+ it "should update the database if the schema is off by a major release" do
192
+ DulyNoted::VERSION = "2.0.0"
193
+ DulyNoted.redis = nil # Force a reset of the redis instance variable
194
+ DulyNoted.track "page_views"
195
+ DulyNoted.redis.get("dn:version").should eq("2.0.0")
196
+ end
197
+ end
96
198
  end
data/spec/spec_helper.rb CHANGED
@@ -7,4 +7,5 @@ unless Kernel.respond_to?(:require_relative)
7
7
  end
8
8
 
9
9
  require_relative "../lib/duly_noted"
10
- require 'chronic'
10
+ require 'chronic'
11
+ require 'timecop'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duly_noted
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-06 00:00:00.000000000 Z
12
+ date: 2012-03-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
16
- requirement: &70245889112380 !ruby/object:Gem::Requirement
16
+ requirement: &70176012443720 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70245889112380
24
+ version_requirements: *70176012443720
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &70245889111680 !ruby/object:Gem::Requirement
27
+ requirement: &70176012459660 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70245889111680
35
+ version_requirements: *70176012459660
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rake
38
- requirement: &70245889110680 !ruby/object:Gem::Requirement
38
+ requirement: &70176012459240 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70245889110680
46
+ version_requirements: *70176012459240
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rb-fsevent
49
- requirement: &70245889109440 !ruby/object:Gem::Requirement
49
+ requirement: &70176012458820 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70245889109440
57
+ version_requirements: *70176012458820
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: guard-rspec
60
- requirement: &70245889108340 !ruby/object:Gem::Requirement
60
+ requirement: &70176012458400 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70245889108340
68
+ version_requirements: *70176012458400
69
69
  - !ruby/object:Gem::Dependency
70
- name: growl
71
- requirement: &70245889107640 !ruby/object:Gem::Requirement
70
+ name: guard-bundler
71
+ requirement: &70176012457980 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70245889107640
79
+ version_requirements: *70176012457980
80
80
  - !ruby/object:Gem::Dependency
81
- name: guard-bundler
82
- requirement: &70245889106880 !ruby/object:Gem::Requirement
81
+ name: chronic
82
+ requirement: &70176012457560 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,21 @@ dependencies:
87
87
  version: '0'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70245889106880
90
+ version_requirements: *70176012457560
91
91
  - !ruby/object:Gem::Dependency
92
- name: chronic
93
- requirement: &70245889106080 !ruby/object:Gem::Requirement
92
+ name: timecop
93
+ requirement: &70176012457140 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *70176012457140
102
+ - !ruby/object:Gem::Dependency
103
+ name: ruby_gntp
104
+ requirement: &70176012456720 !ruby/object:Gem::Requirement
94
105
  none: false
95
106
  requirements:
96
107
  - - ! '>='
@@ -98,8 +109,8 @@ dependencies:
98
109
  version: '0'
99
110
  type: :development
100
111
  prerelease: false
101
- version_requirements: *70245889106080
102
- description: keep detailed metrics on your project with a speedy, powerful redis backend.
112
+ version_requirements: *70176012456720
113
+ description: a simple redis based stat-tracker
103
114
  email:
104
115
  - will@willcosgrove.com
105
116
  executables: []
@@ -117,6 +128,7 @@ files:
117
128
  - duly_noted.gemspec
118
129
  - lib/duly_noted.rb
119
130
  - lib/duly_noted/helpers.rb
131
+ - lib/duly_noted/updater.rb
120
132
  - lib/duly_noted/version.rb
121
133
  - spec/duly_noted_spec.rb
122
134
  - spec/spec_helper.rb
@@ -132,18 +144,24 @@ required_ruby_version: !ruby/object:Gem::Requirement
132
144
  - - ! '>='
133
145
  - !ruby/object:Gem::Version
134
146
  version: '0'
147
+ segments:
148
+ - 0
149
+ hash: 2593334725054151131
135
150
  required_rubygems_version: !ruby/object:Gem::Requirement
136
151
  none: false
137
152
  requirements:
138
153
  - - ! '>='
139
154
  - !ruby/object:Gem::Version
140
155
  version: '0'
156
+ segments:
157
+ - 0
158
+ hash: 2593334725054151131
141
159
  requirements: []
142
160
  rubyforge_project:
143
161
  rubygems_version: 1.8.16
144
162
  signing_key:
145
163
  specification_version: 3
146
- summary: a simple redis based stat-tracker
164
+ summary: keep detailed metrics on your project with a speedy, powerful redis backend.
147
165
  test_files:
148
166
  - spec/duly_noted_spec.rb
149
167
  - spec/spec_helper.rb