snowplow-tracker 0.7.0.pre.alpha.0 → 0.8.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.
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2013-2014 Snowplow Analytics Ltd. All rights reserved.
1
+ # Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved.
2
2
  #
3
3
  # This program is licensed to you under the Apache License Version 2.0,
4
4
  # and you may not use this file except in compliance with the Apache License Version 2.0.
@@ -9,335 +9,596 @@
9
9
  # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
10
  # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
11
11
 
12
- # Author:: Alex Dean, Fred Blundun (mailto:support@snowplowanalytics.com)
13
- # Copyright:: Copyright (c) 2013-2014 Snowplow Analytics Ltd
12
+ # Author:: Snowplow Analytics Ltd
13
+ # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd
14
14
  # License:: Apache License Version 2.0
15
15
 
16
- require 'contracts'
16
+
17
17
  require 'securerandom'
18
18
  require 'set'
19
19
 
20
20
  module SnowplowTracker
21
-
21
+ # Allows the tracking of events. The tracker accepts event properties to its
22
+ # various `track_x_event` methods, and creates an appropriate event payload.
23
+ # This payload is passed to one or more Emitters for sending to the event
24
+ # collector.
25
+ #
26
+ # A Tracker is always associated with one {Subject}, and one or more
27
+ # {Emitter}. The Subject object stores information about the user, and will be
28
+ # generated automatically if one is not provided during initialization. It can
29
+ # be swapped out for another Subject using {#set_subject}.
30
+ #
31
+ # Tracker objects can access the methods of their associated {Subject}, e.g.
32
+ # {#set_user_id}.
33
+ #
34
+ # The Emitter, or an array of Emitters, must be given during initialization.
35
+ # They will send the prepared events to the event collector. It's possible to
36
+ # add further Emitters to an existing Tracker, using {#add_emitter}. However,
37
+ # Emitters cannot be removed from Trackers.
38
+ #
39
+ # At initialization, two Tracker parameters can be set which will be added to
40
+ # all events. The first is the Tracker namespace. This is especially useful to
41
+ # distinguish between events from different Trackers, if more than one is
42
+ # being used. The namespace value will be sent as the `tna` field in the raw
43
+ # event, mapping to `name_tracker` in the processed event.
44
+ #
45
+ # The second user-set Tracker property is the app ID (`aid`; `app_id`). This
46
+ # is the unique identifier for the site or application, and is particularly
47
+ # useful for distinguishing between events when Snowplow tracking has been
48
+ # implemented in multiple apps.
49
+ #
50
+ # The final initialization parameter is a setting for the base64-encoding of
51
+ # any JSONs in the event payload. These will be the {SelfDescribingJson}s used
52
+ # to provide context to events, or in the {#track_self_describing_event}
53
+ # method. The default is for JSONs to be encoded. Once the Tracker has been
54
+ # instantiated, it is not possible to change this setting.
55
+ #
56
+ # # Tracking events
57
+ #
58
+ # The Tracker `#track_x_event` methods all work similarly. An event payload is
59
+ # created containing the relevant properties, which is passed to an {Emitter}
60
+ # for sending. All payloads have a unique event ID (`event_id`) added to them
61
+ # (a type-4 UUID created using the SecureRandom module). This is sent as the
62
+ # `eid` field in the raw event.
63
+ #
64
+ # The Ruby tracker provides the ability to track multiple types of events
65
+ # out-of-the-box. The `#track_x_event` methods range from single purpose
66
+ # methods, such as {#track_page_view}, to the more complex but flexible
67
+ # {#track_self_describing_event}, which can be used to track any kind of
68
+ # event. We strongly recommend using {#track_self_describing_event} for your
69
+ # tracking, as it allows you to design custom event types to match your
70
+ # business requirements.
71
+ #
72
+ # This table gives the event type in the raw and processed events, defined in
73
+ # the Snowplow Tracker Protocol. This is the `e` or `event` parameter. Note
74
+ # that {#track_screen_view} calls {#track_self_describing_event} behind the
75
+ # scenes, resulting in a `ue` event type.
76
+ #
77
+ # <br>
78
+ #
79
+ # | Tracker method | `e` (raw) | `event` (processed) |
80
+ # | --- | --- | --- |
81
+ # | {#track_self_describing_event} | `ue` | `unstruct` |
82
+ # | {#track_struct_event} | `se` | `struct` |
83
+ # | {#track_page_view} | `pv` | `page_view` |
84
+ # | {#track_ecommerce_transaction} | `tr` and `ti` | `transaction` and `transaction_item` |
85
+ # | {#track_screen_view} | `ue` | `unstruct` |
86
+ #
87
+ # <br>
88
+ #
89
+ # The name `ue`, "unstructured event", is partially depreciated. This event
90
+ # type was originally created as a counterpart to "structured event", but the
91
+ # name is misleading. An `unstruct` event requires a schema ruleset and
92
+ # therefore can be considered more structured than a `struct` event. We prefer
93
+ # the name "self-describing event", after the {SelfDescribingJson} schema.
94
+ # Changing the event name in the Tracker Protocol would be a breaking change,
95
+ # so for now the self-describing events are still sent as "unstruct".
96
+ #
97
+ # All the `#track_x_event` methods share common features and parameters. Every
98
+ # type of event can have an optional context, {Subject}, and {Page} added. A
99
+ # {Timestamp} can also be provided for all event types to override the default
100
+ # event timestamp.
101
+ #
102
+ # [Event
103
+ # context](https://docs.snowplowanalytics.com/docs/understanding-tracking-design/understanding-events-entities/)
104
+ # can be provided as an array of {SelfDescribingJson}. Each element of the
105
+ # array is called an entity. Contextual entities can be used to describe the
106
+ # setting in which the event occurred. For example, a "user" entity could be
107
+ # created and attached to all events from each user. For a search event,
108
+ # entities could be attached for each of the search results. The Ruby tracker
109
+ # does not automatically add any event context. This is in contrast to the
110
+ # [Snowplow JavaScript Tracker](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-trackers/),
111
+ # which automatically attaches a "webpage" entity to every event that it tracks,
112
+ # containing a unique ID for that loaded page.
113
+ #
114
+ # @see Subject
115
+ # @see Emitter
116
+ # @see
117
+ # https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol
118
+ # the Snowplow Tracker Protocol
119
+ # @see
120
+ # https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/tracking-events/
121
+ # the Snowplow docs page about tracking events
122
+ # @see
123
+ # https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/enriching-your-events/
124
+ # the Snowplow docs page about adding context and other extra data to events
125
+ # @see
126
+ # https://docs.snowplowanalytics.com/docs/understanding-tracking-design/introduction-to-tracking-design/
127
+ # introduction to Snowplow tracking design
128
+ # @api public
22
129
  class Tracker
