ahoy_email 1.0.2 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []