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