130
+ # @!group Public constants
23
131
 
24
- include Contracts
25
-
26
- @@EmitterInput = Or[lambda {|x| x.is_a? Emitter}, ArrayOf[lambda {|x| x.is_a? Emitter}]]
27
-
28
- @@required_transaction_keys = Set.new(%w(order_id total_value))
29
- @@recognised_transaction_keys = Set.new(%w(order_id total_value affiliation tax_value shipping city state country currency))
30
-
31
- @@Transaction = lambda { |x|
32
- return false unless x.class == Hash
33
- transaction_keys = Set.new(x.keys)
34
- @@required_transaction_keys.subset? transaction_keys and
35
- transaction_keys.subset? @@recognised_transaction_keys
36
- }
37
-
38
- @@required_item_keys = Set.new(%w(sku price quantity))
39
- @@recognised_item_keys = Set.new(%w(sku price quantity name category context))
132
+ # SelfDescribingJson objects are sent encoded by default
133
+ DEFAULT_ENCODE_BASE64 = true
40
134
 
41
- @@Item = lambda { |x|
42
- return false unless x.class == Hash
43
- item_keys = Set.new(x.keys)
44
- @@required_item_keys.subset? item_keys and
45
- item_keys.subset? @@recognised_item_keys
46
- }
135
+ # @private
136
+ BASE_SCHEMA_PATH = 'iglu:com.snowplowanalytics.snowplow'
137
+ # @private
138
+ SCHEMA_TAG = 'jsonschema'
139
+ # @private
140
+ CONTEXT_SCHEMA = "#{BASE_SCHEMA_PATH}/contexts/#{SCHEMA_TAG}/1-0-1"
141
+ # @private
142
+ UNSTRUCT_EVENT_SCHEMA = "#{BASE_SCHEMA_PATH}/unstruct_event/#{SCHEMA_TAG}/1-0-0"
143
+ # @private
144
+ SCREEN_VIEW_SCHEMA = "#{BASE_SCHEMA_PATH}/screen_view/#{SCHEMA_TAG}/1-0-0"
47
145
 
48
- @@required_augmented_item_keys = Set.new(%w(sku price quantity tstamp order_id))
49
- @@recognised_augmented_item_keys = Set.new(%w(sku price quantity name category context tstamp order_id currency))
146
+ # @!endgroup
50
147
 
