mongoid_traffic 0.2.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a609625a589b09ecf9ddde37e133c90407a3824c3c52123ed323d985394136c6
4
- data.tar.gz: 5f04c2ccf598f18f955865422ebc7f2d554a8cd4e4ac0b3db59bec92c7459235
3
+ metadata.gz: e2e697436b535a20d799099b53576a88bf7e0145ca1110010b7b27b05cab2f4d
4
+ data.tar.gz: d3d50e3806776e41747c2a46abe7d38da795cc8bc961794f3a273acaea0b5510
5
5
  SHA512:
6
- metadata.gz: 8754362224ed919e02b548a248ec046538b4707f0e96be418a740ecd51dc24339b9221c3acba0b6dd8caa5c3e202bd953d42c4a7cd1410bd04c31ec1c508e7c9
7
- data.tar.gz: ca9d9b15acb37b7897276afccf95886e06428bc43294751111e2e50169a681f58031e42926699151753f9e625a3ade8e0bab947cb98c4e3d47ce1549ceed1a05
6
+ metadata.gz: dddd2282c221bd0a955ed6ffd23e0c886a0a1c79119fe8b3106bc55b9714e7330f62399c07af92fea2cd88aa85dfbcbab6ceaa16224ba8cbb8ee9ab004e85a17
7
+ data.tar.gz: 1a86c94669b57cfa3a08b20658ec456fa3623c1d1a45c172929253a333d98a4b812a861fa66ecf8e2ee7c717194bbb6ff6ef054c78f41936b38fb7e678a53b16
@@ -1,5 +1,15 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.0.0
4
+
5
+ * BREAKING: refactored and abstracted for more general purpose
6
+ * removed bot detection
7
+ * removed user_agent
8
+ * removed geo_ip
9
+ * removed referer
10
+ * removed rails controller additions
11
+ * removed scope
12
+
3
13
  ## 0.2.5
4
14
 
5
15
  * Mongoid 7 compatibility
data/README.md CHANGED
@@ -30,154 +30,140 @@ Setup your class for storing the log:
30
30
 
31
31
  ```ruby
32
32
  class MyLog
33
- include Mongoid::Document
34
- include MongoidTraffic::Log
33
+ include Mongoid::Document
34
+ include MongoidTraffic::Log
35
35
  end
36
36
  ```
37
37
 
38
38
  Log your traffic like this:
39
39
 
40
40
  ```ruby
41
- MongoidTraffic::Logger.log(MyLog, *args)
41
+ MongoidTraffic::Logger.log(MyLog)
42
42
  ```
43
43
 
44
44
  Or, if you prefer, directly on the `MyLog` class:
45
45
 
46
46
  ```ruby
47
- MyLog.log(*args)
47
+ MyLog.log
48
48
  ```
49
49
 
50
- This will (by default) create two `MyLog` documents: first with `:df(date_from)` and `:dt(date_to)` fields specifying monthly log, second with dates specifying daily log. Each log has an `:access_count` attribute that is incremented with subsequent `.log` calls.
50
+ This will (by default) create four `MyLog` documents: each with `:df(date_from)` and `:dt(date_to)` fields specifying yearly, monthly, weekly and daily log. Each log has an `:access_count` attribute incremented with subsequent `.log` calls.
51
51
 
52
52
  ### Optional arguments
53
53
 
54
54
  #### Time scope:
55
55
 
56
- By default, the `.log` method creates/updates a document with aggregations for month and a document with aggregations for a day. You can however customize this behaviour like this:
56
+ By default, the `.log` method creates/updates a document with aggregations for year, month, week and day. Subset of those can be specified as:
57
57
 
58
58
  ```ruby
59
- MyLog.log(time_scope: %i(month week day))
59
+ MyLog.log(time_scope: %i[day])
60
60
  ```
61
61
 
62
- The available options are: `%(year month week day)`
62
+ The available options are: `%i[year month week day]`
63
63
 
64
64
  #### Scope:
65
65
 
66
- ```ruby
67
- MyLog.log(scope: '/pages/123')
68
- ```
69
-
70
- Allows to create several logs for different scopes of your application (typically URLs).
71
-
72
- #### User-Agent:
66
+ It is possible to scope the log by an arbitrary number of parameters.
73
67
 
