analytics_instrumentation 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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +225 -0
- data/Rakefile +40 -0
- data/lib/analytics_instrumentation/analytics_attribution.rb +74 -0
- data/lib/analytics_instrumentation/analytics_mapping.rb +42 -0
- data/lib/analytics_instrumentation/config.rb +47 -0
- data/lib/analytics_instrumentation/version.rb +3 -0
- data/lib/analytics_instrumentation.rb +189 -0
- data/lib/config/routes.rb +7 -0
- data/lib/controllers/analytics_implementation_controller.rb +11 -0
- data/lib/controllers/application_controller.rb +3 -0
- data/lib/tasks/analytics_instrumentation_tasks.rake +4 -0
- data/test/analytics_instrumentation_test.rb +7 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config/application.rb +26 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/test_helper.rb +19 -0
- metadata +189 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 23edc0fcddb60777d22506a73d5644a823beed1d
|
4
|
+
data.tar.gz: 7229fb73b5606f178e55dbaba1df3be2c2b09e34
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d092014e81ee505b97ca8b5d6f47d8671c492513e0f1367276073b672c21b1da74cbb8e07efe7d561b59946d73f12b8d0a0f9985f254ff8575a588143128402e
|
7
|
+
data.tar.gz: ab0dd4da0f9d16afb9944b75109a424696643736e84a74a4e55d424d5102bc431e139809b31bb215110fa53e8772fbc29a59df07293e28d8aa69f628bedb642c
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 Jordan Feldstein
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
= AnalyticsInstrumentation
|
2
|
+
|
3
|
+
== Install
|
4
|
+
|
5
|
+
gem 'analytics_instrumentation'
|
6
|
+
|
7
|
+
or
|
8
|
+
|
9
|
+
gem install 'analytics_instrumentation'
|
10
|
+
|
11
|
+
|
12
|
+
== Usage
|
13
|
+
|
14
|
+
Once installed, write your initializer and yml mappings.
|
15
|
+
|
16
|
+
== Initializer & Defaults
|
17
|
+
|
18
|
+
Most behaviors in AnalyticsInstrumentation are one-size-fits-all.
|
19
|
+
|
20
|
+
A few behaviors can be hooked into via an initializer (eg. `./config/initializers/analytics_instrumentation.rb`) to be customized to your app.
|
21
|
+
|
22
|
+
The behaviors exposed are:
|
23
|
+
|
24
|
+
# ./config/initializers/analytics_instrumentation.rb
|
25
|
+
|
26
|
+
AnalyticsImplementation.configure do |config|
|
27
|
+
# Your Segment.io write key is required. We will instantiate the client.
|
28
|
+
config.segment_write_key = ENV["SEGMENT_API_KEY"] || ""
|
29
|
+
|
30
|
+
# Define a hash of traits to be passed to `#identify()` calls for each user.
|
31
|
+
config.custom_user_traits = (user) -> {
|
32
|
+
{
|
33
|
+
name: user.full_name,
|
34
|
+
email: user.email,
|
35
|
+
created_at: user.created_at,
|
36
|
+
provider: user.provider,
|
37
|
+
provider_username: user.provider_username,
|
38
|
+
postings_count: user.postings_count,
|
39
|
+
public_collections_count: user.collections_count,
|
40
|
+
private_collections_count: user.private_collections_count,
|
41
|
+
followers_count: user.followers_count,
|
42
|
+
following_count: user.followed_users_count
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
# # AnalyticsInstrumentation sends a variety of properties with each of your events:
|
47
|
+
# 1. Event-specific properties are configured via the YML files (see below)
|
48
|
+
# 2. System-wide properties are included for you with all events (see below)
|
49
|
+
# 3. To have your own system-wide properties included on all events, have them returned by a Proc set to `config.extra_event_properties`, like so:
|
50
|
+
config.extra_event_properties = -> {
|
51
|
+
{
|
52
|
+
user_sees_experiment_a: current_user.experiment_a?
|
53
|
+
user_sees_experiment_b: current_user.experiment_b?
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
# To capture and handle errors that happen within AnalyticsInstrumentation,
|
58
|
+
# Define an error_handler, like so:
|
59
|
+
config.error_handler = (e, msg) -> {
|
60
|
+
if Rails.env.production?
|
61
|
+
Rollbar.error(e, msg)
|
62
|
+
else
|
63
|
+
raise
|
64
|
+
end
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
== Configuring Events & Their Properties
|
69
|
+
|
70
|
+
AnalyticsInstrumentation maps controller#action pairs to events using YML files in `./config/analytics/*.yml`.
|
71
|
+
|
72
|
+
These files also define how properties for each event should be constructed, based on the corresponding request/response's view_assigns. (Calling @var_name in a view)
|
73
|
+
|
74
|
+
Examples:
|
75
|
+
|
76
|
+
1. Requests -> Event names
|
77
|
+
|
78
|
+
# ./config/analytics/home.yml
|
79
|
+
|
80
|
+
home#landing:
|
81
|
+
name: Viewed Home page
|
82
|
+
home#press:
|
83
|
+
name: Viewed Press page
|
84
|
+
home#app:
|
85
|
+
name: Viewed App page
|
86
|
+
|
87
|
+
2. Requests -> Events with simple properties
|
88
|
+
|
89
|
+
# ./config/analytics/issues.yml
|
90
|
+
|
91
|
+
issues#index:
|
92
|
+
name: Viewed Issue List
|
93
|
+
|
94
|
+
issues#show:
|
95
|
+
name: Viewed Issue
|
96
|
+
page_identifier: "@issue.id"
|
97
|
+
parameters:
|
98
|
+
issue_number: "params[:issue_number]"
|
99
|
+
title: "@issue.title"
|
100
|
+
|
101
|
+
3. Requests -> Events with dynamic properties
|
102
|
+
|
103
|
+
# ./config/analytics/products.yml
|
104
|
+
|
105
|
+
api#post_product:
|
106
|
+
name: 'if @likedProduct then "Liked Product" else "Unliked Product" end'
|
107
|
+
|
108
|
+
products#show:
|
109
|
+
name: Viewed Product
|
110
|
+
page_identifier: "@product.id"
|
111
|
+
parameters:
|
112
|
+
product_id: "@product.id"
|
113
|
+
name: '@product.name.gsub("\"", "")'
|
114
|
+
brand: "@brand.try(:name)"
|
115
|
+
category: "@category.try(:name)"
|
116
|
+
|
117
|
+
products#create_comment:
|
118
|
+
name: Created Comment
|
119
|
+
parameters:
|
120
|
+
product_id: "@product.id"
|
121
|
+
product_name: "@product.name"
|
122
|
+
comment_text: "@comment.message"
|
123
|
+
previous_comment_count: "@product.comments_count"
|
124
|
+
|
125
|
+
|
126
|
+
== Simple "Page View" Events
|
127
|
+
|
128
|
+
AnalyticsInstrumentation will automatically trigger a "Page View" event for every request, passing `request.path` as a `page` property, like so:
|
129
|
+
|
130
|
+
properties = {
|
131
|
+
page: request.path
|
132
|
+
}
|
133
|
+
|
134
|
+
This is in addition to the rest of the system-wide properties attached to all events.
|
135
|
+
|
136
|
+
|
137
|
+
== System Wide Properties Attached to All Events
|
138
|
+
|
139
|
+
{
|
140
|
+
"Raw Analytics ID" => raw_analytics_id,
|
141
|
+
"Ajax" => !request.xhr?.nil?,
|
142
|
+
|
143
|
+
"logged_in" => !!current_user,
|
144
|
+
"source" => params[:source], if params[:source]
|
145
|
+
|
146
|
+
"Originating Page Identifier" => session["previous-page-identifier"],
|
147
|
+
"Originating Page Type" => session["previous-page-type"]
|
148
|
+
}
|
149
|
+
|
150
|
+
All new visitors are cookied with a `Raw Analytics ID`, useful as the Actor property when looking at the behavior of logged-out traffic. `Raw Analytics Id` is aliased to user_id when a user logs in, if any of your analytics tools support `#alias()`.
|
151
|
+
|
152
|
+
`Originating Page Type/ID` are set automatically based on the previous internal request. `Type` is the Name of the most recently triggered custom event. `Identifier` is the corresponding `page_identifier:`. Search the YML examples for this in action. Useful to see "How did people get here?".
|
153
|
+
|
154
|
+
Send `params[:source]` with any GET or POST request to differentiate where in the UI a given event was triggered. eg:
|
155
|
+
|
156
|
+
POST /user/create
|
157
|
+
{
|
158
|
+
user: {...},
|
159
|
+
source: 'Navbar'
|
160
|
+
}
|
161
|
+
|
162
|
+
vs
|
163
|
+
|
164
|
+
POST /user/create
|
165
|
+
{
|
166
|
+
user: {...},
|
167
|
+
source: 'Primary CTA'
|
168
|
+
}
|
169
|
+
|
170
|
+
|
171
|
+
== System Wide Properties Attached Only to Logged In Requests
|
172
|
+
|
173
|
+
Any user traits you return from your initializer's `custom_user_traits` config will be merged with the following default user traits:
|
174
|
+
|
175
|
+
{
|
176
|
+
"User Created At" => current_user.created_at,
|
177
|
+
"Username" => current_user.try(:username),
|
178
|
+
"Full name" => current_user.try(:full_name),
|
179
|
+
"User ID" => current_user.id,
|
180
|
+
"Login Provider" => current_user.try(:provider) || "Email",
|
181
|
+
}
|
182
|
+
|
183
|
+
== Marketing Attribution
|
184
|
+
|
185
|
+
AnalyticsInstrumentation makes it easy to see which of your UTM campaigns and external referrers are contributing to product engagement and revenue events deeper in the funnel.
|
186
|
+
|
187
|
+
This is done by tracking the first-ever and most-recent set of UTM parameters, as well as the first-ever and most-recent external HTTP referrer for each unique visitor to your site.
|
188
|
+
|
189
|
+
These data are passed into all named events (the ones in your YML files) as:
|
190
|
+
|
191
|
+
{
|
192
|
+
first_external_referrer: 'google.com',
|
193
|
+
latest_external_referrer: 'twitter.com',
|
194
|
+
first_utm: {
|
195
|
+
name: 'Spring Campaign 1',
|
196
|
+
source: 'twitter',
|
197
|
+
medium: 'card',
|
198
|
+
term: 'flowers',
|
199
|
+
content: 'variant1'
|
200
|
+
},
|
201
|
+
latest_utm: {
|
202
|
+
name: 'Spring Campaign 1',
|
203
|
+
source: 'twitter',
|
204
|
+
medium: 'card',
|
205
|
+
term: 'flowers',
|
206
|
+
content: 'variant1'
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
This is useful for doing things like "Count the number of new users generated by each ad campaign" or "How many posts were created by users from that buzzfeed article?".
|
211
|
+
|
212
|
+
|
213
|
+
== License
|
214
|
+
|
215
|
+
MIT
|
216
|
+
|
217
|
+
== Thoughts, Feedback, Ideas?
|
218
|
+
|
219
|
+
Please tweet to [@jfeldstein](twitter.com/jfeldstein) or [@msfeldstein](twitter.com/msfeldstein) or open a Github Issue if you have feedback.
|
220
|
+
|
221
|
+
Thanks!
|
222
|
+
|
223
|
+
== Used This Code?
|
224
|
+
|
225
|
+
Please let us know if you've used this code in any products or projects. We'd love to hear about your experience! Tweet to [@jfeldstein](twitter.com/jfeldstein) or [@msfeldstein](twitter.com/msfeldstein)
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
$:.push File.expand_path("../lib", __FILE__)
|
5
|
+
require "analytics_instrumentation"
|
6
|
+
|
7
|
+
# Rake::TestTask.new(:test) do |test|
|
8
|
+
# test.libs << 'lib' << 'test'
|
9
|
+
# test.pattern = 'test/{functional,unit}/**/*_test.rb'
|
10
|
+
# end
|
11
|
+
|
12
|
+
# namespace :test do
|
13
|
+
# Rake::TestTask.new(:lint) do |test|
|
14
|
+
# test.libs << 'lib' << 'test'
|
15
|
+
# test.pattern = 'test/test_active_model_lint.rb'
|
16
|
+
# end
|
17
|
+
|
18
|
+
# task :all => ['test', 'test:lint']
|
19
|
+
# end
|
20
|
+
|
21
|
+
# task :default => 'test:all'
|
22
|
+
|
23
|
+
desc 'Builds the gem'
|
24
|
+
task :build do
|
25
|
+
sh "gem build analytics_instrumentation.gemspec"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'Builds and installs the gem'
|
29
|
+
task :install => :build do
|
30
|
+
sh "gem install analytics_instrumentation-#{AnalyticsInstrumentation::VERSION}"
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'Tags version, pushes to remote, and pushes gem'
|
34
|
+
task :release => :build do
|
35
|
+
sh "git tag v#{AnalyticsInstrumentation::VERSION}"
|
36
|
+
sh "git push origin master"
|
37
|
+
sh "git push origin v#{AnalyticsInstrumentation::VERSION}"
|
38
|
+
sh "gem push analytics_instrumentation-#{AnalyticsInstrumentation::VERSION}.gem"
|
39
|
+
sh "rm analytics_instrumentation-#{AnalyticsInstrumentation::VERSION}.gem"
|
40
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module AnalyticsAttribution
|
2
|
+
def add_attribution(props)
|
3
|
+
# Gather
|
4
|
+
attribution_data = {
|
5
|
+
first_external_referrer: get_first_referrer,
|
6
|
+
latest_external_referrer: get_latest_referrer,
|
7
|
+
latest_utm: get_latest_utm
|
8
|
+
}
|
9
|
+
|
10
|
+
# Persist
|
11
|
+
attribution_data.each do |k,v|
|
12
|
+
set_cookie k, v
|
13
|
+
end
|
14
|
+
|
15
|
+
# Merge
|
16
|
+
props.merge! attribution_data
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def get_first_referrer
|
21
|
+
get_cookie("first_external_referrer") || get_latest_referrer
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_latest_referrer
|
25
|
+
ref = request.referrer
|
26
|
+
bad_host = attribution_host(ref).nil?
|
27
|
+
our_host = attribution_host(ref) == attribution_host(request.original_url)
|
28
|
+
if ref.blank? || our_host || bad_host
|
29
|
+
get_cookie "latest_external_referrer"
|
30
|
+
else
|
31
|
+
attribution_host(ref)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_latest_utm
|
36
|
+
name = params[:utm_campaign]
|
37
|
+
source = params[:utm_source]
|
38
|
+
medium = params[:utm_medium]
|
39
|
+
term = params[:utm_term]
|
40
|
+
content = params[:utm_content]
|
41
|
+
|
42
|
+
if name.blank?
|
43
|
+
get_cookie 'latest_utm'
|
44
|
+
else
|
45
|
+
{
|
46
|
+
name: name,
|
47
|
+
source: source,
|
48
|
+
medium: medium,
|
49
|
+
term: term,
|
50
|
+
content: content
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def attribution_host(url)
|
56
|
+
return nil if url.nil?
|
57
|
+
uri = URI.parse(url)
|
58
|
+
uri.host && uri.host.sub(/\Awww\./,'').downcase
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_cookie(key, value)
|
62
|
+
stored_value = value.is_a?(Hash) ? value.to_json : value
|
63
|
+
cookies[key] = stored_value
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_cookie(key)
|
67
|
+
output = cookies[key]
|
68
|
+
if output && output[0] == "{"
|
69
|
+
JSON.parse(output) rescue nil
|
70
|
+
else
|
71
|
+
output
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class AnalyticsMapping
|
2
|
+
def self.createMappings
|
3
|
+
# TODO: Reload this on Rails Auto-reload
|
4
|
+
mappings = {}
|
5
|
+
@mappingFiles = Dir.glob("config/analytics/*.yml")
|
6
|
+
for file in @mappingFiles
|
7
|
+
yaml = YAML.load_file(file)
|
8
|
+
mappings.merge! yaml
|
9
|
+
end
|
10
|
+
mappings
|
11
|
+
end
|
12
|
+
|
13
|
+
@@mappings = AnalyticsMapping.createMappings
|
14
|
+
|
15
|
+
def self.to_event(params, view_assigns)
|
16
|
+
methodName = "#{params[:controller]}##{params[:action]}"
|
17
|
+
analysis = @@mappings[methodName]
|
18
|
+
|
19
|
+
return nil if analysis.nil?
|
20
|
+
|
21
|
+
replaceAllTokens(analysis, params, view_assigns)
|
22
|
+
|
23
|
+
analysis
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.replaceAllTokens(obj, params, view_assigns)
|
27
|
+
if obj.is_a? String
|
28
|
+
replaceTokens(obj, params, view_assigns)
|
29
|
+
elsif obj.is_a? Hash
|
30
|
+
obj.each {|k, v| replaceAllTokens(v, params, view_assigns)}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.replaceTokens(str, params, view_assigns)
|
35
|
+
return if str["@"].nil? && str["params["].nil?
|
36
|
+
properties = {}
|
37
|
+
view_assigns.each {|k, v| properties.instance_variable_set "@#{k}", v}
|
38
|
+
properties["params"] = params
|
39
|
+
result = ERB.new("<%= #{str} %>").result(properties.instance_eval {binding})
|
40
|
+
str[0..-1] = result
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module AnalyticsInstrumentation
|
2
|
+
class Config
|
3
|
+
include ActiveModel::Validations
|
4
|
+
|
5
|
+
class Invalid < StandardError; end
|
6
|
+
|
7
|
+
attr_accessor :extra_event_properties
|
8
|
+
attr_accessor :custom_user_traits
|
9
|
+
attr_accessor :error_handler
|
10
|
+
attr_accessor :segment_write_key
|
11
|
+
|
12
|
+
validates_presence_of :segment_write_key
|
13
|
+
|
14
|
+
@@REQUIRED_CALLABLES = [
|
15
|
+
:extra_event_properties,
|
16
|
+
:custom_user_traits,
|
17
|
+
:error_handler
|
18
|
+
]
|
19
|
+
validate do
|
20
|
+
@@REQUIRED_CALLABLES.each |callable| do
|
21
|
+
unless self.send(callable).respond_to?(:call)
|
22
|
+
errors.add(callable, "must be a callable object (eg. Proc)")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
self.extra_event_properties = -> {}
|
29
|
+
self.custom_user_traits = -> {}
|
30
|
+
self.error_handler = (msg) -> { raise }
|
31
|
+
end
|
32
|
+
|
33
|
+
def custom_user_traits(user)
|
34
|
+
self.custom_user_traits(user) || {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def extra_event_properties
|
38
|
+
self.extra_event_properties() || {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def intercom?
|
42
|
+
Intercom rescue false
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'analytics_instrumentation/analytics_attribution'
|
2
|
+
require 'analytics_instrumentation/analytics_mapping'
|
3
|
+
|
4
|
+
module AnalyticsInstrumentation
|
5
|
+
include AnalyticsAttribution
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def included(base)
|
9
|
+
@segment = Segment::Analytics.new({
|
10
|
+
write_key: @config.segment_write_key,
|
11
|
+
on_error: @config.error_handler
|
12
|
+
})
|
13
|
+
|
14
|
+
base.class_eval do
|
15
|
+
base.send(:after_filter, :analyticsLogPageView)
|
16
|
+
base.send(:after_filter, :analyticsCheckSessionStart)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure(&proc)
|
21
|
+
@config ||= Config.new
|
22
|
+
yield @config
|
23
|
+
|
24
|
+
unless @config.valid?
|
25
|
+
errors = @config.errors.full_messages.join(', ')
|
26
|
+
raise Config::Invalid.new(errors)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def analyticsCheckSessionStart
|
32
|
+
begin
|
33
|
+
return if skip_analytics?
|
34
|
+
if current_user
|
35
|
+
if !session[:last_seen] || session[:last_seen] < 30.minutes.ago
|
36
|
+
analyticsTrackEvent("Session Start")
|
37
|
+
if @config.intercom? && Rails.env.production?
|
38
|
+
Intercom.post("https://api.intercom.io/users", {user_id:current_user.id, new_session:true})
|
39
|
+
end
|
40
|
+
end
|
41
|
+
session[:last_seen] = Time.now
|
42
|
+
else
|
43
|
+
if session[:last_seen_logged_out].nil? || session[:last_seen_logged_out] < 30.minutes.ago
|
44
|
+
analyticsTrackEvent("Session Start")
|
45
|
+
end
|
46
|
+
session[:last_seen_logged_out] = Time.now
|
47
|
+
end
|
48
|
+
rescue => e
|
49
|
+
@config.error_handler(e, "Analytics Check Session Crash: #{request.filtered_path}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def analyticsLogPageView
|
54
|
+
begin
|
55
|
+
return if skip_analytics?
|
56
|
+
return if self.status >= 400
|
57
|
+
|
58
|
+
page_view_event = AnalyticsMapping.to_event(params, self.view_assigns)
|
59
|
+
|
60
|
+
if page_view_event
|
61
|
+
if current_user
|
62
|
+
analyticsSetPerson(current_user)
|
63
|
+
end
|
64
|
+
add_attribution page_view_event[:parameters]
|
65
|
+
analyticsTrackEvent page_view_event[:name], page_view_event[:parameters]
|
66
|
+
analyticsStoreOriginatingPage page_view_event
|
67
|
+
end
|
68
|
+
|
69
|
+
properties = {
|
70
|
+
page: request.path
|
71
|
+
}
|
72
|
+
properties.merge! analyticsSuperProperties
|
73
|
+
analyticsTrackEvent "Page View", properties
|
74
|
+
rescue => e
|
75
|
+
@config.error_handler(e, "Analytics Crash: #{request.filtered_path}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def analyticsStoreOriginatingPage(page_view_event)
|
80
|
+
if !request.xhr?
|
81
|
+
session["previous-page-type"] = page_view_event[:name]
|
82
|
+
session["previous-page-identifier"] = page_view_event[:page_identifier]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def analyticsApplyOriginatingPage(properties)
|
87
|
+
properties["Originating Page Identifier"] = session["previous-page-identifier"]
|
88
|
+
properties["Originating Page Type"] = session["previous-page-type"]
|
89
|
+
end
|
90
|
+
|
91
|
+
def analyticsAliasUser(user_id)
|
92
|
+
return if skip_analytics?
|
93
|
+
|
94
|
+
aliasProperties = {
|
95
|
+
previous_id: session[:analytics_id],
|
96
|
+
user_id: user_id
|
97
|
+
}
|
98
|
+
|
99
|
+
logger.debug "Analytics.alias #{aliasProperties}"
|
100
|
+
@segment.alias(aliasProperties)
|
101
|
+
@segment.flush
|
102
|
+
end
|
103
|
+
|
104
|
+
def analyticsSetPerson(user)
|
105
|
+
return if skip_analytics?
|
106
|
+
|
107
|
+
properties = {
|
108
|
+
user_id: user.id,
|
109
|
+
traits: @config.custom_user_traits(user)
|
110
|
+
}
|
111
|
+
|
112
|
+
logger.debug "Analytics.identify #{JSON.pretty_generate(properties)}"
|
113
|
+
@segment.identify(properties)
|
114
|
+
end
|
115
|
+
|
116
|
+
def analyticsSuperProperties
|
117
|
+
superProperties = {
|
118
|
+
"Raw Analytics ID" => raw_analytics_id,
|
119
|
+
"Ajax" => !request.xhr?.nil?
|
120
|
+
}
|
121
|
+
if current_user
|
122
|
+
superProperties.merge!({
|
123
|
+
"User Created At" => current_user.created_at,
|
124
|
+
"Username" => current_user.try(:username),
|
125
|
+
"Full name" => current_user.try(:full_name),
|
126
|
+
"User ID" => current_user.id,
|
127
|
+
"Login Provider" => current_user.try(:provider) || "Email"
|
128
|
+
})
|
129
|
+
end
|
130
|
+
superProperties
|
131
|
+
end
|
132
|
+
|
133
|
+
def analyticsTrackEvent(name, properties={})
|
134
|
+
return if skip_analytics?
|
135
|
+
|
136
|
+
properties ||= {}
|
137
|
+
|
138
|
+
properties["logged_in"] = !!current_user
|
139
|
+
properties["source"] = params[:source] if params[:source]
|
140
|
+
|
141
|
+
properties.merge! analyticsSuperProperties
|
142
|
+
properties.merge! @config.extra_event_properties
|
143
|
+
|
144
|
+
analyticsApplyOriginatingPage properties
|
145
|
+
|
146
|
+
analyticsProperties = {
|
147
|
+
user_id: analyticsID,
|
148
|
+
event: name,
|
149
|
+
properties: properties,
|
150
|
+
context: {
|
151
|
+
userAgent: request.env['HTTP_USER_AGENT'],
|
152
|
+
ip: request.remote_ip,
|
153
|
+
'Google Analytics' => {
|
154
|
+
clientId: googleAnalyticsID
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
logger.debug "Analytics.track #{JSON.pretty_generate(analyticsProperties)}"
|
160
|
+
@segment.track(analyticsProperties)
|
161
|
+
end
|
162
|
+
|
163
|
+
def raw_analytics_id
|
164
|
+
session[:analytics_id] ||= (rand * 1000000000000000).to_i
|
165
|
+
session[:analytics_id]
|
166
|
+
end
|
167
|
+
|
168
|
+
def analyticsID
|
169
|
+
if current_user then return current_user.id end
|
170
|
+
raw_analytics_id
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
def googleAnalyticsID
|
175
|
+
ck = cookies[:_ga]
|
176
|
+
return "1.1" if ck.nil?
|
177
|
+
parts = ck.split(".")
|
178
|
+
"#{parts[2]}.#{parts[3]}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def skip_analytics?
|
182
|
+
return true if Rails.env.test?
|
183
|
+
return true if request.bot?
|
184
|
+
return true if request.user_agent.nil?
|
185
|
+
bad_strings = ["http:", "https:", "twitterbot", "bingbot", "googlebot", "gediapartners-google"]
|
186
|
+
return true if bad_strings.any? { |s| !request.user_agent.downcase.index(s).nil? }
|
187
|
+
false
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class AnalyticsImplementationController < ApplicationController
|
2
|
+
|
3
|
+
# Exposes an endpoint to which one can ajax handwritten events.
|
4
|
+
def analytics_event
|
5
|
+
name = params[:name]
|
6
|
+
properties = params[:properties]
|
7
|
+
analyticsTrackEvent(name, properties)
|
8
|
+
render text: ""
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|