trackoid 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION +1 -1
- data/lib/trackoid/aggregates.rb +109 -0
- data/lib/trackoid/errors.rb +19 -0
- data/lib/trackoid/tracker.rb +3 -3
- data/lib/trackoid/tracking.rb +35 -12
- data/lib/trackoid.rb +13 -1
- data/spec/aggregates_spec.rb +118 -0
- data/spec/trackoid_spec.rb +50 -3
- data/trackoid.gemspec +7 -3
- metadata +8 -4
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.1
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Mongoid #:nodoc:
|
2
|
+
module Tracking
|
3
|
+
|
4
|
+
module Aggregates
|
5
|
+
# This module includes aggregate data extensions to Trackoid instances
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
extend ClassMethods
|
9
|
+
include InstanceMethods
|
10
|
+
|
11
|
+
class_inheritable_accessor :aggregate_fields, :aggregate_klass
|
12
|
+
self.aggregate_fields = []
|
13
|
+
self.aggregate_klass = nil
|
14
|
+
# delegate :aggregate_fields, :aggregate_klass, :to => "self.class"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
def aggregate(name, &block)
|
21
|
+
define_aggregate_model if aggregate_klass.nil?
|
22
|
+
has_many name.to_sym, :class_name => aggregate_klass.to_s
|
23
|
+
add_aggregate_field(name, block)
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
# Returns the internal representation of the aggregates class name
|
28
|
+
def internal_aggregates_name
|
29
|
+
str = self.to_s.underscore + "_aggregates"
|
30
|
+
str.camelize
|
31
|
+
end
|
32
|
+
|
33
|
+
def define_aggregate_model
|
34
|
+
raise Errors::ClassAlreadyDefined.new(internal_aggregates_name) if foreign_class_defined
|
35
|
+
define_klass do
|
36
|
+
include Mongoid::Document
|
37
|
+
include Mongoid::Tracking
|
38
|
+
field :name, :type => String, :default => "Dummy Text"
|
39
|
+
# belongs_to :
|
40
|
+
end
|
41
|
+
self.aggregate_klass = internal_aggregates_name.constantize
|
42
|
+
end
|
43
|
+
|
44
|
+
def foreign_class_defined
|
45
|
+
Object.const_defined?(internal_aggregates_name.to_sym)
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_aggregate_field(name, block)
|
49
|
+
aggregate_fields << { name => block }
|
50
|
+
end
|
51
|
+
|
52
|
+
def define_klass(&block)
|
53
|
+
# klass = Class.new Object, &block
|
54
|
+
klass = Object.const_set internal_aggregates_name, Class.new
|
55
|
+
klass.class_eval(&block)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
module InstanceMethods
|
61
|
+
def aggregated?
|
62
|
+
!self.class.aggregate_klass.nil?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
# class Aggregate
|
70
|
+
# include Mongoid::Document
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# track :visits
|
74
|
+
# aggregate :browsers do
|
75
|
+
# ["google"]
|
76
|
+
# end
|
77
|
+
# aggregate :referers do
|
78
|
+
# ["domain.com"]
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
#
|
82
|
+
# self.visits.inc("Google engine")
|
83
|
+
#
|
84
|
+
#
|
85
|
+
# users
|
86
|
+
# { _id: 32334333, name:"pepe", visits_data:{} }
|
87
|
+
#
|
88
|
+
# users_aggregates
|
89
|
+
# { _id: 11221223, data_for: 32334333, ns: "browsers", key: nil, visits_data:{} }
|
90
|
+
# { _id: 11223223, data_for: 32334333, ns: "browsers", key: "google", visits_data:{} }
|
91
|
+
# { _id: 11224432, data_for: 32334333, ns: "browsers", key: "firefox", visits_data:{} }
|
92
|
+
#
|
93
|
+
#
|
94
|
+
# class UsersAggregate
|
95
|
+
# include Mongoid::Document
|
96
|
+
# include Mongoid::Tracking
|
97
|
+
#
|
98
|
+
# belongs_to :users
|
99
|
+
# field :ns
|
100
|
+
# field :key
|
101
|
+
#
|
102
|
+
# track :visits
|
103
|
+
# track :uniques
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
#
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Mongoid #:nodoc
|
3
|
+
module Errors #:nodoc
|
4
|
+
|
5
|
+
class ClassAlreadyDefined < RuntimeError
|
6
|
+
def initialize(klass)
|
7
|
+
@klass = klass
|
8
|
+
end
|
9
|
+
def message
|
10
|
+
"#{@klass} already defined, can't aggregate!"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class ModelNotSaved < RuntimeError; end
|
15
|
+
|
16
|
+
class NotMongoid < RuntimeError; end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/trackoid/tracker.rb
CHANGED
@@ -5,12 +5,12 @@ module Mongoid #:nodoc:
|
|
5
5
|
class Tracker
|
6
6
|
def initialize(owner, field)
|
7
7
|
@owner, @for = owner, field
|
8
|
-
@data = @owner.read_attribute(@for)
|
8
|
+
@data = @owner.read_attribute(@for) || {}
|
9
9
|
end
|
10
10
|
|
11
11
|
# Update methods
|
12
12
|
def add(how_much = 1, date = DateTime.now)
|
13
|
-
raise "Can
|
13
|
+
raise Errors::ModelNotSaved, "Can't update a new record" if @owner.new_record?
|
14
14
|
|
15
15
|
update_data(data_for(date) + how_much, date)
|
16
16
|
@owner.collection.update( @owner._selector,
|
@@ -27,7 +27,7 @@ module Mongoid #:nodoc:
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def set(how_much, date = DateTime.now)
|
30
|
-
raise "Can
|
30
|
+
raise Errors::ModelNotSaved, "Can't update a new record" if @owner.new_record?
|
31
31
|
|
32
32
|
update_data(how_much, date)
|
33
33
|
@owner.collection.update( @owner._selector,
|
data/lib/trackoid/tracking.rb
CHANGED
@@ -1,15 +1,20 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
require 'trackoid/tracker'
|
3
|
-
|
4
2
|
module Mongoid #:nodoc:
|
5
|
-
module Tracking
|
3
|
+
module Tracking #:nodoc:
|
4
|
+
|
6
5
|
# Include this module to add analytics tracking into a +root level+ document.
|
7
6
|
# Use "track :field" to add a field named :field and an associated mongoid
|
8
7
|
# field named after :field
|
9
8
|
def self.included(base)
|
10
9
|
base.class_eval do
|
11
|
-
raise "Must be included in a Mongoid::Document" unless self.ancestors.include? Mongoid::Document
|
10
|
+
raise Errors::NotMongoid, "Must be included in a Mongoid::Document" unless self.ancestors.include? Mongoid::Document
|
11
|
+
|
12
|
+
include Aggregates
|
12
13
|
extend ClassMethods
|
14
|
+
|
15
|
+
class_inheritable_accessor :tracked_fields
|
16
|
+
self.tracked_fields = []
|
17
|
+
delegate :tracked_fields, :internal_track_name, :to => "self.class"
|
13
18
|
end
|
14
19
|
end
|
15
20
|
|
@@ -20,18 +25,36 @@ module Mongoid #:nodoc:
|
|
20
25
|
# field. This is necessary so that Mongoid does not "dirty" the field
|
21
26
|
# potentially overwriting the original data.
|
22
27
|
def track(name)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
+
set_tracking_field(name)
|
29
|
+
create_tracking_accessors(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
# Returns the internal representation of the tracked field name
|
34
|
+
def internal_track_name(name)
|
35
|
+
"#{name}_data".to_sym
|
36
|
+
end
|
28
37
|
|
38
|
+
# Configures the internal fields for tracking. Additionally also creates
|
39
|
+
# an index for the internal tracking field.
|
40
|
+
def set_tracking_field(name)
|
41
|
+
field internal_track_name(name), :type => Hash, :default => {}
|
42
|
+
# Shoul we make an index for this field?
|
43
|
+
index internal_track_name(name)
|
44
|
+
tracked_fields << internal_track_name(name)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates the tracking field accessor and also disables the original
|
48
|
+
# ones from Mongoid. Hidding here the original accessors for the
|
49
|
+
# Mongoid fields ensures they doesn't get dirty, so Mongoid does not
|
50
|
+
# overwrite old data.
|
51
|
+
def create_tracking_accessors(name)
|
29
52
|
define_method("#{name}") do
|
30
|
-
Tracker.new(self,
|
53
|
+
Tracker.new(self, "#{name}_data".to_sym)
|
31
54
|
end
|
32
55
|
|
33
56
|
# Should we just "undef" this methods?
|
34
|
-
# They override the
|
57
|
+
# They override the ones defined from Mongoid
|
35
58
|
define_method("#{name}_data") do
|
36
59
|
raise NoMethodError
|
37
60
|
end
|
@@ -39,8 +62,8 @@ module Mongoid #:nodoc:
|
|
39
62
|
define_method("#{name}_data=") do
|
40
63
|
raise NoMethodError
|
41
64
|
end
|
42
|
-
|
43
65
|
end
|
66
|
+
|
44
67
|
end
|
45
68
|
|
46
69
|
end
|
data/lib/trackoid.rb
CHANGED
@@ -2,4 +2,16 @@ require 'rubygems'
|
|
2
2
|
|
3
3
|
gem "mongoid", ">= 1.9.0"
|
4
4
|
|
5
|
-
require 'trackoid/
|
5
|
+
require 'trackoid/errors'
|
6
|
+
require 'trackoid/tracker'
|
7
|
+
require 'trackoid/aggregates'
|
8
|
+
require 'trackoid/tracking'
|
9
|
+
|
10
|
+
module Mongoid #:nodoc:
|
11
|
+
module Tracking
|
12
|
+
|
13
|
+
VERSION = File.read(File.expand_path("../VERSION", File.dirname(__FILE__)))
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
class TestModel
|
4
|
+
include Mongoid::Document
|
5
|
+
include Mongoid::Tracking
|
6
|
+
|
7
|
+
field :name # Dummy field
|
8
|
+
track :visits
|
9
|
+
|
10
|
+
aggregate :browsers do
|
11
|
+
"Mozilla"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class SecondTestModel
|
16
|
+
include Mongoid::Document
|
17
|
+
include Mongoid::Tracking
|
18
|
+
|
19
|
+
field :name # Dummy field
|
20
|
+
track :visits
|
21
|
+
|
22
|
+
aggregate :browsers do
|
23
|
+
"Chrome"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe Mongoid::Tracking::Aggregates do
|
28
|
+
|
29
|
+
before(:all) do
|
30
|
+
@mock = TestModel.new(:name => "TestInstance")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should define a class model named after the original model" do
|
34
|
+
defined?(TestModelAggregates).should_not be_nil
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should define a class model named after the original second model" do
|
38
|
+
defined?(SecondTestModelAggregates).should_not be_nil
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should create a has_many relationship in the original model" do
|
42
|
+
@mock.class.method_defined?(:browsers).should be_true
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should have the aggregates klass in a instance var" do
|
46
|
+
@mock.aggregate_klass == TestModelAggregates
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should create an array in the class with all aggregate fields" do
|
50
|
+
@mock.class.aggregate_fields.map(&:keys).flatten.should == [ :browsers ]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should create an array in the class with all aggregate fields even when monkey patching" do
|
54
|
+
class TestModel
|
55
|
+
aggregate :referers do
|
56
|
+
"(none)"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
@mock.class.aggregate_fields.map(&:keys).flatten.should == [ :browsers, :referers ]
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should indicate this is an aggregated traking object with aggregated?" do
|
63
|
+
@mock.aggregated?.should be_true
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should raise error if already defined class with the same aggregated klass name" do
|
67
|
+
lambda {
|
68
|
+
class MockTestAggregates
|
69
|
+
def dummy; true; end
|
70
|
+
end
|
71
|
+
class MockTest
|
72
|
+
include Mongoid::Document
|
73
|
+
include Mongoid::Tracking
|
74
|
+
track :something
|
75
|
+
aggregate :other_something do
|
76
|
+
"other"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
}.should raise_error Mongoid::Errors::ClassAlreadyDefined
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should NOT raise error if the already defined class is our aggregated model" do
|
83
|
+
lambda {
|
84
|
+
class MockTest2
|
85
|
+
include Mongoid::Document
|
86
|
+
include Mongoid::Tracking
|
87
|
+
track :something
|
88
|
+
end
|
89
|
+
class MockTest2
|
90
|
+
include Mongoid::Document
|
91
|
+
include Mongoid::Tracking
|
92
|
+
track :something_else
|
93
|
+
aggregate :other_something do
|
94
|
+
"other"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
}.should_not raise_error Mongoid::Errors::ClassAlreadyDefined
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should raise error although the already defined class includes tracking" do
|
101
|
+
lambda {
|
102
|
+
class MockTest3Aggregates
|
103
|
+
include Mongoid::Document
|
104
|
+
include Mongoid::Tracking
|
105
|
+
track :something
|
106
|
+
end
|
107
|
+
class MockTest3
|
108
|
+
include Mongoid::Document
|
109
|
+
include Mongoid::Tracking
|
110
|
+
track :something_else
|
111
|
+
aggregate :other_something do
|
112
|
+
"other"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
}.should raise_error Mongoid::Errors::ClassAlreadyDefined
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
data/spec/trackoid_spec.rb
CHANGED
@@ -9,20 +9,30 @@ class Test
|
|
9
9
|
end
|
10
10
|
|
11
11
|
describe Mongoid::Tracking do
|
12
|
+
|
13
|
+
before(:all) do
|
14
|
+
@trackoid_version = File.read(File.expand_path("../VERSION", File.dirname(__FILE__)))
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should expose the same version as the VERSION file" do
|
18
|
+
Mongoid::Tracking::VERSION.should == @trackoid_version
|
19
|
+
end
|
12
20
|
|
13
21
|
it "should raise error when used in a class not of class Mongoid::Document" do
|
14
22
|
lambda {
|
15
23
|
class NotMongoidClass
|
16
24
|
include Mongoid::Tracking
|
25
|
+
track :something
|
17
26
|
end
|
18
|
-
}.should raise_error
|
27
|
+
}.should raise_error Mongoid::Errors::NotMongoid
|
19
28
|
end
|
20
29
|
|
21
|
-
it "should not
|
30
|
+
it "should not raise error when used in a class of class Mongoid::Document" do
|
22
31
|
lambda {
|
23
32
|
class MongoidedDocument
|
24
33
|
include Mongoid::Document
|
25
34
|
include Mongoid::Tracking
|
35
|
+
track :something
|
26
36
|
end
|
27
37
|
}.should_not raise_error
|
28
38
|
end
|
@@ -46,8 +56,19 @@ describe Mongoid::Tracking do
|
|
46
56
|
@mock.visits.class.should == Mongoid::Tracking::Tracker
|
47
57
|
end
|
48
58
|
|
59
|
+
it "should create an array in the class with all tracking fields" do
|
60
|
+
@mock.class.tracked_fields.should == [ :visits_data ]
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should create an array in the class with all tracking fields even when monkey patching" do
|
64
|
+
class Test
|
65
|
+
track :something_else
|
66
|
+
end
|
67
|
+
@mock.class.tracked_fields.should == [ :visits_data, :something_else_data ]
|
68
|
+
end
|
69
|
+
|
49
70
|
it "should not update stats when new record" do
|
50
|
-
lambda { @mock.inc }.should raise_error
|
71
|
+
lambda { @mock.visits.inc }.should raise_error Mongoid::Errors::ModelNotSaved
|
51
72
|
end
|
52
73
|
|
53
74
|
it "shold create an empty hash as the internal representation" do
|
@@ -66,6 +87,10 @@ describe Mongoid::Tracking do
|
|
66
87
|
@mock.visits.last_days(0).should == [@mock.visits.today]
|
67
88
|
end
|
68
89
|
|
90
|
+
it "should not be aggregated" do
|
91
|
+
@mock.aggregated?.should be_false
|
92
|
+
end
|
93
|
+
|
69
94
|
end
|
70
95
|
|
71
96
|
describe "when using a model in the database" do
|
@@ -168,4 +193,26 @@ describe Mongoid::Tracking do
|
|
168
193
|
|
169
194
|
end
|
170
195
|
|
196
|
+
|
197
|
+
|
198
|
+
context "regression test for github issues" do
|
199
|
+
|
200
|
+
it "should not raise undefined method [] for nil:NilClass for objects already saved" do
|
201
|
+
class TestModel
|
202
|
+
include Mongoid::Document
|
203
|
+
include Mongoid::Tracking
|
204
|
+
field :name
|
205
|
+
end
|
206
|
+
TestModel.delete_all
|
207
|
+
TestModel.create(:name => "dummy")
|
208
|
+
|
209
|
+
class TestModel
|
210
|
+
track :something
|
211
|
+
end
|
212
|
+
tm = TestModel.first
|
213
|
+
tm.something.today.should == 0
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
|
171
218
|
end
|
data/trackoid.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{trackoid}
|
8
|
-
s.version = "0.1.
|
8
|
+
s.version = "0.1.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Jose Miguel Perez"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-06-04}
|
13
13
|
s.description = %q{Trackoid uses an embeddable approach to track analytics data using the poweful features of MongoDB for scalability}
|
14
14
|
s.email = %q{josemiguel@perezruiz.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -24,8 +24,11 @@ Gem::Specification.new do |s|
|
|
24
24
|
"Rakefile",
|
25
25
|
"VERSION",
|
26
26
|
"lib/trackoid.rb",
|
27
|
+
"lib/trackoid/aggregates.rb",
|
28
|
+
"lib/trackoid/errors.rb",
|
27
29
|
"lib/trackoid/tracker.rb",
|
28
30
|
"lib/trackoid/tracking.rb",
|
31
|
+
"spec/aggregates_spec.rb",
|
29
32
|
"spec/spec.opts",
|
30
33
|
"spec/spec_helper.rb",
|
31
34
|
"spec/trackoid_spec.rb",
|
@@ -37,7 +40,8 @@ Gem::Specification.new do |s|
|
|
37
40
|
s.rubygems_version = %q{1.3.7}
|
38
41
|
s.summary = %q{Trackoid is an easy scalable analytics tracker using MongoDB and Mongoid}
|
39
42
|
s.test_files = [
|
40
|
-
"spec/
|
43
|
+
"spec/aggregates_spec.rb",
|
44
|
+
"spec/spec_helper.rb",
|
41
45
|
"spec/trackoid_spec.rb"
|
42
46
|
]
|
43
47
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trackoid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jose Miguel Perez
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-06-04 00:00:00 +02:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -51,8 +51,11 @@ files:
|
|
51
51
|
- Rakefile
|
52
52
|
- VERSION
|
53
53
|
- lib/trackoid.rb
|
54
|
+
- lib/trackoid/aggregates.rb
|
55
|
+
- lib/trackoid/errors.rb
|
54
56
|
- lib/trackoid/tracker.rb
|
55
57
|
- lib/trackoid/tracking.rb
|
58
|
+
- spec/aggregates_spec.rb
|
56
59
|
- spec/spec.opts
|
57
60
|
- spec/spec_helper.rb
|
58
61
|
- spec/trackoid_spec.rb
|
@@ -92,5 +95,6 @@ signing_key:
|
|
92
95
|
specification_version: 3
|
93
96
|
summary: Trackoid is an easy scalable analytics tracker using MongoDB and Mongoid
|
94
97
|
test_files:
|
98
|
+
- spec/aggregates_spec.rb
|
95
99
|
- spec/spec_helper.rb
|
96
100
|
- spec/trackoid_spec.rb
|