staccato 0.1.1 → 0.2.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9a674a0fa88901074b28c03b98647b7751322412
4
+ data.tar.gz: d6db8298721e606f0855c54959eb42537e0d496c
5
+ SHA512:
6
+ metadata.gz: 323fdd8c54466b98790b5f446c1d20464bb3ce8271a2785881d49d4edf3257602367e05cdfdc38db84d21d5fc5164684c1b25f87d422ee35933c5da7a840b0d5
7
+ data.tar.gz: 5b290b2d0602444e10562619feef4763161e546aea5dedad33ad0c3eb680633a22566cc37da718674a2587515fd4921e6b86c044a02c61f4a5fb9d273feaef3b
@@ -4,4 +4,5 @@ rvm:
4
4
  - 1.9.3
5
5
  - 2.0.0
6
6
  - 2.1.0
7
+ - 2.2.0
7
8
  script: bundle exec rake
@@ -1,3 +1,11 @@
1
+ ## Staccato 0.2.0 ##
2
+
3
+ * Enhanced Ecommerce Measurements
4
+ * Measurable module for further extension of hits
5
+ * New global hit options for ecommerce
6
+
7
+ *Tony Pitale <@tpitale>*
8
+
1
9
  ## Staccato 0.1.1 ##
2
10
 
3
11
  * fixes NoopTracker when track! is called on a hit *martin1keogh*
data/README.md CHANGED
@@ -133,7 +133,10 @@ Staccato::Hit::GLOBAL_OPTIONS.keys # =>
133
133
  :application_id,
134
134
  :application_installer_id,
135
135
  :experiment_id,
136
- :experiment_variant]
136
+ :experiment_variant,
137
+ :product_action,
138
+ :product_action_list,
139
+ :promotion_action]
137
140
  ```
138
141
 
139
142
  Boolean options like `anonymize_ip` will be converted from `true`/`false` into `1`/`0` as per the tracking API docs.
@@ -200,13 +203,218 @@ tracker.pageview(path: '/videos/123')
200
203
  tracker.pageview(path: '/videos/987')
201
204
  ```
202
205
 
