ahoy_email 1.0.2 → 2.0.1

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.
@@ -0,0 +1,79 @@
1
+ module AhoyEmail
2
+ class RedisSubscriber
3
+ attr_reader :redis, :prefix
4
+
5
+ def initialize(redis: nil, prefix: "ahoy_email")
6
+ @redis = redis || Redis.new
7
+ @prefix = prefix
8
+ end
9
+
10
+ def track_send(event)
11
+ campaign_prefix = campaign_key(event[:campaign])
12
+ redis.pipelined do
13
+ redis.incr("#{campaign_prefix}:sends")
14
+ redis.sadd(campaigns_key, event[:campaign])
15
+ end
16
+ end
17
+
18
+ def track_click(event)
19
+ campaign_prefix = campaign_key(event[:campaign])
20
+ redis.pipelined do
21
+ redis.incr("#{campaign_prefix}:clicks")
22
+ redis.pfadd("#{campaign_prefix}:unique_clicks", event[:token])
23
+ end
24
+ end
25
+
26
+ def stats(campaign = nil)
27
+ if campaign
28
+ # return nil instead of zeros if not a campaign
29
+ if campaign_exists?(campaign)
30
+ campaign_stats(campaign)
31
+ end
32
+ else
33
+ campaigns.inject({}) do |memo, campaign|
34
+ memo[campaign] = campaign_stats(campaign)
35
+ memo
36
+ end
37
+ end
38
+ end
39
+
40
+ def campaigns
41
+ redis.smembers(campaigns_key)
42
+ end
43
+
44
+ def campaign_exists?(campaign)
45
+ redis.sismember(campaigns_key, campaign)
46
+ end
47
+
48
+ private
49
+
50
+ def campaigns_key
51
+ "#{prefix}:campaigns"
52
+ end
53
+
54
+ def campaign_key(campaign)
55
+ "#{prefix}:campaigns:#{campaign}"
56
+ end
57
+
58
+ def campaign_stats(campaign)
59
+ # scope
60
+ sends = nil
61
+ clicks = nil
62
+ unique_clicks = nil
63
+
64
+ campaign_prefix = campaign_key(campaign)
65
+ redis.pipelined do
66
+ sends = redis.get("#{campaign_prefix}:sends")
67
+ clicks = redis.get("#{campaign_prefix}:clicks")
68
+ unique_clicks = redis.pfcount("#{campaign_prefix}:unique_clicks")
69
+ end
70
+
71
+ {
72
+ sends: sends.value.to_i,
73
+ clicks: clicks.value.to_i,
74
+ unique_clicks: unique_clicks.value,
75
+ ctr: 100 * unique_clicks.value / sends.value.to_f
76
+ }
77
+ end
78
+ end
79
+ end
@@ -9,9 +9,15 @@ module AhoyEmail
9
9
  def perform
10
10
  Safely.safely do
11
11
  # perform_deliveries check still needed in observer
12
- if message.perform_deliveries && message.ahoy_data
13
- data = message.ahoy_data.merge(message: message)
14
- message.ahoy_message = AhoyEmail.track_method.call(data)
12
+ if message.perform_deliveries
13
+ if message.ahoy_data
14
+ data = message.ahoy_data.merge(message: message)
15
+ message.ahoy_message = AhoyEmail.track_method.call(data)
16
+ end
17
+
18
+ if message.ahoy_options && message.ahoy_options[:click]
19
+ Utils.publish(:send, message.ahoy_options.slice(:campaign))
20
+ end
15
21
  end
16
22
  end
17
23
  end
@@ -0,0 +1,35 @@
1
+ module AhoyEmail
2
+ class Utils
3
+ OPTION_KEYS = {
4
+ message: %i(message mailer user extra),
5
+ utm_params: %i(utm_source utm_medium utm_term utm_content utm_campaign),
6
+ click: %i(campaign url_options unsubscribe_links)
7
+ }
8
+
9
+ class << self
10
+ def signature(token:, campaign:, url:)
11
+ # encode and join with a character outside encoding
12
+ data = [token, campaign, url].map { |v| Base64.strict_encode64(v.to_s) }.join("|")
13
+
14
+ Base64.urlsafe_encode64(OpenSSL::HMAC.digest("SHA256", secret_token, data), padding: false)
15
+ end
16
+
17
+ def publish(name, event)
18
+ method_name = "track_#{name}"
19
+ AhoyEmail.subscribers.each do |subscriber|
20
+ subscriber = subscriber.new if subscriber.is_a?(Class)
21
+ if subscriber.respond_to?(method_name)
22
+ subscriber.send(method_name, event.dup)
23
+ elsif name == :click && subscriber.respond_to?(:click)
24
+ # legacy
25
+ subscriber.send(:click, event.dup)
26
+ end
27
+ end
28
+ end
29
+
30
+ def secret_token
31
+ AhoyEmail.secret_token || (raise "Secret token is empty")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,3 @@
1
1
  module AhoyEmail
