snowplow-tracker 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MDU0N2EyMjU3MDk1ODBhMGZlMzRlNjg1Mzg4ZmQ3OTg1N2QwZGFhMw==
4
+ NDkzMWJkNzQyZjVmNjk0NmQ0YjI5ODA5YzkxM2Y1NmZhZmEwNTgzNA==
5
5
  data.tar.gz: !binary |-
6
- NjgxMmRlZGNjNThlZWZkYTIwNDk5Y2ViZTE4ZjA4NmEyYzMzOThiMw==
6
+ NTU5NDI2M2UzYTQ0M2ZhNGEyYjM2ZDdjMzQwZjA4ZGYyNDQ3OWU0OA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MTg4MDk0ZGM5ZjI0MjhiZmI2NzE0N2E3ZjJlMjNkNjVlZmVkZTg5MzhlMDQx
10
- NTFhNmIwYWMxMGRhMmM0ODVkMzJkZTcwM2JmZjg4ODFlYTMwNDM0Y2MxNjE3
11
- YjE3YWZkYTVhNjkwN2M3MWNiYmRlOTUyZGFlMGYyMzc3OGEwNDA=
9
+ NjJiM2Y1ZWM2Y2Y3MTNkZDNmNWM0N2IwMGNkOTAzMTlhNjAxZjk5MTk3ZmUz
10
+ MjcyNTYwNmZjZjhmMWVlZGNiMmJiNGZiOGZlMmJlY2YxNDE1MjY0MGFlYzI0
11
+ M2E2OGY5YTkwM2M4Y2M1NjQyNGEyNzIxNzYzNDk1ZDY1MGU0NDg=
12
12
  data.tar.gz: !binary |-
