pushpop 0.1.0
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.
- 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
|