2
- VERSION = "1.0.2"
2
+ VERSION = "2.0.1"
3
3
  end
@@ -0,0 +1,20 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module Ahoy
4
+ module Generators
5
+ module Clicks
6
+ class ActiverecordGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+ source_root File.join(__dir__, "templates")
9
+
10
+ def copy_migration
11
+ migration_template "migration.rb", "db/migrate/create_ahoy_clicks.rb", migration_version: migration_version
12
+ end
13
+
14
+ def migration_version
15
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :ahoy_clicks do |t|
4
+ t.string :campaign, index: true
5
+ t.string :token
6
+ end
7
+
8
+ add_column :ahoy_messages, :campaign, :string
9
+ add_index :ahoy_messages, :campaign
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require "rails/generators"
2
+
3
+ module Ahoy
4
+ module Generators
5
+ class ClicksGenerator < Rails::Generators::Base
6
+ def copy_templates
7
+ invoke "ahoy:clicks:activerecord"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,40 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module Ahoy
4
+ module Generators
5
+ module Messages
6
+ class ActiverecordGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+ source_root File.join(__dir__, "templates")
9
+
10
+ class_option :unencrypted, type: :boolean
11
+
12
+ def copy_migration
13
+ migration_template "migration.rb", "db/migrate/create_ahoy_messages.rb", migration_version: migration_version
14
+ end
15
+
16
+ def copy_template
17
+ if encrypted?
18
+ template "model_encrypted.rb", "app/models/ahoy/message.rb"
19
+ end
20
+ end
21
+
22
+ def migration_version
23
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
24
+ end
25
+
26
+ def to_column
27
+ if encrypted?
28
+ "t.text :to_ciphertext\n t.string :to_bidx, index: true"
29
+ else
30
+ "t.string :to, index: true"
31
+ end
32
+ end
33
+
34
+ def encrypted?
35
+ !options[:unencrypted]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ require "rails/generators"
2
+
3
+ module Ahoy
4
+ module Generators
5
+ module Messages
6
+ class MongoidGenerator < Rails::Generators::Base
7
+ source_root File.join(__dir__, "templates")
8
+
9
+ class_option :unencrypted, type: :boolean
10
+
11
+ def copy_templates
12
+ if options[:unencrypted]
13
+ template "mongoid.rb", "app/models/ahoy/message.rb"
14
+ else
15
+ template "mongoid_encrypted.rb", "app/models/ahoy/message.rb"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,10 +2,10 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
2
2
  def change
3
3
  create_table :ahoy_messages do |t|
4
4
  t.references :user, polymorphic: true
5
- t.text :to
5
+ <%= to_column %>
6
6
  t.string :mailer
7
7
  t.text :subject
8
- t.timestamp :sent_at
8
+ t.datetime :sent_at
9
9
  end
10
10
  end
11
11
  end
