ahoy_email 0.5.2 → 1.0.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 +13 -0
- data/CONTRIBUTING.md +47 -0
- data/LICENSE.txt +1 -1
- data/README.md +135 -99
- data/app/controllers/ahoy/messages_controller.rb +21 -19
- data/app/models/ahoy/message.rb +1 -1
- data/config/routes.rb +1 -3
- data/lib/ahoy_email.rb +43 -19
- data/lib/ahoy_email/engine.rb +0 -2
- data/lib/ahoy_email/interceptor.rb +1 -1
- data/lib/ahoy_email/mailer.rb +27 -21
- data/lib/ahoy_email/processor.rb +40 -60
- data/lib/ahoy_email/tracker.rb +21 -0
- data/lib/ahoy_email/version.rb +1 -1
- data/lib/generators/ahoy_email/templates/install.rb.tt +11 -0
- metadata +25 -59
- data/.gitignore +0 -24
- data/.travis.yml +0 -16
- data/Gemfile +0 -6
- data/Rakefile +0 -9
- data/ahoy_email.gemspec +0 -35
- data/lib/generators/ahoy_email/templates/install.rb +0 -30
- data/test/gemfiles/actionmailer42.gemfile +0 -6
- data/test/gemfiles/actionmailer50.gemfile +0 -6
- data/test/gemfiles/actionmailer51.gemfile +0 -6
- data/test/internal/config/database.yml +0 -3
- data/test/internal/config/routes.rb +0 -3
- data/test/internal/db/schema.rb +0 -30
- data/test/mailer_test.rb +0 -153
- data/test/test_helper.rb +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: acb3c3744cff970d75c695f1c79edec32e7dd7a9bea7342576b7fc87be2d3e4a
|
4
|
+
data.tar.gz: 1cd08f10de1fa686aa09312cdee054694debe9f7835bb97fd9eb35aa9fc09c01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3eb7a246d87edfec081d873c8a464c90a682b81acd5d38877c8551770ba911f0c222bf778b03c2abf390fadf5bf48edabe863187627740c673b86718e082a9e4
|
7
|
+
data.tar.gz: 15c5a59995db66c0d76bfa5273e8a7f4404b11d80078fbf73765fcfb157d8ed3ff744b7bc694e75c8eb8fc2b4afc5494727fa68fb2f60769f91cf2d1f86e7dec
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
## 1.0.0
|
2
|
+
|
3
|
+
- Removed support for Rails < 4.2
|
4
|
+
|
5
|
+
Breaking changes
|
6
|
+
|
7
|
+
- UTM tagging, open tracking, and click tracking are no longer enabled by default
|
8
|
+
- Only sent emails are recorded
|
9
|
+
- Proc options are now executed in the context of the mailer and take no arguments
|
10
|
+
- Invalid options now throw an `ArgumentError`
|
11
|
+
- `AhoyEmail.track` was removed in favor of `AhoyEmail.default_options`
|
12
|
+
- The `heuristic_parse` option was removed and is now the default
|
13
|
+
|
1
14
|
## 0.5.2
|
2
15
|
|
3
16
|
- Fixed secret token for Rails 5.2
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
First, thanks for wanting to contribute. You’re awesome! :heart:
|
4
|
+
|
5
|
+
## Help
|
6
|
+
|
7
|
+
We’re not able to provide support through GitHub Issues. If you’re looking for help with your code, try posting on [Stack Overflow](https://stackoverflow.com/).
|
8
|
+
|
9
|
+
All features should be documented. If you don’t see a feature in the docs, assume it doesn’t exist.
|
10
|
+
|
11
|
+
## Bugs
|
12
|
+
|
13
|
+
Think you’ve discovered a bug?
|
14
|
+
|
15
|
+
1. Search existing issues to see if it’s been reported.
|
16
|
+
2. Try the `master` branch to make sure it hasn’t been fixed.
|
17
|
+
|
18
|
+
```rb
|
19
|
+
gem "ahoy_email", github: "ankane/ahoy_email"
|
20
|
+
```
|
21
|
+
|
22
|
+
If the above steps don’t help, create an issue. Include:
|
23
|
+
|
24
|
+
- Detailed steps to reproduce
|
25
|
+
- Complete backtraces for exceptions
|
26
|
+
|
27
|
+
## New Features
|
28
|
+
|
29
|
+
If you’d like to discuss a new feature, create an issue and start the title with `[Idea]`.
|
30
|
+
|
31
|
+
## Pull Requests
|
32
|
+
|
33
|
+
Fork the project and create a pull request. A few tips:
|
34
|
+
|
35
|
+
- Keep changes to a minimum. If you have multiple features or fixes, submit multiple pull requests.
|
36
|
+
- Follow the existing style. The code should read like it’s written by a single person.
|
37
|
+
- Add one or more tests if possible. Make sure existing tests pass with:
|
38
|
+
|
39
|
+
```sh
|
40
|
+
bundle exec rake test
|
41
|
+
```
|
42
|
+
|
43
|
+
Feel free to open an issue to get feedback on your idea before spending too much time on it.
|
44
|
+
|
45
|
+
---
|
46
|
+
|
47
|
+
This contributing guide is released under [CCO](https://creativecommons.org/publicdomain/zero/1.0/) (public domain). Use it for your own project without attribution.
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
# Ahoy Email
|
2
2
|
|
3
|
-
:postbox:
|
3
|
+
:postbox: Email analytics for Rails
|
4
4
|
|
5
5
|
You get:
|
6
6
|
|
7
7
|
- A history of emails sent to each user
|
8
8
|
- Easy UTM tagging
|
9
|
-
-
|
9
|
+
- Optional open and click tracking
|
10
10
|
|
11
|
-
|
11
|
+
**Ahoy Email 1.0 was recently released!** See [how to upgrade](#upgrading)
|
12
12
|
|
13
13
|
:bullettrain_side: To manage unsubscribes, check out [Mailkick](https://github.com/ankane/mailkick)
|
14
14
|
|
@@ -33,27 +33,37 @@ rails db:migrate
|
|
33
33
|
|
34
34
|
## How It Works
|
35
35
|
|
36
|
-
|
36
|
+
### Message History
|
37
|
+
|
38
|
+
Ahoy creates an `Ahoy::Message` record for each email sent by default. You can disable history for a mailer:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class UserMailer < ApplicationMailer
|
42
|
+
track message: false # use only/except to limit actions
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
Or by default:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
AhoyEmail.default_options[:message] = false
|
50
|
+
```
|
37
51
|
|
38
52
|
### Users
|
39
53
|
|
40
|
-
Ahoy
|
54
|
+
Ahoy records the user a message is sent to - not just the email address. This gives you a full history of messages for each user, even if he or she changes addresses.
|
41
55
|
|
42
|
-
By default, Ahoy tries `User.
|
56
|
+
By default, Ahoy tries `@user` then `params[:user]` then `User.find_by(email: message.to.first)` to find the user.
|
43
57
|
|
44
58
|
You can pass a specific user with:
|
45
59
|
|
46
60
|
```ruby
|
47
61
|
class UserMailer < ApplicationMailer
|
48
|
-
|
49
|
-
# ...
|
50
|
-
track user: user
|
51
|
-
mail to: user.email
|
52
|
-
end
|
62
|
+
track user: -> { params[:some_user] }
|
53
63
|
end
|
54
64
|
```
|
55
65
|
|
56
|
-
The user association is [polymorphic](
|
66
|
+
The user association is [polymorphic](https://railscasts.com/episodes/154-polymorphic-association), so use it with any model.
|
57
67
|
|
58
68
|
To get all messages sent to a user, add an association:
|
59
69
|
|
@@ -69,124 +79,148 @@ And run:
|
|
69
79
|
user.messages
|
70
80
|
```
|
71
81
|
|
72
|
-
###
|
82
|
+
### Extra Attributes
|
73
83
|
|
74
|
-
|
84
|
+
Record extra attributes on the `Ahoy::Message` model.
|
75
85
|
|
76
|
-
|
86
|
+
Create a migration to add extra attributes to the `ahoy_messages` table. For example:
|
77
87
|
|
78
|
-
|
79
|
-
|
80
|
-
|
88
|
+
```ruby
|
89
|
+
class AddCouponIdToAhoyMessages < ActiveRecord::Migration[5.2]
|
90
|
+
def change
|
91
|
+
add_column :ahoy_messages, :coupon_id, :integer
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
81
95
|
|
82
|
-
|
96
|
+
Then use:
|
83
97
|
|
98
|
+
```ruby
|
99
|
+
class CouponMailer < ApplicationMailer
|
100
|
+
track extra: {coupon_id: 1}
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
You can use a proc as well.
|
84
105
|
|
85
|
-
```
|
86
|
-
|
106
|
+
```ruby
|
107
|
+
class CouponMailer < ApplicationMailer
|
108
|
+
track extra: -> { {coupon_id: params[:coupon].id} }
|
109
|
+
end
|
87
110
|
```
|
88
111
|
|
89
|
-
###
|
112
|
+
### UTM Tagging
|
90
113
|
|
91
|
-
|
114
|
+
Automatically add UTM parameters to links.
|
92
115
|
|
93
|
-
|
116
|
+
```ruby
|
117
|
+
class CouponMailer < ApplicationMailer
|
118
|
+
track utm_params: true # use only/except to limit actions
|
119
|
+
end
|
120
|
+
```
|
94
121
|
|
95
|
-
|
122
|
+
The defaults are:
|
96
123
|
|
97
|
-
|
124
|
+
- `utm_medium` - `email`
|
125
|
+
- `utm_source` - the mailer name like `coupon_mailer`
|
126
|
+
- `utm_campaign` - the mailer action like `offer`
|
98
127
|
|
99
|
-
|
128
|
+
You can customize them with:
|
100
129
|
|
101
|
-
```
|
102
|
-
|
130
|
+
```ruby
|
131
|
+
class CouponMailer < ApplicationMailer
|
132
|
+
track utm_params: true, utm_campaign: -> { "coupon#{params[:coupon].id}" }
|
133
|
+
end
|
103
134
|
```
|
104
135
|
|
105
|
-
|
136
|
+
Skip specific links with:
|
106
137
|
|
138
|
+
```erb
|
139
|
+
<%= link_to "Go", some_url, data: {skip_utm_params: true} %>
|
107
140
|
```
|
108
|
-
https://yoursite.com/ahoy/messages/rAnDoMtOkEn/click?url=https%3A%2F%2Fchartkick.com&signature=...
|
109
|
-
```
|
110
|
-
|
111
|
-
A signature is added to prevent [open redirects](https://www.owasp.org/index.php/Open_redirect).
|
112
141
|
|
113
|
-
|
142
|
+
### Opens & Clicks
|
114
143
|
|
115
|
-
|
116
|
-
<a data-skip-click="true" href="...">Can't touch this</a>
|
117
|
-
```
|
144
|
+
#### Setup
|
118
145
|
|
119
|
-
|
146
|
+
Additional setup is required to track opens and clicks.
|
120
147
|
|
121
|
-
Create a migration
|
148
|
+
Create a migration with:
|
122
149
|
|
123
150
|
```ruby
|
124
|
-
class
|
151
|
+
class AddTokenToAhoyMessages < ActiveRecord::Migration[5.2]
|
125
152
|
def change
|
126
|
-
add_column :ahoy_messages, :
|
153
|
+
add_column :ahoy_messages, :token, :string
|
154
|
+
add_column :ahoy_messages, :opened_at, :timestamp
|
155
|
+
add_column :ahoy_messages, :clicked_at, :timestamp
|
156
|
+
|
157
|
+
add_index :ahoy_messages, :token
|
127
158
|
end
|
128
159
|
end
|
129
160
|
```
|
130
161
|
|
131
|
-
|
162
|
+
Create an initializer `config/initializers/ahoy_email.rb` with:
|
132
163
|
|
133
164
|
```ruby
|
134
|
-
|
165
|
+
AhoyEmail.api = true
|
135
166
|
```
|
136
167
|
|
137
|
-
|
168
|
+
And add to mailers you want to track:
|
138
169
|
|
139
|
-
|
170
|
+
```ruby
|
171
|
+
class UserMailer < ApplicationMailer
|
172
|
+
track open: true, click: true # use only/except to limit actions
|
173
|
+
end
|
174
|
+
```
|
140
175
|
|
141
|
-
|
176
|
+
#### How It Works
|
142
177
|
|
143
|
-
|
144
|
-
- mailer
|
145
|
-
- subject
|
146
|
-
- content
|
178
|
+
For opens, an invisible pixel is added right before the `</body>` tag in HTML emails. If the recipient has images enabled in their email client, the pixel is loaded and the open time recorded.
|
147
179
|
|
148
|
-
|
180
|
+
For clicks, a redirect is added to links to track clicks in HTML emails.
|
149
181
|
|
150
|
-
|
182
|
+
```
|
183
|
+
https://chartkick.com
|
184
|
+
```
|
151
185
|
|
152
|
-
|
186
|
+
becomes
|
153
187
|
|
154
|
-
``` ruby
|
155
|
-
class UserMailer < ApplicationMailer
|
156
|
-
def welcome_email(user)
|
157
|
-
# ...
|
158
|
-
track user: user
|
159
|
-
mail to: user.email
|
160
|
-
end
|
161
|
-
end
|
162
188
|
```
|
189
|
+
https://yoursite.com/ahoy/messages/rAnDoMtOkEn/click?url=https%3A%2F%2Fchartkick.com&signature=...
|
190
|
+
```
|
191
|
+
|
192
|
+
A signature is added to prevent [open redirects](https://www.owasp.org/index.php/Open_redirect).
|
163
193
|
|
164
|
-
|
194
|
+
Skip specific links with:
|
195
|
+
|
196
|
+
```erb
|
197
|
+
<%= link_to "Go", some_url, data: {skip_click: true} %>
|
198
|
+
```
|
199
|
+
|
200
|
+
By default, unsubscribe links are excluded. To change this, use:
|
165
201
|
|
166
202
|
```ruby
|
167
|
-
|
168
|
-
track utm_campaign: "boom"
|
169
|
-
end
|
203
|
+
AhoyEmail.default_options[:unsubscribe_links] = true
|
170
204
|
```
|
171
205
|
|
172
|
-
|
206
|
+
You can specify the domain to use with:
|
173
207
|
|
174
208
|
```ruby
|
175
|
-
AhoyEmail.
|
209
|
+
AhoyEmail.default_options[:url_options] = {host: "mydomain.com"}
|
176
210
|
```
|
177
211
|
|
178
|
-
|
212
|
+
#### Events
|
179
213
|
|
180
|
-
Subscribe to open and click events
|
214
|
+
Subscribe to open and click events by adding to the initializer:
|
181
215
|
|
182
216
|
```ruby
|
183
217
|
class EmailSubscriber
|
184
218
|
def open(event)
|
185
|
-
#
|
219
|
+
# your code
|
186
220
|
end
|
187
221
|
|
188
222
|
def click(event)
|
189
|
-
#
|
223
|
+
# your code
|
190
224
|
end
|
191
225
|
end
|
192
226
|
|
@@ -211,54 +245,56 @@ AhoyEmail.subscribers << EmailSubscriber.new
|
|
211
245
|
|
212
246
|
## Reference
|
213
247
|
|
214
|
-
|
248
|
+
Set global options
|
215
249
|
|
216
250
|
```ruby
|
217
|
-
|
251
|
+
AhoyEmail.default_options[:user] = -> { params[:admin] }
|
218
252
|
```
|
219
253
|
|
220
|
-
|
254
|
+
Use a different model
|
221
255
|
|
222
256
|
```ruby
|
223
|
-
|
257
|
+
AhoyEmail.message_model = -> { UserMessage }
|
224
258
|
```
|
225
259
|
|
226
|
-
Or
|
260
|
+
Or fully customize how messages are tracked
|
227
261
|
|
228
262
|
```ruby
|
229
|
-
|
230
|
-
|
263
|
+
AhoyEmail.track_method = lambda do |data|
|
264
|
+
# your code
|
265
|
+
end
|
231
266
|
```
|
232
267
|
|
233
|
-
|
268
|
+
## Upgrading
|
234
269
|
|
235
|
-
|
236
|
-
AhoyEmail.track message: false
|
237
|
-
```
|
270
|
+
### 1.0
|
238
271
|
|
239
|
-
|
272
|
+
Breaking changes
|
240
273
|
|
241
|
-
|
242
|
-
track url_options: {host: "mydomain.com"}
|
243
|
-
```
|
274
|
+
- UTM tagging, open tracking, and click tracking are no longer enabled by default. To enable, create an initializer with:
|
244
275
|
|
245
|
-
|
276
|
+
```ruby
|
277
|
+
AhoyEmail.api = true
|
246
278
|
|
247
|
-
|
248
|
-
|
249
|
-
|
279
|
+
AhoyEmail.default_options[:open] = true
|
280
|
+
AhoyEmail.default_options[:click] = true
|
281
|
+
AhoyEmail.default_options[:utm_params] = true
|
282
|
+
```
|
250
283
|
|
251
|
-
|
284
|
+
- Only sent emails are recorded
|
285
|
+
- Proc options are now executed in the context of the mailer and take no arguments
|
252
286
|
|
253
|
-
```ruby
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
## Upgrading
|
287
|
+
```ruby
|
288
|
+
# old
|
289
|
+
user: ->(mailer, message) { User.find_by(email: message.to.first) }
|
258
290
|
|
259
|
-
|
291
|
+
# new
|
292
|
+
user: -> { User.find_by(email: message.to.first) }
|
293
|
+
```
|
260
294
|
|
261
|
-
|
295
|
+
- Invalid options now throw an `ArgumentError`
|
296
|
+
- `AhoyEmail.track` was removed in favor of `AhoyEmail.default_options`
|
297
|
+
- The `heuristic_parse` option was removed and is now the default
|
262
298
|
|
263
299
|
## History
|
264
300
|
|
@@ -1,17 +1,24 @@
|
|
1
1
|
module Ahoy
|
2
|
-
class MessagesController <
|
3
|
-
|
4
|
-
|
2
|
+
class MessagesController < ApplicationController
|
3
|
+
filters = _process_action_callbacks.map(&:filter) - AhoyEmail.preserve_callbacks
|
4
|
+
if Rails::VERSION::MAJOR >= 5
|
5
|
+
skip_before_action(*filters, raise: false)
|
6
|
+
skip_after_action(*filters, raise: false)
|
7
|
+
skip_around_action(*filters, raise: false)
|
5
8
|
else
|
6
|
-
|
9
|
+
skip_action_callback *filters
|
7
10
|
end
|
8
11
|
|
12
|
+
before_action :set_message
|
13
|
+
|
9
14
|
def open
|
10
15
|
if @message && !@message.opened_at
|
11
16
|
@message.opened_at = Time.now
|
12
17
|
@message.save!
|
13
18
|
end
|
19
|
+
|
14
20
|
publish :open
|
21
|
+
|
15
22
|
send_data Base64.decode64("R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="), type: "image/gif", disposition: "inline"
|
16
23
|
end
|
17
24
|
|
@@ -21,10 +28,17 @@ module Ahoy
|
|
21
28
|
@message.opened_at ||= @message.clicked_at
|
22
29
|
@message.save!
|
23
30
|
end
|
31
|
+
|
32
|
+
user_signature = params[:signature].to_s
|
24
33
|
url = params[:url].to_s
|
25
|
-
|
26
|
-
|
27
|
-
|
34
|
+
|
35
|
+
# TODO sign more than just url and transition to HMAC-SHA256
|
36
|
+
digest = "SHA1"
|
37
|
+
signature = OpenSSL::HMAC.hexdigest(digest, AhoyEmail.secret_token, url)
|
38
|
+
|
39
|
+
if ActiveSupport::SecurityUtils.secure_compare(user_signature, signature)
|
40
|
+
publish :click, url: params[:url]
|
41
|
+
|
28
42
|
redirect_to url
|
29
43
|
else
|
30
44
|
redirect_to AhoyEmail.invalid_redirect_url || main_app.root_url
|
@@ -46,17 +60,5 @@ module Ahoy
|
|
46
60
|
end
|
47
61
|
end
|
48
62
|
end
|
49
|
-
|
50
|
-
# from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/message_verifier.rb
|
51
|
-
# constant-time comparison algorithm to prevent timing attacks
|
52
|
-
def secure_compare(a, b)
|
53
|
-
return false unless a.bytesize == b.bytesize
|
54
|
-
|
55
|
-
l = a.unpack "C#{a.bytesize}"
|
56
|
-
|
57
|
-
res = 0
|
58
|
-
b.each_byte { |byte| res |= byte ^ l.shift }
|
59
|
-
res == 0
|
60
|
-
end
|
61
63
|
end
|
62
64
|
end
|