snowplow-tracker 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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