51
- @@AugmentedItem = lambda { |x|
52
- return false unless x.class == Hash
53
- augmented_item_keys = Set.new(x.keys)
54
- @@required_augmented_item_keys.subset? augmented_item_keys and
55
- augmented_item_keys.subset? @@recognised_augmented_item_keys
56
- }
57
-
58
- @@ContextsInput = ArrayOf[SelfDescribingJson]
59
-
60
- @@version = TRACKER_VERSION
61
- @@default_encode_base64 = true
62
-
63
- @@base_schema_path = "iglu:com.snowplowanalytics.snowplow"
64
- @@schema_tag = "jsonschema"
65
- @@context_schema = "#{@@base_schema_path}/contexts/#{@@schema_tag}/1-0-1"
66
- @@unstruct_event_schema = "#{@@base_schema_path}/unstruct_event/#{@@schema_tag}/1-0-0"
67
-
68
- Contract @@EmitterInput, Maybe[Subject], Maybe[String], Maybe[String], Bool => Tracker
69
- def initialize(emitters, subject=nil, namespace=nil, app_id=nil, encode_base64=@@default_encode_base64)
148
+ # Create a new Tracker. `emitters` is the only strictly required parameter.
149
+ #
150
+ # @param emitters [Emitter, Array<Emitter>] one or more Emitter objects
151
+ # @param subject [Subject] a Subject object
152
+ # @param namespace [String] a name for the Tracker
153
+ # @param app_id [String] the app ID
154
+ # @param encode_base64 [Bool] whether JSONs will be base64-encoded or not
155
+ # @example Initializing a Tracker with all possible options
156
+ # SnowplowTracker::Tracker.new(
157
+ # emitters: SnowplowTracker::Emitter.new(endpoint: 'collector.example.com'),
158
+ # subject: SnowplowTracker::Subject.new,
159
+ # namespace: 'tracker_no_encode',
160
+ # app_id: 'rails_main',
161
+ # encode_base64: false
162
+ # )
163
+ # @api public
164
+ #
165
+ # @note All the Tracker instance methods return the Tracker object, allowing
166
+ # method chaining, e.g.
167
+ # `SnowplowTracker::Tracker.new.set_user_id('12345').track_page_view(page_url: 'www.example.com`
168
+ def initialize(emitters:, subject: nil, namespace: nil, app_id: nil, encode_base64: DEFAULT_ENCODE_BASE64)
70
169
  @emitters = Array(emitters)
71
- if subject.nil?
72
- @subject = Subject.new
73
- else
74
- @subject = subject
75
- end
76
- @standard_nv_pairs = {
170
+ @subject = if subject.nil?
171
+ Subject.new
172
+ else
173
+ subject
174
+ end
175
+ @settings = {
77
176
  'tna' => namespace,
78
- 'tv' => @@version,
177
+ 'tv' => TRACKER_VERSION,
79
178
  'aid' => app_id
80
179
  }
81
- @config = {
82
- 'encode_base64' => encode_base64
83
- }
84
-
85
- self
180
+ @encode_base64 = encode_base64
86
181
  end
87
182
 
88
- # Call subject methods from tracker instance
89
- #
183
+ # @!method set_color_depth(depth)
184
+ # call {Subject#set_color_depth}
185
+ # @!method set_domain_session_id(sid)
186
+ # call {Subject#set_domain_session_id}
187
+ # @!method set_domain_session_idx(vid)
188
+ # call {Subject#set_domain_session_idx}
189
+ # @!method set_domain_user_id(duid)
190
+ # call {Subject#set_domain_user_id}
191
+ # @!method set_fingerprint(fingerprint)
192
+ # call {Subject#set_fingerprint}
193
+ # @!method set_ip_address(ip)
194
+ # call {Subject#set_ip_address}
195
+ # @!method set_lang(lang)
196
+ # call {Subject#set_lang}
197
+ # @!method set_network_user_id(nuid)
198
+ # call {Subject#set_network_user_id}
199
+ # @!method set_platform(platform)
200
+ # call {Subject#set_platform}
201
+ # @!method set_screen_resolution(width:, height:)
202
+ # call {Subject#set_screen_resolution}
203
+ # @!method set_timezone(timezone)
204
+ # call {Subject#set_timezone}
205
+ # @!method set_user_id(user_id)
206
+ # call {Subject#set_user_id}
207
+ # @!method set_useragent(useragent)
208
+ # call {Subject#set_useragent}
209
+ # @!method set_viewport(width:, height:)
210
+ # call {Subject#set_viewport}
90
211
  Subject.instance_methods(false).each do |name|
91
- define_method name, ->(*splat) do
92
- @subject.method(name.to_sym).call(*splat)
212
+ if RUBY_VERSION >= '3.0.0'
213
+ define_method name, ->(*args, **kwargs) do
214
+ @subject.method(name.to_sym).call(*args, **kwargs)
93
215
 
94
- self
216
+ self
217
+ end
218
+ else
219
+ define_method name, ->(*args) do
220
+ @subject.method(name.to_sym).call(*args)
221
+
222
+ self
223
+ end
95
224
  end
96
225
  end
97
226
 
98
227
  # Generates a type-4 UUID to identify this event
99
- Contract nil => String
100
- def get_event_id()
228
+ # @private
229
+ def event_id
101
230
  SecureRandom.uuid
102
231
  end
103
232
 
104
- # Generates the timestamp (in milliseconds) to be attached to each event
105
- #
106
- Contract nil => Num
107
- def get_timestamp
108
- (Time.now.to_f * 1000).to_i
109
- end
110
-
111
- # Builds a self-describing JSON from an array of custom contexts
112
- #
113
- Contract @@ContextsInput => Hash
233
+ # Builds a single self-describing JSON from an array of custom contexts
234
+ # @private
114
235
  def build_context(context)
115
236
  SelfDescribingJson.new(
116
- @@context_schema,
117
- context.map {|c| c.to_json}
118
- ).to_json
237
+ CONTEXT_SCHEMA,
238
+ context.map(&:to_json)
239
+ ).to_json
119
240
  end
120
241
 
