snowplow-tracker 0.7.0.pre.alpha.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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