minuteman 1.0.3 → 2.0.0.pre
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/.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
|