121
- # Tracking methods
122
-
123
- # Attaches all the fields in @standard_nv_pairs to the request
124
- # Only attaches the context vendor if the event has a custom context
125
- #
126
- Contract Payload => nil
127
- def track(pb)
128
- pb.add_dict(@subject.standard_nv_pairs)
129
- pb.add_dict(@standard_nv_pairs)
130
- pb.add('eid', get_event_id())
131
- @emitters.each{ |emitter| emitter.input(pb.context)}
242
+ # Sends the payload hash as a request to the Emitter(s)
243
+ # @private
244
+ def track(payload)
245
+ @emitters.each { |emitter| emitter.input(payload.data) }
132
246
 
133
247
  nil
134
248
  end
135
249
 
136
- # Log a visit to this page with an inserted device timestamp
137
- #
138
- Contract String, Maybe[String], Maybe[String], Maybe[@@ContextsInput], Maybe[Num] => Tracker
139
- def track_page_view(page_url, page_title=nil, referrer=nil, context=nil, tstamp=nil)
140
- if tstamp.nil?
141
- tstamp = get_timestamp
142
- end
143
-
144
- track_page_view(page_url, page_title, referrer, context, DeviceTimestamp.new(tstamp))
250
+ # Ensures that either a DeviceTimestamp or TrueTimestamp is associated with
251
+ # every event.
252
+ # @private
253
+ def process_tstamp(tstamp)
254
+ tstamp = Timestamp.create if tstamp.nil?
255
+ tstamp = DeviceTimestamp.new(tstamp) if tstamp.is_a? Numeric
256
+ tstamp
145
257
  end
146
258
 
147
- # Log a visit to this page
259
+ # Attaches the more generic fields to the event payload. This includes
260
+ # context, Subject, and Page if they are present. The timestamp is added, as
261
+ # well as all fields from `@settings`.
148
262
  #
149
- Contract String, Maybe[String], Maybe[String], Maybe[@@ContextsInput], SnowplowTracker::Timestamp => Tracker
150
- def track_page_view(page_url, page_title=nil, referrer=nil, context=nil, tstamp=nil)
151
- pb = Payload.new
152
- pb.add('e', 'pv')
153
- pb.add('url', page_url)
154
- pb.add('page', page_title)
155
- pb.add('refr', referrer)
156
-
157
- unless context.nil?
158
- pb.add_json(build_context(context), @config['encode_base64'], 'cx', 'co')
263
+ # Finally, the Tracker generates and attaches an event ID.
264
+ # @private
265
+ def finalise_payload(payload, context, tstamp, event_subject, page)
266
+ payload.add_json(build_context(context), @encode_base64, 'cx', 'co') unless context.nil? || context.empty?
267
+ payload.add_hash(page.details) unless page.nil?
268
+
269
+ if event_subject.nil?
270
+ payload.add_hash(@subject.details)
271
+ else
272
+ payload.add_hash(@subject.details.merge(event_subject.details))
159
273
  end
160
274
 
161
- pb.add(tstamp.type, tstamp.value)
162
-
163
- track(pb)
275
+ payload.add(tstamp.type, tstamp.value)
276
+ payload.add_hash(@settings)
277
+ payload.add('eid', event_id)
164
278
 
165
- self
279
+ nil
166
280
  end
167
281
 
168
- # Track a single item within an ecommerce transaction
169
- # Not part of the public API
170
- #
171
- Contract @@AugmentedItem => self
172
- def track_ecommerce_transaction_item(argmap)
173
- pb = Payload.new
174
- pb.add('e', 'ti')
175
- pb.add('ti_id', argmap['order_id'])
176
- pb.add('ti_sk', argmap['sku'])
177
- pb.add('ti_pr', argmap['price'])
178
- pb.add('ti_qu', argmap['quantity'])
179
- pb.add('ti_nm', argmap['name'])
180
- pb.add('ti_ca', argmap['category'])
181
- pb.add('ti_cu', argmap['currency'])
182
- unless argmap['context'].nil?
183
- pb.add_json(build_context(argmap['context']), @config['encode_base64'], 'cx', 'co')
184
- end
185
- pb.add(argmap['tstamp'].type, argmap['tstamp'].value)
186
- track(pb)
282
+ # Track a visit to a page.
283
+ # @example
284
+ # SnowplowTracker::Tracker.new.track_page_view(page_url: 'www.example.com',
285
+ # page_title: 'example',
286
+ # referrer: 'www.referrer.com')
287
+ #
288
+ # @param page_url [String] the URL of the page
289
+ # @param page_title [String] the page title
290
+ # @param referrer [String] the URL of the referrer page
291
+ # @param context [Array<SelfDescribingJson>] an array of SelfDescribingJson objects
292
+ # @param tstamp [DeviceTimestamp, TrueTimestamp, Num] override the default DeviceTimestamp of the event
293
+ # @param subject [Subject] event-specific Subject object
294
+ # @param page [Page] override the page_url, page_title, or referrer
295
+ #
296
+ # @api public
297
+ def track_page_view(page_url:, page_title: nil, referrer: nil,
298
+ context: nil, tstamp: nil, subject: nil, page: nil)
299
+ tstamp = process_tstamp(tstamp)
187
300
 
