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,239 +9,418 @@
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 'net/https'
17
18
  require 'set'
18
19
  require 'logger'
19
- require 'contracts'
20
20
 
21
21
  module SnowplowTracker
22
-
22
+ # @see Emitter
23
+ # For logging Emitter activity messages
23
24
  LOGGER = Logger.new(STDERR)
24
25
  LOGGER.level = Logger::INFO
25
26
 
27
+ # This class sends events to the event collector. All {Tracker}s must have at
28
+ # least one associated Emitter or the subclass AsyncEmitter.
29
+ #
30
+ # The network settings are defined as part of the Emitter initalization. This
31
+ # table displays the default Emitter settings:
32
+ #
33
+ # | Property | Default setting |
34
+ # | --- | --- |
35
+ # | Protocol | HTTP |
36
+ # | Method | GET |
37
+ # | Buffer size | 1 |
38
+ # | Path | `/i` |
39
+ #
40
+ # The buffer size is the number of events which will be buffered before they
41
+ # are all sent simultaneously. The process of sending all buffered events is
42
+ # called "flushing". The default buffer size is 1 because GET requests can
43
+ # only contain one event.
44
+ #
45
+ # If you choose to use POST requests, the buffer_size defaults to 10, and the
46
+ # buffered events are all sent together in a single request. The default path
47
+ # is '/com.snowplowanalytics.snowplow/tp2' for Emitters using POST.
48
+ #
49
+ # # Logging
50
+ # Emitters log their activity to STDERR by default, using the Ruby standard
51
+ # library Logger class. A different logger can be configured during Emitter
52
+ # initialization. For example, to disable logging, you could provide
53
+ # `Logger.new(IO::NULL)` in the options hash.
54
+ #
55
+ # By default, only messages with priority "INFO" or higher will be logged.
56
+ # This can be changed at any time for the default logger, which is saved as a
57
+ # module constant (`LOGGER = Logger.new(STDERR)`). If you are not using the
58
+ # default logger, set the message level before initializing your Emitter.
59
+ #
60
+ # @see https://ruby-doc.org/stdlib-2.7.2/libdoc/logger/rdoc/Logger.html Logger documentation
61
+ #
62
+ # @example Changing the logger message level.
63
+ # require 'logger'
64
+ # SnowplowTracker::LOGGER.level = Logger::DEBUG
26
65
  class Emitter
