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 +97 -33
- data/Guardfile +1 -0
- data/README.md +14 -0
- data/duly_noted.gemspec +5 -3
- data/lib/duly_noted/helpers.rb +46 -0
- data/lib/duly_noted/updater.rb +30 -0
- data/lib/duly_noted/version.rb +1 -1
- data/lib/duly_noted.rb +225 -108
- data/spec/duly_noted_spec.rb +109 -7
- data/spec/spec_helper.rb +2 -1
- metadata +41 -23
data/DOCS.md
CHANGED
@@ -1,58 +1,74 @@
|
|
1
1
|
##Dependency
|
2
2
|
* Redis
|
3
3
|
|
4
|
-
The **DulyNoted** module contains
|
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
|
-
##
|
12
|
+
##Parameter Descriptions
|
13
|
+
`metric_name`: The name of the metric to track, ex: `page_views`, `downloads`
|
12
14
|
|
13
|
-
|
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
|
-
`
|
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
|
-
`
|
20
|
+
`meta`: A hash with whatever meta data fields you might want to store, ex: `ip_address`, `file_type`
|
18
21
|
|
19
|
-
`
|
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
|
-
`
|
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
|
-
`
|
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
|
-
|
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
|
-
|
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
|
-
|
36
|
+
##Track
|
34
37
|
|
35
|
-
`for`
|
38
|
+
_parameters: `metric_name`, `for`(optional), `generated_at`(optional), `meta`(optional), `ref_id`(optional), `editable_for`(optional)_
|
36
39
|
|
37
|
-
`
|
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
|
-
|
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
|
-
|
52
|
+
##Update
|
48
53
|
|
49
|
-
`metric_name
|
54
|
+
_parameters: `metric_name`, `ref_id`, `meta`(optional), `editable_for`(optional)_
|
50
55
|
|
51
|
-
`
|
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
|
-
|
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`(
|
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
|
-
|
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
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{
|
8
|
-
gem.summary = %q{a
|
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
|
data/lib/duly_noted/helpers.rb
CHANGED
@@ -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
|
data/lib/duly_noted/version.rb
CHANGED
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
#
|
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 =
|
77
|
-
key <<
|
78
|
-
DulyNoted.redis.
|
79
|
-
|
80
|
-
|
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
|
-
#
|
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
|
-
# `
|
134
|
+
# _parameters: `metric_name`, `ref_id`, `meta`(optional), `editable_for`(optional)_
|
94
135
|
#
|
95
|
-
# `
|
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
|
-
#
|
102
|
-
#
|
145
|
+
# meta: { time_on_page: 30 },
|
146
|
+
# editable_for: 30)
|
103
147
|
|
104
148
|
def update(metric_name, ref_id, options={})
|
105
|
-
key =
|
106
|
-
key << ":#{
|
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`(
|
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
|
-
#
|
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 =
|
171
|
+
key = build_key(metric_name)
|
146
172
|
parse_time_range(options)
|
147
|
-
key <<
|
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
|
-
|
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
|
-
|
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`(
|
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
|
-
#
|
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:
|
204
|
-
# time_end:
|
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 =
|
214
|
-
|
215
|
-
|
216
|
-
|
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
|
219
|
-
|
220
|
-
|
221
|
-
|
240
|
+
keys.each do |key|
|
241
|
+
sum += DulyNoted.redis.zcard(key)
|
242
|
+
end
|
243
|
+
return sum
|
222
244
|
end
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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
|
data/spec/duly_noted_spec.rb
CHANGED
@@ -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
|
-
|
64
|
-
|
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
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:
|
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-
|
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: &
|
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: *
|
24
|
+
version_requirements: *70176012443720
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec
|
27
|
-
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: *
|
35
|
+
version_requirements: *70176012459660
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rake
|
38
|
-
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: *
|
46
|
+
version_requirements: *70176012459240
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rb-fsevent
|
49
|
-
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: *
|
57
|
+
version_requirements: *70176012458820
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: guard-rspec
|
60
|
-
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: *
|
68
|
+
version_requirements: *70176012458400
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
71
|
-
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: *
|
79
|
+
version_requirements: *70176012457980
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
|
-
name:
|
82
|
-
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: *
|
90
|
+
version_requirements: *70176012457560
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
|
-
name:
|
93
|
-
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: *
|
102
|
-
description:
|
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
|
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
|