188
- self
189
- end
301
+ payload = Payload.new
302
+ payload.add('e', 'pv')
303
+ payload.add('url', page_url)
304
+ payload.add('page', page_title)
305
+ payload.add('refr', referrer)
190
306
 
191
- # Track an ecommerce transaction and all the items in it
192
- # Set the timestamp as the device timestamp
193
- Contract @@Transaction, ArrayOf[@@Item], Maybe[@@ContextsInput], Maybe[Num] => Tracker
194
- def track_ecommerce_transaction(transaction,
195
- items,
196
- context=nil,
197
- tstamp=nil)
198
- if tstamp.nil?
199
- tstamp = get_timestamp
200
- end
307
+ finalise_payload(payload, context, tstamp, subject, page)
308
+ track(payload)
201
309
 
202
- track_ecommerce_transaction(transaction, items, context, DeviceTimestamp.new(tstamp))
310
+ self
203
311
  end
204
312
 
205
- # Track an ecommerce transaction and all the items in it
206
- #
207
- Contract @@Transaction, ArrayOf[@@Item], Maybe[@@ContextsInput], Timestamp => Tracker
208
- def track_ecommerce_transaction(transaction, items,
209
- context=nil, tstamp=nil)
210
- pb = Payload.new
211
- pb.add('e', 'tr')
212
- pb.add('tr_id', transaction['order_id'])
213
- pb.add('tr_tt', transaction['total_value'])
214
- pb.add('tr_af', transaction['affiliation'])
215
- pb.add('tr_tx', transaction['tax_value'])
216
- pb.add('tr_sh', transaction['shipping'])
217
- pb.add('tr_ci', transaction['city'])
218
- pb.add('tr_st', transaction['state'])
219
- pb.add('tr_co', transaction['country'])
220
- pb.add('tr_cu', transaction['currency'])
221
- unless context.nil?
222
- pb.add_json(build_context(context), @config['encode_base64'], 'cx', 'co')
223
- end
224
-
225
- pb.add(tstamp.type, tstamp.value)
226
-
227
- track(pb)
228
-
229
- for item in items
313
+ # Track an eCommerce transaction, and all the items in it.
314
+ #
315
+ # This method is unique in sending multiple events: one `transaction` event,
316
+ # and one `transaction_item` event for each item. If Subject or Page objects
317
+ # are provided, their parameters will be merged into both `transaction` and
318
+ # `transaction_item` events. The timestamp and event ID of the
319
+ # `transaction_item` events will always be the same as the `transaction`.
320
+ # Transaction items are also automatically populated with the `order_id` and
321
+ # `currency` fields from the transaction.
322
+ #
323
+ # Event context is handled differently for `transaction` and
324
+ # `transaction_item` events. A context array argument provided to this
325
+ # method will be attached to the `transaction` event only. To attach a
326
+ # context array to a transaction item, use the key "context" in the item
327
+ # hash.
328
+ #
329
+ # The transaction and item hash arguments must contain the correct keys, as
330
+ # shown in the tables below.
331
+ #
332
+ # | Transaction fields | Description | Required? | Type |
333
+ # | --- | --- | --- | --- |
334
+ # | order_id | ID of the eCommerce transaction | Yes | String |
335
+ # | total_value | Total transaction value | Yes | Num |
336
+ # | affiliation | Transaction affiliation | No | String |
337
+ # | tax_value | Transaction tax value | No | Num |
338
+ # | shipping | Delivery cost charged | No | Num |
339
+ # | city | Delivery address city | No | String |
340
+ # | state | Delivery address state | No | String |
341
+ # | country | Delivery address country | No | String |
342
+ # | currency | Transaction currency | No | String |
343
+ #
344
+ # <br>
345
+ #
346
+ # | Item fields | Description | Required? | Type |
347
+ # | --- | --- | --- | --- |
348
+ # | sku | Item SKU | Yes | String |
349
+ # | price | Item price | Yes | Num |
350
+ # | quantity | Item quantity | Yes | Integer |
351
+ # | name | Item name | No | String |
352
+ # | category | Item category | No | String |
353
+ # | context | Item event context | No | Array[{SelfDescribingJson}] |
354
+ #
355
+ # @example Tracking a transaction containing two items
356
+ # SnowplowTracker::Tracker.new.track_ecommerce_transaction(
357
+ # transaction: {
358
+ # 'order_id' => '12345',
359
+ # 'total_value' => 49.99,
360
+ # 'affiliation' => 'my_company',
361
+ # 'tax_value' => 0,
362
+ # 'shipping' => 0,
363
+ # 'city' => 'Phoenix',
364
+ # 'state' => 'Arizona',
365
+ # 'country' => 'USA',
366
+ # 'currency' => 'USD'
367
+ # },
368
+ # items: [
369
+ # {
370
+ # 'sku' => 'pbz0026',
371
+ # 'price' => 19.99,
372
+ # 'quantity' => 1
373
+ # },
374
+ # {
375
+ # 'sku' => 'pbz0038',
376
+ # 'price' => 15,
377
+ # 'quantity' => 2,
378
+ # 'name' => 'crystals',
379
+ # 'category' => 'magic'
380
+ # }
381
+ # ]
382
+ # )
383
+ #
384
+ # @param transaction [Hash] the correctly structured transaction hash
385
+ # @param items [Array<Hash>] an array of correctly structured item hashes
386
+ # @param context [Array<SelfDescribingJson>] an array of SelfDescribingJson objects
387
+ # @param tstamp [DeviceTimestamp, TrueTimestamp, Num] override the default DeviceTimestamp of the event
388
+ # @param subject [Subject] event-specific Subject object
389
+ # @param page [Page] event-specific Page object
390
+ #
391
+ # @api public
392
+ def track_ecommerce_transaction(transaction:, items:,
393
+ context: nil, tstamp: nil,
394
+ subject: nil, page: nil)
395
+ tstamp = process_tstamp(tstamp)
396
+
397
+ transform_keys(transaction)
398
+
399
+ payload = Payload.new
400
+ payload.add('e', 'tr')
401
+ payload.add('tr_id', transaction['order_id'])
402
+ payload.add('tr_tt', transaction['total_value'])
403
+ payload.add('tr_af', transaction['affiliation'])
404
+ payload.add('tr_tx', transaction['tax_value'])
405
+ payload.add('tr_sh', transaction['shipping'])
406
+ payload.add('tr_ci', transaction['city'])
407
+ payload.add('tr_st', transaction['state'])
408
+ payload.add('tr_co', transaction['country'])
409
+ payload.add('tr_cu', transaction['currency'])
410
+
411
+ finalise_payload(payload, context, tstamp, subject, page)
412
+
413
+ track(payload)
414
+
415
+ items.each do |item|
416
+ transform_keys(item)
230
417
  item['tstamp'] = tstamp