27
-
28
- include Contracts
29
-
30
- @@ConfigHash = ({
31
- :protocol => Maybe[Or['http', 'https']],
32
- :port => Maybe[Num],
33
- :method => Maybe[Or['get', 'post']],
34
- :buffer_size => Maybe[Num],
35
- :on_success => Maybe[Func[Num => Any]],
36
- :on_failure => Maybe[Func[Num, Hash => Any]],
37
- :thread_count => Maybe[Num]
38
- })
39
-
40
- @@StrictConfigHash = And[@@ConfigHash, lambda { |x|
41
- x.class == Hash and Set.new(x.keys).subset? Set.new(@@ConfigHash.keys)
42
- }]
43
-
44
- @@DefaultConfig = {
45
- :protocol => 'http',
46
- :method => 'get'
66
+ # Default Emitter settings
67
+ DEFAULT_CONFIG = {
68
+ protocol: 'http',
69
+ method: 'get'
47
70
  }
48
71
 
49
- Contract String, @@StrictConfigHash => lambda { |x| x.is_a? Emitter }
50
- def initialize(endpoint, config={})
51
- config = @@DefaultConfig.merge(config)
72
+ # @private
73
+ attr_reader :logger
74
+
75
+ # Create a new Emitter instance. The endpoint is required.
76
+ #
77
+ # @example Initializing an Emitter with all the possible extra configuration.
78
+ # success_callback = ->(success_count) { puts "#{success_count} events sent successfully" }
79
+ # failure_callback = ->(success_count, failures) do
80
+ # puts "#{success_count} events sent successfully, #{failures.size} sent unsuccessfully"
81
+ # end
82
+ #
83
+ # SnowplowTracker::Emitter.new(endpoint: 'collector.example.com',
84
+ # options: { path: '/my-pipeline/1',
85
+ # protocol: 'https',
86
+ # port: 443,
87
+ # method: 'post',
88
+ # buffer_size: 5,
89
+ # on_success: success_callback,
90
+ # on_failure: failure_callback,
91
+ # logger: Logger.new(STDOUT) })
92
+ #
93
+ # The options hash can have any of these optional parameters:
94
+ #
95
+ # | Parameter | Description | Type |
96
+ # | --- | --- | --- |
97
+ # | path | Override the default path for appending to the endpoint | String |
98
+ # | protocol | 'http' or 'https' | String |
99
+ # | port | The port for the connection | Integer |
100
+ # | method | 'get' or 'post' | String |
101
+ # | buffer_size | Number of events to send at once | Integer |
102
+ # | on_success | A method to call if events were sent successfully | Method |
103
+ # | on_failure | A method to call if events did not send | Method |
104
+ # | thread_count | Number of threads to use | Integer |
105
+ # | logger | Log somewhere other than STDERR | Logger |
106
+ #
107
+ # Note that `thread_count` is relevant only to the subclass {AsyncEmitter},
108
+ # and will be ignored if provided to an Emitter.
109
+ #
110
+ # If you choose to use HTTPS, we recommend using port 443.
111
+ #
112
+ # Only 2xx and 3xx status codes are considered successes.
113
+ #
114
+ # The `on_success` callback should accept one argument: the number of
115
+ # requests sent this way. The `on_failure` callback should accept two
116
+ # arguments: the number of successfully sent events, and an array containing
117
+ # the unsuccessful events.
118
+ #
119
+ # @param endpoint [String] the endpoint to send the events to
120
+ # @param options [Hash] allowed configuration options
121
+ #
122
+ # @see AsyncEmitter#initialize
123
+ # @api public
124
+ def initialize(endpoint:, options: {})
125
+ config = DEFAULT_CONFIG.merge(options)
52
126
  @lock = Monitor.new
53
- @collector_uri = as_collector_uri(endpoint, config[:protocol], config[:port], config[:method])
127
+ path = confirm_path(config)
128
+ @collector_uri = create_collector_uri(endpoint, config[:protocol], config[:port], path)
54
129
  @buffer = []
55
- if not config[:buffer_size].nil?
56
- @buffer_size = config[:buffer_size]
57
- elsif config[:method] == 'get'
58
- @buffer_size = 1
59
- else
60
- @buffer_size = 10
61
- end
130
+ @buffer_size = confirm_buffer_size(config)
62
131
  @method = config[:method]
63
132
  @on_success = config[:on_success]
64
133
  @on_failure = config[:on_failure]
65
- LOGGER.info("#{self.class} initialized with endpoint #{@collector_uri}")
134
+ @logger = config[:logger] || LOGGER
135
+ logger.info("#{self.class} initialized with endpoint #{@collector_uri}")
136
+ end
137
+
138
+ # Creates the `@buffer_size` variable during initialization. Unless
139
+ # otherwise defined, it's 1 for Emitters using GET and 10 for Emitters using
140
+ # POST requests.
141
+ # @private
142
+ def confirm_buffer_size(config)
143
+ return config[:buffer_size] unless config[:buffer_size].nil?
66
144
 
67
- self
145
+ config[:method] == 'get' ? 1 : 10
68
146
  end
69
147
 
70
- # Build the collector URI from the configuration hash
71
- #
72
- Contract String, String, Maybe[Num], String => String
73
- def as_collector_uri(endpoint, protocol, port, method)
74
- port_string = port == nil ? '' : ":#{port.to_s}"
75
- path = method == 'get' ? '/i' : '/com.snowplowanalytics.snowplow/tp2'
148
+ # Creates the `@path` variable during initialization. Allows a non-standard
149
+ # path to be provided.
150
+ # @private
151
+ def confirm_path(config)
152
+ return config[:path] unless config[:path].nil?
153
+
154
+ config[:method] == 'get' ? '/i' : '/com.snowplowanalytics.snowplow/tp2'
155
+ end
156
+
157
+ # Creates the `@collector_uri` variable during initialization.
158
+ # The default is "http://{endpoint}/i".
159
+ # @private
160
+ def create_collector_uri(endpoint, protocol, port, path)
161
+ port_string = port.nil? ? '' : ":#{port}"
76
162
 
77
163
  "#{protocol}://#{endpoint}#{port_string}#{path}"
78
164
  end
79
165
 
80
- # Add an event to the buffer and flush it if maximum size has been reached
166
+ # Add an event to the buffer and flush it if maximum size has been reached.
167
+ # This method is not required for standard Ruby tracker usage. A {Tracker}
168
+ # privately calls this method once the event payload is ready to send.
169
+ #
170
+ # We have included it as part of the public API for its possible use in the
171
+ # `on_failure` callback. This is the optional method, provided in the
172
+ # `options` Emitter initalization hash, that is called when events fail
173
+ # to send. You could use {#input} as part of your callback to immediately
174
+ # retry the failed event.
81
175
  #
82
- Contract Hash => nil
176
+ # The `on_failure` callback should accept two arguments: the number of
177
+ # successfully sent events, and an array containing the unsuccessful events.
178
+ #
179
+ # @example A possible `on_failure` method using `#input`
180
+ # def retry_on_failure(failed_event_count, failed_events)
181
+ # # possible backoff-and-retry timeout here
182
+ # failed_events.each do |event|
183
+ # my_emitter.input(event)
184
+ # end
185
+ # end
186
+ #
187
+ # @api public
83
188
  def input(payload)
84
- payload.each { |k,v| payload[k] = v.to_s}
189
+ payload.each { |k, v| payload[k] = v.to_s }
85
190
  @lock.synchronize do
86
191
  @buffer.push(payload)
87
- if @buffer.size >= @buffer_size
88
- flush
89
- end
192
+ flush if @buffer.size >= @buffer_size
90
193
  end
91
194
 
92
195
  nil
93
196
  end
94
197
 
95
- # Flush the buffer
198
+ # Flush the Emitter, forcing it to send all the events in its
199
+ # buffer, even if the buffer is not full. {Emitter} objects, unlike
200
+ # {AsyncEmitter}s, can only `flush` synchronously. A {Tracker} can manually flush all
201
+ # its Emitters by calling {Tracker#flush}, part of the public API which
202
+ # calls this method.
96
203
  #
97
- Contract Bool => nil
98
- def flush(async=true)
204
+ # The unused async parameter here is to avoid ArgumentError, since
205
+ # {AsyncEmitter#flush} does take an argument.
206
+ #
207
+ # @see AsyncEmitter#flush
208
+ # @private
209
+ def flush(_async = true)
99
210
  @lock.synchronize do
100
211
  send_requests(@buffer)
101
212
  @buffer = []
102
213
  end
214
+
103
215
  nil
104
216
  end
105
217
 
106
218
  # Send all events in the buffer to the collector
107
- #
108
- Contract ArrayOf[Hash] => nil
109
- def send_requests(evts)
110
- if evts.size < 1
111
- LOGGER.info("Skipping sending events since buffer is empty")
219
+ # @private
220
+ def send_requests(events)
221
+ if events.empty?
222
+ logger.info('Skipping sending events since buffer is empty')
112
223
  return
113
224
  end
114
- LOGGER.info("Attempting to send #{evts.size} request#{evts.size == 1 ? '' : 's'}")
115
225
 
116
- evts.each do |event|
117
- event['stm'] = (Time.now.to_f * 1000).to_i.to_s # add the sent timestamp, overwrite if already exists
226
+ logger.info("Attempting to send #{events.size} request#{events.size == 1 ? '' : 's'}")
227
+
228
+ events.each do |event|
229
+ # add the sent timestamp, overwrite if already exists
230
+ event['stm'] = Timestamp.create.to_s
118
231
  end
119
232
 
120
233
  if @method == 'post'
121
- post_succeeded = false
122
- begin
123
- request = http_post(SelfDescribingJson.new(
124
- 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4',
125
- evts
126
- ).to_json)
127
- post_succeeded = is_good_status_code(request.code)
128
- rescue StandardError => se
129
- LOGGER.warn(se)
130
- end
131
- if post_succeeded
132
- unless @on_success.nil?
133
- @on_success.call(evts.size)
134
- end
135
- else
136
- unless @on_failure.nil?
137
- @on_failure.call(0, evts)
138
- end
139
- end
140
-
234
+ send_requests_with_post(events)
141
235
  elsif @method == 'get'
142
- success_count = 0
143
- unsent_requests = []
144
- evts.each do |evt|
145
- get_succeeded = false
146
- begin
147
- request = http_get(evt)
148
- get_succeeded = is_good_status_code(request.code)
149
- rescue StandardError => se
150
- LOGGER.warn(se)
151
- end
152
- if get_succeeded
153
- success_count += 1
154
- else
155
- unsent_requests << evt
156
- end
157
- end
158
- if unsent_requests.size == 0
159
- unless @on_success.nil?
160
- @on_success.call(success_count)
161
- end
162
- else
163
- unless @on_failure.nil?
164
- @on_failure.call(success_count, unsent_requests)
165
- end
166
- end
236
+ send_requests_with_get(events)
167
237
  end
168
238
 
169
239
  nil
170
240
  end
171
241
 
172
- # Send a GET request
173
- #
174
- Contract Hash => lambda { |x| x.is_a? Net::HTTPResponse }
242
+ # Part of {#send_requests}.
243
+ # @private
244
+ def send_requests_with_post(events)
245
+ post_succeeded = false
246
+ begin
247
+ request = http_post(SelfDescribingJson.new(
248
+ 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4',
249
+ events
250
+ ).to_json)
251
+ post_succeeded = good_status_code?(request.code)
252
+ rescue StandardError => standard_error
253
+ logger.warn(standard_error)
254
+ end
255
+
256
+ if post_succeeded
257
+ @on_success.call(events.size) unless @on_success.nil?
258
+ else
259
+ @on_failure.call(0, events) unless @on_failure.nil?
260
+ end
261
+
262
+ nil
263
+ end
264
+
265
+ # Part of {#send_requests}.
266
+ # @private
267
+ def send_requests_with_get(events)
268
+ success_count = 0
269
+ unsent_requests = []
270
+
271
+ events.each do |event|
272
+ request = process_get_event(event)
273
+ request ? success_count += 1 : unsent_requests << event
274
+ end
275
+
276
+ if unsent_requests.size.zero?
277
+ @on_success.call(success_count) unless @on_success.nil?
278
+ else
279
+ @on_failure.call(success_count, unsent_requests) unless @on_failure.nil?
280
+ end
281
+
282
+ nil
283
+ end
284
+
285
+ # Part of {#send_requests_with_get}.
286
+ # @private
287
+ def process_get_event(event)
288
+ get_succeeded = false
289
+ begin
290
+ request = http_get(event)
291
+ get_succeeded = good_status_code?(request.code)
292
+ rescue StandardError => standard_error
293
+ logger.warn(standard_error)
294
+ end
295
+ get_succeeded
296
+ end
297
+
298
+ # Part of {#process_get_event}. This sends a GET request.
299
+ # @private
175
300
  def http_get(payload)
176
301
  destination = URI(@collector_uri + '?' + URI.encode_www_form(payload))
177
- LOGGER.info("Sending GET request to #{@collector_uri}...")
178
- LOGGER.debug("Payload: #{payload}")
302
+ logger.info("Sending GET request to #{@collector_uri}...")
303
+ logger.debug("Payload: #{payload}")
179
304
  http = Net::HTTP.new(destination.host, destination.port)
180
305
  request = Net::HTTP::Get.new(destination.request_uri)
181
- if destination.scheme == 'https'
182
- http.use_ssl = true
183
- end
306
+ http.use_ssl = true if destination.scheme == 'https'
184
307
  response = http.request(request)
185
- LOGGER.add(is_good_status_code(response.code) ? Logger::INFO : Logger::WARN) {
308
+ logger.add(good_status_code?(response.code) ? Logger::INFO : Logger::WARN) do
186
309
  "GET request to #{@collector_uri} finished with status code #{response.code}"
187
- }
310
+ end
188
311
 
189
312
  response
190
313
  end
191
314
 
192
- # Send a POST request
193
- #
194
- Contract Hash => lambda { |x| x.is_a? Net::HTTPResponse }
315
+ # Part of {#send_requests_with_post}. This sends a POST request.
316
+ # @private
195
317
  def http_post(payload)
196
- LOGGER.info("Sending POST request to #{@collector_uri}...")
197
- LOGGER.debug("Payload: #{payload}")
318
+ logger.info("Sending POST request to #{@collector_uri}...")
319
+ logger.debug("Payload: #{payload}")
198
320
  destination = URI(@collector_uri)
199
321
  http = Net::HTTP.new(destination.host, destination.port)
200
322
  request = Net::HTTP::Post.new(destination.request_uri)
201
- if destination.scheme == 'https'
202
- http.use_ssl = true
203
- end
323
+ http.use_ssl = true if destination.scheme == 'https'
204
324
  request.body = payload.to_json
205
325
  request.set_content_type('application/json; charset=utf-8')
206
326
  response = http.request(request)
207
- LOGGER.add(is_good_status_code(response.code) ? Logger::INFO : Logger::WARN) {
327
+ logger.add(good_status_code?(response.code) ? Logger::INFO : Logger::WARN) do
208
328
  "POST request to #{@collector_uri} finished with status code #{response.code}"
209
- }
329
+ end
210
330
 
211
331
  response
212
332
  end
213
333
 
214
- # Only 2xx and 3xx status codes are considered successes
215
- #
216
- Contract String => Bool
217
- def is_good_status_code(status_code)
334
+ # Check if the response is good.
335
+ # Only 2xx and 3xx status codes are considered successes.
336
+ # @private
337
+ def good_status_code?(status_code)
218
338
  status_code.to_i >= 200 && status_code.to_i < 400
219
339
  end
220
340
 
221
- private :as_collector_uri,
341
+ private :create_collector_uri,
222
342
  :http_get,
223
343
  :http_post
224
-
225
344
  end
226
345
 
227
-
346
+ # This {Emitter} subclass provides asynchronous event sending. Whenever the
347
+ # buffer is flushed, the AsyncEmitter places the flushed events in a work
348
+ # queue. The AsyncEmitter asynchronously sends events in this queue using a
349
+ # thread pool of a fixed size. The size of the thread pool is 1 by default,
350
+ # but can be configured as part of the options hash during initialization.
351
+ #
352
+ # @see Emitter
353
+ # @api public
228
354
  class AsyncEmitter < Emitter
229
-
230
- Contract String, @@StrictConfigHash => lambda { |x| x.is_a? Emitter }
231
- def initialize(endpoint, config={})
232
- @queue = Queue.new()
355
+ # Create a new AsyncEmitter object. The endpoint is required.
356
+ #
357
+ # @example Initializing an AsyncEmitter with all the possible extra configuration.
358
+ # success_callback = ->(success_count) { puts "#{success_count} events sent successfully" }
359
+ # failure_callback = ->(success_count, failures) do
360
+ # puts "#{success_count} events sent successfully, #{failures.size} sent unsuccessfully"
361
+ # end
362
+ #
363
+ # SnowplowTracker::Emitter.new(endpoint: 'collector.example.com',
364
+ # options: { path: '/my-pipeline/1',
365
+ # protocol: 'https',
366
+ # port: 443,
367
+ # method: 'post',
368
+ # buffer_size: 5,
369
+ # on_success: success_callback,
370
+ # on_failure: failure_callback,
371
+ # logger: Logger.new(STDOUT),
372
+ # thread_count: 5 })
373
+ #
374
+ # The options hash can have any of these optional parameters:
375
+ #
376
+ # | Parameter | Description | Type |
377
+ # | --- | --- | --- |
378
+ # | path | Override the default path for appending to the endpoint | String |
379
+ # | protocol | 'http' or 'https' | String |
380
+ # | port | The port for the connection | Integer |
381
+ # | method | 'get' or 'post' | String |
382
+ # | buffer_size | Number of events to send at once | Integer |
383
+ # | on_success | A function to call if events were sent successfully | Function |
384
+ # | on_failure | A function to call if events did not send | Function |
385
+ # | thread_count | Number of threads to use | Integer |
386
+ # | logger | Log somewhere other than STDERR | Logger |
387
+ #
388
+ # The `thread_count` determines the number of worker threads which will be
389
+ # used to send events.
390
+ #
391
+ # If you choose to use HTTPS, we recommend using port 443.
392
+ #
393
+ # Only 2xx and 3xx status codes are considered successes.
394
+ #
395
+ # The `on_success` callback should accept one argument: the number of
396
+ # requests sent this way. The `on_failure` callback should accept two
397
+ # arguments: the number of successfully sent events, and an array containing
398
+ # the unsuccessful events.
399
+ #
400
+ # @note if you test the AsyncEmitter by using a short script to send an
401
+ # event, you may find that the event fails to send. This is because the
402
+ # process exits before the flushing thread is finished. You can get round
403
+ # this either by adding a sleep(10) to the end of your script or by using
404
+ # the synchronous flush.
405
+ #
406
+ # @param endpoint [String] the endpoint to send the events to
407
+ # @param options [Hash] allowed configuration options
408
+ #
409
+ # @see Emitter#initialize
410
+ # @api public
411
+ def initialize(endpoint:, options: {})
412
+ @queue = Queue.new
233
413
  # @all_processed_condition and @results_unprocessed are used to emulate Python's Queue.task_done()
234
414
  @queue.extend(MonitorMixin)
235
415
  @all_processed_condition = @queue.new_cond
236
416
  @results_unprocessed = 0
237
- (config[:thread_count] || 1).times do
238
- t = Thread.new do
239
- consume
240
- end
241
- end
242
- super(endpoint, config)
417
+ (options[:thread_count] || 1).times { Thread.new { consume } }
418
+ super(endpoint: endpoint, options: options)
243
419
  end
244
420
 
421
+ # AsyncEmitters use the MonitorMixin module, which provides the
422
+ # `synchronize` and `broadcast` methods.
423
+ # @private
245
424
  def consume
246
425
  loop do
247
426
  work_unit = @queue.pop
@@ -253,28 +432,34 @@ module SnowplowTracker
253
432
  end
254
433
  end
255
434
 
256
- # Flush the buffer
257
- # If async is false, block until the queue is empty
435
+ # Flush the Emitter, forcing it to send all the events in its buffer, even
436
+ # if the buffer is not full.
258
437
  #
259
- def flush(async=true)
438
+ # If `async` is true (the default), events are sent even if the queue is not
439
+ # empty. If `async` is false, it blocks until all queued events have been
440
+ # sent. Note that this method can be called by public API method
441
+ # {Tracker#flush}, which has a default of `async` being false.
442
+ #
443
+ # @param async [Bool] whether to flush asynchronously or not
444
+ #
445
+ # @see Emitter#flush
446
+ # @private
447
+ def flush(async = true)
260
448
  loop do
261
449
  @lock.synchronize do
262
- @queue.synchronize do
263
- @results_unprocessed += 1
264
- end
450
+ @queue.synchronize { @results_unprocessed += 1 }
265
451
  @queue << @buffer
266
452
  @buffer = []
267
453
  end
268
- if not async
269
- LOGGER.info('Starting synchronous flush')
454
+ unless async
455
+ logger.info('Starting synchronous flush')
270
456
  @queue.synchronize do
271
457
  @all_processed_condition.wait_while { @results_unprocessed > 0 }
272
- LOGGER.info('Finished synchronous flush')
458
+ logger.info('Finished synchronous flush')
273
459
  end
274
460
  end
275
- break if @buffer.size < 1
461
+ break if @buffer.empty?
276
462
  end
277
463
  end
278
464
  end
279
-
280
465
  end
@@ -0,0 +1,55 @@
1
+ # Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved.
2
+ #
3
+ # This program is licensed to you under the Apache License Version 2.0,
4
+ # and you may not use this file except in compliance with the Apache License Version 2.0.
5
+ # You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
6
+ #
7
+ # Unless required by applicable law or agreed to in writing,
8
+ # software distributed under the Apache License Version 2.0 is distributed on an
9
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
11
+
12
+ # Author:: Snowplow Analytics Ltd
13
+ # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd
14
+ # License:: Apache License Version 2.0
15
+
16
+
17
+ module SnowplowTracker
18
+ # If the Ruby tracker is incorporated into a website server, the events
19
+ # tracked will describe user activity on specific webpages. Knowing on which
20
+ # page an event occurred can be very valuable.
21
+ #
22
+ # Add page URL, page title and referrer URL to any event by adding a Page
23
+ # object to any {Tracker} `#track_x_event` method call.
24
+ #
25
+ # Page parameters are saved into the tracked event as part of the 'atomic'
26
+ # event properties, which have their own column in the eventual events table.
27
+ # For example, a Page's `page_url` parameter will be sent as `url` in the
28
+ # raw event payload, ending up in the `page_url` column.
29
+ #
30
+ #
31
+ # @note For {Tracker#track_page_view}, properties set in the Page object will
32
+ # override those properties given as arguments.
33
+ class Page
34
+ # @return [Hash] the stored page properties
35
+ attr_reader :details
36
+
37
+ # Create a Page object for attaching page properties to events.
38
+ #
39
+ # Page properties will directly populate the event's `page_url`, `page_title` and `referrer` parameters.
40
+ #
41
+ # @example Creating a Page
42
+ # SnowplowTracker::Page.new(page_url: 'http://www.example.com/second-page',
43
+ # page_title: 'Example title',
44
+ # referrer: 'http://www.example.com/first-page')
45
+ #
46
+ # @param page_url [String] the page URL
47
+ # @param page_title [String] the title of the page
48
+ # @param referrer [String] the URL of the previous page
49
+ def initialize(page_url: nil, page_title: nil, referrer: nil)
50
+ @details = { 'url' => page_url,
51
+ 'page' => page_title,
52
+ 'refr' => referrer }
53
+ end
54
+ end
55
+ end