minuteman 1.0.3 → 2.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gems +3 -0
- data/.travis.yml +4 -3
- data/ORIGIN.md +21 -0
- data/README.md +115 -122
- data/Rakefile +3 -3
- data/SPEC.md +21 -0
- data/lib/minuteman.rb +88 -122
- data/lib/minuteman/analyzable.rb +96 -0
- data/lib/minuteman/analyzer.rb +21 -0
- data/lib/minuteman/configuration.rb +25 -0
- data/lib/minuteman/counter.rb +21 -0
- data/lib/minuteman/event.rb +12 -0
- data/lib/minuteman/lua/operations.lua +37 -0
- data/lib/minuteman/model.rb +36 -0
- data/lib/minuteman/result.rb +12 -0
- data/lib/minuteman/trigger.rb +4 -0
- data/lib/minuteman/user.rb +38 -0
- data/minuteman.gemspec +4 -5
- data/test/helper.rb +2 -0
- data/test/minuteman_bench.rb +72 -0
- data/test/minuteman_test.rb +204 -0
- metadata +44 -73
- data/Gemfile +0 -4
- data/Gemfile.lock +0 -30
- data/lib/minuteman/bit_operations.rb +0 -97
- data/lib/minuteman/bit_operations/data.rb +0 -33
- data/lib/minuteman/bit_operations/operation.rb +0 -115
- data/lib/minuteman/bit_operations/plain.rb +0 -34
- data/lib/minuteman/bit_operations/result.rb +0 -15
- data/lib/minuteman/bit_operations/with_data.rb +0 -56
- data/lib/minuteman/keys_methods.rb +0 -23
- data/lib/minuteman/time_events.rb +0 -18
- data/lib/minuteman/time_span.rb +0 -36
- data/lib/minuteman/time_spans.rb +0 -7
- data/lib/minuteman/time_spans/day.rb +0 -17
- data/lib/minuteman/time_spans/hour.rb +0 -19
- data/lib/minuteman/time_spans/minute.rb +0 -19
- data/lib/minuteman/time_spans/month.rb +0 -17
- data/lib/minuteman/time_spans/week.rb +0 -18
- data/lib/minuteman/time_spans/year.rb +0 -17
- data/test/bench/minuteman_bench.rb +0 -37
- data/test/test_helper.rb +0 -9
- data/test/unit/minuteman_test.rb +0 -225
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
|
3
|
+
module Minuteman
|
4
|
+
module Analyzable
|
5
|
+
module ErrorPatterns
|
6
|
+
DUPLICATE = /(UniqueIndexViolation: (\w+))/.freeze
|
7
|
+
NOSCRIPT = /^NOSCRIPT/.freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
def &(event)
|
11
|
+
operation("AND", [self, event])
|
12
|
+
end
|
13
|
+
|
14
|
+
def |(event)
|
15
|
+
operation("OR", [self, event])
|
16
|
+
end
|
17
|
+
alias_method :+, :|
|
18
|
+
|
19
|
+
def ^(event)
|
20
|
+
operation("XOR", [self, event])
|
21
|
+
end
|
22
|
+
|
23
|
+
def -@()
|
24
|
+
operation("NOT", [self])
|
25
|
+
end
|
26
|
+
alias :~@ :-@
|
27
|
+
|
28
|
+
def -(event)
|
29
|
+
operation("MINUS", [self, event])
|
30
|
+
end
|
31
|
+
|
32
|
+
def count
|
33
|
+
Minuteman.config.redis.call("BITCOUNT", key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def include?(user)
|
37
|
+
Minuteman.config.redis.call("GETBIT", key, user.id) == 1
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def key_exists?(key)
|
43
|
+
Minuteman.config.redis.call("EXISTS", key) == 1
|
44
|
+
end
|
45
|
+
|
46
|
+
def operation(action, events = [])
|
47
|
+
base_key = Minuteman.config.operations_prefix
|
48
|
+
|
49
|
+
destination_key = if action == "NOT"
|
50
|
+
"#{base_key}#{events[0].id}:#{action}"
|
51
|
+
else
|
52
|
+
src, dst = events[0].id, events[1].id
|
53
|
+
"#{base_key}#{src}:#{action}:#{dst}"
|
54
|
+
end
|
55
|
+
|
56
|
+
if key_exists?(destination_key)
|
57
|
+
return Minuteman::Result.new(destination_key)
|
58
|
+
end
|
59
|
+
|
60
|
+
script(Minuteman::LUA_OPERATIONS, 0, action.upcase.to_msgpack,
|
61
|
+
events.map(&:key).to_msgpack, destination_key.to_msgpack)
|
62
|
+
|
63
|
+
Minuteman::Result.new(destination_key)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Stolen from Ohm
|
67
|
+
def script(file, *args)
|
68
|
+
begin
|
69
|
+
cache = Minuteman::LUA_CACHE[Minuteman.config.redis.url]
|
70
|
+
|
71
|
+
if cache.key?(file)
|
72
|
+
sha = cache[file]
|
73
|
+
else
|
74
|
+
src = File.read(file)
|
75
|
+
sha = Minuteman.config.redis.call("SCRIPT", "LOAD", src)
|
76
|
+
|
77
|
+
cache[file] = sha
|
78
|
+
end
|
79
|
+
|
80
|
+
Minuteman.config.redis.call!("EVALSHA", sha, *args)
|
81
|
+
|
82
|
+
rescue RuntimeError
|
83
|
+
case $!.message
|
84
|
+
when ErrorPatterns::NOSCRIPT
|
85
|
+
Minuteman::LUA_CACHE[Minuteman.config.redis.url].clear
|
86
|
+
retry
|
87
|
+
when ErrorPatterns::DUPLICATE
|
88
|
+
raise UniqueIndexViolation, $1
|
89
|
+
else
|
90
|
+
raise $!
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'minuteman/event'
|
2
|
+
|
3
|
+
module Minuteman
|
4
|
+
class Analyzer
|
5
|
+
def initialize(action, klass = Minuteman::Event, user = nil)
|
6
|
+
Minuteman.patterns.keys.each do |method|
|
7
|
+
define_singleton_method(method) do |time = Time.now.utc|
|
8
|
+
if !Minuteman.patterns.include?(method)
|
9
|
+
raise MissingPattern.new(method)
|
10
|
+
end
|
11
|
+
|
12
|
+
key = Minuteman.patterns[method].call(time)
|
13
|
+
search = { type: action, time: key }
|
14
|
+
search[:user_id] = user.id if !user.nil?
|
15
|
+
|
16
|
+
klass.find_or_create(search)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Minuteman
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :redis, :patterns, :prefix, :parallel, :operations_prefix
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@redis = Ohm.redis
|
7
|
+
@prefix = "Minuteman".freeze
|
8
|
+
@operations_prefix = "#{@prefix}::Operations:"
|
9
|
+
@parallel = false
|
10
|
+
|
11
|
+
@patterns = {
|
12
|
+
year: -> (time) { time.strftime("%Y") },
|
13
|
+
month: -> (time) { time.strftime("%Y-%m") },
|
14
|
+
day: -> (time) { time.strftime("%Y-%m-%d") },
|
15
|
+
hour: -> (time) { time.strftime("%Y-%m-%d %H") },
|
16
|
+
minute: -> (time) { time.strftime("%Y-%m-%d %H:%m") },
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def redis=(redis)
|
21
|
+
@redis = redis
|
22
|
+
Ohm.redis = redis
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'minuteman/model'
|
2
|
+
|
3
|
+
module Minuteman
|
4
|
+
class Counter < Minuteman::Model
|
5
|
+
class User < Counter
|
6
|
+
attribute :user_id
|
7
|
+
|
8
|
+
def key
|
9
|
+
"#{super}:#{user_id}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def incr
|
14
|
+
Minuteman.config.redis.call("INCR", key)
|
15
|
+
end
|
16
|
+
|
17
|
+
def count
|
18
|
+
Minuteman.config.redis.call("GET", key).to_i
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
---
|
2
|
+
redis.log(redis.LOG_NOTICE, 'Minuteman')
|
3
|
+
|
4
|
+
local action = cmsgpack.unpack(ARGV[1])
|
5
|
+
local keys = cmsgpack.unpack(ARGV[2])
|
6
|
+
local dest = cmsgpack.unpack(ARGV[3])
|
7
|
+
|
8
|
+
local function operate(action, keys)
|
9
|
+
if type(keys) == "string" then keys = { keys } end
|
10
|
+
|
11
|
+
redis.call("BITOP", action, dest, unpack(keys) )
|
12
|
+
|
13
|
+
return dest
|
14
|
+
end
|
15
|
+
|
16
|
+
local function AND(keys) return operate("AND", keys) end
|
17
|
+
local function OR(keys) return operate("OR", keys) end
|
18
|
+
local function XOR(keys) return operate("XOR", keys) end
|
19
|
+
local function NOT(keys) return operate("NOT", keys) end
|
20
|
+
|
21
|
+
local function MINUS(keys)
|
22
|
+
local items = keys
|
23
|
+
local src = table.remove(items, 1)
|
24
|
+
local and_op = AND(keys)
|
25
|
+
|
26
|
+
return XOR({ src, and_op })
|
27
|
+
end
|
28
|
+
|
29
|
+
local function operation(action, keys)
|
30
|
+
if action == "MINUS" then
|
31
|
+
return MINUS(keys)
|
32
|
+
else
|
33
|
+
return operate(action, keys)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
return operation(action, keys)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'ohm'
|
2
|
+
|
3
|
+
module Minuteman
|
4
|
+
class Model < ::Ohm::Model
|
5
|
+
attribute :type
|
6
|
+
attribute :time
|
7
|
+
|
8
|
+
def self.find(*args)
|
9
|
+
looked_up = "#{self.name}::#{args.first[:type]}:#{args.first[:time]}:id"
|
10
|
+
potential_id = Minuteman.config.redis.call("GET", looked_up)
|
11
|
+
|
12
|
+
return nil if !potential_id
|
13
|
+
|
14
|
+
event = self[potential_id]
|
15
|
+
event.type = args.first[:type]
|
16
|
+
event.time = args.first[:time]
|
17
|
+
|
18
|
+
event
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.find_or_create(*args)
|
22
|
+
find(*args) || create(*args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.create(*args)
|
26
|
+
event = super(*args)
|
27
|
+
Minuteman.config.redis.call("SET", "#{event.key}:id", event.id)
|
28
|
+
event
|
29
|
+
end
|
30
|
+
|
31
|
+
def key
|
32
|
+
"#{self.class.name}::#{type}:#{time}"
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'ohm'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module Minuteman
|
5
|
+
class User < ::Ohm::Model
|
6
|
+
attribute :uid
|
7
|
+
attribute :identifier
|
8
|
+
|
9
|
+
unique :uid
|
10
|
+
unique :identifier
|
11
|
+
|
12
|
+
def save
|
13
|
+
self.uid ||= SecureRandom.uuid
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def track(action, time = Time.now.utc)
|
18
|
+
Minuteman.track(action, self, time)
|
19
|
+
end
|
20
|
+
|
21
|
+
def add(action, time = Time.now.utc)
|
22
|
+
Minuteman.add(action, time, self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def count(action, time = Time.now.utc)
|
26
|
+
Minuteman::Analyzer.new(action, Minuteman::Counter::User, self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def promote(identifier)
|
30
|
+
self.identifier = identifier
|
31
|
+
save
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.[](identifier_or_uuid)
|
35
|
+
with(:uid, identifier_or_uuid) || with(:identifier, identifier_or_uuid)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/minuteman.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "minuteman"
|
3
|
-
s.version = "
|
3
|
+
s.version = "2.0.0.pre"
|
4
4
|
s.summary = "Bit Analytics"
|
5
5
|
s.description = "Fast and furious tracking system using Redis bitwise operations"
|
6
6
|
s.authors = ["elcuervo"]
|
@@ -10,9 +10,8 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.files = `git ls-files`.split("\n")
|
11
11
|
s.test_files = `git ls-files test`.split("\n")
|
12
12
|
|
13
|
-
s.add_dependency("
|
13
|
+
s.add_dependency("redic", "~> 1.5.0")
|
14
|
+
s.add_dependency("ohm", "~> 2.3.0")
|
14
15
|
|
15
|
-
s.add_development_dependency("
|
16
|
-
s.add_development_dependency("minitest-given", "~> 3.0.0")
|
17
|
-
s.add_development_dependency("redis-namespace", "~> 1.2.1")
|
16
|
+
s.add_development_dependency("cutest", "~> 1.2.2")
|
18
17
|
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'minuteman'
|
2
|
+
require 'benchmark/ips'
|
3
|
+
|
4
|
+
Minuteman.configure do |config|
|
5
|
+
config.redis = Redic.new("redis://127.0.0.1:6379/1")
|
6
|
+
config.parallel = true
|
7
|
+
end
|
8
|
+
|
9
|
+
Minuteman.config.redis.call("FLUSHDB")
|
10
|
+
|
11
|
+
users = Array.new(5000) { Minuteman::User.create }
|
12
|
+
users_one = users.sample(5000)
|
13
|
+
users_two = users.sample(2000)
|
14
|
+
users_three = users.sample(1000)
|
15
|
+
|
16
|
+
Minuteman.track("first:page", users_one)
|
17
|
+
Minuteman.track("second:page", users_two)
|
18
|
+
Minuteman.track("third:page", users_three)
|
19
|
+
|
20
|
+
Benchmark.ips do |x|
|
21
|
+
x.report("tracking users") { Minuteman.track("first:page", users_three) }
|
22
|
+
|
23
|
+
x.report("tracking annoymous users") {
|
24
|
+
Minuteman.track("first:page")
|
25
|
+
}
|
26
|
+
|
27
|
+
x.report("operation: AND") {
|
28
|
+
Minuteman("first:page").day & Minuteman("second:page").day
|
29
|
+
}
|
30
|
+
|
31
|
+
x.report("operation: OR") {
|
32
|
+
Minuteman("first:page").day | Minuteman("second:page").day
|
33
|
+
}
|
34
|
+
|
35
|
+
x.report("operation: XOR") {
|
36
|
+
Minuteman("first:page").day ^ Minuteman("second:page").day
|
37
|
+
}
|
38
|
+
|
39
|
+
x.report("operation: NOT") {
|
40
|
+
-Minuteman("second:page").day
|
41
|
+
}
|
42
|
+
|
43
|
+
x.report("operation: MINUS") {
|
44
|
+
Minuteman("first:page").day + Minuteman("second:page").day
|
45
|
+
}
|
46
|
+
|
47
|
+
x.report("complex operations") {
|
48
|
+
(
|
49
|
+
Minuteman("first:page").day + Minuteman("second:page").day
|
50
|
+
) - Minuteman("third:page").day
|
51
|
+
}
|
52
|
+
|
53
|
+
x.report("adding to the counter") {
|
54
|
+
Minuteman.add("first:counter")
|
55
|
+
}
|
56
|
+
|
57
|
+
x.report("checking the counter") {
|
58
|
+
Counterman("first:counter").month.count
|
59
|
+
}
|
60
|
+
|
61
|
+
x.report("tracking through a user") {
|
62
|
+
users.sample.track("some:event")
|
63
|
+
}
|
64
|
+
|
65
|
+
x.report("counting through a user") {
|
66
|
+
users.sample.add("some:event")
|
67
|
+
}
|
68
|
+
|
69
|
+
x.report("checking the counter through a user") {
|
70
|
+
users.sample.count("some:event").day.count
|
71
|
+
}
|
72
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
@patterns = Minuteman.patterns
|
4
|
+
|
5
|
+
prepare do
|
6
|
+
Minuteman.configure do |config|
|
7
|
+
config.redis = Redic.new("redis://127.0.0.1:6379/1")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
setup do
|
12
|
+
Minuteman.config.redis.call("FLUSHDB")
|
13
|
+
|
14
|
+
Minuteman.configure do |config|
|
15
|
+
config.patterns = @patterns
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
test "a connection" do
|
20
|
+
assert_equal Minuteman.config.redis.class, Redic
|
21
|
+
end
|
22
|
+
|
23
|
+
test "models in minuteman namespace" do
|
24
|
+
assert_equal Minuteman::User.create.key, "Minuteman::User:1"
|
25
|
+
end
|
26
|
+
|
27
|
+
test "an anonymous user" do
|
28
|
+
user = Minuteman::User.create
|
29
|
+
|
30
|
+
assert user.is_a?(Minuteman::User)
|
31
|
+
assert !!user.uid
|
32
|
+
assert !user.identifier
|
33
|
+
assert user.id
|
34
|
+
end
|
35
|
+
|
36
|
+
test "access a user with and id or an uuid" do
|
37
|
+
user = Minuteman::User.create(identifier: 5)
|
38
|
+
|
39
|
+
assert Minuteman::User[user.uid].is_a?(Minuteman::User)
|
40
|
+
assert Minuteman::User[user.identifier].is_a?(Minuteman::User)
|
41
|
+
end
|
42
|
+
|
43
|
+
test "track an anonymous user" do
|
44
|
+
user = Minuteman.track("anonymous:user")
|
45
|
+
assert user.uid
|
46
|
+
end
|
47
|
+
|
48
|
+
test "track an user" do
|
49
|
+
user = Minuteman::User.create
|
50
|
+
|
51
|
+
assert Minuteman.track("login:successful", user)
|
52
|
+
|
53
|
+
analyzer = Minuteman.analyze("login:successful")
|
54
|
+
assert analyzer.day(Time.now.utc).count == 1
|
55
|
+
end
|
56
|
+
|
57
|
+
test "tracks an anonymous user and the promotes it to a real one" do
|
58
|
+
user = Minuteman.track("enter:website")
|
59
|
+
assert user.identifier == nil
|
60
|
+
|
61
|
+
user.promote(42)
|
62
|
+
|
63
|
+
assert user.identifier == 42
|
64
|
+
assert Minuteman::User[42].uid == user.uid
|
65
|
+
assert Minuteman("enter:website").day.include?(user)
|
66
|
+
end
|
67
|
+
|
68
|
+
test "create your own storage patterns and access analyzer" do
|
69
|
+
Minuteman.configure do |config|
|
70
|
+
config.patterns = {
|
71
|
+
dia: -> (time) { time.strftime("%Y-%m-%d") }
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
Minuteman.track("logeo:exitoso")
|
76
|
+
assert Minuteman("logeo:exitoso").dia.count == 1
|
77
|
+
end
|
78
|
+
|
79
|
+
test "use the method shortcut" do
|
80
|
+
5.times { Minuteman.track("enter:website") }
|
81
|
+
|
82
|
+
assert Minuteman("enter:website").day.count == 5
|
83
|
+
end
|
84
|
+
|
85
|
+
scope "operations" do
|
86
|
+
setup do
|
87
|
+
Minuteman.config.redis.call("FLUSHDB")
|
88
|
+
|
89
|
+
@users = Array.new(3) { Minuteman::User.create }
|
90
|
+
@users.each do |user|
|
91
|
+
Minuteman.track("landing_page:new", @users)
|
92
|
+
end
|
93
|
+
|
94
|
+
Minuteman.track("buy:product", @users[0])
|
95
|
+
Minuteman.track("buy:product", @users[2])
|
96
|
+
end
|
97
|
+
|
98
|
+
test "AND" do
|
99
|
+
and_op = Minuteman("landing_page:new").day & Minuteman("buy:product").day
|
100
|
+
assert and_op.count == 2
|
101
|
+
end
|
102
|
+
|
103
|
+
test "OR" do
|
104
|
+
or_op = Minuteman("landing_page:new").day | Minuteman("buy:product").day
|
105
|
+
assert or_op.count == 3
|
106
|
+
end
|
107
|
+
|
108
|
+
test "XOR" do
|
109
|
+
xor_op = Minuteman("landing_page:new").day ^ Minuteman("buy:product").day
|
110
|
+
assert xor_op.count == 1
|
111
|
+
end
|
112
|
+
|
113
|
+
test "NOT" do
|
114
|
+
assert Minuteman("buy:product").day.include?(@users[2])
|
115
|
+
|
116
|
+
not_op = -Minuteman("buy:product").day
|
117
|
+
assert !not_op.include?(@users[2])
|
118
|
+
end
|
119
|
+
|
120
|
+
test "MINUS" do
|
121
|
+
assert Minuteman("landing_page:new").day.include?(@users[2])
|
122
|
+
assert Minuteman("buy:product").day.include?(@users[2])
|
123
|
+
|
124
|
+
minus_op = Minuteman("landing_page:new").day - Minuteman("buy:product").day
|
125
|
+
|
126
|
+
assert !minus_op.include?(@users[2])
|
127
|
+
assert minus_op.include?(@users[1])
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
scope "complex operations" do
|
132
|
+
setup do
|
133
|
+
Minuteman.config.redis.call("FLUSHDB")
|
134
|
+
@users = Array.new(6) { Minuteman::User.create }
|
135
|
+
|
136
|
+
[ @users[0], @users[1], @users[2] ].each do |u|
|
137
|
+
Minuteman.track("promo:email", u)
|
138
|
+
end
|
139
|
+
|
140
|
+
[ @users[3], @users[4], @users[5] ].each do |u|
|
141
|
+
Minuteman.track("promo:facebook", u)
|
142
|
+
end
|
143
|
+
|
144
|
+
[ @users[1], @users[4], @users[6] ].each do |u|
|
145
|
+
Minuteman.track("user:new", u)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
test "verbose" do
|
150
|
+
got_promos = Minuteman("promo:email").day + Minuteman("promo:facebook").day
|
151
|
+
|
152
|
+
@users[0..5].each do |u|
|
153
|
+
assert got_promos.include?(u)
|
154
|
+
end
|
155
|
+
|
156
|
+
new_users = Minuteman("user:new").day
|
157
|
+
query = got_promos & new_users
|
158
|
+
|
159
|
+
[ @users[1], @users[4] ].each do |u|
|
160
|
+
assert query.include?(u)
|
161
|
+
end
|
162
|
+
|
163
|
+
assert query.count == 2
|
164
|
+
end
|
165
|
+
|
166
|
+
test "readable" do
|
167
|
+
query = (
|
168
|
+
Minuteman("promo:email").day + Minuteman("promo:facebook").day
|
169
|
+
) & Minuteman("user:new").day
|
170
|
+
|
171
|
+
assert query.count == 2
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
test "count a given event" do
|
176
|
+
10.times { Minuteman.add("enter:new_landing") }
|
177
|
+
|
178
|
+
assert Counterman("enter:new_landing").day.count == 10
|
179
|
+
end
|
180
|
+
|
181
|
+
test "count events on some dates" do
|
182
|
+
day = Time.new(2015, 10, 15)
|
183
|
+
next_day = Time.new(2015, 10, 16)
|
184
|
+
|
185
|
+
5.times { Minuteman.add("drink:beer", day) }
|
186
|
+
2.times { Minuteman.add("drink:beer", next_day) }
|
187
|
+
|
188
|
+
assert Counterman("drink:beer").month(day).count == 7
|
189
|
+
assert Counterman("drink:beer").day(day).count == 5
|
190
|
+
end
|
191
|
+
|
192
|
+
scope "do actions through a user" do
|
193
|
+
test "track an event" do
|
194
|
+
user = Minuteman::User.create
|
195
|
+
user.track("login:page")
|
196
|
+
|
197
|
+
3.times { user.add("login:attempts") }
|
198
|
+
2.times { Minuteman.add("login:attempts") }
|
199
|
+
|
200
|
+
assert Minuteman("login:page").day.include?(user)
|
201
|
+
assert Counterman("login:attempts").day.count == 5
|
202
|
+
assert user.count("login:attempts").day.count == 3
|
203
|
+
end
|
204
|
+
end
|