231
418
  item['order_id'] = transaction['order_id']
232
419
  item['currency'] = transaction['currency']
233
- track_ecommerce_transaction_item(item)
420
+ track_ecommerce_transaction_item(item, subject, page)
234
421
  end
235
422
 
236
423
  self
237
424
  end
238
425
 
239
- # Track a structured event
240
- # set the timestamp to the device timestamp
241
- Contract String, String, Maybe[String], Maybe[String], Maybe[Num], Maybe[@@ContextsInput], Maybe[Num] => Tracker
242
- def track_struct_event(category, action, label=nil, property=nil, value=nil, context=nil, tstamp=nil)
243
- if tstamp.nil?
244
- tstamp = get_timestamp
245
- end
246
-
247
- track_struct_event(category, action, label, property, value, context, DeviceTimestamp.new(tstamp))
426
+ # Makes sure all hash keys are strings rather than symbols.
427
+ # The Ruby core language added a method for this in Ruby 2.5.
428
+ # @private
429
+ def transform_keys(hash)
430
+ hash.keys.each { |key| hash[key.to_s] = hash.delete key }
248
431
  end
249
- # Track a structured event
250
- #
251
- Contract String, String, Maybe[String], Maybe[String], Maybe[Num], Maybe[@@ContextsInput], Timestamp => Tracker
252
- def track_struct_event(category, action, label=nil, property=nil, value=nil, context=nil, tstamp=nil)
253
- pb = Payload.new
254
- pb.add('e', 'se')
255
- pb.add('se_ca', category)
256
- pb.add('se_ac', action)
257
- pb.add('se_la', label)
258
- pb.add('se_pr', property)
259
- pb.add('se_va', value)
260
- unless context.nil?
261
- pb.add_json(build_context(context), @config['encode_base64'], 'cx', 'co')
262
- end
263
432
 
264
- pb.add(tstamp.type, tstamp.value)
265
- track(pb)
433
+ # Track a single item within an ecommerce transaction.
434
+ # @private
435
+ def track_ecommerce_transaction_item(details, subject, page)
436
+ payload = Payload.new
437
+ payload.add('e', 'ti')
438
+ payload.add('ti_id', details['order_id'])
439
+ payload.add('ti_sk', details['sku'])
440
+ payload.add('ti_pr', details['price'])
441
+ payload.add('ti_qu', details['quantity'])
442
+ payload.add('ti_nm', details['name'])
443
+ payload.add('ti_ca', details['category'])
444
+ payload.add('ti_cu', details['currency'])
445
+
446
+ finalise_payload(payload, details['context'], details['tstamp'], subject, page)
447
+ track(payload)
266
448
 
267
449
  self
268
450
  end
269
451
 
270
- # Track a screen view event
452
+ # Track a structured event. `category` and `action` are required.
271
453
  #