203
- ## Google Documentation
206
+ ## Additional Measurements ##
207
+
208
+ Additional Measurements can be added to any Hit type, but most commonly used with pageviews or events. The current set of measurements is for handling [Enhanced Ecommerce](https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide#enhancedecom) measurements. I've grouped these into ImpressionList, Product, ProductImpression, Promotion, Transaction, Checkout, and CheckoutOption (w/ ImpressionList). Each can be added and combined – per Google's documentation – onto an existing Hit.
209
+
210
+ **Note:** Certain Measurements require an `index`. This is an integer (usually) between 1 and 200 inclusive.
211
+
212
+ **Note:** Certain Measurements require a `product_action` to be set. This is a global option, and should be set on the original hit. The `product_action` can be any one of:
213
+
214
+ * `detail`
215
+ * `click`
216
+ * `add`
217
+ * `remove`
218
+ * `checkout`
219
+ * `checkout_option`
220
+ * `purchase`
221
+ * `refund`
222
+
223
+ ### Transaction w/ Product ###
224
+
225
+ Using a pageview to track a transaction with a product (using the 'purchase' as the `product_action`:
226
+
227
+ ```ruby
228
+ pageview = tracker.build_pageview(path: '/receipt', hostname: 'mystore.com', title: 'Your Receipt', product_action: 'purchase')
229
+
230
+ pageview.add_measurement(:transaction, {
231
+ transaction_id: 'T12345',
232
+ affiliation: 'Your Store',
233
+ revenue: 37.39,
234
+ tax: 2.85,
235
+ shipping: 5.34,
236
+ currency: 'USD',
237
+ coupon_code: 'SUMMERSALE'
238
+ })
239
+
240
+ pageview.add_measurement(:product, {
241
+ index: 1, # this is our first product, value may be 1-200
242
+ id: 'P12345',
243
+ name: 'T-Shirt',
244
+ category: 'Apparel',
245
+ brand: 'Your Brand',
246
+ variant: 'Purple',
247
+ quantity: 2,
248
+ position: 1,
249
+ price: 14.60,
250
+ coupon_code: 'ILUVTEES'
251
+ })
252
+
253
+ pageview.track!
254
+ ```
255
+
256
+ ### Transaction Refund ###
257
+
258
+ The combination of `product_action: 'refund'` and `transaction` measurement setting a matching `id` to a previous transaction.
259
+
260
+ ```ruby
261
+ event = tracker.build_event(category: 'order', action: 'refund', non_interactive: true, product_action: 'refund')
262
+
263
+ event.add_measurement(:transaction, id: 'T12345')
264
+
265
+ event.track!
266
+ ```
267
+
268
+ ### Transaction & Product Refund ###
269
+
270
+ The combination of `product_action: 'refund'` and `transaction` measurement setting a matching `id` to a previous transaction. You can also specify a product (or products, using `index`) with a `quantity` (for partial refunds) to refund.
271
+
272
+ ```ruby
273
+ event = tracker.build_event(category: 'order', action: 'refund', non_interactive: true, product_action: 'refund')
274
+
275
+ event.add_measurement(:transaction, id: 'T12345')
276
+ event.add_measurement(:product, index: 1, id: 'P12345', quantity: 1)
277
+
278
+ event.track!
279
+ ```
280
+
281
+ ### Promotion Impression ###
282
+
283
+ ```ruby
284
+ pageview = tracker.build_pageview(path: '/search', hostname: 'mystore.com', title: 'Search Results')
285
+
286
+ pageview.add_measurement(:promotion, {
287
+ index: 1,
288
+ id: 'PROMO_1234',
289
+ name: 'Summer Sale',
290
+ creative: 'summer_sale_banner',
291
+ position: 'banner_1'
292
+ })
293
+
294
+ pageview.track!
295
+ ```
296
+
297
+ ### Promotion Click ###
298
+
299
+ Promotion also supports a `promotion_action`, similar to `product_action`. This is another global option on `Hit`.
300
+
301
+ ```ruby
302
+ event = tracker.build_event(category: 'promotions', action: 'click', label: 'internal', promotion_action: 'click')
303
+
304
+ event.add_measurement(:promotion, {
305
+ index: 1,
306
+ id: 'PROMO_1234',
307
+ name: 'Summer Sale',
308
+ creative: 'summer_sale_banner',
309
+ position: 'banner_1'
310
+ })
311
+
312
+ event.track!
313
+ ```
314
+
315
+ ### Product Click ###
316
+
317
+ ```ruby
318
+ event = tracker.build_event(category: 'search', action: 'click', label: 'results', product_action: 'click', product_action_list: 'Search Results')
319
+
320
+ event.add_measurement(:product, {
321
+ index: 1,
322
+ id: 'P12345',
323
+ name: 'T-Shirt',
324
+ category: 'Apparel',
325
+ brand: 'Your Brand',
326
+ variant: 'Purple',
327
+ quantity: 2,
328
+ position: 1,
329
+ price: 14.60,
330
+ coupon_code: 'ILUVTEES'
331
+ })
332
+
333
+ event.track!
334
+ ```
335
+
336
+ ### Checkout ###
337
+
338
+ ```ruby
339
+ pageview = tracker.build_pageview(path: '/checkout', hostname: 'mystore.com', title: 'Complete Your Checkout', product_action: 'checkout')
340
+
341
+ pageview.add_measurement(:product, {
342
+ index: 1, # this is our first product, value may be 1-200
343
+ id: 'P12345',
344
+ name: 'T-Shirt',
345
+ category: 'Apparel',
346
+ brand: 'Your Brand',
347
+ variant: 'Purple',
348
+ quantity: 2,
349
+ position: 1,
350
+ price: 14.60,
351
+ coupon_code: 'ILUVTEES'
352
+ })
353
+
354
+ pageview.add_measurement(:checkout, {
355
+ step: 1,
356
+ step_option: 'Visa'
357
+ })
358
+
359
+ pageview.track!
360
+ ```
361
+
362
+ ### Checkout Option ###
363
+
364
+ ```ruby
365
+ event = tracker.build_event(category: 'checkout', action: 'option', non_interactive: true, product_action: 'checkout_option')
366
+
367
+ event.add_measurement(:checkout_options, {
368
+ step: 2,
369
+ step_option: 'Fedex'
370
+ })
371
+
372
+ event.track!
373
+ ```
374
+
375
+ ### Impression List & Product Impression ###
376
+
377
+ ```ruby
378
+ pageview = tracker.build_pageview(path: '/home', hostname: 'mystore.com', title: 'Home Page')
379
+
380
+ pageview.add_measurement(:impression_list, index: 1, name: 'Search Results')
381
+
382
+ pageview.add_measurement(:product_impression, {
383
+ index: 1,
384
+ list_index: 1, # match the impression_list above
385
+ id: 'P12345',
386
+ name: 'T-Shirt',
387
+ category: 'Apparel',
388
+ brand: 'Your Brand',
389
+ variant: 'Purple',
390
+ position: 1,
391
+ price: 14.60
392
+ })
393
+
394
+ pageview.add_measurement(:impression_list, index: 2, name: 'Recommendations')
395
+
396
+ pageview.add_measurement(:product_impression, {
397
+ index: 1,
398
+ list_index: 2,
399
+ name: 'Yellow Tee'
400
+ })
401
+
402
+ pageview.add_measurement(:product_impression, {
403
+ index: 2,
404
+ list_index: 2,
405
+ name: 'Red Tee'
406
+ })
407
+
408
+ pageview.track!
409
+ ```
410
+
411
+ ## Google Documentation ##
204
412
 
205
413
  https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide
206
414
  https://developers.google.com/analytics/devguides/collection/protocol/v1/reference
207
415
  https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters
208
416
 
209
- ## Contributing
417
+ ## Contributing ##
210
418
 
211
419
  1. Fork it
212
420
  2. Create your feature branch (`git checkout -b my-new-feature`)
@@ -38,6 +38,7 @@ module Staccato
38
38
  end
39
39
  end
40
40
 
41
+ require 'staccato/boolean_helpers'
41
42
  require 'staccato/option_set'
42
43
  require 'staccato/hit'
43
44
  require 'staccato/pageview'
@@ -48,3 +49,4 @@ require 'staccato/timing'
48
49
  require 'staccato/transaction'
49
50
  require 'staccato/transaction_item'
50
51
  require 'staccato/tracker'
52
+ require 'staccato/measurement'
@@ -0,0 +1,40 @@
1
+ # Collection of methods for converting to
2
+ # google's boolean integers from ruby's booleans
3
+ module BooleanHelpers
4
+ # Convert each boolean in the hash to integer
5
+ # if it is a boolean field
6
+ # @param hash [Hash]
7
+ # @return [Hash]
8
+ def convert_booleans(hash)
9
+ hash.each_pair.with_object({}, &method(:convert_boolean))
10
+ end
11
+
12
+ # Method to convert a single field from bool to int
13
+ # @param hash [#[]=] the collector object
14
+ def convert_boolean((k,v), hash)
15
+ hash[k] = boolean_field?(k) ? integer_for(v) : v
16
+ end
17
+
18
+ # Is this key one of the boolean fields
19
+ # @param key [Symbol] field key
20
+ # @return [Boolean]
21
+ def boolean_field?(key)
22
+ boolean_fields.include?(key)
23
+ end
24
+
25
+ # Convert a value to appropriate int
26
+ # @param value [nil, true, false, Integer]
27
+ # @return [nil, Integer]
28
+ def integer_for(value)
29
+ case value
30
+ when Integer
31
+ value
32
+ when TrueClass
33
+ 1
34
+ when FalseClass
35
+ 0
36
+ when NilClass
37
+ nil
38
+ end
39
+ end
40
+ end
@@ -15,8 +15,10 @@ module Staccato
15
15
  :exception
16
16
  end
17
17
 
18
+ # Boolean fields from Hit plus exception-specific field
19
+ # @return [Array<Symbol>]
18
20
  def boolean_fields
19
- super << :fatal
21
+ super + [:fatal]
20
22
  end
21
23
  end
22
24
  end
@@ -18,6 +18,7 @@ module Staccato
18
18
  end
19
19
  end
20
20
 
21
+ # Hit global options may be set on any hit type options
21
22
  GLOBAL_OPTIONS = {
22
23
  anonymize_ip: 'aip', # boolean
23
24
  queue_time: 'qt', # integer
@@ -63,14 +64,22 @@ module Staccato
63
64
 
64
65
  # Content Experiments
65
66
  experiment_id: 'xid',
66
- experiment_variant: 'xvar'
67
- }
67
+ experiment_variant: 'xvar',
68
68
 
69
+ # Product
70
+ product_action: 'pa',
71
+ product_action_list: 'pal',
72
+
73
+ # Promotion
74
+ promotion_action: 'promoa'
75
+ }.freeze
76
+
77
+ # Fields which should be converted to boolean for google
69
78
  BOOLEAN_FIELDS = [
70
79
  :non_interactive,
71
80
  :anonymize_ip,
72
81
  :java_enabled
73
- ]
82
+ ].freeze
74
83
 
