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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c6f895cc438acdeb4982e684a4ba32efc86c05b
4
- data.tar.gz: 19750f27ae10e0828b4f3e7813c3a236413fa1b1
3
+ metadata.gz: 7d245ad6df5fbb64f6b38292b8b139a1382dda4e
4
+ data.tar.gz: cd822e8f05cf9eb01a1c37979b67428c2c7fd999
5
5
  SHA512:
6
- metadata.gz: 88ba353c711f596a745a8c406a222859c517c1392571791d91c833713d46a56a15f313428eae6af39e042b7c8ffc8aa1b75db969d1f950def5b824bb6b95fe19
7
- data.tar.gz: 082bba0e14fab78aba984e1d3e1b94ce08841c092ff24e834804bf15478c8cc02c2050df8ab1c15701530158d30954222261f86c7ac09abcbc24bca6ee4a62be
6
+ metadata.gz: 7c598c2697d2ca2167e2ccb6e5fdedb20a27f831f71b0f7ed0a4907d9b4646e24f6cd02cdbb7be54328dfec81cf9b353d57a3c80bd08cbf21a510b58e3da4463
7
+ data.tar.gz: a0649a7c6a0c575dc87a8aac7d36e1a53aab6d475c198a04131597001797442d1f3cdcedd442ec3f5f2ba4f64b3a2e4d9ab8d26af1d43d9984d7b060ea181d10
@@ -1,3 +1,3 @@
1
- ## 0.1.0 [unreleased]
1
+ ## 0.1.0
2
2
 
3
3
  - First major release
data/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # Ahoy Email
2
2
 
3
- :construction: Coming soon - May 1, 2014
3
+ :postbox: Simple, powerful email tracking for Rails
4
4
 
5
- :envelope: Simple, powerful email tracking for Rails
5
+ You get:
6
6
 
7
- Keep track of emails:
7
+ - A history of emails sent to each user
8
+ - Open and click tracking
9
+ - Easy UTM tagging
8
10
 
9
- - sent
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` record when an email is sent.
30
+ Ahoy creates an `Ahoy::Message` every time an email is sent by default.
31
31
 
32
- ### Open
32
+ ### Users
33
33
 
34
- An invisible pixel is added right before the closing `</body>` tag to HTML emails.
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
- If a recipient has images enabled in his / her email client, the pixel is loaded and an open is recorded.
36
+ By default, Ahoy tries `User.where(email: message.to.first).first` to find the user.
37
37
 
38
- ### Click
38
+ You can pass a specific user with:
39
39
 
40
- Links in HTML emails are rewritten to pass through your server.
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://yoursite.com/ahoy/messages/rAnDoMtOken/click?url=http%3A%2F%2Fchartkick.com&signature=...
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
- Keep specific links from being tracked with `<a data-disable-tracking="true" href="..."></a>`.
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 each link if they don’t already exist.
98
+ UTM parameters are added to links if they don’t already exist.
59
99
 
60
- By default, `utm_medium` is set to `email`.
100
+ The defaults are:
61
101
 
62
- ### User
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
- To specify the user, use:
106
+ Use `track utm_params: false` to skip tagging, or skip specific links with:
65
107
 
66
- ```ruby
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
- ahoy user: user
123
+ track user: user
71
124
  mail to: user.email
72
125
  end
73
126
  end
74
127
  ```
75
128
 
76
- User is [polymorphic](http://railscasts.com/episodes/154-polymorphic-association), so use it with any model.
77
-
78
- ## Customize
129
+ ### Mailer
79
130
 
80
- There are 3 places to set options.
131
+ ```ruby
132
+ class UserMailer < ActionMailer::Base
133
+ track utm_campaign: "boom"
134
+ end
135
+ ```
81
136
 
82
137
  ### Global
83
138
 
84
- The defaults are listed below.
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
- AhoyEmail.options = {
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
- ### Mailers
151
+ Disable tracking for an email
100
152
 
101
153
  ```ruby
102
- class UserMailer < ActionMailer::Base
103
- ahoy utm_campaign: "boom"
104
- end
154
+ track message: false
105
155
  ```
106
156
 
107
- ### Action
157
+ Or by default
108
158
 
109
- ``` ruby
110
- class UserMailer < ActionMailer::Base
111
- def welcome_email(user)
112
- # ...
113
- ahoy user: user
114
- mail to: user.email
115
- end
116
- end
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
@@ -27,7 +27,7 @@ module Ahoy
27
27
  protected
28
28
 
29
29
  def set_message
30
- @message = Ahoy::Message.where(token: params[:id]).first
30
+ @message = AhoyEmail.message_model.where(token: params[:id]).first
31
31
  end
32
32
 
33
33
  end
@@ -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
- create_message: true,
17
- track_open: true,
18
- track_click: true,
19
- utm_source: nil,
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: nil
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
- def mail_with_ahoy(headers = {}, &block)
43
- message = mail_without_ahoy(headers, &block)
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
@@ -3,7 +3,7 @@ module AhoyEmail
3
3
  class << self
4
4
 
5
5
  def delivering_email(message)
6
- AhoyEmail::Processor.new(message).mark_sent!
6
+ AhoyEmail::Processor.new(message).track_send
7
7
  end
8
8
 
9
9
  end
@@ -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
@@ -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[:create_message]
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
- track_utm_parameters!
19
- track_open! if options[:track_open]
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 mark_sent!
31
+ def track_send
33
32
  if (message_id = message["Ahoy-Message-Id"])
34
- ahoy_message = Ahoy::Message.where(id: message_id).first
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 track_utm_parameters!
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 track_click!
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
- key = "data-disable-tracking"
102
- if link[key]
103
- # remove attribute
104
- link.remove_attribute(key)
105
- else
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
@@ -1,3 +1,3 @@
1
1
  module AhoyEmail
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -8,6 +8,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration
8
8
  t.string :user_type
9
9
 
10
10
  # optional
11
+ # feel free to remove
11
12
  t.text :subject
12
13
  t.text :content
13
14
 
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.2
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-29 00:00:00.000000000 Z
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