272
- Contract Maybe[String], Maybe[String], Maybe[@@ContextsInput], Or[Timestamp, Num, nil] => Tracker
273
- def track_screen_view(name=nil, id=nil, context=nil, tstamp=nil)
274
- screen_view_properties = {}
275
- unless name.nil?
276
- screen_view_properties['name'] = name
277
- end
278
- unless id.nil?
279
- screen_view_properties['id'] = id
280
- end
281
- screen_view_schema = "#{@@base_schema_path}/screen_view/#{@@schema_tag}/1-0-0"
282
-
283
- event_json = SelfDescribingJson.new(screen_view_schema, screen_view_properties)
284
-
285
- self.track_unstruct_event(event_json, context, tstamp)
454
+ # This event type can be used to track many types of user activity, as it is
455
+ # somewhat customizable. This event type is provided particularly for
456
+ # concordance with Google Analytics tracking, where events are structured by
457
+ # "category", "action", "label", and "value".
458
+ #
459
+ # For fully customizable event tracking, we recommend you use
460
+ # self-describing events.
461
+ #
462
+ # @example
463
+ # SnowplowTracker::Tracker.new.track_struct_event(
464
+ # category: 'shop',
465
+ # action: 'add-to-basket',
466
+ # property: 'pcs',
467
+ # value: 2
468
+ # )
469
+ #
470
+ #
471
+ # @see #track_self_describing_event
472
+ #
473
+ # @param category [String] the event category
474
+ # @param action [String] the action performed
475
+ # @param label [String] an event label
476
+ # @param property [String] an event property
477
+ # @param value [Num] a value for the event
478
+ # @param context [Array<SelfDescribingJson>] an array of SelfDescribingJson objects
479
+ # @param tstamp [DeviceTimestamp, TrueTimestamp, Num] override the default DeviceTimestamp of the event
480
+ # @param subject [Subject] event-specific Subject object
481
+ # @param page [Page] event-specific Page object
482
+ #
483
+ # @api public
484
+ def track_struct_event(category:, action:, label: nil, property: nil,
485
+ value: nil, context: nil, tstamp: nil, subject: nil, page: nil)
486
+ tstamp = process_tstamp(tstamp)
487
+
488
+ payload = Payload.new
489
+ payload.add('e', 'se')
490
+ payload.add('se_ca', category)
491
+ payload.add('se_ac', action)
492
+ payload.add('se_la', label)
493
+ payload.add('se_pr', property)
494
+ payload.add('se_va', value)
495
+
496
+ finalise_payload(payload, context, tstamp, subject, page)
497
+ track(payload)
286
498
 
287
499
  self
288
500
  end
289
501
 
290
- # Better name for track unstruct event
502
+ # Track a screen view event. Note that while the `name` and `id` parameters
503
+ # are both optional, you must provided at least one of them to create a
504
+ # valid event.
291
505
  #
292
- Contract SelfDescribingJson, Maybe[@@ContextsInput], Timestamp => Tracker
293
- def track_self_describing_event(event_json, context=nil, tstamp=nil)
294
- track_unstruct_event(event_json, context, tstamp)
295
- end
296
-
297
- # Better name for track unstruct event
298
- # set the timestamp to the device timestamp
299
- Contract SelfDescribingJson, Maybe[@@ContextsInput], Maybe[Num] => Tracker
300
- def track_self_describing_event(event_json, context=nil, tstamp=nil)
301
- track_unstruct_event(event_json, context, tstamp)
302
- end
506
+ # This method creates an `unstruct` event, by creating a
507
+ # {SelfDescribingJson} and calling {#track_self_describing_event}. The
508
+ # schema ID for this is
509
+ # "iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0", and
510
+ # the data field will contain the name and/or ID.
511
+ #
512
+ # @example
513
+ # SnowplowTracker::Tracker.new.track_screen_view(name: 'HUD > Save Game',
514
+ # id: 'screen23')
515
+ #
516
+ #
517
+ # @see #track_page_view
518
+ # @see #track_self_describing_event
519
+ #
520
+ # @param name [String] the screen name (human readable)
521
+ # @param id [String] the unique screen ID
522
+ # @param context [Array<SelfDescribingJson>] an array of SelfDescribingJson objects
523
+ # @param tstamp [DeviceTimestamp, TrueTimestamp, Num] override the default DeviceTimestamp of the event
524
+ # @param subject [Subject] event-specific Subject object
525
+ # @param page [Page] event-specific Page object
526
+ #
527
+ # @api public
528
+ def track_screen_view(name: nil, id: nil, context: nil, tstamp: nil, subject: nil, page: nil)
529
+ screen_view_properties = {}
530
+ screen_view_properties['name'] = name unless name.nil?
531
+ screen_view_properties['id'] = id unless id.nil?
303
532
 
304
- # Track an unstructured event
305
- # set the timestamp to the device timstamp
306
- Contract SelfDescribingJson, Maybe[@@ContextsInput], Maybe[Num] => Tracker
307
- def track_unstruct_event(event_json, context=nil, tstamp=nil)
308
- if tstamp.nil?
309
- tstamp = get_timestamp
310
- end
533
+ event_json = SelfDescribingJson.new(SCREEN_VIEW_SCHEMA, screen_view_properties)
534
+ track_unstruct_event(event_json: event_json, context: context,
535
+ tstamp: tstamp, subject: subject, page: page)
311
536
 
312
- track_unstruct_event(event_json, context, DeviceTimestamp.new(tstamp))
537
+ self
313
538
  end