75
84
  # sets up a new hit
76
85
  # @param tracker [Staccato::Tracker] the tracker to collect to
@@ -88,31 +97,60 @@ module Staccato
88
97
 
89
98
  # collects the parameters from options for this hit type
90
99
  def params
91
- base_params.
92
- merge(tracker_default_params).
93
- merge(global_options_params).
94
- merge(hit_params).
95
- merge(custom_dimensions).
96
- merge(custom_metrics).
97
- reject {|_,v| v.nil?}
98
- end
99
-
100
- def add_custom_dimension(position, value)
101
- self.custom_dimensions["cd#{position}"] = value
102
- end
103
-
100
+ {}.
101
+ merge!(base_params).
102
+ merge!(tracker_default_params).
103
+ merge!(global_options_params).
104
+ merge!(hit_params).
105
+ merge!(custom_dimensions).
106
+ merge!(custom_metrics).
107
+ merge!(measurement_params).
108
+ reject {|_,v| v.nil?}
109
+ end
110
+
111
+ # Set a custom dimension value at an index
112
+ # @param index [Integer]
113
+ # @param value
114
+ def add_custom_dimension(index, value)
115
+ self.custom_dimensions["cd#{index}"] = value
116
+ end
117
+
118
+ # Custom dimensions for this hit
119
+ # @return [Hash]
104
120
  def custom_dimensions
