trackoid 0.3.8 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +22 -0
- data/Gemfile +5 -9
- data/{README.rdoc → README.md} +42 -34
- data/Rakefile +2 -40
- data/config/mongoid.yml +8 -0
- data/lib/{trackoid → mongoid}/tracking.rb +6 -25
- data/lib/{trackoid → mongoid/tracking}/aggregates.rb +17 -13
- data/lib/mongoid/tracking/core_ext.rb +3 -0
- data/lib/{trackoid → mongoid/tracking}/core_ext/range.rb +0 -0
- data/lib/{trackoid → mongoid/tracking}/core_ext/time.rb +2 -1
- data/lib/mongoid/tracking/errors.rb +40 -0
- data/lib/{trackoid → mongoid/tracking}/reader_extender.rb +0 -0
- data/lib/{trackoid → mongoid/tracking}/readers.rb +0 -0
- data/lib/{trackoid → mongoid/tracking}/tracker.rb +35 -46
- data/lib/{trackoid → mongoid/tracking}/tracker_aggregates.rb +2 -2
- data/lib/trackoid.rb +9 -19
- data/lib/trackoid/version.rb +5 -0
- data/spec/aggregates_spec.rb +132 -133
- data/spec/embedded_spec.rb +97 -0
- data/spec/spec_helper.rb +7 -17
- data/spec/trackoid_spec.rb +74 -78
- data/trackoid.gemspec +16 -69
- metadata +108 -113
- data/VERSION +0 -1
- data/lib/trackoid/core_ext.rb +0 -3
- data/lib/trackoid/errors.rb +0 -38
data/.gitignore
ADDED
data/Gemfile
CHANGED
@@ -1,10 +1,6 @@
|
|
1
|
-
source
|
1
|
+
source "http://rubygems.org"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
gem 'jeweler'
|
8
|
-
gem 'rspec', '>= 2.2.0'
|
9
|
-
gem 'mocha', '0.11.0'
|
10
|
-
end
|
3
|
+
# Declare your gem's dependencies in trackoid.gemspec.
|
4
|
+
# Bundler will treat runtime dependencies like base dependencies, and
|
5
|
+
# development dependencies will be added by default to the :development group.
|
6
|
+
gemspec
|
data/{README.rdoc → README.md}
RENAMED
@@ -1,23 +1,31 @@
|
|
1
|
-
|
1
|
+
# Trackoid
|
2
2
|
|
3
3
|
Trackoid is an analytics tracking system made specifically for MongoDB using Mongoid as ORM.
|
4
4
|
|
5
|
-
|
5
|
+
## IMPORTANT upgrade information
|
6
6
|
|
7
|
-
Trackoid Version 0.
|
7
|
+
**Trackoid Version 0.4.0** is updated to work with Mongoid 3. It's NOT backwards compatible with any previous version of Mongoid. A dependency on Ruby 1.9.x has also been added.
|
8
8
|
|
9
|
-
|
9
|
+
**Trackoid Version 0.3.0** changes the internal representation of tracking data. So **YOU WILL NOT SEE PREVIOUS DATA** when you update.
|
10
10
|
|
11
|
-
|
11
|
+
Hopefully, due to the magic of MongoDB, data is **NOT LOST**. In fact it's never lost unless you delete it. :-) _Just it's not visible right away_.
|
12
12
|
|
13
|
+
See **Changes for TZ support** below for an explanation of changes in the internal representation of tracked data.
|
13
14
|
|
14
|
-
|
15
|
+
### Should I 'lock' the Trackoid version with Bundler?
|
16
|
+
|
17
|
+
If you are new to Trackoid and don't know what I'm talking about **you're safe** upgrading from 0.4.x onwards.
|
18
|
+
|
19
|
+
If you are having problems with Ruby 1.9.3 or not using Mongoid 3.x, probably you'll want to lock on 0.3.x, since 0.4.x *requires Mongoid 3.x** and hence, requires Ruby 1.9.3+.
|
20
|
+
|
21
|
+
|
22
|
+
# Requirements
|
15
23
|
|
16
24
|
Trackoid requires Mongoid, which obviously in turn requires MongoDB. Although you can only use Trackoid in Rails projects using Mongoid, it can easily be ported to MongoMapper or other ORM. You can also port it to work directly using MongoDB.
|
17
25
|
|
18
26
|
Please feel free to fork and port to other libraries. However, Trackoid really requires MongoDB since it is build from scratch to take advantage of several MongoDB features (please let me know if you dare enough to port Trackoid into CouchDB or similar, I will be glad to know).
|
19
27
|
|
20
|
-
|
28
|
+
# Using Trackoid to track analytics information for models
|
21
29
|
|
22
30
|
Given the most obvious use for Trackoid, consider this example:
|
23
31
|
|
@@ -29,12 +37,12 @@ Given the most obvious use for Trackoid, consider this example:
|
|
29
37
|
|
30
38
|
track :visits
|
31
39
|
end
|
32
|
-
|
40
|
+
|
33
41
|
This class models a web page, and by using `track :visits` we add a `visits` field to track... well... visits. :-) Later, in out controller we can do:
|
34
42
|
|
35
43
|
def view
|
36
44
|
@page = WebPage.find(params[:webpage_id])
|
37
|
-
|
45
|
+
|
38
46
|
@page.visits.inc # Increment a visit to this page
|
39
47
|
end
|
40
48
|
|
@@ -52,11 +60,11 @@ Of course, you can also show visits in a time range:
|
|
52
60
|
<% end %>
|
53
61
|
</ul>
|
54
62
|
|
55
|
-
|
63
|
+
## Not only visits...
|
56
64
|
|
57
65
|
Of course, you can use Trackoid to track all actions who require numeric analytics in a date frame.
|
58
66
|
|
59
|
-
|
67
|
+
### Prevent login to a control panel with a maximum login attemps
|
60
68
|
|
61
69
|
You can track invalid logins so you can prevent login for a user when certain invalid login had been made. Imagine your login controller:
|
62
70
|
|
@@ -64,14 +72,14 @@ You can track invalid logins so you can prevent login for a user when certain in
|
|
64
72
|
class User
|
65
73
|
include Mongoid::Document
|
66
74
|
include Mongoid::Tracking
|
67
|
-
|
75
|
+
|
68
76
|
track :failed_logins
|
69
77
|
end
|
70
78
|
|
71
79
|
# User controller
|
72
80
|
def login
|
73
81
|
user = User.find(params[:email])
|
74
|
-
|
82
|
+
|
75
83
|
# Stop login if failed attemps > 3
|
76
84
|
redirect(root_path) if user.failed_logins.today > 3
|
77
85
|
|
@@ -92,7 +100,7 @@ Note that additionally you have the full failed login history for free. :-)
|
|
92
100
|
@user.failed_logins.this_month
|
93
101
|
|
94
102
|
|
95
|
-
|
103
|
+
### Automatically saving a history of document changes
|
96
104
|
|
97
105
|
You can combine Trackoid with the power of callbacks to automatically track certain operations, for example modification of a document. This way you have a history of document changes.
|
98
106
|
|
@@ -112,7 +120,7 @@ You can combine Trackoid with the power of callbacks to automatically track cert
|
|
112
120
|
end
|
113
121
|
|
114
122
|
|
115
|
-
|
123
|
+
### Track temperature history for a nuclear plant
|
116
124
|
|
117
125
|
Imagine you need a web service to track the temperature of all rooms of a nuclear plant. Now you have a simple method to do this:
|
118
126
|
|
@@ -120,7 +128,7 @@ Imagine you need a web service to track the temperature of all rooms of a nuclea
|
|
120
128
|
class Room
|
121
129
|
include Mongoid::Document
|
122
130
|
include Mongoid::Tracking
|
123
|
-
|
131
|
+
|
124
132
|
track :temperature
|
125
133
|
end
|
126
134
|
|
@@ -128,7 +136,7 @@ Imagine you need a web service to track the temperature of all rooms of a nuclea
|
|
128
136
|
# Temperature controller
|
129
137
|
def set_temperature_for_room
|
130
138
|
@room = Room.find(params[:room_number])
|
131
|
-
|
139
|
+
|
132
140
|
@room.temperature.set(current_temperature)
|
133
141
|
end
|
134
142
|
|
@@ -137,12 +145,12 @@ So, you are not restricted into incrementing or decrementing a value, you can al
|
|
137
145
|
@room.temperature.last_days(30).max
|
138
146
|
|
139
147
|
|
140
|
-
|
148
|
+
# How does it works?
|
141
149
|
|
142
150
|
Trakoid works by embedding date tracking information into the models. The date tracking information is limited by a granularity of days, but you can use aggregates if you absolutely need hour or minutes granularity.
|
143
151
|
|
144
152
|
|
145
|
-
|
153
|
+
## Scalability and performance
|
146
154
|
|
147
155
|
Trackoid is made from the ground up to take advantage of the great scalability features of MongoDB. Trackoid uses "upsert" operations, bypassing Mongoid controllers so that it can be used in a distributed system without data loss. This is perfect for a cloud hosted SaaS application!
|
148
156
|
|
@@ -168,12 +176,12 @@ This way, the collection can receive multiple incremental operations without req
|
|
168
176
|
|
169
177
|
In practice, we don't need visits information so fine grained, but it's good to take this into account.
|
170
178
|
|
171
|
-
|
179
|
+
## Embedding tracking information into models
|
172
180
|
|
173
181
|
Tracking analytics data in SQL databases was historicaly saved into her own table, perhaps called `site_visits` with a relation to the sites table and each row saving an integer for each day.
|
174
182
|
|
175
183
|
Table "site_visits"
|
176
|
-
|
184
|
+
|
177
185
|
SiteID Date Visits
|
178
186
|
------ ---------- ------
|
179
187
|
1234 2010-05-01 34
|
@@ -184,7 +192,7 @@ With this schema, it's easy to get visits for a website using single SQL stateme
|
|
184
192
|
|
185
193
|
Trackoid uses an embedding approach to tackle this. For the above examples, Trackoid would embedd a ruby Hash into the Site model. This means the tracking information is already saved "inside" the Site, and we don't have to reach the database for any date querying! Moreover, since the data retrieved with the accessor methods like "last_days", "this_month" and the like, are already arrays, we could use Array methods like sum, count, max, min, etc...
|
186
194
|
|
187
|
-
|
195
|
+
## Memory implications
|
188
196
|
|
189
197
|
Since storing all tracking information with the model implies we add additional information that can grow, and grow, and grow... You can be wondering yourself if this is a good idea. Yes, it's is, or at least I think so. Let me convice you...
|
190
198
|
|
@@ -195,7 +203,7 @@ A year full of statistical data takes only 2.8Kb, if you store integers. If your
|
|
195
203
|
For comparison, this README is already 8.5Kb in size...
|
196
204
|
|
197
205
|
|
198
|
-
|
206
|
+
# Changes for TZ support
|
199
207
|
|
200
208
|
Well, this is the time (no pun intended) to add TZ support to Trackoid.
|
201
209
|
|
@@ -203,7 +211,7 @@ The problem is that "today" is not the same "today" for everyone, so unless you
|
|
203
211
|
|
204
212
|
But... Okay, given the fact that "today" is not the same "today" for everyone, this is the brand new Trackoid, with TZ support.
|
205
213
|
|
206
|
-
|
214
|
+
## What has changed?
|
207
215
|
|
208
216
|
In the surface, almost nothing, but internally there has been a major rewrite of the tracking code (the 'inc', 'set' methods) and the readers ('today', 'yesterday', etc). This is due to the changes I've made to the MongoDB structure of the tracking data.
|
209
217
|
|
@@ -275,7 +283,7 @@ The contents of every "day record" is another hash with 24 keys, one for each ho
|
|
275
283
|
{ :upsert => true, :safe => false }
|
276
284
|
)
|
277
285
|
|
278
|
-
|
286
|
+
## What "today" is it?
|
279
287
|
|
280
288
|
All dates are saved in UTC. That means Trackoid returns a whole 24 hour block for "today" only where the TZ is exactly UTC/GMT (no offset). If you live in a country where there is an offset into UTC, Trackoid must read a whole block and some hours from the block before or after to build "your today".
|
281
289
|
|
@@ -292,11 +300,11 @@ Example: I live in GMT+0200 (Daylight saving in effect, or summer time), then if
|
|
292
300
|
"02" : 30,
|
293
301
|
""
|
294
302
|
}
|
295
|
-
|
303
|
+
|
296
304
|
This is a more graphical representation:
|
297
305
|
|
298
306
|
Hours 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
299
|
-
------ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
307
|
+
------ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
300
308
|
GMT+2: 00 00 00 XX 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
301
309
|
UTC: ---> 00 XX 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
302
310
|
Shift up ---> 2 hours.
|
@@ -305,7 +313,7 @@ This is a more graphical representation:
|
|
305
313
|
For timezones with a negative offset from UTC (Like PDT/PST) the process is reversed: UTC values are shifted down and holes filled with the following day.
|
306
314
|
|
307
315
|
|
308
|
-
|
316
|
+
## How should I tell Trackoid how TZ to use?
|
309
317
|
|
310
318
|
Piece of cake: Use the reader methods "today", "yesterday", "last_days(N)" and Trackoid will use the effective Time Zone of your Rails/Ruby application.
|
311
319
|
|
@@ -313,7 +321,8 @@ Trackoid will correctly translate dates for you (hopefully) if you pass a date t
|
|
313
321
|
|
314
322
|
|
315
323
|
|
316
|
-
|
324
|
+
# Revision History
|
325
|
+
0.3.8 - Fixed support for Ruby 1.9.3
|
317
326
|
|
318
327
|
0.3.7 - Fixed support for Rails 3.1 and Mongoid 2.2
|
319
328
|
|
@@ -341,7 +350,7 @@ Trackoid will correctly translate dates for you (hopefully) if you pass a date t
|
|
341
350
|
|
342
351
|
- Renamed the internal field of ReaderExtender to "total" so that
|
343
352
|
converting to json automatically gives you:
|
344
|
-
|
353
|
+
|
345
354
|
{
|
346
355
|
"total": <total value>
|
347
356
|
"hours": [<hours array>]
|
@@ -356,13 +365,13 @@ Trackoid will correctly translate dates for you (hopefully) if you pass a date t
|
|
356
365
|
* Reset does the same as "set" but also sets aggregate fields.
|
357
366
|
|
358
367
|
Example:
|
359
|
-
|
368
|
+
|
360
369
|
A) model.value(aggregate_data).set(5)
|
361
370
|
B) model.value(aggregate_data).reset(5)
|
362
|
-
|
371
|
+
|
363
372
|
A will set "5" to the 'value' and to the aggregate.
|
364
373
|
B will set "5" to the 'value' and all aggregates.
|
365
|
-
|
374
|
+
|
366
375
|
* Erase resets the values in the mongo database. Note that this
|
367
376
|
is completely different of doing 'reset(0)'. (With erase you
|
368
377
|
can actually recall space from the database).
|
@@ -416,4 +425,3 @@ Trackoid will correctly translate dates for you (hopefully) if you pass a date t
|
|
416
425
|
|
417
426
|
0.1.5 - Added support for namespaced models and aggregations
|
418
427
|
- Enabled "set" operations on aggregates
|
419
|
-
|
data/Rakefile
CHANGED
@@ -1,40 +1,2 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
begin
|
5
|
-
require 'jeweler'
|
6
|
-
Jeweler::Tasks.new do |gem|
|
7
|
-
gem.name = "trackoid"
|
8
|
-
gem.summary = %Q{Trackoid is an easy scalable analytics tracker using MongoDB and Mongoid}
|
9
|
-
gem.description = %Q{Trackoid uses an embeddable approach to track analytics data using the poweful features of MongoDB for scalability}
|
10
|
-
gem.email = "josemiguel@perezruiz.com"
|
11
|
-
gem.homepage = "http://github.com/twoixter/trackoid"
|
12
|
-
gem.authors = ["Jose Miguel Perez"]
|
13
|
-
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
14
|
-
end
|
15
|
-
Jeweler::GemcutterTasks.new
|
16
|
-
rescue LoadError
|
17
|
-
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
18
|
-
end
|
19
|
-
|
20
|
-
require 'rspec/core/rake_task'
|
21
|
-
RSpec::Core::RakeTask.new(:spec) do |spec|
|
22
|
-
spec.pattern = 'spec/**/*_spec.rb'
|
23
|
-
end
|
24
|
-
|
25
|
-
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
26
|
-
spec.pattern = 'spec/**/*_spec.rb'
|
27
|
-
spec.rcov = true
|
28
|
-
end
|
29
|
-
|
30
|
-
task :default => :spec
|
31
|
-
|
32
|
-
require 'rake/rdoctask'
|
33
|
-
Rake::RDocTask.new do |rdoc|
|
34
|
-
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
35
|
-
|
36
|
-
rdoc.rdoc_dir = 'rdoc'
|
37
|
-
rdoc.title = "trackoid #{version}"
|
38
|
-
rdoc.rdoc_files.include('README*')
|
39
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
40
|
-
end
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
data/config/mongoid.yml
ADDED
@@ -10,13 +10,13 @@ module Mongoid #:nodoc:
|
|
10
10
|
unless self.ancestors.include? Mongoid::Document
|
11
11
|
raise Errors::NotMongoid, "Must be included in a Mongoid::Document"
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
include Aggregates
|
15
15
|
extend ClassMethods
|
16
|
-
|
16
|
+
|
17
17
|
class_attribute :tracked_fields
|
18
18
|
self.tracked_fields = []
|
19
|
-
delegate :tracked_fields, :internal_track_name, :
|
19
|
+
delegate :tracked_fields, :internal_track_name, to: "self.class"
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
@@ -41,15 +41,12 @@ module Mongoid #:nodoc:
|
|
41
41
|
# Configures the internal fields for tracking. Additionally also creates
|
42
42
|
# an index for the internal tracking field.
|
43
43
|
def set_tracking_field(name)
|
44
|
-
field internal_track_name(name), :type => Hash # , :default => {}
|
45
|
-
|
46
44
|
# DONT make an index for this field. MongoDB indexes have limited
|
47
45
|
# size and seems that this is not a good target for indexing.
|
48
46
|
# index internal_track_name(name)
|
49
|
-
|
50
47
|
tracked_fields << name
|
51
48
|
end
|
52
|
-
|
49
|
+
|
53
50
|
# Creates the tracking field accessor and also disables the original
|
54
51
|
# ones from Mongoid. Hidding here the original accessors for the
|
55
52
|
# Mongoid fields ensures they doesn't get dirty, so Mongoid does not
|
@@ -58,29 +55,13 @@ module Mongoid #:nodoc:
|
|
58
55
|
define_method(name) do |*aggr|
|
59
56
|
Tracker.new(self, name, aggr)
|
60
57
|
end
|
61
|
-
|
62
|
-
# Should we just "undef" this methods?
|
63
|
-
# They override the ones defined from Mongoid
|
64
|
-
define_method("#{name}_data") do
|
65
|
-
raise NoMethodError
|
66
|
-
end
|
67
|
-
|
68
|
-
define_method("#{name}_data=") do |m|
|
69
|
-
raise NoMethodError
|
70
|
-
end
|
71
|
-
|
72
|
-
# I think it's important to override also the #{name}_changed? so
|
73
|
-
# as to be sure Mongoid never mark this field as dirty.
|
74
|
-
define_method("#{name}_changed?") do
|
75
|
-
false
|
76
|
-
end
|
77
58
|
end
|
78
|
-
|
59
|
+
|
79
60
|
# Updates the aggregated class for it to include a new tracking field
|
80
61
|
def update_aggregates(name)
|
81
62
|
aggregate_klass.track name
|
82
63
|
end
|
83
|
-
|
64
|
+
|
84
65
|
end
|
85
66
|
|
86
67
|
end
|
@@ -14,7 +14,7 @@ module Mongoid #:nodoc:
|
|
14
14
|
self.aggregate_fields = {}
|
15
15
|
self.aggregate_klass = nil
|
16
16
|
delegate :aggregate_fields, :aggregate_klass, :aggregated?,
|
17
|
-
:
|
17
|
+
to: "self.class"
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -62,7 +62,7 @@ module Mongoid #:nodoc:
|
|
62
62
|
raise Errors::AggregationNameDeprecated.new(name) if DEPRECATED_TOKENS.include? name.to_s
|
63
63
|
|
64
64
|
define_aggregate_model if aggregate_klass.nil?
|
65
|
-
|
65
|
+
has_many internal_accessor_name(name), class_name: aggregate_klass.to_s
|
66
66
|
add_aggregate_field(name, block)
|
67
67
|
create_aggregation_accessors(name)
|
68
68
|
end
|
@@ -90,32 +90,34 @@ module Mongoid #:nodoc:
|
|
90
90
|
end
|
91
91
|
|
92
92
|
parent = self
|
93
|
+
|
93
94
|
define_klass do
|
94
95
|
include Mongoid::Document
|
95
96
|
include Mongoid::Tracking
|
96
97
|
|
97
98
|
# Make the relation to the original class
|
98
|
-
|
99
|
+
belongs_to parent.name.demodulize.underscore.to_sym, class_name: parent.name
|
99
100
|
|
100
101
|
# Internal fields to track aggregation token and keys
|
101
|
-
field :ns,
|
102
|
-
field :key, :
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
102
|
+
field :ns, type: String
|
103
|
+
field :key, type: String
|
104
|
+
|
105
|
+
index({
|
106
|
+
parent.name.foreign_key.to_sym => 1,
|
107
|
+
ns: 1,
|
108
|
+
key: 1
|
109
|
+
}, { unique: true, background: true })
|
107
110
|
|
108
111
|
# Include parent tracking data.
|
109
|
-
parent.tracked_fields.each {|track_field| track track_field }
|
112
|
+
parent.tracked_fields.each { |track_field| track track_field }
|
110
113
|
end
|
114
|
+
|
111
115
|
self.aggregate_klass = internal_aggregates_name.constantize
|
112
116
|
end
|
113
117
|
|
114
118
|
# Returns true if there is a class defined with the same name as our
|
115
119
|
# aggregate class.
|
116
120
|
def foreign_class_defined?
|
117
|
-
# The following construct doesn't work with namespaced constants.
|
118
|
-
# Object.const_defined?(internal_aggregates_name.to_sym)
|
119
121
|
internal_aggregates_name.constantize && true
|
120
122
|
rescue NameError
|
121
123
|
false
|
@@ -132,7 +134,9 @@ module Mongoid #:nodoc:
|
|
132
134
|
def define_klass(&block)
|
133
135
|
scope = internal_aggregates_name.split('::')
|
134
136
|
klass = scope.pop
|
135
|
-
scope = scope.inject(Object)
|
137
|
+
scope = scope.inject(Object) do |scope, const_name|
|
138
|
+
scope.const_get(const_name)
|
139
|
+
end
|
136
140
|
klass = scope.const_set(klass, Class.new)
|
137
141
|
klass.class_eval(&block)
|
138
142
|
end
|