ahoy_email 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|