snowplow-tracker 0.6.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +56 -37
- data/lib/snowplow-tracker/emitters.rb +355 -147
- data/lib/snowplow-tracker/page.rb +60 -0
- data/lib/snowplow-tracker/payload.rb +30 -34
- data/lib/snowplow-tracker/self_describing_json.rb +92 -9
- data/lib/snowplow-tracker/subject.rb +282 -59
- data/lib/snowplow-tracker/timestamp.rb +95 -23
- data/lib/snowplow-tracker/tracker.rb +547 -242
- data/lib/snowplow-tracker/version.rb +33 -4
- data/lib/snowplow-tracker.rb +5 -5
- metadata +15 -17
- data/lib/snowplow-tracker/contracts.rb +0 -29
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2013-
|
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::
|
13
|
-
# Copyright:: Copyright (c) 2013-
|
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
|
-
|
127
|
+
# Contract types
|
27
128
|
|
28
|
-
|
29
|
-
|
129
|
+
# @private
|
130
|
+
EMITTER_INPUT = Or[->(x) { x.is_a? Emitter }, ArrayOf[->(x) { x.is_a? Emitter }]]
|
30
131
|
|
31
|
-
|
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
|
-
|
35
|
-
transaction_keys.subset?
|
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
|
-
|
39
|
-
|
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
|
-
|
45
|
-
item_keys.subset?
|
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
|
-
|
49
|
-
|
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
|
-
|
55
|
-
augmented_item_keys.subset?
|
168
|
+
REQUIRED_AUGMENTED_ITEM_KEYS.subset?(augmented_item_keys) &&
|
169
|
+
augmented_item_keys.subset?(RECOGNISED_AUGMENTED_ITEM_KEYS)
|
56
170
|
}
|
57
171
|
|
58
|
-
|
172
|
+
# @private
|
173
|
+
CONTEXTS_INPUT = ArrayOf[SelfDescribingJson]
|
59
174
|
|
60
|
-
|
61
|
-
@@default_encode_base64 = true
|
175
|
+
# Other constants
|
62
176
|
|
63
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
@
|
215
|
+
@subject = if subject.nil?
|
216
|
+
Subject.new
|
217
|
+
else
|
218
|
+
subject
|
219
|
+
end
|
220
|
+
@settings = {
|
77
221
|
'tna' => namespace,
|
78
|
-
'tv' =>
|
222
|
+
'tv' => TRACKER_VERSION,
|
79
223
|
'aid' => app_id
|
80
224
|
}
|
81
|
-
@
|
82
|
-
'encode_base64' => encode_base64
|
83
|
-
}
|
84
|
-
|
85
|
-
self
|
225
|
+
@encode_base64 = encode_base64
|
86
226
|
end
|
87
227
|
|
88
|
-
#
|
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
|
-
|
92
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
105
|
-
#
|
106
|
-
|
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
|
-
|
117
|
-
context.map
|
118
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
325
|
+
payload.add(tstamp.type, tstamp.value)
|
326
|
+
payload.add_hash(@settings)
|
327
|
+
payload.add('eid', event_id)
|
164
328
|
|
165
|
-
|
329
|
+
nil
|
166
330
|
end
|
167
331
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
189
|
-
|
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
|
-
|
192
|
-
|
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
|
-
|
359
|
+
self
|
203
360
|
end
|
204
361
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
-
#
|
240
|
-
#
|
241
|
-
|
242
|
-
def
|
243
|
-
|
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
|
-
|
265
|
-
|
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
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
-
|
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
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
#
|
298
|
-
#
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
-
|
305
|
-
|
306
|
-
|
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
|
-
|
583
|
+
self
|
313
584
|
end
|
314
585
|
|
315
|
-
|
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
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
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
|
-
|
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
|
-
|
327
|
-
|
328
|
-
end
|
619
|
+
payload = Payload.new
|
620
|
+
payload.add('e', 'ue')
|
329
621
|
|
330
|
-
|
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
|
-
|
625
|
+
finalise_payload(payload, context, tstamp, subject, page)
|
626
|
+
track(payload)
|
333
627
|
|
334
628
|
self
|
335
629
|
end
|
336
630
|
|
337
|
-
|
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
|
-
|
340
|
-
def flush(async
|
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 :
|
365
|
-
:build_context,
|
672
|
+
private :build_context,
|
366
673
|
:track,
|
367
674
|
:track_ecommerce_transaction_item
|
368
|
-
|
369
675
|
end
|
370
|
-
|
371
676
|
end
|