74
68
  ```ruby
75
- MyLog.log(user_agent: user_agent_string)
76
- ```
69
+ class MyLog
70
+ include Mongoid::Document
71
+ include MongoidTraffic::Log
77
72
 
78
- Logs platform-browser-version access count:
73
+ belongs_to :page
79
74
 
80
- ```ruby
81
- { "Macintosh" => { "Safari" => { "8%2E0" => 1, "7%2E1" => 2 } } }
75
+ scope :for_page, -> (page) { where(page: page) }
76
+ end
82
77
  ```
83
78
 
84
- Please note the keys are escaped. You might want to unescape them using for example `CGI::unescape`.
85
-
86
- #### Referer:
79
+ For example:
87
80
 
88
81
  ```ruby
89
- MyLog.log(referer: http_referer_string)
82
+ MongoidTraffic::Logger.log(MyLog.for_page(page))
90
83
  ```
91
84
 
92
- Logs referer access count:
85
+ or
93
86
 
94
87
  ```ruby
95
- { "http%3A%2F%2Fwww%2Egoogle%2Ecom" => 1 }
88
+ MyLog.for_page(page).log(page: my_page)
96
89
  ```
97
90
 
98
- Please note the keys are escaped. You might want to unescape them using for example `CGI::unescape`.
91
+ PS If you query by a scope often do not forget to add corresponding indexes to your log model.
99
92
 
