pushpop 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +72 -0
- data/LICENSE +21 -0
- data/Procfile +1 -0
- data/README.md +582 -0
- data/Rakefile +48 -0
- data/jobs/example_job.rb +20 -0
- data/lib/plugins/keen.rb +78 -0
- data/lib/plugins/sendgrid.rb +94 -0
- data/lib/plugins/twilio.rb +52 -0
- data/lib/pushpop.rb +55 -0
- data/lib/pushpop/job.rb +95 -0
- data/lib/pushpop/step.rb +50 -0
- data/lib/pushpop/version.rb +3 -0
- data/pushpop.gemspec +22 -0
- data/spec/jobs/simple_job.rb +9 -0
- data/spec/plugins/keen_spec.rb +88 -0
- data/spec/plugins/sendgrid_spec.rb +66 -0
- data/spec/pushpop/job_spec.rb +147 -0
- data/spec/pushpop/step_spec.rb +78 -0
- data/spec/pushpop_spec.rb +24 -0
- data/spec/simple_job_spec.rb +9 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/templates/spec.html.erb +6 -0
- data/templates/first_template.html.erb +1 -0
- metadata +113 -0
data/lib/pushpop/step.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Pushpop
|
4
|
+
|
5
|
+
class Step
|
6
|
+
|
7
|
+
TEMPLATES_DIRECTORY = File.expand_path('../../../templates', __FILE__)
|
8
|
+
|
9
|
+
class ERBContext
|
10
|
+
attr_accessor :response
|
11
|
+
attr_accessor :step_responses
|
12
|
+
|
13
|
+
def initialize(response, step_responses)
|
14
|
+
self.response = response
|
15
|
+
self.step_responses = step_responses
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_binding
|
19
|
+
binding
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_accessor :name
|
24
|
+
attr_accessor :plugin
|
25
|
+
attr_accessor :block
|
26
|
+
|
27
|
+
def initialize(name=nil, plugin=nil, &block)
|
28
|
+
self.name = name || plugin || Pushpop.random_name
|
29
|
+
self.plugin = plugin
|
30
|
+
self.block = block
|
31
|
+
end
|
32
|
+
|
33
|
+
def template(filename, response, step_responses={}, directory=TEMPLATES_DIRECTORY)
|
34
|
+
erb_context = ERBContext.new(response, step_responses)
|
35
|
+
ERB.new(get_template_contents(filename, directory)).result(erb_context.get_binding)
|
36
|
+
end
|
37
|
+
|
38
|
+
def run(last_response=nil, step_responses=nil)
|
39
|
+
self.instance_exec(last_response, step_responses, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def get_template_contents(filename, directory)
|
45
|
+
File.read(File.join(directory, filename))
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
data/pushpop.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require 'pushpop/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
|
7
|
+
s.name = "pushpop"
|
8
|
+
s.version = Pushpop::VERSION
|
9
|
+
s.authors = ["Josh Dzielak"]
|
10
|
+
s.email = "josh@keen.io"
|
11
|
+
s.homepage = "https://github.com/keenlabs/pushpop"
|
12
|
+
s.summary = "Automatic delivery of reports and alerts based on Keen IO events"
|
13
|
+
s.description = "Pushpop is a simple but powerful Ruby app that sends notifications based on events captured with Keen IO."
|
14
|
+
|
15
|
+
s.add_dependency "clockwork"
|
16
|
+
s.add_dependency "keen"
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Pushpop::Keen do
|
4
|
+
|
5
|
+
describe '#configure' do
|
6
|
+
|
7
|
+
it 'should set various params' do
|
8
|
+
|
9
|
+
step = Pushpop::Keen.new do
|
10
|
+
event_collection 'pageviews'
|
11
|
+
analysis_type 'count'
|
12
|
+
timeframe 'last_3_days'
|
13
|
+
target_property 'trinkets'
|
14
|
+
group_by 'referer'
|
15
|
+
interval 'hourly'
|
16
|
+
filters [{ property_value: 'referer',
|
17
|
+
operator: 'ne',
|
18
|
+
property_value: 'yahoo.com' }]
|
19
|
+
steps [{ event_collection: 'pageviews',
|
20
|
+
actor_property: 'user.id' }]
|
21
|
+
analyses [{ analysis_type: 'count' }]
|
22
|
+
end
|
23
|
+
|
24
|
+
step.configure
|
25
|
+
|
26
|
+
step._event_collection.should == 'pageviews'
|
27
|
+
step._analysis_type.should == 'count'
|
28
|
+
step._timeframe.should == 'last_3_days'
|
29
|
+
step._group_by.should == 'referer'
|
30
|
+
step._interval.should == 'hourly'
|
31
|
+
step._steps.should == [{
|
32
|
+
event_collection: 'pageviews',
|
33
|
+
actor_property: 'user.id'
|
34
|
+
}]
|
35
|
+
step._analyses.should == [{ analysis_type: 'count' }]
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#run' do
|
41
|
+
it 'should run the query based on the analysis type' do
|
42
|
+
Keen.stub(:count).with('pageviews', {
|
43
|
+
timeframe: 'last_3_days'
|
44
|
+
}).and_return(365)
|
45
|
+
|
46
|
+
step = Pushpop::Keen.new('one') do
|
47
|
+
event_collection 'pageviews'
|
48
|
+
analysis_type 'count'
|
49
|
+
timeframe 'last_3_days'
|
50
|
+
end
|
51
|
+
response = step.run
|
52
|
+
response.should == 365
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#to_analysis_options' do
|
57
|
+
it 'should include various options' do
|
58
|
+
step = Pushpop::Keen.new('one') do end
|
59
|
+
step._timeframe = 'last_4_days'
|
60
|
+
step._group_by = 'referer'
|
61
|
+
step._target_property = 'trinkets'
|
62
|
+
step._interval = 'hourly'
|
63
|
+
step._filters = [{ property_value: 'referer',
|
64
|
+
operator: 'ne',
|
65
|
+
property_value: 'yahoo.com' }]
|
66
|
+
step._steps = [{ event_collection: 'pageviews',
|
67
|
+
actor_property: 'user.id' }]
|
68
|
+
step._analyses = [{ analysis_type: 'count' }]
|
69
|
+
step.to_analysis_options.should == {
|
70
|
+
timeframe: 'last_4_days',
|
71
|
+
target_property: 'trinkets',
|
72
|
+
group_by: 'referer',
|
73
|
+
interval: 'hourly',
|
74
|
+
filters: [{ property_value: 'referer',
|
75
|
+
operator: 'ne',
|
76
|
+
property_value: 'yahoo.com' }],
|
77
|
+
steps: [{ event_collection: 'pageviews',
|
78
|
+
actor_property: 'user.id' }],
|
79
|
+
analyses: [{ analysis_type: 'count' }]
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should not include nils' do
|
84
|
+
step = Pushpop::Keen.new('one') do end
|
85
|
+
step.to_analysis_options.should == {}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
SPEC_TEMPLATES_DIRECTORY ||= File.expand_path('../../templates', __FILE__)
|
4
|
+
|
5
|
+
describe Pushpop::Sendgrid do
|
6
|
+
|
7
|
+
describe '#configure' do
|
8
|
+
|
9
|
+
it 'should set various params' do
|
10
|
+
|
11
|
+
step = Pushpop::Sendgrid.new do
|
12
|
+
to 'josh@keen.io'
|
13
|
+
from 'depths@hell.com'
|
14
|
+
subject 'time is up'
|
15
|
+
body 'use code 3:16 for high leniency'
|
16
|
+
preview true
|
17
|
+
end
|
18
|
+
|
19
|
+
step.configure
|
20
|
+
|
21
|
+
step._to.should == 'josh@keen.io'
|
22
|
+
step._from.should == 'depths@hell.com'
|
23
|
+
step._subject.should == 'time is up'
|
24
|
+
step._body.should == 'use code 3:16 for high leniency'
|
25
|
+
step._preview.should be_true
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#run' do
|
32
|
+
|
33
|
+
it 'should send some email' do
|
34
|
+
|
35
|
+
Mail.stub(:deliver)
|
36
|
+
|
37
|
+
step = Pushpop::Sendgrid.new do |response|
|
38
|
+
to 'josh@keen.io'
|
39
|
+
from 'alerts+pushpop@keen.io'
|
40
|
+
subject "There were #{response} Pageviews Today!"
|
41
|
+
body 'hey wats up'
|
42
|
+
end
|
43
|
+
|
44
|
+
step.run(365)
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#body' do
|
51
|
+
|
52
|
+
it 'should use a string if given 1 arg' do
|
53
|
+
step = Pushpop::Sendgrid.new
|
54
|
+
step.body 'hello world'
|
55
|
+
step._body.should == 'hello world'
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should use a template if more than 1 arg is passed' do
|
59
|
+
step = Pushpop::Sendgrid.new
|
60
|
+
step.body('spec.html.erb', 500, {}, SPEC_TEMPLATES_DIRECTORY)
|
61
|
+
step._body.strip.should == '<pre>500</pre>'
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Pushpop::Job do
|
4
|
+
|
5
|
+
let (:empty_job) { Pushpop::Job.new('foo') do end }
|
6
|
+
let (:empty_step) { Pushpop::Step.new('bar') do end }
|
7
|
+
|
8
|
+
describe '#register_plugins' do
|
9
|
+
it 'should register a plugin' do
|
10
|
+
Pushpop::Job.register_plugin('blaz', Class)
|
11
|
+
Pushpop::Job.plugins['blaz'].should == Class
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#initialize' do
|
16
|
+
it 'should set a name and evaluate a block' do
|
17
|
+
block_ran = false
|
18
|
+
job = Pushpop::Job.new('foo') do block_ran = true end
|
19
|
+
job.name.should == 'foo'
|
20
|
+
job.every_duration.should be_nil
|
21
|
+
job.every_options.should == {}
|
22
|
+
block_ran.should be_true
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should auto-generate a name' do
|
26
|
+
job = Pushpop::Job.new do end
|
27
|
+
job.name.should_not be_nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#every' do
|
32
|
+
it 'should set duration and options' do
|
33
|
+
job = empty_job
|
34
|
+
job.every(10.seconds, at: '01:02')
|
35
|
+
job.every_duration.should == 10
|
36
|
+
job.every_options.should == { at: '01:02' }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#step' do
|
41
|
+
it 'should add the step to the internal list of steps' do
|
42
|
+
empty_proc = Proc.new {}
|
43
|
+
job = empty_job
|
44
|
+
job.step('blah', &empty_proc)
|
45
|
+
job.steps.first.name.should == 'blah'
|
46
|
+
job.steps.first.block.should == empty_proc
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'plugin specified' do
|
50
|
+
class FakeStep < Pushpop::Step
|
51
|
+
end
|
52
|
+
|
53
|
+
before do
|
54
|
+
Pushpop::Job.register_plugin('blaz', FakeStep)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should use the registered plugin to instantiate the class' do
|
58
|
+
empty_proc = Proc.new {}
|
59
|
+
job = empty_job
|
60
|
+
job.step('blah', 'blaz', &empty_proc)
|
61
|
+
job.steps.first.name.should == 'blah'
|
62
|
+
job.steps.first.plugin.should == 'blaz'
|
63
|
+
job.steps.first.class.should == FakeStep
|
64
|
+
job.steps.first.block.should == empty_proc
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should throw an exception for an unregistered plugin' do
|
68
|
+
empty_proc = Proc.new {}
|
69
|
+
job = empty_job
|
70
|
+
expect {
|
71
|
+
job.step('blah', 'blaze', &empty_proc)
|
72
|
+
}.to raise_error /No plugin configured/
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#run' do
|
78
|
+
it 'should call each step with the response to the previous' do
|
79
|
+
job = Pushpop::Job.new('foo') do
|
80
|
+
step 'one' do
|
81
|
+
10
|
82
|
+
end
|
83
|
+
|
84
|
+
step 'two' do |response|
|
85
|
+
response + 20
|
86
|
+
end
|
87
|
+
end
|
88
|
+
job.run.should == [30, { 'one' => 10, 'two' => 30 }]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#schedule' do
|
93
|
+
it 'should add the job to clockwork' do
|
94
|
+
frequency = 1.seconds
|
95
|
+
simple_job = Pushpop::Job.new('foo') do
|
96
|
+
every frequency
|
97
|
+
step 'track_times_run' do
|
98
|
+
@times_run ||= 0
|
99
|
+
@times_run += 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
simple_job.schedule
|
104
|
+
|
105
|
+
Clockwork.manager.tick(Time.now)
|
106
|
+
simple_job.run.first.should == 2
|
107
|
+
Clockwork.manager.tick(Time.now + frequency)
|
108
|
+
simple_job.run.first.should == 4
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe '#method_missing' do
|
113
|
+
class FakeStep < Pushpop::Step
|
114
|
+
end
|
115
|
+
|
116
|
+
before do
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should assume its a registered plugin name and try to create a step' do
|
120
|
+
Pushpop::Job.register_plugin('blaz', FakeStep)
|
121
|
+
simple_job = job do
|
122
|
+
blaz 'hi' do end
|
123
|
+
end
|
124
|
+
simple_job.steps.first.name.should == 'hi'
|
125
|
+
simple_job.steps.first.class.should == FakeStep
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should not assume a name' do
|
129
|
+
Pushpop::Job.register_plugin('blaz', FakeStep)
|
130
|
+
simple_job = job do
|
131
|
+
blaz do end
|
132
|
+
end
|
133
|
+
simple_job.steps.first.name.should_not be_nil
|
134
|
+
simple_job.steps.first.plugin.should == 'blaz'
|
135
|
+
simple_job.steps.first.class.should == FakeStep
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should raise an exception if there is no registered plugin' do
|
139
|
+
expect {
|
140
|
+
job do
|
141
|
+
blaze do end
|
142
|
+
end
|
143
|
+
}.to raise_error /undefined method/
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
SPEC_TEMPLATES_DIRECTORY ||= File.expand_path('../../templates', __FILE__)
|
4
|
+
|
5
|
+
describe Pushpop::Step do
|
6
|
+
|
7
|
+
describe 'initialize' do
|
8
|
+
|
9
|
+
it 'should set a name, a plugin, and a block' do
|
10
|
+
empty_proc = Proc.new {}
|
11
|
+
step = Pushpop::Step.new('foo', 'foopie', &empty_proc)
|
12
|
+
step.name.should == 'foo'
|
13
|
+
step.plugin.should == 'foopie'
|
14
|
+
step.block.should == empty_proc
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should auto-generate a name if not given and plugin not given' do
|
18
|
+
empty_proc = Proc.new {}
|
19
|
+
step = Pushpop::Step.new(&empty_proc)
|
20
|
+
step.name.should_not be_nil
|
21
|
+
step.plugin.should be_nil
|
22
|
+
step.block.should == empty_proc
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should set name to plugin name if not given' do
|
26
|
+
empty_proc = Proc.new {}
|
27
|
+
step = Pushpop::Step.new(nil, 'keen', &empty_proc)
|
28
|
+
step.name.should == 'keen'
|
29
|
+
step.plugin.should == 'keen'
|
30
|
+
step.block.should == empty_proc
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should not require a plugin' do
|
34
|
+
empty_proc = Proc.new {}
|
35
|
+
step = Pushpop::Step.new('foo', &empty_proc)
|
36
|
+
step.name.should == 'foo'
|
37
|
+
step.block.should == empty_proc
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'run' do
|
43
|
+
|
44
|
+
it 'should call the block with the same args' do
|
45
|
+
arg1, arg2 = nil
|
46
|
+
times_run = 0
|
47
|
+
empty_proc = Proc.new { |a1, a2| arg1 = a1; arg2 = a2; times_run += 1 }
|
48
|
+
step = Pushpop::Step.new('foo', &empty_proc)
|
49
|
+
step.run('foo', 'bar')
|
50
|
+
arg1.should == 'foo'
|
51
|
+
arg2.should == 'bar'
|
52
|
+
times_run.should == 1
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should execute the block bound to the step' do
|
56
|
+
_self = nil
|
57
|
+
step = Pushpop::Step.new(nil, nil) do
|
58
|
+
_self = self
|
59
|
+
end
|
60
|
+
step.run
|
61
|
+
_self.should == step
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'template' do
|
67
|
+
it 'should render the named template with the response binding' do
|
68
|
+
step = Pushpop::Step.new
|
69
|
+
step.template('spec.html.erb', 500, {}, SPEC_TEMPLATES_DIRECTORY).strip.should == '<pre>500</pre>'
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should render the named template with the step_response binding' do
|
73
|
+
step = Pushpop::Step.new
|
74
|
+
step.template('spec.html.erb', nil, { test: 600 }, SPEC_TEMPLATES_DIRECTORY).strip.should == '<pre>600</pre>'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|