314
539
 
315
- # Track an unstructured event
540
+ # Track a self-describing event. These are custom events based on
541
+ # {SelfDescribingJson}, i.e. a JSON schema and a defined set of properties.
542
+ #
543
+ # This is useful for tracking specific or proprietary event types, or events
544
+ # with unpredicable or frequently changing properties.
545
+ #
546
+ # This method creates an `unstruct` event type. It is actually an alias for
547
+ # {#track_unstruct_event}, which is depreciated due to its unhelpful name.
548
+ #
549
+ # @example
550
+ # self_desc_json = SnowplowTracker::SelfDescribingJson.new(
551
+ # "iglu:com.example_company/save_game/jsonschema/1-0-2",
552
+ # {
553
+ # "saveId" => "4321",
554
+ # "level" => 23,
555
+ # "difficultyLevel" => "HARD",
556
+ # "dlContent" => true
557
+ # }
558
+ # )
316
559
  #
317
- Contract SelfDescribingJson, Maybe[@@ContextsInput], Timestamp => Tracker
318
- def track_unstruct_event(event_json, context=nil, tstamp=nil)
319
- pb = Payload.new
320
- pb.add('e', 'ue')
321
-
322
- envelope = SelfDescribingJson.new(@@unstruct_event_schema, event_json.to_json)
560
+ # SnowplowTracker::Tracker.new.track_self_describing_event(event_json: self_desc_json)
561
+ #
562
+ #
563
+ # @param event_json [SelfDescribingJson] a SelfDescribingJson object
564
+ # @param context [Array<SelfDescribingJson>] an array of SelfDescribingJson objects
565
+ # @param tstamp [DeviceTimestamp, TrueTimestamp, Num] override the default DeviceTimestamp of the event
566
+ # @param subject [Subject] event-specific Subject object
567
+ # @param page [Page] event-specific Page object
568
+ #
569
+ # @api public
570
+ def track_self_describing_event(event_json:, context: nil, tstamp: nil, subject: nil, page: nil)
571
+ track_unstruct_event(event_json: event_json, context: context,
572
+ tstamp: tstamp, subject: subject, page: page)
573
+ end
323
574
 
324
- pb.add_json(envelope.to_json, @config['encode_base64'], 'ue_px', 'ue_pr')
575
+ # @deprecated Use {#track_self_describing_event} instead.
576
+ #
577
+ # @api public
578
+ def track_unstruct_event(event_json:, context: nil, tstamp: nil, subject: nil, page: nil)
579
+ tstamp = process_tstamp(tstamp)
325
580
 
326
- unless context.nil?
327
- pb.add_json(build_context(context), @config['encode_base64'], 'cx', 'co')
328
- end
581
+ payload = Payload.new
582
+ payload.add('e', 'ue')
329
583
 
330
- pb.add(tstamp.type, tstamp.value)
584
+ envelope = SelfDescribingJson.new(UNSTRUCT_EVENT_SCHEMA, event_json.to_json)
585
+ payload.add_json(envelope.to_json, @encode_base64, 'ue_px', 'ue_pr')
331
586
 
332
- track(pb)
587
+ finalise_payload(payload, context, tstamp, subject, page)
588
+ track(payload)
333
589
 
334
590
  self
335
591
  end
336
592
 
337
- # Flush all events stored in all emitters
593
+ # Manually flush all events stored in all Tracker-associated Emitters. By
594
+ # default, this happens synchronously. {Emitter}s can only send events
595
+ # synchronously, while {AsyncEmitter}s can send either synchronously or
596
+ # asynchronously.
338
597
  #
339
- Contract Bool => Tracker
340
- def flush(async=false)
598
+ # @param async [Bool] whether to flush asynchronously or not
599
+ #
600
+ # @api public
601
+ def flush(async: false)
341
602
  @emitters.each do |emitter|
342
603
  emitter.flush(async)
343
604
  end
@@ -345,27 +606,30 @@ module SnowplowTracker
345
606
  self
346
607
  end
347
608
 
348
- # Set the subject of the events fired by the tracker
609
+ # Replace the existing Tracker-associated Subject with the provided one. All
610
+ # subsequent events will have the properties of the new Subject, unless they
611
+ # are overriden by event-specific Subject parameters.
612
+ #
613
+ # @param subject [Subject] a Subject object
349
614
  #
350
- Contract Subject => Tracker
615
+ # @api public
351
616
  def set_subject(subject)
352
617
  @subject = subject
353
618
  self
354
619
  end
355
620
 
356
- # Add a new emitter
621
+ # Add a new Emitter to the internal array of Tracker-associated Emitters.
622
+ #
623
+ # @param emitter [Emitter] an Emitter object
357
624
  #
358
- Contract Emitter => Tracker
625
+ # @api public
359
626
  def add_emitter(emitter)
360
627
  @emitters.push(emitter)
361
628
  self
362
629
  end
363
630
 
364
- private :get_timestamp,
365
- :build_context,
631
+ private :build_context,
366
632
  :track,
367
633
  :track_ecommerce_transaction_item
368
-
369
634
  end
370
-
371
635
  end