100
- If the referer is included in the [bot list](http://www.user-agents.org/allagents.xml) the log will not be created.
93
+ #### Arbitrary counter:
101
94
 
102
- #### Country (via IP address):
103
-
104
- ```ruby
105
- MyLog.log(ip_address: '123.123.123.123')
106
- ```
95
+ It is possible to count any number of arbitrary additional values. For example, to count unique country etc.
107
96
 
108
- Logs access count by country code 2:
97
+ First, specify the additional counter in the log model (prefer short aliased field names):
109
98
 
110
99
  ```ruby
111
- { "CZ" => 100, "DE" => 1 }
112
- ```
113
-
114
- Uses the [GeoIP](https://github.com/cjheath/geoip) library to infer country_code from IP address.
115
-
116
- You can use the [countries gem](https://github.com/hexorx/countries) to convert the country code to country name etc.
117
-
118
- #### Unique id:
100
+ class MyLog
101
+ include Mongoid::Document
102
+ include MongoidTraffic::Log
119
103
 
120
- ```ruby
121
- MyLog.log(unique_id: unique_id_string)
104
+ additional_counter :c, as: :country
105
+ end
122
106
  ```
123
107
 
124
- Logs access count by id:
108
+ Track access by country as:
125
109
 
126
110
  ```ruby
127
- { "0123456789" => 100, "ABCDEFGHIJ" => 1 }
111
+ MyLog.log(country: 'CZ')
112
+ MyLog.log(country: 'NL')
128
113
  ```
129
114
 
130
- Typically you would pass it something like `session_id` to track unique visitors.
131
-
132
- ## Rails
133
-
134
- In case of Rails, you can use the `.after_action` macro with the `#log_traffic` helper method in your controllers:
115
+ Which will create Hash with counts per country:
135
116
 
136
117
  ```ruby
137
- class MyController < ApplicationController
138
- after_action -> { log_traffic(MyLog) }, only: [:show]
139
- end
118
+ { _id: …, df(date_from): …, dt(date_to): …, ac(access_count): 2, c(country): { 'CZ' => 1, 'NL' => 1 } }
140
119
  ```
141
120
 
142
- The method automatically infers most of the options from the controller `request` method (User-Agent, Referer, IP address) and unique id from the Rails session.
143
-
144
- Additionally the `:log_scoped_traffic` method adds a scope by the current request path (`/pages/123`):
121
+ You can take advantage of the fact, that the underlying queries support dot-notation, and track on deeply nested hashes. For example, should you want to track access per browser:
145
122
 
146
123
  ```ruby
147
- class MyController < ApplicationController
148
- after_action -> { log_scoped_traffic(MyLog) }, only: [:show]
124
+ class MyLog
125
+ include Mongoid::Document
126
+ include MongoidTraffic::Log
127
+
128
+ additional_counter :b, as: :browser
149
129
  end
150
130
  ```
151
131
 
152
- You can override this behavior with custom scope like this:
132
+ Then track access by browser as:
153
133
 
154
134
  ```ruby
155
- class MyController < ApplicationController
156
- after_action -> { log_scoped_traffic(MyLog, scope: 'my-scope-comes-here') }, only: [:show]
157
- end
135
+ MyLog.log(browser: "Mac.Safari.8") # log access by Mac Safari 8
136
+ MyLog.log(browser: "Mac.Safari.7%2E1") # log access by Mac Safari 7.1
158
137
  ```
159
138
 
160
- It might be good idea to use both methods in order to log access to the whole site as well as access to individual pages:
139
+ Which will create following log document:
161
140
 
162
141
  ```ruby
163
- class MyController < ApplicationController
164
- after_action -> { log_traffic(MyLog) }, only: [:show]
165
- after_action -> { log_scoped_traffic(MyLog) }, only: [:show]
166
- end
142
+ { _id: …, df(date_from): …, dt(date_to): …, ac(access_count): 2, b(browser): { 'Mac' => { 'Safari' => { '8' => 1, '7%2E1' => 1 } } } }
167
143
  ```
168
144
 
145
+ Please note all `.` not intended to denote nesting need to be escaped (here as `%2E`).
146
+
169
147
  ## Accessing the log
170
148
 
171
- The log is accessed with a combination of Mongoid Criteria and aggregation methods.
149
+ The log can be accessed using a combination of Mongoid Criteria and aggregation methods.
172
150
 
173
151
  ### Criteria
174
152
 
175
153
  The following time based criteria are predefined as Mongoid scopes:
176
154
 
177
- * `.yearly(year)`
178
- * `.monthly(month, year)`
179
- * `.weekly(week, year)`
180
- * `.daily(date)`
155
+ * `.day(date)`
156
+ * `.week(week, year)`
157
+ * `.month(month, year)`
158
+ * `.year(year)`
159
+ * `.for_dates(date_from, date_to)`
160
+
161
+ To select by log type:
162
+
163
+ * `.daily`
164
+ * `.weekly`
165
+ * `.monthly`
166
+ * `.yearly`
181
167
 
182
168
  To narrow down by scope:
183
169
 
@@ -186,48 +172,59 @@ To narrow down by scope:
186
172
  ### Aggregation method
187
173
 
188
174
  * `.aggregate_on(:access_count)`
189
- * `.aggregate_on(:browsers)`
190
- * `.aggregate_on(:referers)`
191
- * `.aggregate_on(:countries)`
192
- * `.aggregate_on(:unique_ids)`
175
+ * `.aggregate_on(ARBITRARY_COUNTER)`
193
176
 
194
177
  Behind the scenes, this method will take all documents returned by your criteria and combines the values of the specified field (in case of `:access_count` it is simple sum of the values, in other cases it is sum of nested hashes).
195
178
 
196
- ### Unique visits
197
-
198
- Lastly, to retrieve the number of unique visits:
179
+ ### Examples
199
180
 
200
- * `.sum(:unique_ids)`
181
+ #### Time
201
182
 
202
- ### Examples
183
+ ```ruby
184
+ MyLog.day(Date.today)
185
+ ```
203
186
 
204
- Typically you first query by time:
187
+ Eventually by date range (when using the `.for_dates` scope make sure to specify which log type you wish to access):
205
188
 
206
189
  ```ruby
207
- MyLog.daily(Date.today)
190
+ MyLog.daily.for_dates(Date.yesterday, Date.today)
208
191
  ```
209
192
 
210
- And eventually by scope:
193
+ #### Scope
211
194
 
212
195
  ```ruby
213
- MyLog.daily(Date.today).scoped_to('/pages/123')
196
+ MyLog.for_page(page).day(Date.today)
214
197
  ```
215
198
 
216
- Followed by an aggregation. For example on access count:
199
+ Make sure that the order of drilling down corresponds to the indexes on your model.
200
+
201
+ #### Aggregations
202
+
203
+ On access count:
217
204
 
218
205
  ```ruby
219
- MyLog.daily(Date.today).scoped_to('/pages/123').aggregate_on(:access_count)
206
+ MyLog.day(Date.today).scoped_to('/pages/123').aggregate_on(:access_count) # => 1
220
207
  ```
221
208
 
222
209
  The scope query accepts regular expressions, which allows for aggregations on specific parts of your site. For example should you want to query for all pages that have path beginning with '/blog':
223
210
 
224
211
  ```ruby
225
- MyLog.monthly(8, 2014).scoped_to(/\A\/blog/).aggregate_on(:countries)
212
+ MyLog.month(8, 2014).scoped_to(/\A\/blog/).aggregate_on(:access_count) # => 1
226
213
  ```
227
214
 
228
- ## TODO
215
+ On additional counter:
229
216
 
230
- * JavaScript + Rails helper to track client-side properties (screen/browser size)
217
+ ```ruby
218
+ MyLog.day(Date.today).scoped_to('/pages/123').aggregate_on(:country) # => { 'CZ' => 1, 'NL' => 1 }
219
+ ```
220
+
221
+ #### Pluck
222
+
223
+ For plotting charts you might make use of standard `:pluck`:
224
+
225
+ ```ruby
226
+ MyLog.daily.for_dates(Date.today - 1.week, Date.today).pluck(:date_from, :access_count) # => returns array of dates and counts per day
227
+ ```
231
228
 
232
229
  ## Further reading
233
230
 
data/Rakefile CHANGED
@@ -1,26 +1,9 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rake/testtask'
3
-
3
+
4
4
  Rake::TestTask.new do |t|
5
5
  t.libs << 'test'
6
6
  t.pattern = 'test/**/*_test.rb'
7
7
  end
8
8
 
9
- namespace :mongoid_traffic do
10
- require File.dirname(__FILE__) + "/lib/mongoid_traffic/logger/bots"
11
- require File.dirname(__FILE__) + "/lib/mongoid_traffic/logger/geo_ip"
12
-
13
- desc "output the list of bots"
14
- task :update_bots_data do
15
- `cd vendor/mongoid_traffic && curl -O #{MongoidTraffic::Logger::Bots::DATA_URL}`
16
- end
17
-
18
- desc "output the geoip.dat"
19
- task :update_geoip_data do
20
- `cd vendor/mongoid_traffic && curl -O #{MongoidTraffic::Logger::GeoIp::DATA_URL}`
21
- `cd vendor/mongoid_traffic && gunzip -f GeoIP.dat.gz`
22
- end
23
-
24
- end
25
-
26
- task :default => :test
9
+ task :default => :test
@@ -1,5 +1,8 @@
1
1
  require 'mongoid_traffic/version'
2
2
 
3
- require 'mongoid_traffic/controller_additions'
4
3
  require 'mongoid_traffic/log'
5
4
  require 'mongoid_traffic/logger'
5
+
6
+ module MongoidTraffic
7
+ TIME_SCOPE_OPTIONS = { year: 3, month: 2, week: 1, day: 0 }.freeze
8
+ end
@@ -1,83 +1,63 @@
1
- require 'logger'
2
-
3
1
  module MongoidTraffic
4
2
  module Log
5
3
  def self.included(base)
6
4
  base.extend ClassMethods
7
5
  base.class_eval do
8
- field :s, as: :scope, type: String
9
-
10
- field :ac, as: :access_count, type: Integer
11
-
12
- field :b, as: :browsers, type: Hash, default: {}
13
- field :c, as: :countries, type: Hash, default: {}
14
- field :r, as: :referers, type: Hash, default: {}
15
- field :u, as: :unique_ids, type: Hash, default: {}
16
-
17
- field :uat, as: :updated_at, type: Time
18
-
19
- # ---------------------------------------------------------------------
20
-
21
6
  field :df, as: :date_from, type: Date
22
7
  field :dt, as: :date_to, type: Date
8
+ field :ts, as: :time_scope, type: Integer
23
9
 
24
- # ---------------------------------------------------------------------
10
+ field :ac, as: :access_count, type: Integer
11
+ field :uat, as: :updated_at, type: Time
25
12
 
26
13
  validates :date_from, presence: true
27
14
  validates :date_to, presence: true
28
15
 
29
- # ---------------------------------------------------------------------
16
+ scope :yearly, -> { where(ts: TIME_SCOPE_OPTIONS[:year]) }
17
+ scope :monthly, -> { where(ts: TIME_SCOPE_OPTIONS[:month]) }
18
+ scope :weekly, -> { where(ts: TIME_SCOPE_OPTIONS[:year]) }
19
+ scope :daily, -> { where(ts: TIME_SCOPE_OPTIONS[:day]) }
30
20
 
31
- default_scope -> { where(scope: nil) }
21
+ scope :for_dates, ->(date_from, date_to) { where(:date_from.lte => date_to, :date_to.gte => date_from) }
22
+ scope :year, ->(year) { yearly.for_dates(Date.parse("01/01/#{year}"), Date.parse("01/01/#{year}").at_end_of_year) }
23
+ scope :month, ->(month, year) { monthly.for_dates(Date.parse("01/#{month}/#{year}"), Date.parse("01/#{month}/#{year}").at_end_of_month) }
24
+ scope :week, ->(week, year) { weekly.for_dates(Date.commercial(year, week), Date.commercial(year, week).at_end_of_week) }
25
+ scope :day, ->(date) { daily.for_dates(date, date) }
32
26
 
33
- scope :for_dates, -> (date_from, date_to) { where(date_from: date_from, date_to: date_to) }
34
-
35
- scope :yearly, -> (year) { for_dates(Date.parse("01/01/#{year}"), Date.parse("01/01/#{year}").at_end_of_year) }
36
- scope :monthly, -> (month, year) { for_dates(Date.parse("01/#{month}/#{year}"), Date.parse("01/#{month}/#{year}").at_end_of_month) }
37
- scope :weekly, -> (week, year) { for_dates(Date.commercial(year, week), Date.commercial(year, week).at_end_of_week) }
38
- scope :daily, -> (date) { for_dates(date, date) }
39
-
40
- scope :scoped_to, -> (scope) { where(scope: scope) }
41
-
42
- # ---------------------------------------------------------------------
43
-
44
- index(scope: 1, date_from: 1, date_to: 1)
27
+ index time_scope: 1, date_from: 1, date_to: 1
45
28
  end
46
29
  end
47
30
 
48
- # =====================================================================
49
-
50
31
  module ClassMethods
51
32
  def log(*args)
52
33
  MongoidTraffic::Logger.log(self, *args)
53
34
  end
54
35
 
55
- def aggregate_on(att)
56
- case find_field_by_name(att).type.to_s
57
- when 'Integer' then sum(att)
58
- when 'Hash' then sum_hash(att)
59
- end
36
+ def additional_counter(name, as: nil)
37
+ field name, as: as, type: Hash, default: {}
60
38
  end
61
39
 
62
- def sum(att)
63
- if att.to_sym == :unique_ids
64
- aggregate_on(:unique_ids).keys.count
65
- else
66
- super(att)
40
+ def aggregate_on(att)
41
+ field = find_field(att)
42
+ case field.type.to_s
43
+ when 'Integer' then sum(field.name)
44
+ when 'Hash' then sum_hash(field.name)
67
45
  end
68
46
  end
69
47
 
70
- def find_field_by_name(field_name)
71
- return unless f = fields.detect { |k, v| k == field_name.to_s || v.options[:as].to_s == field_name.to_s }
72
- f.last
48
+ private
49
+
50
+ def find_field(name)
51
+ fields.detect do |field_name, field|
52
+ field_name == name.to_s || field.options[:as].to_s == name.to_s
53
+ end.try(:last)
73
54
  end
74
55
 
75
56
  def sum_hash(field_name)
76
57
  pluck(field_name).inject({}) do |res, h|
77
- merger = proc do |_key, v1, v2|
78
- case
79
- when Hash === v1 && Hash === v2 then v1.merge(v2, &merger)
80
- when Hash === v2 then v2
58
+ merger = proc do |_, v1, v2|
59
+ if Hash === v1 && Hash === v2 then v1.merge(v2, &merger)
60
+ elsif Hash === v2 then v2
81
61
  else v1.to_i + v2.to_i
82
62
  end
83
63
  end