@@ -0,0 +1,8 @@
1
+ class Ahoy::Message < ActiveRecord::Base
2
+ self.table_name = "ahoy_messages"
3
+
4
+ belongs_to :user, polymorphic: true, optional: true
5
+
6
+ encrypts :to
7
+ blind_index :to
8
+ end
@@ -0,0 +1,12 @@
1
+ class Ahoy::Message
2
+ include Mongoid::Document
3
+
4
+ belongs_to :user, polymorphic: true, optional: true, index: true
5
+
6
+ field :to, type: String
7
+ field :mailer, type: String
8
+ field :subject, type: String
9
+ field :sent_at, type: Time
10
+
11
+ index({to: 1})
12
+ end
@@ -0,0 +1,16 @@
1
+ class Ahoy::Message
2
+ include Mongoid::Document
3
+
4
+ belongs_to :user, polymorphic: true, optional: true, index: true
5
+
6
+ field :to_ciphertext, type: String
7
+ field :to_bidx, type: String
8
+ field :mailer, type: String
9
+ field :subject, type: String
10
+ field :sent_at, type: Time
11
+
12
+ index({to_bidx: 1})
13
+
14
+ encrypts :to
15
+ blind_index :to
16
+ end
@@ -0,0 +1,39 @@
1
+ require "rails/generators"
2
+
3
+ module Ahoy
4
+ module Generators
5
+ class MessagesGenerator < Rails::Generators::Base
6
+ class_option :unencrypted, type: :boolean
7
+
8
+ def copy_templates
9
+ activerecord = defined?(ActiveRecord)
10
+ mongoid = defined?(Mongoid)
11
+
12
+ selection =
13
+ if activerecord && mongoid
14
+ puts <<-MSG
15
+
16
+ Which data store would you like to use?
17
+ 1. Active Record (default)
18
+ 2. Mongoid
19
+ MSG
20
+
21
+ ask(">")
22
+ elsif activerecord
23
+ "1"
24
+ else
25
+ "2"
26
+ end
27
+
28
+ case selection
29
+ when "", "1"
30
+ invoke "ahoy:messages:activerecord"
31
+ when "2"
32
+ invoke "ahoy:messages:mongoid"
33
+ else
34
+ abort "Error: must enter a number [1-2]"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ahoy_email
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-02 00:00:00.000000000 Z
11
+ date: 2021-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionmailer
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
19
+ version: '5'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '4.2'
26
+ version: '5'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: addressable
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -66,106 +66,8 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.1.1
69
- - !ruby/object:Gem::Dependency
70
- name: bundler
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: rake
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: minitest
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: activerecord
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: combustion
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: rails
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- - !ruby/object:Gem::Dependency
154
- name: sqlite3
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: '0'
167
- description:
168
- email: andrew@chartkick.com
69
+ description:
70
+ email: andrew@ankane.org
169
71
  executables: []
170
72
  extensions: []
171
73
  extra_rdoc_files: []
@@ -175,22 +77,35 @@ files:
175
77
  - LICENSE.txt
176
78
  - README.md
177
79
  - app/controllers/ahoy/messages_controller.rb
80
+ - app/models/ahoy/click.rb
178
81
  - app/models/ahoy/message.rb
179
82
  - config/routes.rb
180
83
  - lib/ahoy_email.rb
84
+ - lib/ahoy_email/database_subscriber.rb
181
85
  - lib/ahoy_email/engine.rb
182
86
  - lib/ahoy_email/mailer.rb
87
+ - lib/ahoy_email/message_subscriber.rb
183
88
  - lib/ahoy_email/observer.rb
184
89
  - lib/ahoy_email/processor.rb
90
+ - lib/ahoy_email/redis_subscriber.rb
185
91
  - lib/ahoy_email/tracker.rb
92
+ - lib/ahoy_email/utils.rb
186
93
  - lib/ahoy_email/version.rb
187
- - lib/generators/ahoy_email/install_generator.rb
188
- - lib/generators/ahoy_email/templates/install.rb.tt
94
+ - lib/generators/ahoy/clicks/activerecord_generator.rb
95
+ - lib/generators/ahoy/clicks/templates/migration.rb.tt
96
+ - lib/generators/ahoy/clicks_generator.rb
97
+ - lib/generators/ahoy/messages/activerecord_generator.rb
98
+ - lib/generators/ahoy/messages/mongoid_generator.rb
99
+ - lib/generators/ahoy/messages/templates/migration.rb.tt
100
+ - lib/generators/ahoy/messages/templates/model_encrypted.rb.tt
101
+ - lib/generators/ahoy/messages/templates/mongoid.rb.tt
102
+ - lib/generators/ahoy/messages/templates/mongoid_encrypted.rb.tt
103
+ - lib/generators/ahoy/messages_generator.rb
189
104
  homepage: https://github.com/ankane/ahoy_email
190
105
  licenses:
191
106
  - MIT
192
107
  metadata: {}
193
- post_install_message:
108
+ post_install_message:
194
109
  rdoc_options: []
195
110
  require_paths:
196
111
  - lib
@@ -198,16 +113,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
198
113
  requirements:
199
114
  - - ">="
200
115
  - !ruby/object:Gem::Version
201
- version: '2.2'
116
+ version: '2.6'
202
117
  required_rubygems_version: !ruby/object:Gem::Requirement
203
118
  requirements:
204
119
  - - ">="
205
120
  - !ruby/object:Gem::Version
206
121
  version: '0'
207
122
  requirements: []
208
- rubyforge_project:
209
- rubygems_version: 2.7.7
210
- signing_key:
123
+ rubygems_version: 3.2.3
124
+ signing_key:
211
125
  specification_version: 4
212
- summary: Email analytics for Rails
126
+ summary: First-party email analytics for Rails
213
127
  test_files: []