13
- MDBmMDgwOGM4ZWI1Y2IyMTNlZWQzMThjMWM1ZDk5MWQwZTJhNGIyMGIxOTQw
14
- YTc5YmJjZjRjYzg2NDJmNGNlODFkMGFmYjkyNzQxZjc1MGI4MzZhMGUzYjkz
15
- NzU4MTk0MTBmN2M4Njg1ODczNzIzM2Q3YTQ4YmRmNTQyOWVkMjA=
13
+ YjgxODJjMWY0NmIwOTNjODNmZmQ1NTZkOTMwNDBkODMxODZhMzc1Njk2ODc1
14
+ NDkzZDYzNWMyZDQ0MzBjYzRiNzI3ZTdkNjI4OWY5ZjUyMTgzMjI3ZWYyOTI4
15
+ NGZhYTljNTFkOTRhYWQ5MzEyMGFmMmU0ZGNhNGFjZWQ0YzQ2NmI=
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Ruby Analytics for Snowplow
2
2
  [![Gem Version](https://badge.fury.io/rb/snowplow-tracker.svg)](http://badge.fury.io/rb/snowplow-tracker)
3
- [![Build Status](https://travis-ci.org/snowplow/snowplow-ruby-tracker.png)](https://travis-ci.org/snowplow/snowplow-ruby-tracker)
3
+ [![Build Status](https://travis-ci.org/snowplow/snowplow-ruby-tracker.png?branch=master)](https://travis-ci.org/snowplow/snowplow-ruby-tracker)
4
4
  [![Code Climate](https://codeclimate.com/github/snowplow/snowplow-ruby-tracker.png)](https://codeclimate.com/github/snowplow/snowplow-ruby-tracker)
5
5
  [![Coverage Status](https://coveralls.io/repos/snowplow/snowplow-ruby-tracker/badge.png)](https://coveralls.io/r/snowplow/snowplow-ruby-tracker)
6
6
  [![License][license-image]][license]
@@ -15,6 +15,7 @@
15
15
 
16
16
  require 'snowplow-tracker/contracts.rb'
17
17
  require 'snowplow-tracker/version.rb'
18
+ require 'snowplow-tracker/self_describing_json.rb'
18
19
  require 'snowplow-tracker/payload.rb'
19
20
  require 'snowplow-tracker/subject.rb'
20
21
  require 'snowplow-tracker/emitters.rb'
@@ -14,7 +14,6 @@
14
14
  # License:: Apache License Version 2.0
15
15
 
16
16
  require 'contracts'
17
- include Contracts
18
17
 
19
18
  module SnowplowTracker
20
19
 
@@ -17,7 +17,6 @@ require 'net/https'
17
17
  require 'set'
18
18
  require 'logger'
19
19
  require 'contracts'
20
- include Contracts
21
20
 
22
21
  module SnowplowTracker
23
22
 
@@ -26,13 +25,16 @@ module SnowplowTracker
26
25
 
27
26
  class Emitter
28
27
 
28
+ include Contracts
29
+
29
30
  @@ConfigHash = ({
30
31
  :protocol => Maybe[Or['http', 'https']],
31
32
  :port => Maybe[Num],
32
33
  :method => Maybe[Or['get', 'post']],
33
34
  :buffer_size => Maybe[Num],
34
35
  :on_success => Maybe[Func[Num => Any]],
35
- :on_failure => Maybe[Func[Num, Hash => Any]]
36
+ :on_failure => Maybe[Func[Num, Hash => Any]],
37
+ :thread_count => Maybe[Num]
36
38
  })
37
39
 
38
40
  @@StrictConfigHash = And[@@ConfigHash, lambda { |x|
@@ -47,19 +49,19 @@ module SnowplowTracker
47
49
  Contract String, @@StrictConfigHash => lambda { |x| x.is_a? Emitter }
48
50
  def initialize(endpoint, config={})
49
51
  config = @@DefaultConfig.merge(config)
52
+ @lock = Monitor.new
50
53
  @collector_uri = as_collector_uri(endpoint, config[:protocol], config[:port], config[:method])
51
54
  @buffer = []
52
55
  if not config[:buffer_size].nil?
53
56
  @buffer_size = config[:buffer_size]
54
57
  elsif config[:method] == 'get'
55
- @buffer_size = 0
58
+ @buffer_size = 1
56
59
  else
57
60
  @buffer_size = 10
58
61
  end
59
62
  @method = config[:method]
60
63
  @on_success = config[:on_success]
61
64
  @on_failure = config[:on_failure]
62
- @threads = []
63
65
  LOGGER.info("#{self.class} initialized with endpoint #{@collector_uri}")
64
66
 
65
67
  self
@@ -80,9 +82,11 @@ module SnowplowTracker
80
82
  Contract Hash => nil
81
83
  def input(payload)
82
84
  payload.each { |k,v| payload[k] = v.to_s}
83
- @buffer.push(payload)
84
- if @buffer.size > @buffer_size
85
- flush
85
+ @lock.synchronize do
86
+ @buffer.push(payload)
87
+ if @buffer.size >= @buffer_size
88
+ flush
89
+ end
86
90
  end
87
91
 
88
92
  nil
@@ -91,58 +95,70 @@ module SnowplowTracker
91
95
  # Flush the buffer
92
96
  #
93
97
  Contract Bool => nil
94
- def flush(sync=false)
95
- send_requests
96
-
98
+ def flush(async=true)
99
+ @lock.synchronize do
100
+ send_requests(@buffer)
101
+ @buffer = []
102
+ end
97
103
  nil
98
104
  end
99
105
 
100
106
  # Send all events in the buffer to the collector
101
107
  #
102
- Contract None => nil
103
- def send_requests
104
- LOGGER.info("Attempting to send #{@buffer.size} request#{@buffer.size == 1 ? '' : 's'}")
105
- temp_buffer = @buffer
106
- @buffer = []
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")
112
+ return
113
+ end
114
+ LOGGER.info("Attempting to send #{evts.size} request#{evts.size == 1 ? '' : 's'}")
115
+
116
+ if @method == 'post'
117
+ post_succeeded = false
118
+ begin
119
+ request = http_post(SelfDescribingJson.new(
120
+ 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-2',
121
+ evts
122
+ ).to_json)
123
+ post_succeeded = is_good_status_code(request.code)
124
+ rescue StandardError => se
125
+ LOGGER.warn(se)
126
+ end
127
+ if post_succeeded
128
+ unless @on_success.nil?
129
+ @on_success.call(evts.size)
130
+ end
131
+ else
132
+ unless @on_failure.nil?
133
+ @on_failure.call(0, evts)
134
+ end
135
+ end
107
136
 
108
- if @method == 'get'
137
+ elsif @method == 'get'
109
138
  success_count = 0
110
139
  unsent_requests = []
111
- temp_buffer.each do |payload|
112
- request = http_get(payload)
113
- if request.code.to_i == 200
114
- success_count += 1
115
- else
116
- unsent_requests.push(payload)
140
+ evts.each do |evt|
141
+ get_succeeded = false
142
+ begin
143
+ request = http_get(evt)
144
+ get_succeeded = is_good_status_code(request.code)
145
+ rescue StandardError => se
146
+ LOGGER.warn(se)
117
147
  end
118
- if unsent_requests.size == 0
119
- unless @on_success.nil?
120
- @on_success.call(success_count)
121
- end
148
+ if get_succeeded
149
+ success_count += 1
122
150
  else
123
- unless @on_failure.nil?
124
- @on_failure.call(success_count, unsent_requests)
125
- end
151
+ unsent_requests << evt
126
152
  end
127
153
  end
128
-
129
- elsif @method == 'post'
130
- if temp_buffer.size > 0
131
- request = http_post({
132
- 'schema' => 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-2',
133
- 'data' => temp_buffer
134
- })
135
-
136
- if request.code.to_i == 200
137
- unless @on_success.nil?
138
- @on_success.call(temp_buffer.size)
139
- end
140
- else
141
- unless @on_failure.nil?
142
- @on_failure.call(0, temp_buffer)
143
- end
154
+ if unsent_requests.size == 0
155
+ unless @on_success.nil?
156
+ @on_success.call(success_count)
157
+ end
158
+ else
159
+ unless @on_failure.nil?
160
+ @on_failure.call(success_count, unsent_requests)
144
161
  end
145
-
146
162
  end
147
163
  end
148
164
 
@@ -162,7 +178,7 @@ module SnowplowTracker
162
178
  http.use_ssl = true
163
179
  end
164
180
  response = http.request(request)
165
- LOGGER.add(response.code == '200' ? Logger::INFO : Logger::WARN) {
181
+ LOGGER.add(is_good_status_code(response.code) ? Logger::INFO : Logger::WARN) {
166
182
  "GET request to #{@collector_uri} finished with status code #{response.code}"
167
183
  }
168
184
 
@@ -184,13 +200,20 @@ module SnowplowTracker
184
200
  request.body = payload.to_json
185
201
  request.set_content_type('application/json; charset=utf-8')
186
202
  response = http.request(request)
187
- LOGGER.add(response.code == '200' ? Logger::INFO : Logger::WARN) {
203
+ LOGGER.add(is_good_status_code(response.code) ? Logger::INFO : Logger::WARN) {
188
204
  "POST request to #{@collector_uri} finished with status code #{response.code}"
189
205
  }
190
206
 
191
207
  response
192
208
  end
193
209
 
210
+ # Only 2xx and 3xx status codes are considered successes
211
+ #
212
+ Contract String => Bool
213
+ def is_good_status_code(status_code)
214
+ status_code.to_i >= 200 && status_code.to_i < 400
215
+ end
216
+
194
217
  private :as_collector_uri,
195
218
  :http_get,
196
219
  :http_post
@@ -200,27 +223,54 @@ module SnowplowTracker
200
223
 
201
224
  class AsyncEmitter < Emitter
202
225
 
203
- # Flush the buffer in a new thread
204
- # If sync is true, block until all flushing threads have exited
205
- #
206
- def flush(sync=false)
207
- t = Thread.new do
208
- send_requests
209
- end
210
- t.abort_on_exception = true
211
- @threads.select!{ |thread| thread.alive?}
212
- @threads.push(t)
213
-
214
- if sync
215
- LOGGER.info('Starting synchronous flush')
216
- @threads.each do |thread|
217
- thread.join(10)
226
+ Contract String, @@StrictConfigHash => lambda { |x| x.is_a? Emitter }
227
+ def initialize(endpoint, config={})
228
+ @queue = Queue.new()
229
+ # @all_processed_condition and @results_unprocessed are used to emulate Python's Queue.task_done()
230
+ @queue.extend(MonitorMixin)
231
+ @all_processed_condition = @queue.new_cond
232
+ @results_unprocessed = 0
233
+ (config[:thread_count] || 1).times do
234
+ t = Thread.new do
235
+ consume
218
236
  end
219
237
  end
238
+ super(endpoint, config)
239
+ end
220
240
 
221
- nil
241
+ def consume
242
+ loop do
243
+ work_unit = @queue.pop
244
+ send_requests(work_unit)
245
+ @queue.synchronize do
246
+ @results_unprocessed -= 1
247
+ @all_processed_condition.broadcast
248
+ end
249
+ end
222
250
  end
223
251
 
252
+ # Flush the buffer
253
+ # If async is false, block until the queue is empty
254
+ #
255
+ def flush(async=true)
256
+ loop do
257
+ @lock.synchronize do
258
+ @queue.synchronize do
259
+ @results_unprocessed += 1
260
+ end
261
+ @queue << @buffer
262
+ @buffer = []
263
+ end
264
+ if not async
265
+ LOGGER.info('Starting synchronous flush')
266
+ @queue.synchronize do
267
+ @all_processed_condition.wait_while { @results_unprocessed > 0 }
268
+ LOGGER.info('Finished synchronous flush')
269
+ end
270
+ end
271
+ break if @buffer.size < 1
272
+ end
273
+ end
224
274
  end
225
275
 
226
276
  end
@@ -17,12 +17,13 @@ require 'base64'
17
17
  require 'json'
18
18
  require 'net/http'
19
19
  require 'contracts'
20
- include Contracts
21
20
 
22
21
  module SnowplowTracker
23
22
 
24
23
  class Payload
25
24
 
25
+ include Contracts
26
+
26
27
  attr_reader :context
27
28
 
28
29
  Contract nil => Payload
@@ -0,0 +1,34 @@
1
+ # Copyright (c) 2013-2014 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:: Alex Dean, Fred Blundun (mailto:support@snowplowanalytics.com)
13
+ # Copyright:: Copyright (c) 2013-2014 Snowplow Analytics Ltd
14
+ # License:: Apache License Version 2.0
15
+
16
+ module SnowplowTracker
17
+
18
+ class SelfDescribingJson
19
+
20
+ def initialize(schema, data)
21
+ @schema = schema
22
+ @data = data
23
+ end
24
+
25
+ def to_json
26
+ {
27
+ :schema => @schema,
28
+ :data => @data
29
+ }
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -14,12 +14,13 @@
14
14
  # License:: Apache License Version 2.0
15
15
 
16
16
  require 'contracts'
17
- include Contracts
18
17
 
19
18
  module SnowplowTracker
20
19
 
21
20
  class Subject
22
21
 
22
+ include Contracts
23
+
23
24
  @@default_platform = 'srv'
24
25
  @@supported_platforms = ['pc', 'tv', 'mob', 'cnsl', 'iot']
25
26
 
@@ -52,6 +53,14 @@ module SnowplowTracker
52
53
  self
53
54
  end
54
55
 
56
+ # Set fingerprint for the user
57
+ #
58
+ Contract Num => Subject
59
+ def set_fingerprint(fingerprint)
60
+ @standard_nv_pairs['fp'] = fingerprint
61
+ self
62
+ end
63
+
55
64
  # Set the screen resolution for a device
56
65
  #
57
66
  Contract Num, Num => Subject
@@ -15,14 +15,14 @@
15
15
 
16
16
  require 'contracts'
17
17
  require 'set'
18
- include Contracts
19
-
20
18
  require 'uuid'
21
19
 
22
20
  module SnowplowTracker
23
21
 
24
22
  class Tracker
25
23
 
24
+ include Contracts
25
+
26
26
  @@EmitterInput = Or[lambda {|x| x.is_a? Emitter}, ArrayOf[lambda {|x| x.is_a? Emitter}]]
27
27
 
28
28
  @@required_transaction_keys = Set.new(%w(order_id total_value))
@@ -55,22 +55,14 @@ module SnowplowTracker
55
55
  augmented_item_keys.subset? @@recognised_augmented_item_keys
56
56
  }
57
57
 
58
- @@SelfDescribingJson = Or[{
59
- schema: String,
60
- data: Any
61
- }, {
62
- 'schema' => String,
63
- 'data' => Any
64
- }]
65
-
66
- @@ContextsInput = ArrayOf[@@SelfDescribingJson]
58
+ @@ContextsInput = ArrayOf[SelfDescribingJson]
67
59
 
68
60
  @@version = TRACKER_VERSION
69
61
  @@default_encode_base64 = true
70
62
 
71
63
  @@base_schema_path = "iglu:com.snowplowanalytics.snowplow"
72
64
  @@schema_tag = "jsonschema"
73
- @@context_schema = "#{@@base_schema_path}/contexts/#{@@schema_tag}/1-0-0"
65
+ @@context_schema = "#{@@base_schema_path}/contexts/#{@@schema_tag}/1-0-1"
74
66
  @@unstruct_event_schema = "#{@@base_schema_path}/unstruct_event/#{@@schema_tag}/1-0-0"
75
67
 
76
68
  Contract @@EmitterInput, Maybe[Subject], Maybe[String], Maybe[String], Bool => Tracker
@@ -119,12 +111,12 @@ module SnowplowTracker
119
111
 
120
112
  # Builds a self-describing JSON from an array of custom contexts
121
113
  #
122
- Contract @@ContextsInput => @@SelfDescribingJson
114
+ Contract @@ContextsInput => Hash
123
115
  def build_context(context)
124
- {
125
- schema: @@context_schema,
126
- data: context
127
- }
116
+ SelfDescribingJson.new(
117
+ @@context_schema,
118
+ context.map {|c| c.to_json}
119
+ ).to_json
128
120
  end
129
121
 
130
122
  # Tracking methods
@@ -259,7 +251,8 @@ module SnowplowTracker
259
251
  screen_view_properties['id'] = id
260
252
  end
261
253
  screen_view_schema = "#{@@base_schema_path}/screen_view/#{@@schema_tag}/1-0-0"
262
- event_json = {schema: screen_view_schema, data: screen_view_properties}
254
+
255
+ event_json = SelfDescribingJson.new(screen_view_schema, screen_view_properties)
263
256
 
264
257
  self.track_unstruct_event(event_json, context, tstamp)
265
258
 
@@ -268,16 +261,14 @@ module SnowplowTracker
268
261
 
269
262
  # Track an unstructured event
270
263
  #
271
- Contract @@SelfDescribingJson, Maybe[@@ContextsInput], Maybe[Num] => Tracker
264
+ Contract SelfDescribingJson, Maybe[@@ContextsInput], Maybe[Num] => Tracker
272
265
  def track_unstruct_event(event_json, context=nil, tstamp=nil)
273
266
  pb = Payload.new
274
267
  pb.add('e', 'ue')
275
268
 
276
- envelope = {
277
- schema: @@unstruct_event_schema,
278
- data: event_json
279
- }
280
- pb.add_json(envelope, @config['encode_base64'], 'ue_px', 'ue_pr')
269
+ envelope = SelfDescribingJson.new(@@unstruct_event_schema, event_json.to_json)
270
+
271
+ pb.add_json(envelope.to_json, @config['encode_base64'], 'ue_px', 'ue_pr')
281
272
 
282
273
  unless context.nil?
283
274
  pb.add_json(build_context(context), @config['encode_base64'], 'cx', 'co')
@@ -296,9 +287,9 @@ module SnowplowTracker
296
287
  # Flush all events stored in all emitters
297
288
  #
298
289
  Contract Bool => Tracker
299
- def flush(sync=false)
290
+ def flush(async=false)
300
291
  @emitters.each do |emitter|
301
- emitter.flush(sync)
292
+ emitter.flush(async)
302
293
  end
303
294
 
304
295
  self
@@ -14,6 +14,6 @@
14
14
  # License:: Apache License Version 2.0
15
15
 
16
16
  module SnowplowTracker
17
- VERSION = '0.4.2'
17
+ VERSION = '0.5.0'
18
18
  TRACKER_VERSION = "rb-#{VERSION}"
19
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snowplow-tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Dean
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-04-08 00:00:00.000000000 Z
12
+ date: 2015-08-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: contracts
@@ -17,14 +17,20 @@ dependencies:
17
17
  requirements:
18
18
  - - ~>
19
19
  - !ruby/object:Gem::Version
20
- version: '0.3'
20
+ version: '0.7'
21
+ - - <=
22
+ - !ruby/object:Gem::Version
23
+ version: '0.11'
21
24
  type: :runtime
22
25
  prerelease: false
23
26
  version_requirements: !ruby/object:Gem::Requirement
24
27
  requirements:
25
28
  - - ~>
26
29
  - !ruby/object:Gem::Version
27
- version: '0.3'
30
+ version: '0.7'
31
+ - - <=
32
+ - !ruby/object:Gem::Version
33
+ version: '0.11'
28
34
  - !ruby/object:Gem::Dependency
29
35
  name: uuid
30
36
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +86,7 @@ files:
80
86
  - lib/snowplow-tracker/contracts.rb
81
87
  - lib/snowplow-tracker/emitters.rb
82
88
  - lib/snowplow-tracker/payload.rb
89
+ - lib/snowplow-tracker/self_describing_json.rb
83
90
  - lib/snowplow-tracker/subject.rb
84
91
  - lib/snowplow-tracker/tracker.rb
85
92
  - lib/snowplow-tracker/version.rb
@@ -103,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
110
  version: '0'
104
111
  requirements: []
105
112
  rubyforge_project:
106
- rubygems_version: 2.4.6
113
+ rubygems_version: 2.4.8
107
114
  signing_key:
108
115
  specification_version: 4
109
116
  summary: Ruby Analytics for Snowplow