analytics_instrumentation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +225 -0
  4. data/Rakefile +40 -0
  5. data/lib/analytics_instrumentation/analytics_attribution.rb +74 -0
  6. data/lib/analytics_instrumentation/analytics_mapping.rb +42 -0
  7. data/lib/analytics_instrumentation/config.rb +47 -0
  8. data/lib/analytics_instrumentation/version.rb +3 -0
  9. data/lib/analytics_instrumentation.rb +189 -0
  10. data/lib/config/routes.rb +7 -0
  11. data/lib/controllers/analytics_implementation_controller.rb +11 -0
  12. data/lib/controllers/application_controller.rb +3 -0
  13. data/lib/tasks/analytics_instrumentation_tasks.rake +4 -0
  14. data/test/analytics_instrumentation_test.rb +7 -0
  15. data/test/dummy/README.rdoc +28 -0
  16. data/test/dummy/Rakefile +6 -0
  17. data/test/dummy/app/assets/javascripts/application.js +13 -0
  18. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  19. data/test/dummy/app/controllers/application_controller.rb +5 -0
  20. data/test/dummy/app/helpers/application_helper.rb +2 -0
  21. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  22. data/test/dummy/bin/bundle +3 -0
  23. data/test/dummy/bin/rails +4 -0
  24. data/test/dummy/bin/rake +4 -0
  25. data/test/dummy/bin/setup +29 -0
  26. data/test/dummy/config/application.rb +26 -0
  27. data/test/dummy/config/boot.rb +5 -0
  28. data/test/dummy/config/database.yml +25 -0
  29. data/test/dummy/config/environment.rb +5 -0
  30. data/test/dummy/config/environments/development.rb +41 -0
  31. data/test/dummy/config/environments/production.rb +79 -0
  32. data/test/dummy/config/environments/test.rb +42 -0
  33. data/test/dummy/config/initializers/assets.rb +11 -0
  34. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  35. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  36. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  37. data/test/dummy/config/initializers/inflections.rb +16 -0
  38. data/test/dummy/config/initializers/mime_types.rb +4 -0
  39. data/test/dummy/config/initializers/session_store.rb +3 -0
  40. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  41. data/test/dummy/config/locales/en.yml +23 -0
  42. data/test/dummy/config/routes.rb +56 -0
  43. data/test/dummy/config/secrets.yml +22 -0
  44. data/test/dummy/config.ru +4 -0
  45. data/test/dummy/public/404.html +67 -0
  46. data/test/dummy/public/422.html +67 -0
  47. data/test/dummy/public/500.html +66 -0
  48. data/test/dummy/public/favicon.ico +0 -0
  49. data/test/test_helper.rb +19 -0
  50. 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,3 @@
1
+ module AnalyticsInstrumentation
2
+ VERSION = "0.1.0"
3
+ 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,7 @@
1
+ # TODO: Not namespace this
2
+
3
+ CanopyBackend::Application.routes.draw do
4
+
5
+ post "api/analytics_event" => "analytics_implementation#analytics_event", as: :analytics_event
6
+
7
+ 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
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ include AnalyticsInstrumentation
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :analytics_instrumentation do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class AnalyticsInstrumentationTest < ActiveSupport::TestCase
4
+ test "truth" do
5
+ assert_kind_of Module, AnalyticsInstrumentation
6
+ end
7
+ end