snowplow-tracker 0.7.0.pre.alpha.0 → 0.8.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,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