btrack 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +323 -0
- data/Rakefile +4 -0
- data/btrack.gemspec +28 -0
- data/lib/btrack.rb +24 -0
- data/lib/btrack/config.rb +28 -0
- data/lib/btrack/helper.rb +25 -0
- data/lib/btrack/query.rb +45 -0
- data/lib/btrack/query/criteria.rb +62 -0
- data/lib/btrack/query/lua.rb +74 -0
- data/lib/btrack/redis.rb +30 -0
- data/lib/btrack/time_frame.rb +63 -0
- data/lib/btrack/tracker.rb +68 -0
- data/lib/btrack/version.rb +3 -0
- data/spec/btrack_spec.rb +5 -0
- data/spec/config_spec.rb +19 -0
- data/spec/query_spec.rb +51 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/time_frame_spec.rb +15 -0
- data/spec/tracker_spec.rb +42 -0
- metadata +161 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b730fa2c3a34a3116f0b94591f528443855121f1
|
4
|
+
data.tar.gz: 6ce8856176cd93b9c372e07f61a5380ee0a6e67a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: da1feb3673a195c82b6e8a0cde1006688462b8866fee09587da5dc061c8ac7661a4f99bd753230f103103d309785c45c1e8aadff34a527baa83800ce2e136ad8
|
7
|
+
data.tar.gz: 4b86399ba364d3224f51189a88ec09473e88f4633785c017383f6424ad6325427fe20176a760b66ceb170ba4fc8c21cbed6c5aa8495021f0012f586328b22c27
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.1
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Chen Fisher
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,323 @@
|
|
1
|
+
# Btrack
|
2
|
+
|
3
|
+
**Btrack** is an activity tracker with extensive query mechanism, minimum memory footprint and maximum performance (thanks to redis)
|
4
|
+
|
5
|
+
With **Btrack** you can track any activity of any entity in your website or process
|
6
|
+
|
7
|
+
#### Tracking user logins (user 123 has just logged in):
|
8
|
+
```ruby
|
9
|
+
Btrack.track :logged_in, 123
|
10
|
+
```
|
11
|
+
|
12
|
+
#### Query for total logins:
|
13
|
+
```ruby
|
14
|
+
Btrack.where(logged_in: :today).count
|
15
|
+
```
|
16
|
+
|
17
|
+
#### Query if a specific user visited your website last month:
|
18
|
+
```ruby
|
19
|
+
Btrack.where(visited: :last_month).exists? 123
|
20
|
+
|
21
|
+
# use a cool shortcut
|
22
|
+
Btrack::Query.visited? 123, :today
|
23
|
+
```
|
24
|
+
|
25
|
+
#### You can also plot a graph!
|
26
|
+
``` ruby
|
27
|
+
Btrack.where(clicked_a_button: 1.week.ago..Time.now).plot
|
28
|
+
#=> {2014-07-14 00:00:00 +0300=>10, 2014-07-14 00:00:00 +0300=>5, 2014-07-14 00:00:00 +0300=>30...
|
29
|
+
```
|
30
|
+
|
31
|
+
#### Cohort analysis (for example, all users that signed in last week and visited this week )
|
32
|
+
```ruby
|
33
|
+
Btrack.where([{signed_in: :last_week}, {visited: :this_week}]).plot
|
34
|
+
```
|
35
|
+
|
36
|
+
# Background
|
37
|
+
**Btrack** uses Redis bitmaps to track activities over entities; you can track millions of users with a very small memory footprint and use bitwise operators to determine complex queries in realtime.
|
38
|
+
|
39
|
+
See relevant Redis commands for bitmaps here:
|
40
|
+
[http://redis.io/commands/SETBIT]
|
41
|
+
|
42
|
+
Read this to better understand how bitmaps work in redis and how you can use it for fast, realtime analytics: [http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/]
|
43
|
+
|
44
|
+
|
45
|
+
## Installation
|
46
|
+
|
47
|
+
Add this line to your application's Gemfile:
|
48
|
+
|
49
|
+
gem 'btrack'
|
50
|
+
|
51
|
+
And then execute:
|
52
|
+
|
53
|
+
$ bundle
|
54
|
+
|
55
|
+
Or install it yourself as:
|
56
|
+
|
57
|
+
$ gem install btrack
|
58
|
+
|
59
|
+
## Usage
|
60
|
+
|
61
|
+
## Tracking
|
62
|
+
Basic tracking is done by specifying the event to track (can be a string or a symbol) and the id of the entity you are tracking (**must** be an interger)
|
63
|
+
|
64
|
+
User with id 123 purchased something:
|
65
|
+
```ruby
|
66
|
+
Btrack.track "user:purchased", 123
|
67
|
+
```
|
68
|
+
Item with id 1001 was just purchased:
|
69
|
+
```ruby
|
70
|
+
Btrack.track "item:purchased", 1001
|
71
|
+
```
|
72
|
+
|
73
|
+
### Granularity
|
74
|
+
When tracking an event a default granularity is used (see **configuration** section for more details on default values); the default granularity is :hourly..:monthly, which means :hourly, :daily, :weekly and :monthly
|
75
|
+
|
76
|
+
To track with a different granualrity:
|
77
|
+
```ruby
|
78
|
+
# track with a weekly granularity
|
79
|
+
Btrack.track :logged_in, 123, :weekly
|
80
|
+
|
81
|
+
# track with both daily and weekly granularity:
|
82
|
+
Btrack.track :logged_in, 123, [:daily, :weekly]
|
83
|
+
|
84
|
+
# track with a range of granularities:
|
85
|
+
Btrack.track :logged_in, 123, :hourly..:monthly
|
86
|
+
#=> will track with: hourly, daily, weekly and monthly
|
87
|
+
```
|
88
|
+
|
89
|
+
Available granularities: [:minute, :hourly, :daily, :weekly, :monthly, :yearly]
|
90
|
+
|
91
|
+
|
92
|
+
#### CAVEATS with granularities
|
93
|
+
You should be aware that there is a close relation between tracking and querying in regards to granularities. see **querying/granularity** section for more details.
|
94
|
+
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
# track with daily granularity
|
98
|
+
Btrack.track :logged_in, 123, :daily
|
99
|
+
|
100
|
+
# query with weekly granualrity:
|
101
|
+
Btrack.where [{logged_in: :today, granularity: :weekly}]
|
102
|
+
#=> returns 0
|
103
|
+
|
104
|
+
# tracking with a range of granularities:
|
105
|
+
Btrack.track :logged_in, 123, :daily..:monthly
|
106
|
+
|
107
|
+
# now querying with weekly granularity is OK because it is included in the range:
|
108
|
+
Btrack.where [{logged_in: :today, granularity: :weekly}]
|
109
|
+
#=> returns 1
|
110
|
+
```
|
111
|
+
### Tracking with a block
|
112
|
+
You can track with a block for convenience and for specifying other tracking options:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
Btrack::Tracker.track do |b|
|
116
|
+
b.key = "logged_in"
|
117
|
+
b.id = 123
|
118
|
+
b.granularity = :daily..:monthly
|
119
|
+
b.when = 3.days.ago # when was this event occured
|
120
|
+
b.expiration_for = { daily:6.days } # specify expiration for this event
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
### Tracking in the past tense
|
125
|
+
When tracking an event, the default time is ```Time.now``` which means: the event just happend.
|
126
|
+
|
127
|
+
You can specify a different time when tracking an event:
|
128
|
+
```ruby
|
129
|
+
Btrack::Tracker.track do |b|
|
130
|
+
b.key = :logged_in
|
131
|
+
b.id = 123
|
132
|
+
b.when = 3.days.ago # this event happened 3 days ago
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
### Expiration time
|
137
|
+
You can specity retention for events per granularity. Use this to get rid of granularities you don't need any more and save memory (in redis)
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
Btrack::Tracker.track do |b|
|
141
|
+
b.key = :logged_in
|
142
|
+
b.id = 123
|
143
|
+
b.expiration_for = { minute: 3.days, daily: 3.months }
|
144
|
+
# after 3 days all "minute" granularities for this event will be deleted
|
145
|
+
# and 3 months later all the relevant "daily" granularities will be deleted
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
## Querying
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
# Simple querying
|
153
|
+
Btrack.where(logged_in: :today).count
|
154
|
+
Btrack.where(logged_in: :yesteday).count
|
155
|
+
Btrack.where(logged_in: :last_week).count
|
156
|
+
|
157
|
+
# Query with a time range
|
158
|
+
Btrack.where(logged_in: 3.days.ago..Time.now).count
|
159
|
+
```
|
160
|
+
|
161
|
+
`Btrack.where` has the following form: `where(criteria, options={})`
|
162
|
+
|
163
|
+
**criteria** is an array of hashes, where each hash is the event to query and relevant optional options of that event (don't worry, there are plentty of examples in this section; just make sure to wrap the criteria with array and proper hashes when querying for more than one event or when specifying a granularity for an event)
|
164
|
+
|
165
|
+
**options={}** is preserved for feature use and is meant for specifying options that are not event specific
|
166
|
+
|
167
|
+
### Querying for a specific user/entity
|
168
|
+
```ruby
|
169
|
+
Btrack.where(logged_in: :today).exists? 123
|
170
|
+
#=> returns true if user 123 logged in today
|
171
|
+
|
172
|
+
Btrack.where(visited: 7.days.ago..Time.now)exists? 123
|
173
|
+
#=> returns true if user 123 visited the website in the past 7 days
|
174
|
+
|
175
|
+
# You can use a cool shortcut to query for a specific user:
|
176
|
+
Btrack::Query.visited? 123, 7.days.ago..Time.now
|
177
|
+
#=> same as above, but with a cool shortcut
|
178
|
+
|
179
|
+
Btrack::Query.logged_in? 123, :today
|
180
|
+
#=> true/false
|
181
|
+
```
|
182
|
+
### Lazyness
|
183
|
+
Queries are not "realized" until you perform an action:
|
184
|
+
```ruby
|
185
|
+
a_query = Btrack.where [logged_in: 1.week.ago..Time.now, granularity: :daily]
|
186
|
+
#=> <Btrack::Query::Criteria:0x007fceb248e120 @criteria=[{:logged_in=>:today}], @options={}>
|
187
|
+
|
188
|
+
a_query.count
|
189
|
+
#=> 3
|
190
|
+
|
191
|
+
a_query.exists? 123
|
192
|
+
#=> true
|
193
|
+
|
194
|
+
a_query.plot
|
195
|
+
#=> {2014-07-01 00:00:00 +0300=>10, 2014-07-01 00:00:00 +0300=>5...
|
196
|
+
|
197
|
+
# You can use the "realize!" action to see what's under the hood
|
198
|
+
a_query.realize!
|
199
|
+
#=> [["btrack:logged_in:2014-07-11", "btrack:logged_in:2014-07-12", "btrack:logged_in:2014-07-13"...
|
200
|
+
```
|
201
|
+
|
202
|
+
### Intersection (querying for multiple events)
|
203
|
+
You can query for multiple events
|
204
|
+
```ruby
|
205
|
+
# signed in AND purchased something this month
|
206
|
+
q = Btrack.where([{signed_in: :this_month}, {purchased: :this_month}])
|
207
|
+
|
208
|
+
q.count
|
209
|
+
q.exists? 123
|
210
|
+
q.plot
|
211
|
+
|
212
|
+
# logged last week AND logged in today
|
213
|
+
Btrack.where([{logged_in: :last_week}, {logged_in: :today}])
|
214
|
+
|
215
|
+
# signed in the last 30 days, logged in this week and purchased something
|
216
|
+
Btrack.where([{signed_in: 30.days.ago..Time.now}, {logged_in: :last_week}, {purchased_something: :this_month}])
|
217
|
+
```
|
218
|
+
|
219
|
+
#### The & operator
|
220
|
+
You can use `&` for intersection
|
221
|
+
```ruby
|
222
|
+
signed_in = Btrack.where signed_in: 30.days.ago..Time.now
|
223
|
+
visited = Btrack.where visited: 7.days.ago..Time.now
|
224
|
+
|
225
|
+
signed_in_AND_visited = signed_in & visited
|
226
|
+
signed_in_visited_and_whatever = signed_in_AND_visited & Btrack.where(whatever: :today)
|
227
|
+
```
|
228
|
+
|
229
|
+
### Granularity
|
230
|
+
When querying, you should make sure you are tracking in the same granularity. If you are tracking in the range of :daily..:monthly then you can only query in that range (or you will get wrong results)
|
231
|
+
|
232
|
+
To specify the granualrity when querying, add a :granualrity key to the hash (per event):
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
Btrack.where([{clicked_a_button: 3.hours.ago..Time.now, granularity: :hourly}])
|
236
|
+
|
237
|
+
# granularity is per event:
|
238
|
+
Btrack.where([{logged_in: 1.hour.ago..Time.now, granularity: :minute}, {did_something: :today, granularity: :daily}])
|
239
|
+
|
240
|
+
# see the next section (plotting) about granularity when plotting a graph
|
241
|
+
```
|
242
|
+
|
243
|
+
Another possible error you should be aware of is when querying for a timeframe that is not correlated with the granularity:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
# timeframe is :today, while granularity is :weekly
|
247
|
+
Btrack.where([{logged_in: :today, granularity: :weekly}])
|
248
|
+
# this will result in wrong results because :weekly granularity will refer
|
249
|
+
# to the whole week, while you probably meant to query only :today
|
250
|
+
```
|
251
|
+
|
252
|
+
|
253
|
+
> Default granularity when querying is the highest resolution set in configuration.default_granularity (:hourly..:monthly => :hourly is the default when querying)
|
254
|
+
|
255
|
+
## Plotting
|
256
|
+
Use `plot` to plot a graph
|
257
|
+
```ruby
|
258
|
+
# plot a graph with a daily resolution (granularity)
|
259
|
+
Btrack.where([{logged_in: 30.days.ago..Time.now, granularity: :daily}]).plot
|
260
|
+
|
261
|
+
# plot a graph with an hourly resolution
|
262
|
+
Btrack.where([{logged_in: 30.days.ago..Time.now, granularity: :hourly}]).plot
|
263
|
+
```
|
264
|
+
|
265
|
+
### Cohort
|
266
|
+
You can use what you've learned so far to create a cohort analysis
|
267
|
+
```ruby
|
268
|
+
visits = Btrack.where [visited: 30.days.ago..Time.now, granularity: :daily]
|
269
|
+
purchases = Btrack.where [purchased_something: 30.days.ago..Tome, granularity: :daily]
|
270
|
+
|
271
|
+
visits_and_purchases = visits & purchases
|
272
|
+
|
273
|
+
# now plot a cohort
|
274
|
+
visits_and_purchases.plot
|
275
|
+
#=> {2014-07-01 00:00:00 +0300=>10, 2014-07-01 00:00:00 +0300=>20, ...
|
276
|
+
|
277
|
+
# NOTE that when plotting multiple events (cohort), the returned keys for the plot are named after the first event
|
278
|
+
```
|
279
|
+
|
280
|
+
## Configuration
|
281
|
+
Put this in an `initializer` to configure **Btrack**
|
282
|
+
```ruby
|
283
|
+
Btrack.config do |config|
|
284
|
+
config.namespace = 'btrack' # default namespace for redis keys
|
285
|
+
config.redis_url = nil # redis url to use; defaults to nil meaning localhost and default redis port
|
286
|
+
config.expiration_for = {minute: 1.day, hourly: 1.week, daily: 3.months, weekly: 1.year, monthly: 1.year, yearly: 1.year}
|
287
|
+
config.default_granularity: :daily..:monthly # default granularities when tracking
|
288
|
+
config.silent = false # to break or not to break on redis errors
|
289
|
+
end
|
290
|
+
```
|
291
|
+
|
292
|
+
### namespace
|
293
|
+
Sets the namespace to use for the keys in redis; defaults to **btrack**
|
294
|
+
|
295
|
+
> keys in redis look like this: `btrack:logged_in:2014-07-15`
|
296
|
+
>
|
297
|
+
>and in a general form: `namespace:event:datetime`
|
298
|
+
|
299
|
+
### expiration_for
|
300
|
+
Use expiration_for to set the expiration for a specific granularity
|
301
|
+
```ruby
|
302
|
+
Btrack.config.expiration_for = { daily: 7.months }
|
303
|
+
# will merge :daily expiration with the whole expirations hash
|
304
|
+
# {minute: 1.day, hourly: 1.week, daily: 7.months, weekly: 1.year, monthly: 1.year, yearly: 1.year}
|
305
|
+
```
|
306
|
+
### redis_url
|
307
|
+
Sets the connection url to the redis server; defaults to nil which means localhost and default redis port
|
308
|
+
|
309
|
+
## Alternatives
|
310
|
+
[Minuteman](https://github.com/elcuervo/minuteman) is a nice alternative to **Btrack** but with the following caveats (and more):
|
311
|
+
|
312
|
+
1. It does not support time frames (you cannot query for 30.days.ago..Time.now)
|
313
|
+
2. It eagerly creates a redis key on every bitwise operator, while **Btrack** is lazy
|
314
|
+
3. It uses redis `multi` while **Btrack** uses `lua` for better performance
|
315
|
+
4. No plot option in Minuteman
|
316
|
+
|
317
|
+
## Contributing
|
318
|
+
|
319
|
+
1. Fork it
|
320
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
321
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
322
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
323
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/btrack.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'btrack/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "btrack"
|
8
|
+
spec.version = Btrack::VERSION
|
9
|
+
spec.authors = ["Chen Fisher"]
|
10
|
+
spec.email = ["chen.fisher@gmail.com"]
|
11
|
+
spec.description = %q{Enables tracking and querying of any activity in a website or process with minimum memory signature and maximum performance (thanks to redis)}
|
12
|
+
spec.summary = %q{Activity tracker with minimum memory footprint and maximum performance (thanks to redis)}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activesupport"
|
22
|
+
spec.add_dependency "redis"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
spec.add_development_dependency "wrong"
|
28
|
+
end
|
data/lib/btrack.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require "btrack/version"
|
3
|
+
require "btrack/helper"
|
4
|
+
require "btrack/config"
|
5
|
+
require "btrack/time_frame"
|
6
|
+
require "btrack/redis"
|
7
|
+
require "btrack/tracker"
|
8
|
+
require "btrack/query"
|
9
|
+
|
10
|
+
module Btrack
|
11
|
+
extend self
|
12
|
+
|
13
|
+
delegate :where, to: Query
|
14
|
+
delegate :track, to: Tracker
|
15
|
+
|
16
|
+
def redis
|
17
|
+
@redis ||= Btrack::Redis.create
|
18
|
+
end
|
19
|
+
|
20
|
+
def config
|
21
|
+
yield Btrack::Config if block_given?
|
22
|
+
Btrack::Config
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Btrack
|
2
|
+
class Config
|
3
|
+
|
4
|
+
@config = OpenStruct.new ({
|
5
|
+
namespace: "btrack",
|
6
|
+
redis_url: nil, # nil means localhost with defailt redis port
|
7
|
+
expirations: {minute: 1.day, hourly: 1.week, daily: 3.months, weekly: 1.year, monthly: 1.year, yearly: 1.year},
|
8
|
+
default_granularity: :hourly..:monthly,
|
9
|
+
silent: false # to break or not to break (on redis errors); that is the question
|
10
|
+
})
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
def expiration_for(g)
|
15
|
+
@config[:expirations][g]
|
16
|
+
end
|
17
|
+
|
18
|
+
def expiration_for=(g)
|
19
|
+
@config[:expirations].merge!(g)
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing(method, *args, &block)
|
23
|
+
@config.send(method, *args, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Btrack
|
2
|
+
class Helper
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def key(k, g=Config.default_granularity, w=Time.now)
|
6
|
+
"#{Config.namespace}:#{k}:#{granularity g, w || Time.now}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def keys(k, timeframe)
|
10
|
+
tf = timeframe.is_a?(TimeFrame) ? timeframe : (TimeFrame.new timeframe)
|
11
|
+
tf.splat { |t| key k, tf.granularity, t}
|
12
|
+
end
|
13
|
+
|
14
|
+
def granularity(g=:daily, w=Time.now)
|
15
|
+
return g unless [:minute, :hourly, :daily, :weekly, :monthly, :yearly].include? g
|
16
|
+
w.strftime(format(g))
|
17
|
+
end
|
18
|
+
|
19
|
+
def format(g)
|
20
|
+
{ minute: "%Y-%m-%d-%H-%M", hourly: "%Y-%m-%d-%H", daily: "%Y-%m-%d", weekly: "%G-W%V", monthly: "%Y-%m", yearly: "%Y"}[g] || "%Y-%m-%d"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/lib/btrack/query.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'btrack/query/criteria'
|
2
|
+
require 'btrack/query/lua'
|
3
|
+
|
4
|
+
module Btrack
|
5
|
+
class Query
|
6
|
+
attr_reader :criteria
|
7
|
+
|
8
|
+
delegate :with_sha, :with_silent, to: Btrack::Redis
|
9
|
+
|
10
|
+
class << self
|
11
|
+
delegate :where, to: Criteria
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(criteria = nil)
|
15
|
+
@criteria = criteria.freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
def count
|
19
|
+
with_silent { with_sha { [lua(:count), *@criteria.realize!] } }
|
20
|
+
end
|
21
|
+
|
22
|
+
def exists?(id = nil)
|
23
|
+
c = id ? @criteria.where([], id: id) : @criteria
|
24
|
+
with_silent { with_sha { [lua(:exists), *c.realize!] } == 1 }
|
25
|
+
end
|
26
|
+
|
27
|
+
def plot
|
28
|
+
JSON.parse(with_silent { with_sha { [plot_lua, *@criteria.realize!] } }).inject({}) do |n, (k, v)|
|
29
|
+
g = @criteria.criteria.first[:granularity] || Criteria.default_granularity
|
30
|
+
key = Time.strptime(k.rpartition(":").last, Helper.format(g))
|
31
|
+
n[key] = v
|
32
|
+
n
|
33
|
+
end
|
34
|
+
rescue
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def method_missing(method, *args, &block)
|
40
|
+
return unless method.to_s.end_with? '?'
|
41
|
+
Criteria.where({method.to_s.chomp('?') => args[1]}.merge(args.extract_options!), id: args[0]).exists?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'btrack/helper'
|
2
|
+
require 'btrack/time_frame'
|
3
|
+
|
4
|
+
module Btrack
|
5
|
+
class Query
|
6
|
+
class Criteria
|
7
|
+
attr_reader :options, :criteria
|
8
|
+
delegate :count, :exists?, :plot, to: :query
|
9
|
+
|
10
|
+
# initializes a new crieteria
|
11
|
+
def initialize(criteria, options={})
|
12
|
+
@criteria = parse(criteria).freeze
|
13
|
+
@options = options.freeze
|
14
|
+
end
|
15
|
+
|
16
|
+
# returns a new criteria object with the union of both criterias
|
17
|
+
def where(*args)
|
18
|
+
self & Criteria.new(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
# returns a new criteria object with the union of both criterias
|
22
|
+
def &(criteria)
|
23
|
+
Criteria.new self.criteria + criteria.criteria, self.options.merge(criteria.options)
|
24
|
+
end
|
25
|
+
|
26
|
+
# make this criteria 'real' by extracting keys and args to be passed to redis lua script
|
27
|
+
def realize!
|
28
|
+
prefix = "#{@options[:prefix]}:" if @options[:prefix]
|
29
|
+
|
30
|
+
keys = @criteria.map do |c|
|
31
|
+
cvalue = c.values.first
|
32
|
+
(Helper.keys "#{prefix}#{c.keys.first}", TimeFrame.new(cvalue, c[:granularity] || (cvalue.granularity if cvalue.is_a? TimeFrame) || self.class.default_granularity)).flatten
|
33
|
+
end
|
34
|
+
|
35
|
+
[keys.flatten << @options[:id], keys.map(&:count)]
|
36
|
+
end
|
37
|
+
|
38
|
+
# access methods from class instance
|
39
|
+
# returns a new criteria instance
|
40
|
+
class << self
|
41
|
+
def where(*args)
|
42
|
+
Criteria.new *args
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_granularity
|
46
|
+
Config.default_granularity.is_a?(Range) ? Config.default_granularity.first : Array.wrap(Config.default_granularity).first
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# delegate method
|
51
|
+
def query
|
52
|
+
Query.new self
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def parse(criteria)
|
58
|
+
criteria.is_a?(Array) ? criteria : criteria.map { |k, v| {k => v} }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Btrack
|
2
|
+
class Query
|
3
|
+
def lua(f)
|
4
|
+
%Q{
|
5
|
+
local index = 1
|
6
|
+
|
7
|
+
for i, count in ipairs(ARGV) do
|
8
|
+
if count == '0' then
|
9
|
+
break
|
10
|
+
end
|
11
|
+
|
12
|
+
local bitop = {}
|
13
|
+
|
14
|
+
for c = index, index * count do
|
15
|
+
table.insert(bitop, KEYS[c])
|
16
|
+
index = index + 1
|
17
|
+
end
|
18
|
+
|
19
|
+
if i == 1 then
|
20
|
+
redis.call('bitop', 'or', 'tmp', unpack(bitop))
|
21
|
+
else
|
22
|
+
redis.call('bitop', 'or', 'tmp:or', unpack(bitop))
|
23
|
+
redis.call('bitop', 'and', 'tmp', 'tmp', 'tmp:or')
|
24
|
+
redis.call('del', 'tmp:or')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#{send('lua_' + f.to_s)}
|
29
|
+
|
30
|
+
redis.call('del', 'tmp')
|
31
|
+
return results
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def lua_count
|
36
|
+
"local results = redis.call('bitcount', 'tmp')"
|
37
|
+
end
|
38
|
+
|
39
|
+
def lua_exists
|
40
|
+
"local results = redis.call('getbit', 'tmp', KEYS[#KEYS])"
|
41
|
+
end
|
42
|
+
|
43
|
+
# lua script for plotting
|
44
|
+
# please note - it cannot be used with the prefixed 'lua' like count and exists
|
45
|
+
# this is a standalone script ment for plotting and allowing for cohort analysis
|
46
|
+
# all series must be of the same size
|
47
|
+
def plot_lua
|
48
|
+
%Q{
|
49
|
+
local series_count = #ARGV
|
50
|
+
local length = ARGV[1]
|
51
|
+
|
52
|
+
-- run over the first series
|
53
|
+
-- all series must be of the same size
|
54
|
+
local plot = {}
|
55
|
+
for i = 1, length do
|
56
|
+
local bitop = {}
|
57
|
+
for j = 1, series_count do
|
58
|
+
table.insert(bitop, KEYS[i*j])
|
59
|
+
end
|
60
|
+
|
61
|
+
-- make sure 'tmp' is populated with the first key (so the 'and' op would work as expected)
|
62
|
+
redis.call('bitop', 'or', 'tmp', 'tmp', bitop[1])
|
63
|
+
|
64
|
+
redis.call('bitop', 'and', 'tmp', unpack(bitop))
|
65
|
+
-- table.insert(plot, redis.call('bitcount', 'tmp'))
|
66
|
+
plot[KEYS[i]] = redis.call('bitcount', 'tmp')
|
67
|
+
redis.call('del', 'tmp')
|
68
|
+
end
|
69
|
+
|
70
|
+
return cjson.encode(plot)
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/btrack/redis.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'btrack/config'
|
3
|
+
|
4
|
+
module Btrack
|
5
|
+
class Redis
|
6
|
+
class << self
|
7
|
+
def create
|
8
|
+
(::Redis.new url: Config.redis_url if Config.redis_url) || ::Redis.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def with_silent(&block)
|
12
|
+
yield if block
|
13
|
+
rescue ::Redis::BaseError => e
|
14
|
+
raise e unless Config.silent
|
15
|
+
end
|
16
|
+
|
17
|
+
def with_sha(&block)
|
18
|
+
params = yield; script = params.shift
|
19
|
+
Btrack.redis.evalsha sha(script), *params
|
20
|
+
rescue ::Redis::CommandError => e
|
21
|
+
raise unless e.message.start_with?("NOSCRIPT")
|
22
|
+
Btrack.redis.eval script, *params
|
23
|
+
end
|
24
|
+
|
25
|
+
def sha(script)
|
26
|
+
Digest::SHA1.hexdigest(script)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Btrack
|
2
|
+
class TimeFrame
|
3
|
+
attr_reader :from, :to, :granularity
|
4
|
+
|
5
|
+
def initialize(timeframe, granularity=nil)
|
6
|
+
raise ArgumentError, "TimeFrame should be initialized with Symbol, Hash, Range or Btrack::TimeFrame" unless [Symbol, Hash, Range, TimeFrame, Time].include? timeframe.class
|
7
|
+
|
8
|
+
@from, @to = self.send("init_with_#{timeframe.class.name.demodulize.underscore}", timeframe)
|
9
|
+
@granularity = granularity || (timeframe.granularity if timeframe.is_a?(TimeFrame)) || Config.default_granularity
|
10
|
+
end
|
11
|
+
|
12
|
+
def splat(granularity=self.granularity)
|
13
|
+
[].tap do |keys|
|
14
|
+
(from.to_i .. to.to_i).step(step(granularity)) do |t|
|
15
|
+
keys << (block_given? ? (yield Time.at(t)) : Time.at(t))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def init_with_time_frame(timeframe)
|
22
|
+
[timeframe.from, timeframe.to]
|
23
|
+
end
|
24
|
+
|
25
|
+
def init_with_time(time)
|
26
|
+
[time.beginning_of_day, time.end_of_day]
|
27
|
+
end
|
28
|
+
|
29
|
+
def init_with_symbol(timeframe)
|
30
|
+
case timeframe
|
31
|
+
when :hour, :day, :week, :month, :year
|
32
|
+
return 1.send(timeframe).ago, Time.now
|
33
|
+
when :today
|
34
|
+
return Time.now.beginning_of_day, Time.now
|
35
|
+
when :yesterday
|
36
|
+
return 1.day.ago.beginning_of_day, 1.day.ago.end_of_day
|
37
|
+
when :this_week
|
38
|
+
return Time.now.beginning_of_week, Time.now
|
39
|
+
when :last_week
|
40
|
+
return 1.week.ago.beginning_of_week, 1.week.ago.end_of_week
|
41
|
+
when :this_month
|
42
|
+
return Time.now.beginning_of_month, Time.now
|
43
|
+
when :last_month
|
44
|
+
return 1.month.ago.beginning_of_month, 1.month.ago.end_of_month
|
45
|
+
else
|
46
|
+
return 1.day.ago, Time.now
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def init_with_hash(timeframe)
|
51
|
+
[timeframe[:from] && timeframe[:from].is_a?(String) && Time.parse(timeframe[:from]) || timeframe[:from] || 1.month.ago,
|
52
|
+
timeframe[:to] && timeframe[:to].is_a?(String) && Time.parse(timeframe[:to]) || timeframe[:to] || Time.now]
|
53
|
+
end
|
54
|
+
|
55
|
+
def init_with_range(timeframe)
|
56
|
+
init_with_hash(from: timeframe.first, to: timeframe.last)
|
57
|
+
end
|
58
|
+
|
59
|
+
def step(g)
|
60
|
+
{minute: 1.minute, hourly: 1.hour, daily: 1.day, weekly: 1.week, monthly: 1.month, yearly: 1.year}[g] || 1.day
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Btrack
|
2
|
+
class Tracker
|
3
|
+
class << self
|
4
|
+
#
|
5
|
+
# Tracks an activity of an entity
|
6
|
+
#
|
7
|
+
# For example: User 123 has just logged in
|
8
|
+
# Btrack::Tracker.track :logged_in, 123
|
9
|
+
#
|
10
|
+
# Can be called in 3 different ways (for convenience): with args, hash or a block
|
11
|
+
#
|
12
|
+
# Example Using a block
|
13
|
+
# Btrack::Tracker.track do |b|
|
14
|
+
# b.key = "logged_in"
|
15
|
+
# b.id = 123
|
16
|
+
# b.granularity = :daily..:monthly
|
17
|
+
# b.when = 3.days.ago
|
18
|
+
# b.expiration_for = { daily:6.days }
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# See documentation for full details on args and options
|
22
|
+
def track(*args, &block)
|
23
|
+
return track_with_block(&block) if block_given?
|
24
|
+
return track_with_hash if args.first === Hash
|
25
|
+
|
26
|
+
track_with_args(*args)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def track_with_hash(options)
|
32
|
+
granularity_range(options[:granularity] || Config.default_granularity).each do |g|
|
33
|
+
key = Helper.key options[:key], g, options[:when]
|
34
|
+
|
35
|
+
Btrack::Redis.with_silent do
|
36
|
+
Btrack.redis.pipelined do |r|
|
37
|
+
r.setbit key, options[:id].to_i, 1
|
38
|
+
r.expire key, options[:expiration_for] && options[:expiration_for][g] || Config.expiration_for(g)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def track_with_block
|
45
|
+
yield options = OpenStruct.new
|
46
|
+
|
47
|
+
track_with_hash options
|
48
|
+
end
|
49
|
+
|
50
|
+
def track_with_args(*args)
|
51
|
+
options = args.extract_options!.merge ({
|
52
|
+
key: args[0],
|
53
|
+
id: args[1],
|
54
|
+
granularity: args[2]
|
55
|
+
})
|
56
|
+
|
57
|
+
track_with_hash options
|
58
|
+
end
|
59
|
+
|
60
|
+
def granularity_range(granularities)
|
61
|
+
return [granularities].flatten unless granularities.is_a? Range
|
62
|
+
predefined[predefined.index(granularities.first)..predefined.index(granularities.last)]
|
63
|
+
end
|
64
|
+
|
65
|
+
def predefined; [:minute, :hourly, :daily, :weekly, :monthly, :yearly]; end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/spec/btrack_spec.rb
ADDED
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Btrack::Config do
|
4
|
+
|
5
|
+
Btrack.config do |config|
|
6
|
+
config.namespace = "test"
|
7
|
+
end
|
8
|
+
|
9
|
+
it "sets a namespace" do
|
10
|
+
assert { Btrack::Helper.key(:logged_in).starts_with? "test" }
|
11
|
+
end
|
12
|
+
|
13
|
+
it "sets expiration time" do
|
14
|
+
expected = Btrack.config.expirations.dup
|
15
|
+
Btrack.config.expiration_for = {weekly: 2.days}
|
16
|
+
|
17
|
+
assert { Btrack.config.expirations == expected.merge(weekly: 2.days) }
|
18
|
+
end
|
19
|
+
end
|
data/spec/query_spec.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Btrack::Query do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
10.times do |i|
|
7
|
+
Btrack.track :logged_in, i, :hourly..:monthly # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
8
|
+
Btrack.track :logged_in, i*2, when: 1.days.ago # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
|
9
|
+
|
10
|
+
Btrack.track :visited, i if i%3 == 0 # [0, 3, 6, 9]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
it "returns count of unique logins for today" do
|
16
|
+
assert { Btrack.where(logged_in: :today).count == 10 }
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns count of unique logins yesterday" do
|
20
|
+
assert { Btrack.where(logged_in: :yesterday).count == 10 }
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns count of unique logins in a range of time" do
|
24
|
+
assert { Btrack.where(logged_in: 1.days.ago..Time.now).count == 15 }
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns count with weekly granularity" do
|
28
|
+
assert { Btrack.where([{logged_in: :this_week, granularity: :weekly}]).count == 15 }
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns the intersection of two different time frames" do
|
32
|
+
assert { Btrack.where([{logged_in: :today}, {logged_in: :yesterday}]).count == 5 }
|
33
|
+
end
|
34
|
+
|
35
|
+
it "returns the intersection of two different activities" do
|
36
|
+
assert { Btrack.where([{logged_in: :yesterday}, {visited: :today}]).count == 2 }
|
37
|
+
end
|
38
|
+
|
39
|
+
it "check if id exists in activity" do
|
40
|
+
assert { Btrack.where(logged_in: :today).exists? 5 }
|
41
|
+
end
|
42
|
+
|
43
|
+
it "checks if id did two activities (intersection)" do
|
44
|
+
assert { Btrack.where([{logged_in: :yesterday}, {visited: :today}], id: 6).exists? }
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should return false for id that does not satisfies an intersection" do
|
48
|
+
assert { Btrack.where([{logged_in: :yesterday}, {visited: :today}], id: 3).exists? == false }
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require "wrong/adapters/rspec"
|
4
|
+
require 'btrack'
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.before :each do
|
8
|
+
Btrack.redis.select 15
|
9
|
+
Btrack.redis.flushdb
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def is_set(key, id, granularity = :daily, w = Time.now)
|
14
|
+
Btrack.redis.getbit(Btrack::Helper.key(key, granularity, w), id) == 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def ttl(key, id, granularity = :daily, w = Time.now)
|
18
|
+
Btrack.redis.ttl(Btrack::Helper.key(key, granularity, w))
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Btrack::TimeFrame do
|
4
|
+
it "accepts time as from and to" do
|
5
|
+
tf = Btrack::TimeFrame.new from: 10.days.ago, to: Time.now
|
6
|
+
tf.from.to_i.should eq 10.days.ago.to_i
|
7
|
+
tf.to.to_i.should eq Time.now.to_i
|
8
|
+
end
|
9
|
+
|
10
|
+
it "accepts range as from and to" do
|
11
|
+
tf = Btrack::TimeFrame.new 10.days.ago..Time.now
|
12
|
+
tf.from.to_i.should eq 10.days.ago.to_i
|
13
|
+
tf.to.to_i.should eq Time.now.to_i
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'btrack/helper'
|
3
|
+
|
4
|
+
describe Btrack::Tracker do
|
5
|
+
|
6
|
+
it "tracks with basic args" do
|
7
|
+
Btrack::Tracker.track("logged_in", 123, :weekly)
|
8
|
+
assert { is_set "logged_in", 123, :weekly }
|
9
|
+
end
|
10
|
+
|
11
|
+
it "tracks with default granularity" do
|
12
|
+
Btrack::Tracker.track("logged_in", 123)
|
13
|
+
assert { is_set "logged_in", 123 }
|
14
|
+
end
|
15
|
+
|
16
|
+
it "accepts symbol as key" do
|
17
|
+
Btrack::Tracker.track(:logged_in, 123)
|
18
|
+
assert { is_set :logged_in, 123 }
|
19
|
+
end
|
20
|
+
|
21
|
+
it "accepts different time (past activity)" do
|
22
|
+
Btrack::Tracker.track(:logged_in, 123, :daily, when: 3.days.ago)
|
23
|
+
assert { is_set :logged_in, 123, :daily, 3.days.ago }
|
24
|
+
end
|
25
|
+
|
26
|
+
it "track with expiration time set" do
|
27
|
+
Btrack::Tracker.track(:logged_in, 123, :daily, expiration_for: { daily: 9.days })
|
28
|
+
assert { ttl(:logged_in, 123, :daily) == 9.days }
|
29
|
+
end
|
30
|
+
|
31
|
+
it "tracks with a block" do
|
32
|
+
Btrack::Tracker.track do |t|
|
33
|
+
t.key = :logged_in
|
34
|
+
t.id = 123
|
35
|
+
t.granularity = :weekly
|
36
|
+
t.when = 2.days.ago
|
37
|
+
end
|
38
|
+
|
39
|
+
assert { is_set :logged_in, 123, :weekly, 2.days.ago }
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: btrack
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chen Fisher
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: redis
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: wrong
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Enables tracking and querying of any activity in a website or process
|
98
|
+
with minimum memory signature and maximum performance (thanks to redis)
|
99
|
+
email:
|
100
|
+
- chen.fisher@gmail.com
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- ".gitignore"
|
106
|
+
- ".rspec"
|
107
|
+
- ".ruby-version"
|
108
|
+
- Gemfile
|
109
|
+
- LICENSE.txt
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- btrack.gemspec
|
113
|
+
- lib/btrack.rb
|
114
|
+
- lib/btrack/config.rb
|
115
|
+
- lib/btrack/helper.rb
|
116
|
+
- lib/btrack/query.rb
|
117
|
+
- lib/btrack/query/criteria.rb
|
118
|
+
- lib/btrack/query/lua.rb
|
119
|
+
- lib/btrack/redis.rb
|
120
|
+
- lib/btrack/time_frame.rb
|
121
|
+
- lib/btrack/tracker.rb
|
122
|
+
- lib/btrack/version.rb
|
123
|
+
- spec/btrack_spec.rb
|
124
|
+
- spec/config_spec.rb
|
125
|
+
- spec/query_spec.rb
|
126
|
+
- spec/spec_helper.rb
|
127
|
+
- spec/time_frame_spec.rb
|
128
|
+
- spec/tracker_spec.rb
|
129
|
+
homepage: ''
|
130
|
+
licenses:
|
131
|
+
- MIT
|
132
|
+
metadata: {}
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options: []
|
135
|
+
require_paths:
|
136
|
+
- lib
|
137
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
requirements: []
|
148
|
+
rubyforge_project:
|
149
|
+
rubygems_version: 2.2.2
|
150
|
+
signing_key:
|
151
|
+
specification_version: 4
|
152
|
+
summary: Activity tracker with minimum memory footprint and maximum performance (thanks
|
153
|
+
to redis)
|
154
|
+
test_files:
|
155
|
+
- spec/btrack_spec.rb
|
156
|
+
- spec/config_spec.rb
|
157
|
+
- spec/query_spec.rb
|
158
|
+
- spec/spec_helper.rb
|
159
|
+
- spec/time_frame_spec.rb
|
160
|
+
- spec/tracker_spec.rb
|
161
|
+
has_rdoc:
|