105
121
  @custom_dimensions ||= {}
106
122
  end
107
123
 
108
- def add_custom_metric(position, value)
109
- self.custom_metrics["cm#{position}"] = value
124
+ # Set a custom metric value at an index
125
+ # @param index [Integer]
126
+ # @param value
127
+ def add_custom_metric(index, value)
128
+ self.custom_metrics["cm#{index}"] = value
110
129
  end
111
130
 
131
+ # Custom metrics for this hit
132
+ # @return [Hash]
112
133
  def custom_metrics
113
134
  @custom_metrics ||= {}
114
135
  end
115
136
 
137
+ # Add a measurement by its symbol name with options
138
+ #
139
+ # @param key [Symbol] any one of the measurable classes lookup key
140
+ # @param options [Hash] options for the measurement
141
+ def add_measurement(key, options = {})
142
+ self.measurements << Measurement.lookup(key).new(options)
143
+ end
144
+
145
+ # Measurements for this hit
146
+ # @return [Array<Measurable>]
147
+ def measurements
148
+ @measurements ||= []
149
+ end
150
+
151
+ # Returns the value for session control
152
+ # based on options for session_start/_end
153
+ # @return ['start', 'end']
116
154
  def session_control
117
155
  case
118
156
  when options[:session_start], options[:start_session]
@@ -128,34 +166,12 @@ module Staccato
128
166
  end
129
167
 
130
168
  private
131
- def convert_booleans(hash)
132
- hash.each_pair.with_object({}, &method(:convert_boolean))
133
- end
134
-
135
- def convert_boolean((k,v), hash)
136
- hash[k] = boolean_field?(k) ? integer_for(v) : v
137
- end
138
-
169
+ # @private
139
170
  def boolean_fields
140
171
  BOOLEAN_FIELDS
141
172
  end
142
173
 
143
- def boolean_field?(key)
144
- boolean_fields.include?(key)
145
- end
146
-
147
- def integer_for(value)
148
- case value
149
- when Integer
150
- value
151
- when TrueClass
152
- 1
153
- when FalseClass
154
- 0
155
- when NilClass
156
- nil
157
- end
158
- end
174
+ include BooleanHelpers
159
175
 
160
176
  # @private
161
177
  def base_params
@@ -199,5 +215,10 @@ module Staccato
199
215
  }.compact
200
216
  ]
201
217
  end
218
+
219
+ # @private
220
+ def measurement_params
221
+ measurements.dup.map!(&:params).inject({}, &:merge!)
222
+ end
202
223
  end
203
224
  end