snowplow-tracker-0.8.0/0000755000175100017510000000000014400614605014066 5ustar pravipravisnowplow-tracker-0.8.0/LICENSE-2.0.txt0000644000175100017510000002613514400614605016215 0ustar pravipravi Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.snowplow-tracker-0.8.0/lib/0000755000175100017510000000000014400614605014634 5ustar pravipravisnowplow-tracker-0.8.0/lib/snowplow-tracker/0000755000175100017510000000000014400614605020155 5ustar pravipravisnowplow-tracker-0.8.0/lib/snowplow-tracker/self_describing_json.rb0000644000175100017510000001143614400614605024662 0ustar pravipravi# Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. # # This program is licensed to you under the Apache License Version 2.0, # and you may not use this file except in compliance with the Apache License Version 2.0. # You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. # # Unless required by applicable law or agreed to in writing, # software distributed under the Apache License Version 2.0 is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. # Author:: Snowplow Analytics Ltd # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd # License:: Apache License Version 2.0 module SnowplowTracker # Creates the self-describing JSONs necessary for sending context or # self-describing events. These are a specific kind of [JSON # schema](http://json-schema.org/). # # A unique schema can be designed for each type of event or entity (for event # context) that you want to track. This allows you to track the specific # things that are important to you, in a way that is defined by you. # # A self-describing JSON has two keys, `schema` and `data`. The `schema` value # should point to a valid self-describing JSON schema. They are called # self-describing because the schema will specify the fields allowed in the # `data` value. After events have been collected by the event collector, they # are validated to ensure that the self-describing JSONs are correct. Mistakes # (e.g. extra fields, or incorrect types) will result in events being # processed as Bad Events. # # A SelfDescribingJson is initialized with `schema` and `data` as separate # arguments. These parameters are combined into a complete self-describing # JSON during the event creation, which is stringified and sent as part of the # event. By default, they will be sent base64-encoded. This can be changed on # {Tracker} initialization. # # The `data` argument must be a flat hash of key-value pairs. Either strings # or symbols are accepted as keys. The `schema` argument must be a correctly # formatted schema ID. # # When used to send event context data, stringified self-describing JSONs will # be sent in the raw event as `cx`, or `co` if not encoded. Whether encoded or # not, these strings will be converted back to JSON within the `contexts` # parameter of the processed event. All the event context is contained within # this one parameter, even if multiple context entities were sent. # # Self-describing JSONs in self-describing events are sent in a similar # manner. They are sent as `ue_px` in the raw event, or `ue_pr` if not # encoded. This is processed into the `unstruct_event` parameter of the # finished event. # # @example # # This example schema describes an ad_click event. # # It defines a single property for that event type, a "bannerId". # # { # "$schema": "http://json-schema.org/schema#", # "self": { # "vendor": "com.snowplowanalytics", # "name": "ad_click", # "format": "jsonschema", # "version": "1-0-0" # }, # "type": "object", # "properties": { # "bannerId": { # "type": "string" # } # }, # "required": ["bannerId"], # "additionalProperties": false # } # # # Creating the SelfDescribingJson # schema_name = "iglu:com.snowplowanalytics/ad_click/jsonschema/1-0-0" # event_data = { bannerId: "4acd518feb82" } # SnowplowTracker::SelfDescribingJson.new(schema_name, event_data) # # # The self-describing JSON that will be sent (stringified) with the event # { # "schema": "iglu:com.snowplowanalytics/ad_click/jsonschema/1-0-0", # "data": { # "bannerId": "4acd518feb82" # } # } # # @api public # @see # https://docs.snowplowanalytics.com/docs/understanding-tracking-design/understanding-schemas-and-validation/ # introduction to Snowplow schemas # @see # https://docs.snowplowanalytics.com/docs/pipeline-components-and-applications/iglu/ # schema management using Iglu # @see # https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol # the Snowplow Tracker Protocol class SelfDescribingJson # @param schema [String] schema identifier # @param data [Hash] a flat hash that matches the description in the schema def initialize(schema, data) @schema = schema @data = data end # make the self-describing JSON out of the instance variables # @private def to_json { schema: @schema, data: @data } end end end snowplow-tracker-0.8.0/lib/snowplow-tracker/payload.rb0000644000175100017510000000412614400614605022136 0ustar pravipravi# Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. # # This program is licensed to you under the Apache License Version 2.0, # and you may not use this file except in compliance with the Apache License Version 2.0. # You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. # # Unless required by applicable law or agreed to in writing, # software distributed under the Apache License Version 2.0 is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. # Author:: Snowplow Analytics Ltd # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd # License:: Apache License Version 2.0 require 'base64' require 'json' require 'net/http' module SnowplowTracker # @private # Every`track_x_event` method creates a new Payload object. The Tracker then # uses the Payload instance methods to add properties to the Payload `@data` # hash. These properties form the raw event, after the completed hash is # given to the Emitter. class Payload attr_reader :data def initialize @data = {} end # Add a single name-value pair to @data. def add(name, value) @data[name] = value if (value != '') && !value.nil? end # Add each name-value pair in a hash to @data. def add_hash(hash) hash.each { |key, value| add(key, value) } end # Stringify a JSON and add it to @data. # # In practice, the JSON provided will be a SelfDescribingJson. This method # is used to add context to events, or for `track_self_describing_event`. # @see Tracker#track_unstruct_event # @see Tracker#finalise_payload def add_json(json, encode_base64, type_when_encoded, type_when_not_encoded) return if json.nil? stringified = JSON.generate(json) if encode_base64 add(type_when_encoded, Base64.strict_encode64(stringified)) else add(type_when_not_encoded, stringified) end end end end snowplow-tracker-0.8.0/lib/snowplow-tracker/timestamp.rb0000644000175100017510000001156214400614605022512 0ustar pravipravi# Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. # # This program is licensed to you under the Apache License Version 2.0, # and you may not use this file except in compliance with the Apache License Version 2.0. # You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. # # Unless required by applicable law or agreed to in writing, # software distributed under the Apache License Version 2.0 is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. # Author:: Snowplow Analytics Ltd # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd # License:: Apache License Version 2.0 module SnowplowTracker # Creates timestamps for events. Timestamps are counted in milliseconds since # the Unix epoch (`(Time.now.to_f * 1000).to_i`). # # Snowplow events accrue timestamps as they are processed. When an event is # first generated by a {Tracker}, it has the raw event property `dtm` # ("device timestamp"; `dvce_created_tstamp` in the processed event) # or `ttm` ("true timestamp";`true_tstamp` in the processed event). These two # timestamps are set using the Timestamp subclasses {DeviceTimestamp} and # {TrueTimestamp}. The {Emitter} adds a `stm` ("sent timestamp"; # `dvce_sent_tstamp`) property to the event just before sending it to the # collector. These timestamps are all processed into UTC time. # # Events can have either a device timestamp or a true timestamp, not both. By # default, the `#track_x_event` methods create a DeviceTimestamp. In some # circumstances, the device timestamp may be inaccurate. There are three # methods to override the default device timestamp value when the event is # created. # # 1. Provide your own calculated timestamp, as a Num (e.g. `1633596554978`), # to the `#track_x_event` method. This will be converted into a # DeviceTimestamp by the Tracker, and will still be recorded as `dtm` in # the event. # 2. Manually create a DeviceTimestamp (e.g. # `SnowplowTracker::DeviceTimestamp.new(1633596554978)`), and provide this to the # `#track_x_event` method. This will still be recorded as `dtm` in the # event. # 3. Provide a TrueTimestamp object to the `track_x_event` method (e.g. # `SnowplowTracker::TrueTimestamp.new(1633596554978)`). This will result in a `ttm` field in # the event. # # The timestamps that are added to the event once it has been emitted are not # the responsibility of this class. The collector receives the event and adds a # `collector_tstamp`. A later part of the pipeline adds the `etl_tstamp` when # the event enrichment has finished. # # When DeviceTimestamp is used, a `derived_tstamp` is also calculated and # added to the event. This timestamp attempts to take latency and possible # inaccuracy of the device clock into account. It is calculated by # `collector_tstamp - (dvce_sent_tstamp - dvce_created_tstamp)`. When # TrueTimestamp is used, the `derived_stamp` will be the same as # `true_tstamp`. # # @see # https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol # the Snowplow Tracker Protocol # @see # https://discourse.snowplowanalytics.com/t/which-timestamp-is-the-best-to-see-when-an-event-occurred/538 # A Discourse forums post explaining timestamps # @api public class Timestamp # @private attr_reader :type # @private attr_reader :value # @private def initialize(type, value) @type = type @value = value end # Calculates time since the Unix epoch. # @private def self.create (Time.now.to_f * 1000).to_i end end # @see Timestamp # A very simple class that stores a timestamp, i.e. a numeric value # representing milliseconds since the Unix epoch, and which type of timestamp # it is, namely `ttm`. This raw event `ttm` field will be processed into # `true_tstamp` in the completed event. class TrueTimestamp < Timestamp # @param [Num] value timestamp in milliseconds since the Unix epoch # @example # SnowplowTracker::TrueTimestamp.new(1633596346786) def initialize(value) super 'ttm', value end end # @see Timestamp # A very simple class that stores a timestamp, i.e. a numeric value # representing milliseconds since the Unix epoch, and which type of timestamp # it is, namely `dtm`. This raw event `dtm` field will be processed into # `dvce_created_tstamp` in the completed event. class DeviceTimestamp < Timestamp # @param [Num] value timestamp in milliseconds since the Unix epoch # @example # SnowplowTracker::DeviceTimestamp.new(1633596346786) def initialize(value) super 'dtm', value end end end snowplow-tracker-0.8.0/lib/snowplow-tracker/emitters.rb0000644000175100017510000004232614400614605022345 0ustar pravipravi# Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. # # This program is licensed to you under the Apache License Version 2.0, # and you may not use this file except in compliance with the Apache License Version 2.0. # You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. # # Unless required by applicable law or agreed to in writing, # software distributed under the Apache License Version 2.0 is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. # Author:: Snowplow Analytics Ltd # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd # License:: Apache License Version 2.0 require 'net/https' require 'set' require 'logger' module SnowplowTracker # @see Emitter # For logging Emitter activity messages LOGGER = Logger.new(STDERR) LOGGER.level = Logger::INFO # This class sends events to the event collector. All {Tracker}s must have at # least one associated Emitter or the subclass AsyncEmitter. # # The network settings are defined as part of the Emitter initalization. This # table displays the default Emitter settings: # # | Property | Default setting | # | --- | --- | # | Protocol | HTTP | # | Method | GET | # | Buffer size | 1 | # | Path | `/i` | # # The buffer size is the number of events which will be buffered before they # are all sent simultaneously. The process of sending all buffered events is # called "flushing". The default buffer size is 1 because GET requests can # only contain one event. # # If you choose to use POST requests, the buffer_size defaults to 10, and the # buffered events are all sent together in a single request. The default path # is '/com.snowplowanalytics.snowplow/tp2' for Emitters using POST. # # # Logging # Emitters log their activity to STDERR by default, using the Ruby standard # library Logger class. A different logger can be configured during Emitter # initialization. For example, to disable logging, you could provide # `Logger.new(IO::NULL)` in the options hash. # # By default, only messages with priority "INFO" or higher will be logged. # This can be changed at any time for the default logger, which is saved as a # module constant (`LOGGER = Logger.new(STDERR)`). If you are not using the # default logger, set the message level before initializing your Emitter. # # @see https://ruby-doc.org/stdlib-2.7.2/libdoc/logger/rdoc/Logger.html Logger documentation # # @example Changing the logger message level. # require 'logger' # SnowplowTracker::LOGGER.level = Logger::DEBUG class Emitter # Default Emitter settings DEFAULT_CONFIG = { protocol: 'http', method: 'get' } # @private attr_reader :logger # Create a new Emitter instance. The endpoint is required. # # @example Initializing an Emitter with all the possible extra configuration. # success_callback = ->(success_count) { puts "#{success_count} events sent successfully" } # failure_callback = ->(success_count, failures) do # puts "#{success_count} events sent successfully, #{failures.size} sent unsuccessfully" # end # # SnowplowTracker::Emitter.new(endpoint: 'collector.example.com', # options: { path: '/my-pipeline/1', # protocol: 'https', # port: 443, # method: 'post', # buffer_size: 5, # on_success: success_callback, # on_failure: failure_callback, # logger: Logger.new(STDOUT) }) # # The options hash can have any of these optional parameters: # # | Parameter | Description | Type | # | --- | --- | --- | # | path | Override the default path for appending to the endpoint | String | # | protocol | 'http' or 'https' | String | # | port | The port for the connection | Integer | # | method | 'get' or 'post' | String | # | buffer_size | Number of events to send at once | Integer | # | on_success | A method to call if events were sent successfully | Method | # | on_failure | A method to call if events did not send | Method | # | thread_count | Number of threads to use | Integer | # | logger | Log somewhere other than STDERR | Logger | # # Note that `thread_count` is relevant only to the subclass {AsyncEmitter}, # and will be ignored if provided to an Emitter. # # If you choose to use HTTPS, we recommend using port 443. # # Only 2xx and 3xx status codes are considered successes. # # The `on_success` callback should accept one argument: the number of # requests sent this way. The `on_failure` callback should accept two # arguments: the number of successfully sent events, and an array containing # the unsuccessful events. # # @param endpoint [String] the endpoint to send the events to # @param options [Hash] allowed configuration options # # @see AsyncEmitter#initialize # @api public def initialize(endpoint:, options: {}) config = DEFAULT_CONFIG.merge(options) @lock = Monitor.new path = confirm_path(config) @collector_uri = create_collector_uri(endpoint, config[:protocol], config[:port], path) @buffer = [] @buffer_size = confirm_buffer_size(config) @method = config[:method] @on_success = config[:on_success] @on_failure = config[:on_failure] @logger = config[:logger] || LOGGER logger.info("#{self.class} initialized with endpoint #{@collector_uri}") end # Creates the `@buffer_size` variable during initialization. Unless # otherwise defined, it's 1 for Emitters using GET and 10 for Emitters using # POST requests. # @private def confirm_buffer_size(config) return config[:buffer_size] unless config[:buffer_size].nil? config[:method] == 'get' ? 1 : 10 end # Creates the `@path` variable during initialization. Allows a non-standard # path to be provided. # @private def confirm_path(config) return config[:path] unless config[:path].nil? config[:method] == 'get' ? '/i' : '/com.snowplowanalytics.snowplow/tp2' end # Creates the `@collector_uri` variable during initialization. # The default is "http://{endpoint}/i". # @private def create_collector_uri(endpoint, protocol, port, path) port_string = port.nil? ? '' : ":#{port}" "#{protocol}://#{endpoint}#{port_string}#{path}" end # Add an event to the buffer and flush it if maximum size has been reached. # This method is not required for standard Ruby tracker usage. A {Tracker} # privately calls this method once the event payload is ready to send. # # We have included it as part of the public API for its possible use in the # `on_failure` callback. This is the optional method, provided in the # `options` Emitter initalization hash, that is called when events fail # to send. You could use {#input} as part of your callback to immediately # retry the failed event. # # The `on_failure` callback should accept two arguments: the number of # successfully sent events, and an array containing the unsuccessful events. # # @example A possible `on_failure` method using `#input` # def retry_on_failure(failed_event_count, failed_events) # # possible backoff-and-retry timeout here # failed_events.each do |event| # my_emitter.input(event) # end # end # # @api public def input(payload) payload.each { |k, v| payload[k] = v.to_s } @lock.synchronize do @buffer.push(payload) flush if @buffer.size >= @buffer_size end nil end # Flush the Emitter, forcing it to send all the events in its # buffer, even if the buffer is not full. {Emitter} objects, unlike # {AsyncEmitter}s, can only `flush` synchronously. A {Tracker} can manually flush all # its Emitters by calling {Tracker#flush}, part of the public API which # calls this method. # # The unused async parameter here is to avoid ArgumentError, since # {AsyncEmitter#flush} does take an argument. # # @see AsyncEmitter#flush # @private def flush(_async = true) @lock.synchronize do send_requests(@buffer) @buffer = [] end nil end # Send all events in the buffer to the collector # @private def send_requests(events) if events.empty? logger.info('Skipping sending events since buffer is empty') return end logger.info("Attempting to send #{events.size} request#{events.size == 1 ? '' : 's'}") events.each do |event| # add the sent timestamp, overwrite if already exists event['stm'] = Timestamp.create.to_s end if @method == 'post' send_requests_with_post(events) elsif @method == 'get' send_requests_with_get(events) end nil end # Part of {#send_requests}. # @private def send_requests_with_post(events) post_succeeded = false begin request = http_post(SelfDescribingJson.new( 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4', events ).to_json) post_succeeded = good_status_code?(request.code) rescue StandardError => standard_error logger.warn(standard_error) end if post_succeeded @on_success.call(events.size) unless @on_success.nil? else @on_failure.call(0, events) unless @on_failure.nil? end nil end # Part of {#send_requests}. # @private def send_requests_with_get(events) success_count = 0 unsent_requests = [] events.each do |event| request = process_get_event(event) request ? success_count += 1 : unsent_requests << event end if unsent_requests.size.zero? @on_success.call(success_count) unless @on_success.nil? else @on_failure.call(success_count, unsent_requests) unless @on_failure.nil? end nil end # Part of {#send_requests_with_get}. # @private def process_get_event(event) get_succeeded = false begin request = http_get(event) get_succeeded = good_status_code?(request.code) rescue StandardError => standard_error logger.warn(standard_error) end get_succeeded end # Part of {#process_get_event}. This sends a GET request. # @private def http_get(payload) destination = URI(@collector_uri + '?' + URI.encode_www_form(payload)) logger.info("Sending GET request to #{@collector_uri}...") logger.debug("Payload: #{payload}") http = Net::HTTP.new(destination.host, destination.port) request = Net::HTTP::Get.new(destination.request_uri) http.use_ssl = true if destination.scheme == 'https' response = http.request(request) logger.add(good_status_code?(response.code) ? Logger::INFO : Logger::WARN) do "GET request to #{@collector_uri} finished with status code #{response.code}" end response end # Part of {#send_requests_with_post}. This sends a POST request. # @private def http_post(payload) logger.info("Sending POST request to #{@collector_uri}...") logger.debug("Payload: #{payload}") destination = URI(@collector_uri) http = Net::HTTP.new(destination.host, destination.port) request = Net::HTTP::Post.new(destination.request_uri) http.use_ssl = true if destination.scheme == 'https' request.body = payload.to_json request.set_content_type('application/json; charset=utf-8') response = http.request(request) logger.add(good_status_code?(response.code) ? Logger::INFO : Logger::WARN) do "POST request to #{@collector_uri} finished with status code #{response.code}" end response end # Check if the response is good. # Only 2xx and 3xx status codes are considered successes. # @private def good_status_code?(status_code) status_code.to_i >= 200 && status_code.to_i < 400 end private :create_collector_uri, :http_get, :http_post end # This {Emitter} subclass provides asynchronous event sending. Whenever the # buffer is flushed, the AsyncEmitter places the flushed events in a work # queue. The AsyncEmitter asynchronously sends events in this queue using a # thread pool of a fixed size. The size of the thread pool is 1 by default, # but can be configured as part of the options hash during initialization. # # @see Emitter # @api public class AsyncEmitter < Emitter # Create a new AsyncEmitter object. The endpoint is required. # # @example Initializing an AsyncEmitter with all the possible extra configuration. # success_callback = ->(success_count) { puts "#{success_count} events sent successfully" } # failure_callback = ->(success_count, failures) do # puts "#{success_count} events sent successfully, #{failures.size} sent unsuccessfully" # end # # SnowplowTracker::Emitter.new(endpoint: 'collector.example.com', # options: { path: '/my-pipeline/1', # protocol: 'https', # port: 443, # method: 'post', # buffer_size: 5, # on_success: success_callback, # on_failure: failure_callback, # logger: Logger.new(STDOUT), # thread_count: 5 }) # # The options hash can have any of these optional parameters: # # | Parameter | Description | Type | # | --- | --- | --- | # | path | Override the default path for appending to the endpoint | String | # | protocol | 'http' or 'https' | String | # | port | The port for the connection | Integer | # | method | 'get' or 'post' | String | # | buffer_size | Number of events to send at once | Integer | # | on_success | A function to call if events were sent successfully | Function | # | on_failure | A function to call if events did not send | Function | # | thread_count | Number of threads to use | Integer | # | logger | Log somewhere other than STDERR | Logger | # # The `thread_count` determines the number of worker threads which will be # used to send events. # # If you choose to use HTTPS, we recommend using port 443. # # Only 2xx and 3xx status codes are considered successes. # # The `on_success` callback should accept one argument: the number of # requests sent this way. The `on_failure` callback should accept two # arguments: the number of successfully sent events, and an array containing # the unsuccessful events. # # @note if you test the AsyncEmitter by using a short script to send an # event, you may find that the event fails to send. This is because the # process exits before the flushing thread is finished. You can get round # this either by adding a sleep(10) to the end of your script or by using # the synchronous flush. # # @param endpoint [String] the endpoint to send the events to # @param options [Hash] allowed configuration options # # @see Emitter#initialize # @api public def initialize(endpoint:, options: {}) @queue = Queue.new # @all_processed_condition and @results_unprocessed are used to emulate Python's Queue.task_done() @queue.extend(MonitorMixin) @all_processed_condition = @queue.new_cond @results_unprocessed = 0 (options[:thread_count] || 1).times { Thread.new { consume } } super(endpoint: endpoint, options: options) end # AsyncEmitters use the MonitorMixin module, which provides the # `synchronize` and `broadcast` methods. # @private def consume loop do work_unit = @queue.pop send_requests(work_unit) @queue.synchronize do @results_unprocessed -= 1 @all_processed_condition.broadcast end end end # Flush the Emitter, forcing it to send all the events in its buffer, even # if the buffer is not full. # # If `async` is true (the default), events are sent even if the queue is not # empty. If `async` is false, it blocks until all queued events have been # sent. Note that this method can be called by public API method # {Tracker#flush}, which has a default of `async` being false. # # @param async [Bool] whether to flush asynchronously or not # # @see Emitter#flush # @private def flush(async = true) loop do @lock.synchronize do @queue.synchronize { @results_unprocessed += 1 } @queue << @buffer @buffer = [] end unless async logger.info('Starting synchronous flush') @queue.synchronize do @all_processed_condition.wait_while { @results_unprocessed > 0 } logger.info('Finished synchronous flush') end end break if @buffer.empty? end end end end snowplow-tracker-0.8.0/lib/snowplow-tracker/version.rb0000644000175100017510000000344214400614605022172 0ustar pravipravi# Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. # # This program is licensed to you under the Apache License Version 2.0, # and you may not use this file except in compliance with the Apache License Version 2.0. # You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. # # Unless required by applicable law or agreed to in writing, # software distributed under the Apache License Version 2.0 is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. # Author:: Snowplow Analytics Ltd # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd # License:: Apache License Version 2.0 # The Snowplow Ruby Tracker allows you to track Snowplow events in your Ruby # applications and gems and Ruby on Rails web applications. # # It is compatible with Ruby versions 2.1 to 3.0+. # # See the [demo Rails # app](https://github.com/snowplow-incubator/snowplow-ruby-tracker-examples) to # see an example of how to incorporate the Snowplow Ruby tracker in Ruby on # Rails app. # # @see https://github.com/snowplow/snowplow-ruby-tracker # Ruby tracker on Github # @see https://github.com/snowplow-incubator/snowplow-ruby-tracker-examples # Snowplow Ruby tracker examples in a demo Rails app # @see https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/ # Snowplow documentation # @see https://snowplowanalytics.com/ Snowplow homepage # @api public module SnowplowTracker # The version of Ruby Snowplow tracker you are using VERSION = '0.8.0' # All events from this tracker will have this string TRACKER_VERSION = "rb-#{VERSION}" end snowplow-tracker-0.8.0/lib/snowplow-tracker/tracker.rb0000644000175100017510000006170014400614605022141 0ustar pravipravi# Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. # # This program is licensed to you under the Apache License Version 2.0, # and you may not use this file except in compliance with the Apache License Version 2.0. # You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. # # Unless required by applicable law or agreed to in writing, # software distributed under the Apache License Version 2.0 is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. # Author:: Snowplow Analytics Ltd # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd # License:: Apache License Version 2.0 require 'securerandom' require 'set' module SnowplowTracker # Allows the tracking of events. The tracker accepts event properties to its # various `track_x_event` methods, and creates an appropriate event payload. # This payload is passed to one or more Emitters for sending to the event # collector. # # A Tracker is always associated with one {Subject}, and one or more # {Emitter}. The Subject object stores information about the user, and will be # generated automatically if one is not provided during initialization. It can # be swapped out for another Subject using {#set_subject}. # # Tracker objects can access the methods of their associated {Subject}, e.g. # {#set_user_id}. # # The Emitter, or an array of Emitters, must be given during initialization. # They will send the prepared events to the event collector. It's possible to # add further Emitters to an existing Tracker, using {#add_emitter}. However, # Emitters cannot be removed from Trackers. # # At initialization, two Tracker parameters can be set which will be added to # all events. The first is the Tracker namespace. This is especially useful to # distinguish between events from different Trackers, if more than one is # being used. The namespace value will be sent as the `tna` field in the raw # event, mapping to `name_tracker` in the processed event. # # The second user-set Tracker property is the app ID (`aid`; `app_id`). This # is the unique identifier for the site or application, and is particularly # useful for distinguishing between events when Snowplow tracking has been # implemented in multiple apps. # # The final initialization parameter is a setting for the base64-encoding of # any JSONs in the event payload. These will be the {SelfDescribingJson}s used # to provide context to events, or in the {#track_self_describing_event} # method. The default is for JSONs to be encoded. Once the Tracker has been # instantiated, it is not possible to change this setting. # # # Tracking events # # The Tracker `#track_x_event` methods all work similarly. An event payload is # created containing the relevant properties, which is passed to an {Emitter} # for sending. All payloads have a unique event ID (`event_id`) added to them # (a type-4 UUID created using the SecureRandom module). This is sent as the # `eid` field in the raw event. # # The Ruby tracker provides the ability to track multiple types of events # out-of-the-box. The `#track_x_event` methods range from single purpose # methods, such as {#track_page_view}, to the more complex but flexible # {#track_self_describing_event}, which can be used to track any kind of # event. We strongly recommend using {#track_self_describing_event} for your # tracking, as it allows you to design custom event types to match your # business requirements. # # This table gives the event type in the raw and processed events, defined in # the Snowplow Tracker Protocol. This is the `e` or `event` parameter. Note # that {#track_screen_view} calls {#track_self_describing_event} behind the # scenes, resulting in a `ue` event type. # #
# # | Tracker method | `e` (raw) | `event` (processed) | # | --- | --- | --- | # | {#track_self_describing_event} | `ue` | `unstruct` | # | {#track_struct_event} | `se` | `struct` | # | {#track_page_view} | `pv` | `page_view` | # | {#track_ecommerce_transaction} | `tr` and `ti` | `transaction` and `transaction_item` | # | {#track_screen_view} | `ue` | `unstruct` | # #
# # The name `ue`, "unstructured event", is partially depreciated. This event # type was originally created as a counterpart to "structured event", but the # name is misleading. An `unstruct` event requires a schema ruleset and # therefore can be considered more structured than a `struct` event. We prefer # the name "self-describing event", after the {SelfDescribingJson} schema. # Changing the event name in the Tracker Protocol would be a breaking change, # so for now the self-describing events are still sent as "unstruct". # # All the `#track_x_event` methods share common features and parameters. Every # type of event can have an optional context, {Subject}, and {Page} added. A # {Timestamp} can also be provided for all event types to override the default # event timestamp. # # [Event # context](https://docs.snowplowanalytics.com/docs/understanding-tracking-design/understanding-events-entities/) # can be provided as an array of {SelfDescribingJson}. Each element of the # array is called an entity. Contextual entities can be used to describe the # setting in which the event occurred. For example, a "user" entity could be # created and attached to all events from each user. For a search event, # entities could be attached for each of the search results. The Ruby tracker # does not automatically add any event context. This is in contrast to the # [Snowplow JavaScript Tracker](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-trackers/), # which automatically attaches a "webpage" entity to every event that it tracks, # containing a unique ID for that loaded page. # # @see Subject # @see Emitter # @see # https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol # the Snowplow Tracker Protocol # @see # https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/tracking-events/ # the Snowplow docs page about tracking events # @see # https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/enriching-your-events/ # the Snowplow docs page about adding context and other extra data to events # @see # https://docs.snowplowanalytics.com/docs/understanding-tracking-design/introduction-to-tracking-design/ # introduction to Snowplow tracking design # @api public class Tracker # @!group Public constants # SelfDescribingJson objects are sent encoded by default DEFAULT_ENCODE_BASE64 = true # @private BASE_SCHEMA_PATH = 'iglu:com.snowplowanalytics.snowplow' # @private SCHEMA_TAG = 'jsonschema' # @private CONTEXT_SCHEMA = "#{BASE_SCHEMA_PATH}/contexts/#{SCHEMA_TAG}/1-0-1" # @private UNSTRUCT_EVENT_SCHEMA = "#{BASE_SCHEMA_PATH}/unstruct_event/#{SCHEMA_TAG}/1-0-0" # @private SCREEN_VIEW_SCHEMA = "#{BASE_SCHEMA_PATH}/screen_view/#{SCHEMA_TAG}/1-0-0" # @!endgroup # Create a new Tracker. `emitters` is the only strictly required parameter. # # @param emitters [Emitter, Array] one or more Emitter objects # @param subject [Subject] a Subject object # @param namespace [String] a name for the Tracker # @param app_id [String] the app ID # @param encode_base64 [Bool] whether JSONs will be base64-encoded or not # @example Initializing a Tracker with all possible options # SnowplowTracker::Tracker.new( # emitters: SnowplowTracker::Emitter.new(endpoint: 'collector.example.com'), # subject: SnowplowTracker::Subject.new, # namespace: 'tracker_no_encode', # app_id: 'rails_main', # encode_base64: false # ) # @api public # # @note All the Tracker instance methods return the Tracker object, allowing # method chaining, e.g. # `SnowplowTracker::Tracker.new.set_user_id('12345').track_page_view(page_url: 'www.example.com` def initialize(emitters:, subject: nil, namespace: nil, app_id: nil, encode_base64: DEFAULT_ENCODE_BASE64) @emitters = Array(emitters) @subject = if subject.nil? Subject.new else subject end @settings = { 'tna' => namespace, 'tv' => TRACKER_VERSION, 'aid' => app_id } @encode_base64 = encode_base64 end # @!method set_color_depth(depth) # call {Subject#set_color_depth} # @!method set_domain_session_id(sid) # call {Subject#set_domain_session_id} # @!method set_domain_session_idx(vid) # call {Subject#set_domain_session_idx} # @!method set_domain_user_id(duid) # call {Subject#set_domain_user_id} # @!method set_fingerprint(fingerprint) # call {Subject#set_fingerprint} # @!method set_ip_address(ip) # call {Subject#set_ip_address} # @!method set_lang(lang) # call {Subject#set_lang} # @!method set_network_user_id(nuid) # call {Subject#set_network_user_id} # @!method set_platform(platform) # call {Subject#set_platform} # @!method set_screen_resolution(width:, height:) # call {Subject#set_screen_resolution} # @!method set_timezone(timezone) # call {Subject#set_timezone} # @!method set_user_id(user_id) # call {Subject#set_user_id} # @!method set_useragent(useragent) # call {Subject#set_useragent} # @!method set_viewport(width:, height:) # call {Subject#set_viewport} Subject.instance_methods(false).each do |name| if RUBY_VERSION >= '3.0.0' define_method name, ->(*args, **kwargs) do @subject.method(name.to_sym).call(*args, **kwargs) self end else define_method name, ->(*args) do @subject.method(name.to_sym).call(*args) self end end end # Generates a type-4 UUID to identify this event # @private def event_id SecureRandom.uuid end # Builds a single self-describing JSON from an array of custom contexts # @private def build_context(context) SelfDescribingJson.new( CONTEXT_SCHEMA, context.map(&:to_json) ).to_json end # Sends the payload hash as a request to the Emitter(s) # @private def track(payload) @emitters.each { |emitter| emitter.input(payload.data) } nil end # Ensures that either a DeviceTimestamp or TrueTimestamp is associated with # every event. # @private def process_tstamp(tstamp) tstamp = Timestamp.create if tstamp.nil? tstamp = DeviceTimestamp.new(tstamp) if tstamp.is_a? Numeric tstamp end # Attaches the more generic fields to the event payload. This includes # context, Subject, and Page if they are present. The timestamp is added, as # well as all fields from `@settings`. # # Finally, the Tracker generates and attaches an event ID. # @private def finalise_payload(payload, context, tstamp, event_subject, page) payload.add_json(build_context(context), @encode_base64, 'cx', 'co') unless context.nil? || context.empty? payload.add_hash(page.details) unless page.nil? if event_subject.nil? payload.add_hash(@subject.details) else payload.add_hash(@subject.details.merge(event_subject.details)) end payload.add(tstamp.type, tstamp.value) payload.add_hash(@settings) payload.add('eid', event_id) nil end # Track a visit to a page. # @example # SnowplowTracker::Tracker.new.track_page_view(page_url: 'www.example.com', # page_title: 'example', # referrer: 'www.referrer.com') # # @param page_url [String] the URL of the page # @param page_title [String] the page title # @param referrer [String] the URL of the referrer page # @param context [Array] an array of SelfDescribingJson objects # @param tstamp [DeviceTimestamp, TrueTimestamp, Num] override the default DeviceTimestamp of the event # @param subject [Subject] event-specific Subject object # @param page [Page] override the page_url, page_title, or referrer # # @api public def track_page_view(page_url:, page_title: nil, referrer: nil, context: nil, tstamp: nil, subject: nil, page: nil) tstamp = process_tstamp(tstamp) payload = Payload.new payload.add('e', 'pv') payload.add('url', page_url) payload.add('page', page_title) payload.add('refr', referrer) finalise_payload(payload, context, tstamp, subject, page) track(payload) self end # Track an eCommerce transaction, and all the items in it. # # This method is unique in sending multiple events: one `transaction` event, # and one `transaction_item` event for each item. If Subject or Page objects # are provided, their parameters will be merged into both `transaction` and # `transaction_item` events. The timestamp and event ID of the # `transaction_item` events will always be the same as the `transaction`. # Transaction items are also automatically populated with the `order_id` and # `currency` fields from the transaction. # # Event context is handled differently for `transaction` and # `transaction_item` events. A context array argument provided to this # method will be attached to the `transaction` event only. To attach a # context array to a transaction item, use the key "context" in the item # hash. # # The transaction and item hash arguments must contain the correct keys, as # shown in the tables below. # # | Transaction fields | Description | Required? | Type | # | --- | --- | --- | --- | # | order_id | ID of the eCommerce transaction | Yes | String | # | total_value | Total transaction value | Yes | Num | # | affiliation | Transaction affiliation | No | String | # | tax_value | Transaction tax value | No | Num | # | shipping | Delivery cost charged | No | Num | # | city | Delivery address city | No | String | # | state | Delivery address state | No | String | # | country | Delivery address country | No | String | # | currency | Transaction currency | No | String | # #
# # | Item fields | Description | Required? | Type | # | --- | --- | --- | --- | # | sku | Item SKU | Yes | String | # | price | Item price | Yes | Num | # | quantity | Item quantity | Yes | Integer | # | name | Item name | No | String | # | category | Item category | No | String | # | context | Item event context | No | Array[{SelfDescribingJson}] | # # @example Tracking a transaction containing two items # SnowplowTracker::Tracker.new.track_ecommerce_transaction( # transaction: { # 'order_id' => '12345', # 'total_value' => 49.99, # 'affiliation' => 'my_company', # 'tax_value' => 0, # 'shipping' => 0, # 'city' => 'Phoenix', # 'state' => 'Arizona', # 'country' => 'USA', # 'currency' => 'USD' # }, # items: [ # { # 'sku' => 'pbz0026', # 'price' => 19.99, # 'quantity' => 1 # }, # { # 'sku' => 'pbz0038', # 'price' => 15, # 'quantity' => 2, # 'name' => 'crystals', # 'category' => 'magic' # } # ] # ) # # @param transaction [Hash] the correctly structured transaction hash # @param items [Array] an array of correctly structured item hashes # @param context [Array] an array of SelfDescribingJson objects # @param tstamp [DeviceTimestamp, TrueTimestamp, Num] override the default DeviceTimestamp of the event # @param subject [Subject] event-specific Subject object # @param page [Page] event-specific Page object # # @api public def track_ecommerce_transaction(transaction:, items:, context: nil, tstamp: nil, subject: nil, page: nil) tstamp = process_tstamp(tstamp) transform_keys(transaction) payload = Payload.new payload.add('e', 'tr') payload.add('tr_id', transaction['order_id']) payload.add('tr_tt', transaction['total_value']) payload.add('tr_af', transaction['affiliation']) payload.add('tr_tx', transaction['tax_value']) payload.add('tr_sh', transaction['shipping']) payload.add('tr_ci', transaction['city']) payload.add('tr_st', transaction['state']) payload.add('tr_co', transaction['country']) payload.add('tr_cu', transaction['currency']) finalise_payload(payload, context, tstamp, subject, page) track(payload) items.each do |item| transform_keys(item) item['tstamp'] = tstamp item['order_id'] = transaction['order_id'] item['currency'] = transaction['currency'] track_ecommerce_transaction_item(item, subject, page) end self end # Makes sure all hash keys are strings rather than symbols. # The Ruby core language added a method for this in Ruby 2.5. # @private def transform_keys(hash) hash.keys.each { |key| hash[key.to_s] = hash.delete key } end # Track a single item within an ecommerce transaction. # @private def track_ecommerce_transaction_item(details, subject, page) payload = Payload.new payload.add('e', 'ti') payload.add('ti_id', details['order_id']) payload.add('ti_sk', details['sku']) payload.add('ti_pr', details['price']) payload.add('ti_qu', details['quantity']) payload.add('ti_nm', details['name']) payload.add('ti_ca', details['category']) payload.add('ti_cu', details['currency']) finalise_payload(payload, details['context'], details['tstamp'], subject, page) track(payload) self end # Track a structured event. `category` and `action` are required. # # This event type can be used to track many types of user activity, as it is # somewhat customizable. This event type is provided particularly for # concordance with Google Analytics tracking, where events are structured by # "category", "action", "label", and "value". # # For fully customizable event tracking, we recommend you use # self-describing events. # # @example # SnowplowTracker::Tracker.new.track_struct_event( # category: 'shop', # action: 'add-to-basket', # property: 'pcs', # value: 2 # ) # # # @see #track_self_describing_event # # @param category [String] the event category # @param action [String] the action performed # @param label [String] an event label # @param property [String] an event property # @param value [Num] a value for the event # @param context [Array] an array of SelfDescribingJson objects # @param tstamp [DeviceTimestamp, TrueTimestamp, Num] override the default DeviceTimestamp of the event # @param subject [Subject] event-specific Subject object # @param page [Page] event-specific Page object # # @api public def track_struct_event(category:, action:, label: nil, property: nil, value: nil, context: nil, tstamp: nil, subject: nil, page: nil) tstamp = process_tstamp(tstamp) payload = Payload.new payload.add('e', 'se') payload.add('se_ca', category) payload.add('se_ac', action) payload.add('se_la', label) payload.add('se_pr', property) payload.add('se_va', value) finalise_payload(payload, context, tstamp, subject, page) track(payload) self end # Track a screen view event. Note that while the `name` and `id` parameters # are both optional, you must provided at least one of them to create a # valid event. # # This method creates an `unstruct` event, by creating a # {SelfDescribingJson} and calling {#track_self_describing_event}. The # schema ID for this is # "iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0", and # the data field will contain the name and/or ID. # # @example # SnowplowTracker::Tracker.new.track_screen_view(name: 'HUD > Save Game', # id: 'screen23') # # # @see #track_page_view # @see #track_self_describing_event # # @param name [String] the screen name (human readable) # @param id [String] the unique screen ID # @param context [Array] an array of SelfDescribingJson objects # @param tstamp [DeviceTimestamp, TrueTimestamp, Num] override the default DeviceTimestamp of the event # @param subject [Subject] event-specific Subject object # @param page [Page] event-specific Page object # # @api public def track_screen_view(name: nil, id: nil, context: nil, tstamp: nil, subject: nil, page: nil) screen_view_properties = {} screen_view_properties['name'] = name unless name.nil? screen_view_properties['id'] = id unless id.nil? event_json = SelfDescribingJson.new(SCREEN_VIEW_SCHEMA, screen_view_properties) track_unstruct_event(event_json: event_json, context: context, tstamp: tstamp, subject: subject, page: page) self end # Track a self-describing event. These are custom events based on # {SelfDescribingJson}, i.e. a JSON schema and a defined set of properties. # # This is useful for tracking specific or proprietary event types, or events # with unpredicable or frequently changing properties. # # This method creates an `unstruct` event type. It is actually an alias for # {#track_unstruct_event}, which is depreciated due to its unhelpful name. # # @example # self_desc_json = SnowplowTracker::SelfDescribingJson.new( # "iglu:com.example_company/save_game/jsonschema/1-0-2", # { # "saveId" => "4321", # "level" => 23, # "difficultyLevel" => "HARD", # "dlContent" => true # } # ) # # SnowplowTracker::Tracker.new.track_self_describing_event(event_json: self_desc_json) # # # @param event_json [SelfDescribingJson] a SelfDescribingJson object # @param context [Array] an array of SelfDescribingJson objects # @param tstamp [DeviceTimestamp, TrueTimestamp, Num] override the default DeviceTimestamp of the event # @param subject [Subject] event-specific Subject object # @param page [Page] event-specific Page object # # @api public def track_self_describing_event(event_json:, context: nil, tstamp: nil, subject: nil, page: nil) track_unstruct_event(event_json: event_json, context: context, tstamp: tstamp, subject: subject, page: page) end # @deprecated Use {#track_self_describing_event} instead. # # @api public def track_unstruct_event(event_json:, context: nil, tstamp: nil, subject: nil, page: nil) tstamp = process_tstamp(tstamp) payload = Payload.new payload.add('e', 'ue') envelope = SelfDescribingJson.new(UNSTRUCT_EVENT_SCHEMA, event_json.to_json) payload.add_json(envelope.to_json, @encode_base64, 'ue_px', 'ue_pr') finalise_payload(payload, context, tstamp, subject, page) track(payload) self end # Manually flush all events stored in all Tracker-associated Emitters. By # default, this happens synchronously. {Emitter}s can only send events # synchronously, while {AsyncEmitter}s can send either synchronously or # asynchronously. # # @param async [Bool] whether to flush asynchronously or not # # @api public def flush(async: false) @emitters.each do |emitter| emitter.flush(async) end self end # Replace the existing Tracker-associated Subject with the provided one. All # subsequent events will have the properties of the new Subject, unless they # are overriden by event-specific Subject parameters. # # @param subject [Subject] a Subject object # # @api public def set_subject(subject) @subject = subject self end # Add a new Emitter to the internal array of Tracker-associated Emitters. # # @param emitter [Emitter] an Emitter object # # @api public def add_emitter(emitter) @emitters.push(emitter) self end private :build_context, :track, :track_ecommerce_transaction_item end end snowplow-tracker-0.8.0/lib/snowplow-tracker/subject.rb0000644000175100017510000003472114400614605022150 0ustar pravipravi# Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. # # This program is licensed to you under the Apache License Version 2.0, # and you may not use this file except in compliance with the Apache License Version 2.0. # You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. # # Unless required by applicable law or agreed to in writing, # software distributed under the Apache License Version 2.0 is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. # Author:: Snowplow Analytics Ltd # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd # License:: Apache License Version 2.0 module SnowplowTracker # Subject objects store information about the user associated with the event, # such as their `user_id`, what type of device they used, or what size screen # that device had. Also, they store which platform the event occurred on - # e.g. server-side app, mobile, games console, etc. # # Subject parameters are saved into the tracked event as part of the 'atomic' # event properties, which have their own column in the eventual table of # events. For example, a Subject's `user_id` parameter will be sent as `uid` # in the raw event payload, ending up in the `user_id` column. These # parameters represent part of the [Snowplow Tracker # Protocol](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol), # which defines a Snowplow event. # # Each {Tracker} is initialized with a Subject. This means that every event by # default has the platform (`p` parameter in the raw event) `srv`: server-side # app. Platform is the only preset Subject parameter, which can be overriden # using the {#set_platform} method. All other parameters must be set manually. # This can be done directly on the Subject, or, if it is associated with a # Tracker, via the Tracker, which has access to all the methods of its # Subject. # # Your server-side code may not have access to all these parameters, or they # might not be useful to you. All the Subject parameters are optional, except # `platform`. # # @example Subject methods can be called from their associated Tracker # # Creating the components explicitly # emitter = SnowplowTracker::Emitter.new(endpoint: 'localhost') # subject = SnowplowTracker::Subject.new # tracker = SnowplowTracker::Tracker.new(emitters: emitter, subject: subject) # # # These lines are equivalent # subject.set_user_id('12345') # tracker.set_user_id('12345') # # # This would also be equivalent # emitter = SnowplowTracker::Emitter.new(endpoint: 'localhost') # subject = SnowplowTracker::Subject.new # subject.set_user_id('12345') # tracker = SnowplowTracker::Tracker.new(emitters: emitter, subject: subject) # # @example Adding properties to the auto-generated Tracker-associated Subject # # Creating the components # emitter = SnowplowTracker::Emitter.new(endpoint: 'localhost') # tracker = SnowplowTracker::Tracker.new(emitters: emitter) # # # Set Subject parameters via the Tracker # tracker.set_user_id('12345') # # Since many of the Subject parameters describe the user, different Subject # properties may often be desired for each event, if there are multiple users. # This can be achieved in one of three ways: # # 1. the properties of the Tracker-associated Subject can be overriden by the # properties of an event-specific Subject. A Subject can be added to any # Tracker `#track_x_event` method call, as one of the arguments. Remember to # set the platform for the event Subject if you're not using `srv`. # 2. the Tracker-associated Subject can be swapped for another Subject, using # the Tracker method {Tracker#set_subject}. # 3. the properties of the Tracker-associated Subject can be changed before # every `#track_x_event`, by calling the Subject methods via the Tracker. # # @see Tracker#set_subject # @see # https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol # the Snowplow Tracker Protocol # @see # https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/enriching-your-events/ # the Snowplow docs page about adding context and other extra data to events # @api public # # @note All the Subject instance methods return the Subject object, allowing # method chaining, e.g. # `SnowplowTracker::Subject.new.set_timezone('Europe/London').set_user_id('12345')` class Subject # @private DEFAULT_PLATFORM = 'srv' # @api public # # | `p` value | Platform | # | ---- | ---- | # | app | General App | # | cnsl | Games Console | # | iot | Internet of Things | # | mob | Mobile/Tablet | # | pc | Desktop/Laptop/Netbook | # | srv [DEFAULT] | Server-side App | # | tv | Connected TV | # | web | Web (including Mobile Web) | SUPPORTED_PLATFORMS = %w[app cnsl iot mob pc srv tv web] # Access the Subject parameters # @example # SnowplowTracker::Subject.new.set_user_id('12345').details # => {"p"=>"srv", "uid"=>"12345"} # @api public attr_reader :details # @api public def initialize @details = { 'p' => DEFAULT_PLATFORM } end # Set the platform to one of the supported platform values. # @note Value is sent in the event as `p` (raw event) or `platform` (processed event). # @see Subject::SUPPORTED_PLATFORMS # @param [String] platform a valid platform choice # @example # subject.set_platform('app') # @return self # @api public def set_platform(platform) raise "#{platform} is not a supported platform" unless SUPPORTED_PLATFORMS.include?(platform) @details['p'] = platform self end # Set the unique business-defined user ID for a user. # @note Value is sent in the event as `uid` (raw event) or `user_id` (processed event). # For example, an email address. # @example Example user IDs # # an email address # janet.bloggs@email.com # # # a username # janetabloggs2021 # # @param [String] user_id a unique user ID # @return self # @api public def set_user_id(user_id) @details['uid'] = user_id self end # Set a business-defined fingerprint for a user. # @note Value is sent in the event as `fp` (raw event) or `user_fingerprint` (processed event). # @param [Num] fingerprint a user fingerprint # @return self # @example # subject.set_fingerprint(4048966212) # @api public def set_fingerprint(fingerprint) @details['fp'] = fingerprint self end # Set the device screen resolution. # @note Value is sent in the event as `res` (raw event) or `dvce_screenheight` and `dvce_screenwidth` (processed event). # @param [Integer] width the screen width, in pixels (must be a positive integer) # @param [Integer] height the screen height, in pixels (must be a positive integer) # @return self # @example # subject.set_screen_resolution(width: 2880, height: 1800) # @api public def set_screen_resolution(width:, height:) @details['res'] = "#{width}x#{height}" self end # Set the dimensions of the current viewport. # @note Value is sent in the event as `vp` (raw event) or `br_viewwidth` and `br_viewheight` (processed event). # @param [Integer] width the viewport width, in pixels (must be a positive integer) # @param [Integer] height the viewport height, in pixels (must be a positive integer) # @return self # @example # subject.set_viewport(width: 1440, height: 762) # @api public def set_viewport(width:, height:) @details['vp'] = "#{width}x#{height}" self end # Set the color depth of the browser, in bits per pixel. # @note Value is sent in the event as `cd` (raw event) or `br_colordepth` (processed event). # @param [Num] depth the colour depth # @example # subject.set_color_depth(24) # @return self # @api public def set_color_depth(depth) @details['cd'] = depth self end # Set the timezone to that of the user's OS. # @note Value is sent in the event as `tz` (raw event) or `os_timezone` (processed event). # @example # subject.set_timezone('Africa/Lagos') # @param [String] timezone the timezone # @return self # @api public def set_timezone(timezone) @details['tz'] = timezone self end # Set the language. # @note Value is sent in the event as `lang` (raw event) or `br_lang` (processed event). # @example Setting the language to Spanish # subject.set_lang('es') # @param [String] lang the language being used on the device # @return self # @api public def set_lang(lang) @details['lang'] = lang self end # Set the domain user ID. # @note Value is sent in the event as `duid` (raw event) or `domain_userid` (processed event). # @see Subject#set_network_user_id # @see Subject#set_domain_session_id # @see Subject#set_domain_session_idx # @see https://github.com/simplybusiness/snowplow_ruby_duid/ snowplow_ruby_duid, a third party gem # @see https://github.com/snowplow-incubator/snowplow-ruby-tracker-examples # Ruby tracker example Rails app # @example # subject.set_domain_user_id('aeb1691c5a0ee5a6') # @param [String] duid the unique domain user ID # @return self # @api public # # The `domain_userid` is a client-side unique user ID, which is set by the # browser-based JavaScript tracker, and stored in a first party cookie # (cookie name: `_sp_id`). For stitching together client-side and # server-side events originating from the same user, the domain user ID can # be extracted from the cookie and set using this method. A third party gem, # [snowplow_ruby_duid](https://github.com/simplybusiness/snowplow_ruby_duid/), # has been created to help with this. # # @example Ruby on Rails: getting the domain_user_id from the cookie # # Assuming the Snowplow JavaScript has also been incorporated # # cookies are accessible only within a Controller # def snowplow_domain_userid # sp_cookie = cookies.find { |key, _value| key =~ /^_sp_id/ } # sp_cookie.last.split(".").first if sp_cookie.present? # end # def set_domain_user_id(duid) @details['duid'] = duid self end # Set the domain session ID. # @note Value is sent in the event as `sid` (raw event) or `domain_sessionid` (processed event). # @see Subject#set_network_user_id # @see Subject#set_domain_user_id # @see Subject#set_domain_session_idx # @example # subject.set_domain_session_id('9c65e7f3-8e8e-470d-b243-910b5b300da0') # @param [String] sid the unique domain session ID # @return self # @api public # # The `domain_sessionid` is a client-side unique ID for a user's current # session. It is set by the browser-based JavaScript trackers, and stored in # a first party cookie (cookie name: `_sp_id`), along with other parameters # such as `domain_userid`. For stitching together client-side and # server-side events originating from the same user and session, the domain # session ID can be extracted from the cookie and set using this method. def set_domain_session_id(sid) @details['sid'] = sid self end # Set the domain session index. # @note Value is sent in the event as `vid` (raw event) or `domain_sessionidx` (processed event). # @see Subject#set_network_user_id # @see Subject#set_domain_user_id # @see Subject#set_domain_session_id # @example # subject.set_domain_session_idx(3) # @param [Num] vid the number of sessions # @return self # @api public # # The `domain_sessionidx` is a client-side property that records how many # visits (unique `domain_sessionid`s) a user (a unique `domain_userid`) has # made to the site. It is stored in the first party cookie set by the # JavaScript tracker, along with other parameters such as `domain_userid`. # For stitching together client-side and server-side events originating from # the same user and session, the domain session index can be extracted from # the cookie and set using this method. def set_domain_session_idx(vid) @details['vid'] = vid self end # Set the user's IP address. # @note Value is sent in the event as `ip` (raw event) or `user_ipaddress` (processed event). # @param [String] ip the IP address # @return self # @example # subject.set_ip_address('37.157.33.178') # @api public def set_ip_address(ip) @details['ip'] = ip self end # Set the browser user agent. # @note Value is sent in the event as `ua` (raw event) or `useragent` (processed event). # @example # subject.set_useragent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:92.0) Gecko/20100101 Firefox/92.0') # @param [String] useragent the user agent string # @return self # @api public def set_useragent(useragent) @details['ua'] = useragent self end # Set the network user ID. # @note Value is sent in the event as `tnuid` (raw event) and `network_userid` (processed event). # @see Subject#set_domain_user_id # # The network user ID is, like the `domain_userid`, a cookie-based unique # user ID. It is stored in a third party cookie set by the event collector, # hence the name "network" as it is set at a network level. It is the # server-side user identifier. The raw event does not contain a `nuid` # value; the `network_userid` property is added when the event is processed. # # The default behaviour is for the collector to provide a new cookie/network # user ID for each event it receives. This method provides the ability to # override the collector cookie's value with your own generated ID. # # Domain user IDs set on the Subject in this way are sent as `tnuid` in the # raw event. # # @example # subject.set_network_user_id('ecdff4d0-9175-40ac-a8bb-325c49733607') # @param [String] nuid the network user ID # @return self # @api public def set_network_user_id(nuid) @details['tnuid'] = nuid self end end end snowplow-tracker-0.8.0/lib/snowplow-tracker/page.rb0000644000175100017510000000467314400614605021430 0ustar pravipravi# Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. # # This program is licensed to you under the Apache License Version 2.0, # and you may not use this file except in compliance with the Apache License Version 2.0. # You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. # # Unless required by applicable law or agreed to in writing, # software distributed under the Apache License Version 2.0 is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. # Author:: Snowplow Analytics Ltd # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd # License:: Apache License Version 2.0 module SnowplowTracker # If the Ruby tracker is incorporated into a website server, the events # tracked will describe user activity on specific webpages. Knowing on which # page an event occurred can be very valuable. # # Add page URL, page title and referrer URL to any event by adding a Page # object to any {Tracker} `#track_x_event` method call. # # Page parameters are saved into the tracked event as part of the 'atomic' # event properties, which have their own column in the eventual events table. # For example, a Page's `page_url` parameter will be sent as `url` in the # raw event payload, ending up in the `page_url` column. # # # @note For {Tracker#track_page_view}, properties set in the Page object will # override those properties given as arguments. class Page # @return [Hash] the stored page properties attr_reader :details # Create a Page object for attaching page properties to events. # # Page properties will directly populate the event's `page_url`, `page_title` and `referrer` parameters. # # @example Creating a Page # SnowplowTracker::Page.new(page_url: 'http://www.example.com/second-page', # page_title: 'Example title', # referrer: 'http://www.example.com/first-page') # # @param page_url [String] the page URL # @param page_title [String] the title of the page # @param referrer [String] the URL of the previous page def initialize(page_url: nil, page_title: nil, referrer: nil) @details = { 'url' => page_url, 'page' => page_title, 'refr' => referrer } end end end snowplow-tracker-0.8.0/lib/snowplow-tracker.rb0000644000175100017510000000217014400614605020502 0ustar pravipravi# Copyright (c) 2013-2021 Snowplow Analytics Ltd. All rights reserved. # # This program is licensed to you under the Apache License Version 2.0, # and you may not use this file except in compliance with the Apache License Version 2.0. # You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. # # Unless required by applicable law or agreed to in writing, # software distributed under the Apache License Version 2.0 is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. # Author:: Snowplow Analytics Ltd # Copyright:: Copyright (c) 2013-2021 Snowplow Analytics Ltd # License:: Apache License Version 2.0 require 'snowplow-tracker/version.rb' require 'snowplow-tracker/self_describing_json.rb' require 'snowplow-tracker/payload.rb' require 'snowplow-tracker/subject.rb' require 'snowplow-tracker/page.rb' require 'snowplow-tracker/emitters.rb' require 'snowplow-tracker/timestamp.rb' require 'snowplow-tracker/tracker.rb' snowplow-tracker-0.8.0/README.md0000644000175100017510000001172314400614605015351 0ustar pravipravi# Ruby Analytics for Snowplow [![early-release]][tracker-classification] [![Gem Version](https://badge.fury.io/rb/snowplow-tracker.svg)](https://badge.fury.io/rb/snowplow-tracker) [![Build Status][gh-actions-image]][gh-actions] [![Code Climate](https://codeclimate.com/github/snowplow/snowplow-ruby-tracker.png)](https://codeclimate.com/github/snowplow/snowplow-ruby-tracker) [![Coverage Status](https://coveralls.io/repos/snowplow/snowplow-ruby-tracker/badge.png)](https://coveralls.io/r/snowplow/snowplow-ruby-tracker) [![License][license-image]][license] ## Overview Add analytics to your **[Ruby][ruby]** and **[Ruby on Rails][rails]** apps and **[gems][rubygems]** with the **[Snowplow][snowplow]** event tracker for **[Ruby][ruby]**. Snowplow is a scalable open-source platform for rich, high quality, low-latency data collection. It is designed to collect high quality, complete behavioral data for enterprise business. **To find out more, please check out the [Snowplow website][snowplow] and our [documentation][docs].** ## Quickstart Add this gem to your Gemfile. It is compatible with Ruby versions 2.1 to 3.0+. ```ruby gem "snowplow-tracker", "~> 0.8.0" ``` See our [demo app][demoapp] for an example of implementing the Ruby tracker in a Rails app. ## Find out more | Snowplow Docs | API Docs | Contributing | | ------------------------------ | ----------------------- | ----------------------------------- | | ![i1][techdocs-image] | ![i1][techdocs-image] | ![i4][contributing-image] | | **[Snowplow Docs][techdocs]** | **[API Docs][apidocs]** | **[Contributing](Contributing.md)** | ## Maintainer Quickstart Clone this repo and navigate into the cloned folder. To run the tests locally, you will need [Docker][docker] installed. ```bash docker build . -t ruby-tracker docker run -v "$(pwd)":"/code" ruby-tracker ``` The `-v` flag for `docker run` creates a bind mount for the project directory. This means that changes to the files will be automatically applied within the Docker image. However, if you modify the `Gemfile` or `snowplow-tracker.gemspec` files, the image must be rebuilt. Alternatively, test directly by installing Ruby 2.1+ and [Bundler][bundler]. Then run: ```bash bundle install rspec ``` To generate documentation using YARD, make sure the YARD and redcarpet gems are installed locally. Then run: ```bash yard doc ``` ## Contributing Feedback and contributions are welcome - if you have identified a bug, please log an issue on this repo. For all other feedback, discussion or questions please open a thread on our [Discourse forum][discourse]. ## Copyright and license The Snowplow Ruby Tracker is copyright 2013-2021 Snowplow Analytics Ltd. Licensed under the **[Apache License, Version 2.0][license]** (the "License"); you may not use this software except in compliance with the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. [license-image]: https://img.shields.io/badge/license-Apache--2-blue.svg?style=flat [license]: https://www.apache.org/licenses/LICENSE-2.0 [gh-actions]: https://github.com/snowplow/snowplow-ruby-tracker/actions [gh-actions-image]: https://github.com/snowplow/snowplow-ruby-tracker/workflows/Test/badge.svg [tracker-classification]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/tracker-maintenance-classification/ [early-release]: https://img.shields.io/static/v1?style=flat&label=Snowplow&message=Early%20Release&color=014477&labelColor=9ba0aa&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEVMaXGXANeYANeXANZbAJmXANeUANSQAM+XANeMAMpaAJhZAJeZANiXANaXANaOAM2WANVnAKWXANZ9ALtmAKVaAJmXANZaAJlXAJZdAJxaAJlZAJdbAJlbAJmQAM+UANKZANhhAJ+EAL+BAL9oAKZnAKVjAKF1ALNBd8J1AAAAKHRSTlMAa1hWXyteBTQJIEwRgUh2JjJon21wcBgNfmc+JlOBQjwezWF2l5dXzkW3/wAAAHpJREFUeNokhQOCA1EAxTL85hi7dXv/E5YPCYBq5DeN4pcqV1XbtW/xTVMIMAZE0cBHEaZhBmIQwCFofeprPUHqjmD/+7peztd62dWQRkvrQayXkn01f/gWp2CrxfjY7rcZ5V7DEMDQgmEozFpZqLUYDsNwOqbnMLwPAJEwCopZxKttAAAAAElFTkSuQmCC [ruby]: https://www.ruby-lang.org/en/ [rails]: https://rubyonrails.org/ [rubygems]: https://rubygems.org/ [docker]: https://www.docker.com/ [bundler]: https://bundler.io/ [snowplow]: https://snowplowanalytics.com [docs]: https://docs.snowplowanalytics.com/ [demoapp]: https://github.com/snowplow-incubator/snowplow-ruby-tracker-examples [discourse]: https://discourse.snowplowanalytics.com [techdocs-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/techdocs.png [contributing-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/contributing.png [techdocs]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/ruby-tracker/ [apidocs]: https://snowplow.github.io/snowplow-ruby-tracker/ snowplow-tracker-0.8.0/snowplow-tracker.gemspec0000644000175100017510000000346514400614605020764 0ustar pravipravi######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: snowplow-tracker 0.8.0 ruby lib Gem::Specification.new do |s| s.name = "snowplow-tracker".freeze s.version = "0.8.0" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Snowplow Analytics Ltd".freeze] s.date = "2021-10-29" s.description = "With this tracker you can collect event data from your Ruby applications, Ruby on Rails web applications and Ruby gems.".freeze s.email = "support@snowplowanalytics.com".freeze s.files = ["LICENSE-2.0.txt".freeze, "README.md".freeze, "lib/snowplow-tracker.rb".freeze, "lib/snowplow-tracker/emitters.rb".freeze, "lib/snowplow-tracker/page.rb".freeze, "lib/snowplow-tracker/payload.rb".freeze, "lib/snowplow-tracker/self_describing_json.rb".freeze, "lib/snowplow-tracker/subject.rb".freeze, "lib/snowplow-tracker/timestamp.rb".freeze, "lib/snowplow-tracker/tracker.rb".freeze, "lib/snowplow-tracker/version.rb".freeze] s.homepage = "http://github.com/snowplow/snowplow-ruby-tracker".freeze s.licenses = ["Apache-2.0".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.1".freeze) s.rubygems_version = "3.3.15".freeze s.summary = "Ruby Analytics for Snowplow".freeze if s.respond_to? :specification_version then s.specification_version = 4 end if s.respond_to? :add_runtime_dependency then s.add_development_dependency(%q.freeze, ["~> 3.10"]) s.add_development_dependency(%q.freeze, ["~> 3.14"]) else s.add_dependency(%q.freeze, ["~> 3.10"]) s.add_dependency(%q.freeze, ["~> 3.14"]) end end