ahoy_email 0.0.2 → 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 +1 -1
- data/README.md +100 -48
- data/app/controllers/ahoy/messages_controller.rb +1 -1
- data/lib/ahoy_email.rb +17 -32
- data/lib/ahoy_email/interceptor.rb +1 -1
- data/lib/ahoy_email/mailer.rb +38 -0
- data/lib/ahoy_email/processor.rb +33 -36
- data/lib/ahoy_email/version.rb +1 -1
- data/lib/generators/ahoy_email/templates/install.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d245ad6df5fbb64f6b38292b8b139a1382dda4e
|
4
|
+
data.tar.gz: cd822e8f05cf9eb01a1c37979b67428c2c7fd999
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c598c2697d2ca2167e2ccb6e5fdedb20a27f831f71b0f7ed0a4907d9b4646e24f6cd02cdbb7be54328dfec81cf9b353d57a3c80bd08cbf21a510b58e3da4463
|
7
|
+
data.tar.gz: a0649a7c6a0c575dc87a8aac7d36e1a53aab6d475c198a04131597001797442d1f3cdcedd442ec3f5f2ba4f64b3a2e4d9ab8d26af1d43d9984d7b060ea181d10
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
# Ahoy Email
|
2
2
|
|
3
|
-
:
|
3
|
+
:postbox: Simple, powerful email tracking for Rails
|
4
4
|
|
5
|
-
:
|
5
|
+
You get:
|
6
6
|
|
7
|
-
|
7
|
+
- A history of emails sent to each user
|
8
|
+
- Open and click tracking
|
9
|
+
- Easy UTM tagging
|
8
10
|
|
9
|
-
|
10
|
-
- opened
|
11
|
-
- clicked
|
11
|
+
Works with any email service.
|
12
12
|
|
13
13
|
## Installation
|
14
14
|
|
@@ -27,17 +27,53 @@ rake db:migrate
|
|
27
27
|
|
28
28
|
## How It Works
|
29
29
|
|
30
|
-
Ahoy creates an `Ahoy::Message`
|
30
|
+
Ahoy creates an `Ahoy::Message` every time an email is sent by default.
|
31
31
|
|
32
|
-
###
|
32
|
+
### Users
|
33
33
|
|
34
|
-
|
34
|
+
Ahoy tracks 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.
|
35
35
|
|
36
|
-
|
36
|
+
By default, Ahoy tries `User.where(email: message.to.first).first` to find the user.
|
37
37
|
|
38
|
-
|
38
|
+
You can pass a specific user with:
|
39
39
|
|
40
|
-
|
40
|
+
```ruby
|
41
|
+
class UserMailer < ActionMailer::Base
|
42
|
+
def welcome_email(user)
|
43
|
+
# ...
|
44
|
+
track user: user
|
45
|
+
mail to: user.email
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
The user association is [polymorphic](http://railscasts.com/episodes/154-polymorphic-association), so use it with any model.
|
51
|
+
|
52
|
+
To get all messages sent to a user, add an association:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
class User < ActiveRecord::Base
|
56
|
+
has_many :messages, class_name: "Ahoy::Message"
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
And run:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
user.messages
|
64
|
+
```
|
65
|
+
|
66
|
+
### Opens
|
67
|
+
|
68
|
+
An invisible pixel is added right before the `</body>` tag in HTML emails.
|
69
|
+
|
70
|
+
If the recipient has images enabled in his or her email client, the pixel is loaded and the open time recorded.
|
71
|
+
|
72
|
+
Use `track open: false` to skip this.
|
73
|
+
|
74
|
+
### Clicks
|
75
|
+
|
76
|
+
A redirect is added to links to track clicks in HTML emails.
|
41
77
|
|
42
78
|
````
|
43
79
|
http://chartkick.com
|
@@ -46,78 +82,94 @@ http://chartkick.com
|
|
46
82
|
becomes
|
47
83
|
|
48
84
|
```
|
49
|
-
http://
|
85
|
+
http://you.io/ahoy/messages/rAnDoMtOkEn/click?url=http%3A%2F%2Fchartkick.com&signature=...
|
50
86
|
```
|
51
87
|
|
52
88
|
A signature is added to prevent [open redirects](https://www.owasp.org/index.php/Open_redirect).
|
53
89
|
|
54
|
-
|
90
|
+
Use `track click: false` to skip tracking, or skip specific links with:
|
91
|
+
|
92
|
+
```html
|
93
|
+
<a data-skip-click="true" href="...">Can't touch this</a>
|
94
|
+
```
|
55
95
|
|
56
96
|
### UTM Parameters
|
57
97
|
|
58
|
-
UTM parameters are added to
|
98
|
+
UTM parameters are added to links if they don’t already exist.
|
59
99
|
|
60
|
-
|
100
|
+
The defaults are:
|
61
101
|
|
62
|
-
|
102
|
+
- utm_medium - `email`
|
103
|
+
- utm_source - the mailer name like `user_mailer`
|
104
|
+
- utm_campaign - the mailer action like `welcome_email`
|
63
105
|
|
64
|
-
|
106
|
+
Use `track utm_params: false` to skip tagging, or skip specific links with:
|
65
107
|
|
66
|
-
|
108
|
+
|
109
|
+
```html
|
110
|
+
<a data-skip-utm-params="true" href="...">Break it down</a>
|
111
|
+
```
|
112
|
+
|
113
|
+
## Customize
|
114
|
+
|
115
|
+
There are 3 places to set options. Here’s the order of precedence.
|
116
|
+
|
117
|
+
### Action
|
118
|
+
|
119
|
+
``` ruby
|
67
120
|
class UserMailer < ActionMailer::Base
|
68
121
|
def welcome_email(user)
|
69
122
|
# ...
|
70
|
-
|
123
|
+
track user: user
|
71
124
|
mail to: user.email
|
72
125
|
end
|
73
126
|
end
|
74
127
|
```
|
75
128
|
|
76
|
-
|
77
|
-
|
78
|
-
## Customize
|
129
|
+
### Mailer
|
79
130
|
|
80
|
-
|
131
|
+
```ruby
|
132
|
+
class UserMailer < ActionMailer::Base
|
133
|
+
track utm_campaign: "boom"
|
134
|
+
end
|
135
|
+
```
|
81
136
|
|
82
137
|
### Global
|
83
138
|
|
84
|
-
|
139
|
+
```ruby
|
140
|
+
AhoyEmail.track open: false
|
141
|
+
```
|
142
|
+
|
143
|
+
## Reference
|
144
|
+
|
145
|
+
You can use a `Proc` for any option.
|
85
146
|
|
86
147
|
```ruby
|
87
|
-
|
88
|
-
create_message: true,
|
89
|
-
track_open: true,
|
90
|
-
track_click: true,
|
91
|
-
utm_source: nil,
|
92
|
-
utm_medium: "email",
|
93
|
-
utm_term: nil,
|
94
|
-
utm_content: nil,
|
95
|
-
utm_campaign: nil
|
96
|
-
}
|
148
|
+
track utm_campaign: proc{|message, mailer| mailer.action_name + Time.now.year }
|
97
149
|
```
|
98
150
|
|
99
|
-
|
151
|
+
Disable tracking for an email
|
100
152
|
|
101
153
|
```ruby
|
102
|
-
|
103
|
-
ahoy utm_campaign: "boom"
|
104
|
-
end
|
154
|
+
track message: false
|
105
155
|
```
|
106
156
|
|
107
|
-
|
157
|
+
Or by default
|
108
158
|
|
109
|
-
```
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
159
|
+
```ruby
|
160
|
+
AhoyEmail.track message: false
|
161
|
+
```
|
162
|
+
|
163
|
+
Use a different model
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
AhoyEmail.message_model = UserMessage
|
117
167
|
```
|
118
168
|
|
119
169
|
## TODO
|
120
170
|
|
171
|
+
- Add tests
|
172
|
+
- Open and click hooks for deeper analytics
|
121
173
|
- Subscription management (lists, opt-outs) [separate gem]
|
122
174
|
|
123
175
|
## History
|
data/lib/ahoy_email.rb
CHANGED
@@ -5,49 +5,34 @@ require "addressable/uri"
|
|
5
5
|
require "openssl"
|
6
6
|
require "ahoy_email/processor"
|
7
7
|
require "ahoy_email/interceptor"
|
8
|
+
require "ahoy_email/mailer"
|
8
9
|
require "ahoy_email/engine"
|
9
10
|
|
10
|
-
ActionMailer::Base.register_interceptor AhoyEmail::Interceptor
|
11
|
-
|
12
11
|
module AhoyEmail
|
13
12
|
mattr_accessor :secret_token, :options
|
14
13
|
|
15
14
|
self.options = {
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
message: true,
|
16
|
+
open: true,
|
17
|
+
click: true,
|
18
|
+
utm_params: true,
|
19
|
+
utm_source: proc {|message, mailer| mailer.mailer_name },
|
20
20
|
utm_medium: "email",
|
21
21
|
utm_term: nil,
|
22
22
|
utm_content: nil,
|
23
|
-
utm_campaign:
|
23
|
+
utm_campaign: proc {|message, mailer| mailer.action_name },
|
24
|
+
user: proc{|message, mailer| User.where(email: message.to.first).first rescue nil }
|
24
25
|
}
|
25
|
-
end
|
26
|
-
|
27
|
-
module ActionMailer
|
28
|
-
class Base
|
29
|
-
class_attribute :ahoy_options
|
30
|
-
self.ahoy_options = {}
|
31
|
-
|
32
|
-
class << self
|
33
|
-
def ahoy(options)
|
34
|
-
self.ahoy_options = ahoy_options.merge(options)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def ahoy(options)
|
39
|
-
@ahoy_options = (@ahoy_options || {}).merge(options)
|
40
|
-
end
|
41
26
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
options = AhoyEmail.options.merge(self.class.ahoy_options).merge(@ahoy_options || {})
|
46
|
-
AhoyEmail::Processor.new(message, options).process!
|
47
|
-
|
48
|
-
message
|
49
|
-
end
|
50
|
-
alias_method_chain :mail, :ahoy
|
27
|
+
def self.message_model=(message_model)
|
28
|
+
@message_model = message_model
|
29
|
+
end
|
51
30
|
|
31
|
+
def self.message_model
|
32
|
+
@message_model || Ahoy::Message
|
52
33
|
end
|
34
|
+
|
53
35
|
end
|
36
|
+
|
37
|
+
ActionMailer::Base.send :include, AhoyEmail::Mailer
|
38
|
+
ActionMailer::Base.register_interceptor AhoyEmail::Interceptor
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module AhoyEmail
|
2
|
+
module Mailer
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.class_eval do
|
7
|
+
class_attribute :ahoy_options
|
8
|
+
self.ahoy_options = {}
|
9
|
+
alias_method_chain :mail, :ahoy
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def track(options)
|
15
|
+
self.ahoy_options = ahoy_options.merge(options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def track(options)
|
20
|
+
@ahoy_options = (@ahoy_options || {}).merge(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def mail_with_ahoy(headers = {}, &block)
|
24
|
+
message = mail_without_ahoy(headers, &block)
|
25
|
+
|
26
|
+
options = AhoyEmail.options.merge(self.class.ahoy_options).merge(@ahoy_options || {})
|
27
|
+
options.each do |k, v|
|
28
|
+
if v.respond_to?(:call)
|
29
|
+
options[k] = v.call(message, self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
AhoyEmail::Processor.new(message, options).process
|
33
|
+
|
34
|
+
message
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/ahoy_email/processor.rb
CHANGED
@@ -7,31 +7,30 @@ module AhoyEmail
|
|
7
7
|
def initialize(message, options = {})
|
8
8
|
@message = message
|
9
9
|
@options = options
|
10
|
-
@ahoy_message = Ahoy::Message.new
|
11
10
|
end
|
12
11
|
|
13
|
-
def process
|
14
|
-
if options[:
|
12
|
+
def process
|
13
|
+
if options[:message]
|
14
|
+
@ahoy_message = AhoyEmail.message_model.new
|
15
15
|
ahoy_message.token = generate_token
|
16
16
|
ahoy_message.user = options[:user]
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
track_click! if options[:track_click]
|
18
|
+
track_open if options[:open]
|
19
|
+
track_links if options[:utm_params] or options[:click]
|
21
20
|
|
22
21
|
# save
|
23
22
|
ahoy_message.subject = message.subject if ahoy_message.respond_to?(:subject=)
|
24
23
|
ahoy_message.content = message.to_s if ahoy_message.respond_to?(:content=)
|
25
|
-
ahoy_message.sent_at = Time.now
|
26
24
|
ahoy_message.save
|
25
|
+
message["Ahoy-Message-Id"] = ahoy_message.id
|
27
26
|
end
|
28
27
|
rescue => e
|
29
28
|
report_error(e)
|
30
29
|
end
|
31
30
|
|
32
|
-
def
|
31
|
+
def track_send
|
33
32
|
if (message_id = message["Ahoy-Message-Id"])
|
34
|
-
ahoy_message =
|
33
|
+
ahoy_message = AhoyEmail.message_model.where(id: message_id.to_s).first
|
35
34
|
if ahoy_message
|
36
35
|
ahoy_message.sent_at = Time.now
|
37
36
|
ahoy_message.save
|
@@ -48,27 +47,7 @@ module AhoyEmail
|
|
48
47
|
SecureRandom.urlsafe_base64(32).gsub(/[\-_]/, "").first(32)
|
49
48
|
end
|
50
49
|
|
51
|
-
def
|
52
|
-
if html_part?
|
53
|
-
body = (message.html_part || message).body
|
54
|
-
|
55
|
-
doc = Nokogiri::HTML(body.raw_source)
|
56
|
-
doc.css("a").each do |link|
|
57
|
-
uri = Addressable::URI.parse(link["href"])
|
58
|
-
params = uri.query_values || {}
|
59
|
-
%w[utm_source utm_medium utm_term utm_content utm_campaign].each do |key|
|
60
|
-
params[key] ||= options[key.to_sym] if options[key.to_sym]
|
61
|
-
end
|
62
|
-
uri.query_values = params
|
63
|
-
link["href"] = uri.to_s
|
64
|
-
end
|
65
|
-
|
66
|
-
# hacky
|
67
|
-
body.raw_source.sub!(body.raw_source, doc.to_s)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def track_open!
|
50
|
+
def track_open
|
72
51
|
if html_part?
|
73
52
|
raw_source = (message.html_part || message).body.raw_source
|
74
53
|
regex = /<\/body>/i
|
@@ -92,17 +71,24 @@ module AhoyEmail
|
|
92
71
|
end
|
93
72
|
end
|
94
73
|
|
95
|
-
def
|
74
|
+
def track_links
|
96
75
|
if html_part?
|
97
76
|
body = (message.html_part || message).body
|
98
77
|
|
99
78
|
doc = Nokogiri::HTML(body.raw_source)
|
100
79
|
doc.css("a").each do |link|
|
101
|
-
|
102
|
-
if
|
103
|
-
|
104
|
-
|
105
|
-
|
80
|
+
# utm params first
|
81
|
+
if options[:utm_params] and !skip_attribute?(link, "utm-params")
|
82
|
+
uri = Addressable::URI.parse(link["href"])
|
83
|
+
params = uri.query_values || {}
|
84
|
+
%w[utm_source utm_medium utm_term utm_content utm_campaign].each do |key|
|
85
|
+
params[key] ||= options[key.to_sym] if options[key.to_sym]
|
86
|
+
end
|
87
|
+
uri.query_values = params
|
88
|
+
link["href"] = uri.to_s
|
89
|
+
end
|
90
|
+
|
91
|
+
if options[:click] and !skip_attribute?(link, "click")
|
106
92
|
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new("sha1"), AhoyEmail.secret_token, link["href"])
|
107
93
|
url =
|
108
94
|
AhoyEmail::Engine.routes.url_helpers.url_for(
|
@@ -128,6 +114,17 @@ module AhoyEmail
|
|
128
114
|
(message.html_part || message).content_type =~ /html/
|
129
115
|
end
|
130
116
|
|
117
|
+
def skip_attribute?(link, suffix)
|
118
|
+
attribute = "data-skip-#{suffix}"
|
119
|
+
if link[attribute]
|
120
|
+
# remove it
|
121
|
+
link.remove_attribute(attribute)
|
122
|
+
true
|
123
|
+
else
|
124
|
+
false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
131
128
|
# not a fan of quiet errors
|
132
129
|
# but tracking should *not* break
|
133
130
|
# email delivery in production
|
data/lib/ahoy_email/version.rb
CHANGED
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: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-04-
|
11
|
+
date: 2014-04-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionmailer
|
@@ -100,6 +100,7 @@ files:
|
|
100
100
|
- lib/ahoy_email.rb
|
101
101
|
- lib/ahoy_email/engine.rb
|
102
102
|
- lib/ahoy_email/interceptor.rb
|
103
|
+
- lib/ahoy_email/mailer.rb
|
103
104
|
- lib/ahoy_email/processor.rb
|
104
105
|
- lib/ahoy_email/version.rb
|
105
106
|
- lib/generators/ahoy_email/install_generator.rb
|