heya 0.0.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/LICENSE +85 -0
- data/README.md +418 -0
- data/Rakefile +14 -14
- data/app/mailers/heya/application_mailer.rb +0 -2
- data/app/mailers/heya/campaign_mailer.rb +25 -0
- data/app/models/heya/campaign_membership.rb +9 -0
- data/app/models/heya/campaign_receipt.rb +5 -0
- data/app/views/layouts/heya/campaign_mailer.html.erb +20 -0
- data/app/views/layouts/heya/campaign_mailer.text.erb +1 -0
- data/lib/generators/heya/campaign/USAGE +14 -0
- data/lib/generators/heya/campaign/campaign_generator.rb +53 -0
- data/lib/generators/heya/campaign/templates/application_campaign.rb.tt +3 -0
- data/lib/generators/heya/campaign/templates/campaign.rb.tt +7 -0
- data/lib/generators/heya/campaign/templates/message.html.erb.tt +1 -0
- data/lib/generators/heya/campaign/templates/message.md.erb.tt +1 -0
- data/lib/generators/heya/campaign/templates/message.text.erb.tt +1 -0
- data/lib/generators/heya/install/USAGE +10 -0
- data/lib/generators/heya/install/install_generator.rb +20 -0
- data/lib/generators/heya/install/templates/initializer.rb.tt +16 -0
- data/lib/generators/heya/install/templates/migration.rb.tt +28 -0
- data/lib/heya.rb +44 -1
- data/lib/heya/campaigns/action.rb +27 -0
- data/lib/heya/campaigns/actions/block.rb +24 -0
- data/lib/heya/campaigns/actions/email.rb +15 -0
- data/lib/heya/campaigns/base.rb +151 -0
- data/lib/heya/campaigns/queries.rb +118 -0
- data/lib/heya/campaigns/scheduler.rb +49 -0
- data/lib/heya/campaigns/step.rb +24 -0
- data/lib/heya/campaigns/step_action_job.rb +34 -0
- data/lib/heya/config.rb +17 -0
- data/lib/heya/engine.rb +14 -0
- data/lib/heya/version.rb +3 -1
- data/lib/tasks/heya_tasks.rake +8 -4
- metadata +58 -7
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Heya
|
4
|
+
module Campaigns
|
5
|
+
module Queries
|
6
|
+
NEXT_STEP_SUBQUERY = <<~SQL
|
7
|
+
(WITH steps AS (SELECT * FROM (VALUES :steps_values) AS m (step_gid,campaign_gid,position))
|
8
|
+
SELECT m.step_gid FROM steps AS m
|
9
|
+
WHERE m.campaign_gid = :campaign_gid
|
10
|
+
AND m.position > coalesce((SELECT m.position FROM heya_campaign_receipts AS r
|
11
|
+
INNER JOIN steps AS m ON m.step_gid = r.step_gid
|
12
|
+
AND m.campaign_gid = :campaign_gid
|
13
|
+
WHERE r.user_type = heya_campaign_memberships.user_type
|
14
|
+
AND r.user_id = heya_campaign_memberships.user_id
|
15
|
+
ORDER BY m.position DESC
|
16
|
+
LIMIT 1), -1)
|
17
|
+
ORDER BY m.position ASC
|
18
|
+
LIMIT 1
|
19
|
+
) = :step_gid
|
20
|
+
SQL
|
21
|
+
|
22
|
+
ACTIVE_CAMPAIGN_SUBQUERY = <<~SQL
|
23
|
+
(WITH heya_campaigns AS (SELECT * FROM (VALUES :campaigns_values) AS campaigns (campaign_gid,position))
|
24
|
+
SELECT memberships.campaign_gid FROM heya_campaign_memberships AS memberships
|
25
|
+
INNER JOIN heya_campaigns AS campaigns
|
26
|
+
ON campaigns.campaign_gid = memberships.campaign_gid
|
27
|
+
WHERE memberships.user_type = heya_campaign_memberships.user_type
|
28
|
+
AND memberships.user_id = heya_campaign_memberships.user_id
|
29
|
+
AND memberships.concurrent = FALSE
|
30
|
+
ORDER BY campaigns.position DESC, memberships.created_at ASC
|
31
|
+
LIMIT 1
|
32
|
+
) = :campaign_gid
|
33
|
+
SQL
|
34
|
+
|
35
|
+
# Given a campaign and a step, {Queries::UsersForStep} returns the
|
36
|
+
# users who should complete the step.
|
37
|
+
UsersForStep = ->(campaign, step) {
|
38
|
+
wait_threshold = Time.now.utc - step.wait
|
39
|
+
|
40
|
+
# Safeguard to make sure we never complete the same step twice.
|
41
|
+
receipt_query = CampaignReceipt
|
42
|
+
.select("heya_campaign_receipts.user_id")
|
43
|
+
.where(user_type: campaign.user_class.name)
|
44
|
+
.where("heya_campaign_receipts.step_gid = ?", step.gid)
|
45
|
+
|
46
|
+
# https://www.postgresql.org/docs/9.4/queries-values.html
|
47
|
+
steps_values = campaign.steps.map { |m|
|
48
|
+
ActiveRecord::Base.sanitize_sql_array(
|
49
|
+
["(?, ?, ?)", m.gid, campaign.gid, m.position]
|
50
|
+
)
|
51
|
+
}.join(", ")
|
52
|
+
|
53
|
+
priority = Heya.config.campaigns.priority.reverse.map { |c| c.is_a?(String) ? c : c.name }
|
54
|
+
campaigns_values = Heya.campaigns.map { |c|
|
55
|
+
ActiveRecord::Base.sanitize_sql_array(
|
56
|
+
["(?, ?)", c.gid, priority.index(c.name) || -1]
|
57
|
+
)
|
58
|
+
}.join(", ")
|
59
|
+
|
60
|
+
users = campaign.users
|
61
|
+
users
|
62
|
+
.where.not(id: receipt_query)
|
63
|
+
.where(NEXT_STEP_SUBQUERY.gsub(":steps_values", steps_values), {
|
64
|
+
campaign_gid: campaign.gid,
|
65
|
+
step_gid: step.gid
|
66
|
+
})
|
67
|
+
.merge(
|
68
|
+
users
|
69
|
+
.where("heya_campaign_memberships.concurrent = ?", true)
|
70
|
+
.or(
|
71
|
+
users.where(ACTIVE_CAMPAIGN_SUBQUERY.gsub(":campaigns_values", campaigns_values), {
|
72
|
+
campaign_gid: campaign.gid
|
73
|
+
})
|
74
|
+
)
|
75
|
+
)
|
76
|
+
.where(
|
77
|
+
"heya_campaign_memberships.last_sent_at <= ?", wait_threshold
|
78
|
+
)
|
79
|
+
}
|
80
|
+
|
81
|
+
# Given a campaign and a step, {Queries::UsersCompletedStep}
|
82
|
+
# returns the users who have completed the step.
|
83
|
+
UsersCompletedStep = ->(campaign, step) {
|
84
|
+
receipt_query = CampaignReceipt
|
85
|
+
.select("heya_campaign_receipts.user_id")
|
86
|
+
.where(user_type: campaign.user_class.name)
|
87
|
+
.where("heya_campaign_receipts.step_gid = ?", step.gid)
|
88
|
+
|
89
|
+
campaign.users
|
90
|
+
.where(id: receipt_query)
|
91
|
+
}
|
92
|
+
|
93
|
+
# Given a campaign and a user, {Queries::CampaignMembershipsForUpdate}
|
94
|
+
# returns the user's campaign memberships which should be updated
|
95
|
+
# concurrently.
|
96
|
+
CampaignMembershipsForUpdate = ->(campaign, user) {
|
97
|
+
membership = CampaignMembership.where(user: user, campaign_gid: campaign.gid).first
|
98
|
+
if membership.concurrent?
|
99
|
+
CampaignMembership
|
100
|
+
.where(user: user, campaign_gid: campaign.gid)
|
101
|
+
else
|
102
|
+
CampaignMembership
|
103
|
+
.where(user: user, concurrent: false)
|
104
|
+
end
|
105
|
+
}
|
106
|
+
|
107
|
+
# Given a campaign, {Queries::OrphanedCampaignMemberships} returns the
|
108
|
+
# campaign memberships which belong to users who no longer exist in the
|
109
|
+
# database.
|
110
|
+
OrphanedCampaignMemberships = ->(campaign) {
|
111
|
+
CampaignMembership
|
112
|
+
.where(campaign_gid: campaign.gid)
|
113
|
+
.where(user_type: campaign.user_class.base_class.name)
|
114
|
+
.where.not(user_id: campaign.users.select("id"))
|
115
|
+
}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Heya
|
4
|
+
module Campaigns
|
5
|
+
# {Campaigns::Scheduler} schedules campaign jobs to run for each campaign.
|
6
|
+
#
|
7
|
+
# For each step in each campaign:
|
8
|
+
# 1. Find users who haven't completed step, and are outside the `wait`
|
9
|
+
# window
|
10
|
+
# 2. Match segment
|
11
|
+
# 3. Create CampaignReceipt (excludes user in subsequent steps)
|
12
|
+
# 4. Process job
|
13
|
+
class Scheduler
|
14
|
+
def run
|
15
|
+
Heya.campaigns.each do |campaign|
|
16
|
+
Queries::OrphanedCampaignMemberships.call(campaign).delete_all
|
17
|
+
|
18
|
+
campaign.steps.each do |step|
|
19
|
+
Queries::UsersForStep.call(campaign, step).find_each do |user|
|
20
|
+
self.class.process(campaign, step, user)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
if (last_step = campaign.steps.last)
|
25
|
+
CampaignMembership.where(
|
26
|
+
user: Queries::UsersCompletedStep.call(campaign, last_step),
|
27
|
+
campaign_gid: campaign.gid
|
28
|
+
).delete_all
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.process(campaign, step, user)
|
34
|
+
ActiveRecord::Base.transaction do
|
35
|
+
return if CampaignReceipt.where(user: user, step_gid: step.gid).exists?
|
36
|
+
|
37
|
+
if step.in_segment?(user)
|
38
|
+
now = Time.now.utc
|
39
|
+
Queries::CampaignMembershipsForUpdate.call(campaign, user).update_all(last_sent_at: now)
|
40
|
+
CampaignReceipt.create!(user: user, step_gid: step.gid, sent_at: now)
|
41
|
+
step.action.new(user: user, step: step).deliver_later
|
42
|
+
else
|
43
|
+
CampaignReceipt.create!(user: user, step_gid: step.gid)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ostruct"
|
4
|
+
|
5
|
+
module Heya
|
6
|
+
module Campaigns
|
7
|
+
class Step < OpenStruct
|
8
|
+
include GlobalID::Identification
|
9
|
+
|
10
|
+
def self.find(id)
|
11
|
+
campaign_name, _step_name = id.to_s.split("/")
|
12
|
+
campaign_name.constantize.steps.find { |s| s.id == id }
|
13
|
+
end
|
14
|
+
|
15
|
+
def gid
|
16
|
+
to_gid(app: "heya").to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def in_segment?(user)
|
20
|
+
Heya.in_segments?(user, *campaign.__segments, segment)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Heya
|
4
|
+
module Campaigns
|
5
|
+
class StepActionJob < ActiveJob::Base
|
6
|
+
queue_as { Heya.config.campaigns.queue }
|
7
|
+
|
8
|
+
rescue_from StandardError, with: :handle_exception_with_campaign_class
|
9
|
+
|
10
|
+
def perform(_campaign, user, step)
|
11
|
+
step.action.new(user: user, step: step).deliver_now
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# From ActionMailer: "deserialize" the mailer class name by hand in case
|
17
|
+
# another argument (like a Global ID reference) raised
|
18
|
+
# DeserializationError.
|
19
|
+
def campaign_class
|
20
|
+
if (campaign = (arguments_serialized? && Array(@serialized_arguments).first) || Array(arguments).first)
|
21
|
+
campaign.constantize
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_exception_with_campaign_class(exception)
|
26
|
+
if (klass = campaign_class)
|
27
|
+
klass.handle_exception(exception)
|
28
|
+
else
|
29
|
+
raise exception
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/heya/config.rb
ADDED
data/lib/heya/engine.rb
CHANGED
@@ -1,5 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Heya
|
2
4
|
class Engine < ::Rails::Engine
|
3
5
|
isolate_namespace Heya
|
6
|
+
|
7
|
+
initializer "heya.reload_campaigns" do |app|
|
8
|
+
app.reloader.to_run do
|
9
|
+
Heya.campaigns.clear
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
config.to_prepare do
|
14
|
+
Dir.glob(Rails.root + "app/campaigns/*.rb").each do |c|
|
15
|
+
require_dependency(c)
|
16
|
+
end
|
17
|
+
end
|
4
18
|
end
|
5
19
|
end
|
data/lib/heya/version.rb
CHANGED
data/lib/tasks/heya_tasks.rake
CHANGED
metadata
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: heya
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Wood
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 5.2.3
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 6.1.0
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: 5.2.3
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 6.1.0
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: pg
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,13 +44,32 @@ dependencies:
|
|
38
44
|
- - ">="
|
39
45
|
- !ruby/object:Gem::Version
|
40
46
|
version: '0'
|
41
|
-
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: appraisal
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
description: Heya is a customer communication and automation framework for Ruby on
|
62
|
+
Rails. It's as robust as the hosted alternatives, without the integration and compliance
|
63
|
+
nightmare.
|
42
64
|
email:
|
43
65
|
- josh@honeybadger.io
|
44
66
|
executables: []
|
45
67
|
extensions: []
|
46
68
|
extra_rdoc_files: []
|
47
69
|
files:
|
70
|
+
- CHANGELOG.md
|
71
|
+
- LICENSE
|
72
|
+
- README.md
|
48
73
|
- Rakefile
|
49
74
|
- app/assets/config/heya_manifest.js
|
50
75
|
- app/assets/javascripts/heya/application.js
|
@@ -53,15 +78,41 @@ files:
|
|
53
78
|
- app/helpers/heya/application_helper.rb
|
54
79
|
- app/jobs/heya/application_job.rb
|
55
80
|
- app/mailers/heya/application_mailer.rb
|
81
|
+
- app/mailers/heya/campaign_mailer.rb
|
56
82
|
- app/models/heya/application_record.rb
|
83
|
+
- app/models/heya/campaign_membership.rb
|
84
|
+
- app/models/heya/campaign_receipt.rb
|
57
85
|
- app/views/layouts/heya/application.html.erb
|
86
|
+
- app/views/layouts/heya/campaign_mailer.html.erb
|
87
|
+
- app/views/layouts/heya/campaign_mailer.text.erb
|
58
88
|
- config/routes.rb
|
89
|
+
- lib/generators/heya/campaign/USAGE
|
90
|
+
- lib/generators/heya/campaign/campaign_generator.rb
|
91
|
+
- lib/generators/heya/campaign/templates/application_campaign.rb.tt
|
92
|
+
- lib/generators/heya/campaign/templates/campaign.rb.tt
|
93
|
+
- lib/generators/heya/campaign/templates/message.html.erb.tt
|
94
|
+
- lib/generators/heya/campaign/templates/message.md.erb.tt
|
95
|
+
- lib/generators/heya/campaign/templates/message.text.erb.tt
|
96
|
+
- lib/generators/heya/install/USAGE
|
97
|
+
- lib/generators/heya/install/install_generator.rb
|
98
|
+
- lib/generators/heya/install/templates/initializer.rb.tt
|
99
|
+
- lib/generators/heya/install/templates/migration.rb.tt
|
59
100
|
- lib/heya.rb
|
101
|
+
- lib/heya/campaigns/action.rb
|
102
|
+
- lib/heya/campaigns/actions/block.rb
|
103
|
+
- lib/heya/campaigns/actions/email.rb
|
104
|
+
- lib/heya/campaigns/base.rb
|
105
|
+
- lib/heya/campaigns/queries.rb
|
106
|
+
- lib/heya/campaigns/scheduler.rb
|
107
|
+
- lib/heya/campaigns/step.rb
|
108
|
+
- lib/heya/campaigns/step_action_job.rb
|
109
|
+
- lib/heya/config.rb
|
60
110
|
- lib/heya/engine.rb
|
61
111
|
- lib/heya/version.rb
|
62
112
|
- lib/tasks/heya_tasks.rake
|
63
|
-
homepage: https://
|
64
|
-
licenses:
|
113
|
+
homepage: https://github.com/honeybadger-io/heya
|
114
|
+
licenses:
|
115
|
+
- Prosperity Public License
|
65
116
|
metadata: {}
|
66
117
|
post_install_message:
|
67
118
|
rdoc_options: []
|