analytics-ruby 2.2.6 → 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 472dcf42eeb6b9d83166a526ebeb81bfbbdc74a429511776fedac53ae585da27
4
- data.tar.gz: 70d5bfbf53332c3899cb4cc0071a068c25da7fabee8e934ced0d335bbb08c45f
3
+ metadata.gz: cdf8f490d498d4be6df74dc0e07337670f201aecf2197df18a0cd6bad097f0ec
4
+ data.tar.gz: 5f578db1517f5d9d376c0dfc3b19cdfb68afcc84b80d23cc929c8bf4038bfa3c
5
5
  SHA512:
6
- metadata.gz: 112f00cdfb0e27ab41ae0c74ebe76d8fba6777acd71ec0a46fc6576948f65a096668c59354393ef600f961631f6cf1938bde3bcbb4ff09aafd4fcf1ed73a2aa9
7
- data.tar.gz: 4af5e31b75e80dd20dacf7d3e5aeb58ce2e1280337dbe82b69b63c1588ba650d774a9625f00f930b0a25df612f8d7654ae16947cd5b3369ce6e6e3b4880e6483
6
+ metadata.gz: 6d05d3ba281a5af2d16e883afb17b285c5e5c2ee6500041646c136a412a16fd8f659f848e8f72d1bf86eeaddd9ae61a0565b49d051834d57ca0e9446ef6c8396
7
+ data.tar.gz: b1fb05a4a057fe04dc39ed7234bc07cf2d401e9c5e2ca42eea64e9ff8c58bab879e9f4d42efad31128dcc4d8621e7c534f1f3b6c8f262049b78e4299fc16afe5
data/bin/analytics CHANGED
@@ -29,6 +29,7 @@ command :send do |c|
29
29
  c.option '--userId=<userId>', String, 'the user id to send the event as'
30
30
  c.option '--anonymousId=<anonymousId>', String, 'the anonymous user id to send the event as'
31
31
  c.option '--context=<context>', 'additional context for the event (JSON-encoded)'
32
+ c.option '--integrations=<integrations>', 'additional integrations for the event (JSON-encoded)'
32
33
 
33
34
  c.option '--event=<event>', String, 'the event name to send with the event'
34
35
  c.option '--properties=<properties>', 'the event properties to send (JSON-encoded)'
@@ -38,6 +39,7 @@ command :send do |c|
38
39
  c.option '--traits=<traits>', 'the identify/group traits to send (JSON-encoded)'
39
40
 
40
41
  c.option '--groupId=<groupId>', String, 'the group id'
42
+ c.option '--previousId=<previousId>', String, 'the previous id'
41
43
 
42
44
  c.action do |args, options|
43
45
  Analytics = Segment::Analytics.new({
@@ -52,7 +54,8 @@ command :send do |c|
52
54
  event: options.event,
53
55
  anonymous_id: options.anonymousId,
54
56
  properties: json_hash(options.properties),
55
- context: json_hash(options.context)
57
+ context: json_hash(options.context),
58
+ integrations: json_hash(options.integrations)
56
59
  })
57
60
  when "page"
58
61
  Analytics.page({
@@ -60,22 +63,25 @@ command :send do |c|
60
63
  anonymous_id: options.anonymousId,
61
64
  name: options.name,
62
65
  properties: json_hash(options.properties),
63
- context: json_hash(options.context)
66
+ context: json_hash(options.context),
67
+ integrations: json_hash(options.integrations)
64
68
  })
65
69
  when "screen"
66
70
  Analytics.screen({
67
71
  user_id: options.userId,
68
72
  anonymous_id: options.anonymousId,
69
- name: option.name,
70
- traits: json_hash(options.traits),
71
- properties: json_hash(option.properties)
73
+ name: options.name,
74
+ properties: json_hash(options.properties),
75
+ context: json_hash(options.context),
76
+ integrations: json_hash(options.integrations)
72
77
  })
73
78
  when "identify"
74
79
  Analytics.identify({
75
80
  user_id: options.userId,
76
81
  anonymous_id: options.anonymousId,
77
82
  traits: json_hash(options.traits),
78
- context: json_hash(options.context)
83
+ context: json_hash(options.context),
84
+ integrations: json_hash(options.integrations)
79
85
  })
80
86
  when "group"
81
87
  Analytics.group({
@@ -83,7 +89,16 @@ command :send do |c|
83
89
  anonymous_id: options.anonymousId,
84
90
  group_id: options.groupId,
85
91
  traits: json_hash(options.traits),
86
- context: json_hash(options.context)
92
+ context: json_hash(options.context),
93
+ integrations: json_hash(options.integrations)
94
+ })
95
+ when "alias"
96
+ Analytics.alias({
97
+ previous_id: options.previousId,
98
+ user_id: options.userId,
99
+ anonymous_id: options.anonymousId,
100
+ context: json_hash(options.context),
101
+ integrations: json_hash(options.integrations)
87
102
  })
88
103
  else
89
104
  raise "Invalid Message Type #{options.type}"
@@ -1,11 +1,13 @@
1
1
  require 'segment/analytics/version'
2
2
  require 'segment/analytics/defaults'
3
3
  require 'segment/analytics/utils'
4
+ require 'segment/analytics/field_parser'
4
5
  require 'segment/analytics/client'
5
6
  require 'segment/analytics/worker'
6
- require 'segment/analytics/request'
7
+ require 'segment/analytics/transport'
7
8
  require 'segment/analytics/response'
8
9
  require 'segment/analytics/logging'
10
+ require 'segment/analytics/test_queue'
9
11
 
10
12
  module Segment
11
13
  class Analytics
@@ -17,7 +19,7 @@ module Segment
17
19
  # @option options [Boolean] :stub (false) If true, requests don't hit the
18
20
  # server and are stubbed to be successful.
19
21
  def initialize(options = {})
20
- Request.stub = options[:stub] if options.has_key?(:stub)
22
+ Transport.stub = options[:stub] if options.has_key?(:stub)
21
23
  @client = Segment::Analytics::Client.new options
22
24
  end
23
25
 
@@ -21,11 +21,12 @@ module Segment
21
21
  symbolize_keys!(opts)
22
22
 
23
23
  @queue = Queue.new
24
+ @test = opts[:test]
24
25
  @write_key = opts[:write_key]
25
26
  @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
26
- @options = opts
27
27
  @worker_mutex = Mutex.new
28
- @worker = Worker.new(@queue, @write_key, @options)
28
+ @worker = Worker.new(@queue, @write_key, opts)
29
+ @worker_thread = nil
29
30
 
30
31
  check_write_key!
31
32
 
@@ -43,58 +44,32 @@ module Segment
43
44
  end
44
45
  end
45
46
 
47
+ # @!macro common_attrs
48
+ # @option attrs [String] :anonymous_id ID for a user when you don't know
49
+ # who they are yet. (optional but you must provide either an
50
+ # `anonymous_id` or `user_id`)
51
+ # @option attrs [Hash] :context ({})
52
+ # @option attrs [Hash] :integrations What integrations this event
53
+ # goes to (optional)
54
+ # @option attrs [String] :message_id ID that uniquely
55
+ # identifies a message across the API. (optional)
56
+ # @option attrs [Time] :timestamp When the event occurred (optional)
57
+ # @option attrs [String] :user_id The ID for this user in your database
58
+ # (optional but you must provide either an `anonymous_id` or `user_id`)
59
+ # @option attrs [Hash] :options Options such as user traits (optional)
60
+
46
61
  # Tracks an event
47
62
  #
48
63
  # @see https://segment.com/docs/sources/server/ruby/#track
49
64
  #
50
65
  # @param [Hash] attrs
51
- # @option attrs [String] :anonymous_id ID for a user when you don't know
52
- # who they are yet. (optional but you must provide either an
53
- # `anonymous_id` or `user_id`)
54
- # @option attrs [Hash] :context ({})
66
+ #
55
67
  # @option attrs [String] :event Event name
56
- # @option attrs [Hash] :integrations What integrations this event
57
- # goes to (optional)
58
- # @option attrs [Hash] :options Options such as user traits (optional)
59
68
  # @option attrs [Hash] :properties Event properties (optional)
60
- # @option attrs [Time] :timestamp When the event occurred (optional)
61
- # @option attrs [String] :user_id The ID for this user in your database
62
- # (optional but you must provide either an `anonymous_id` or `user_id`)
63
- # @option attrs [String] :message_id ID that uniquely
64
- # identifies a message across the API. (optional)
69
+ # @macro common_attrs
65
70
  def track(attrs)
66
71
  symbolize_keys! attrs
67
- check_user_id! attrs
68
-
69
- event = attrs[:event]
70
- properties = attrs[:properties] || {}
71
- timestamp = attrs[:timestamp] || Time.new
72
- context = attrs[:context] || {}
73
- message_id = attrs[:message_id].to_s if attrs[:message_id]
74
-
75
- check_timestamp! timestamp
76
-
77
- if event.nil? || event.empty?
78
- raise ArgumentError, 'Must supply event as a non-empty string'
79
- end
80
-
81
- raise ArgumentError, 'Properties must be a Hash' unless properties.is_a? Hash
82
- isoify_dates! properties
83
-
84
- add_context context
85
-
86
- enqueue({
87
- :event => event,
88
- :userId => attrs[:user_id],
89
- :anonymousId => attrs[:anonymous_id],
90
- :context => context,
91
- :options => attrs[:options],
92
- :integrations => attrs[:integrations],
93
- :properties => properties,
94
- :messageId => message_id,
95
- :timestamp => datetime_in_iso8601(timestamp),
96
- :type => 'track'
97
- })
72
+ enqueue(FieldParser.parse_for_track(attrs))
98
73
  end
99
74
 
100
75
  # Identifies a user
@@ -102,46 +77,12 @@ module Segment
102
77
  # @see https://segment.com/docs/sources/server/ruby/#identify
103
78
  #
104
79
  # @param [Hash] attrs
105
- # @option attrs [String] :anonymous_id ID for a user when you don't know
106
- # who they are yet. (optional but you must provide either an
107
- # `anonymous_id` or `user_id`)
108
- # @option attrs [Hash] :context ({})
109
- # @option attrs [Hash] :integrations What integrations this event
110
- # goes to (optional)
111
- # @option attrs [Hash] :options Options such as user traits (optional)
112
- # @option attrs [Time] :timestamp When the event occurred (optional)
80
+ #
113
81
  # @option attrs [Hash] :traits User traits (optional)
114
- # @option attrs [String] :user_id The ID for this user in your database
115
- # (optional but you must provide either an `anonymous_id` or `user_id`)
116
- # @option attrs [String] :message_id ID that uniquely identifies a
117
- # message across the API. (optional)
82
+ # @macro common_attrs
118
83
  def identify(attrs)
119
84
  symbolize_keys! attrs
120
- check_user_id! attrs
121
-
122
- traits = attrs[:traits] || {}
123
- timestamp = attrs[:timestamp] || Time.new
124
- context = attrs[:context] || {}
125
- message_id = attrs[:message_id].to_s if attrs[:message_id]
126
-
127
- check_timestamp! timestamp
128
-
129
- raise ArgumentError, 'Must supply traits as a hash' unless traits.is_a? Hash
130
- isoify_dates! traits
131
-
132
- add_context context
133
-
134
- enqueue({
135
- :userId => attrs[:user_id],
136
- :anonymousId => attrs[:anonymous_id],
137
- :integrations => attrs[:integrations],
138
- :context => context,
139
- :traits => traits,
140
- :options => attrs[:options],
141
- :messageId => message_id,
142
- :timestamp => datetime_in_iso8601(timestamp),
143
- :type => 'identify'
144
- })
85
+ enqueue(FieldParser.parse_for_identify(attrs))
145
86
  end
146
87
 
147
88
  # Aliases a user from one id to another
@@ -149,39 +90,12 @@ module Segment
149
90
  # @see https://segment.com/docs/sources/server/ruby/#alias
150
91
  #
151
92
  # @param [Hash] attrs
152
- # @option attrs [Hash] :context ({})
153
- # @option attrs [Hash] :integrations What integrations this must be
154
- # sent to (optional)
155
- # @option attrs [Hash] :options Options such as user traits (optional)
93
+ #
156
94
  # @option attrs [String] :previous_id The ID to alias from
157
- # @option attrs [Time] :timestamp When the alias occurred (optional)
158
- # @option attrs [String] :user_id The ID to alias to
159
- # @option attrs [String] :message_id ID that uniquely identifies a
160
- # message across the API. (optional)
95
+ # @macro common_attrs
161
96
  def alias(attrs)
162
97
  symbolize_keys! attrs
163
-
164
- from = attrs[:previous_id]
165
- to = attrs[:user_id]
166
- timestamp = attrs[:timestamp] || Time.new
167
- context = attrs[:context] || {}
168
- message_id = attrs[:message_id].to_s if attrs[:message_id]
169
-
170
- check_presence! from, 'previous_id'
171
- check_presence! to, 'user_id'
172
- check_timestamp! timestamp
173
- add_context context
174
-
175
- enqueue({
176
- :previousId => from,
177
- :userId => to,
178
- :integrations => attrs[:integrations],
179
- :context => context,
180
- :options => attrs[:options],
181
- :messageId => message_id,
182
- :timestamp => datetime_in_iso8601(timestamp),
183
- :type => 'alias'
184
- })
98
+ enqueue(FieldParser.parse_for_alias(attrs))
185
99
  end
186
100
 
187
101
  # Associates a user identity with a group.
@@ -189,48 +103,13 @@ module Segment
189
103
  # @see https://segment.com/docs/sources/server/ruby/#group
190
104
  #
191
105
  # @param [Hash] attrs
192
- # @option attrs [String] :anonymous_id ID for a user when you don't know
193
- # who they are yet. (optional but you must provide either an
194
- # `anonymous_id` or `user_id`)
195
- # @option attrs [Hash] :context ({})
106
+ #
196
107
  # @option attrs [String] :group_id The ID of the group
197
- # @option attrs [Hash] :integrations What integrations this event
198
- # goes to (optional)
199
- # @option attrs [Hash] :options Options such as user traits (optional)
200
- # @option attrs [Time] :timestamp When the event occurred (optional)
201
- # @option attrs [String] :user_id The ID for the user that is part of
202
- # the group
203
- # @option attrs [String] :message_id ID that uniquely identifies a
204
- # message across the API. (optional)
108
+ # @option attrs [Hash] :traits User traits (optional)
109
+ # @macro common_attrs
205
110
  def group(attrs)
206
111
  symbolize_keys! attrs
207
- check_user_id! attrs
208
-
209
- group_id = attrs[:group_id]
210
- user_id = attrs[:user_id]
211
- traits = attrs[:traits] || {}
212
- timestamp = attrs[:timestamp] || Time.new
213
- context = attrs[:context] || {}
214
- message_id = attrs[:message_id].to_s if attrs[:message_id]
215
-
216
- raise ArgumentError, '.traits must be a hash' unless traits.is_a? Hash
217
- isoify_dates! traits
218
-
219
- check_presence! group_id, 'group_id'
220
- check_timestamp! timestamp
221
- add_context context
222
-
223
- enqueue({
224
- :groupId => group_id,
225
- :userId => user_id,
226
- :traits => traits,
227
- :integrations => attrs[:integrations],
228
- :options => attrs[:options],
229
- :context => context,
230
- :messageId => message_id,
231
- :timestamp => datetime_in_iso8601(timestamp),
232
- :type => 'group'
233
- })
112
+ enqueue(FieldParser.parse_for_group(attrs))
234
113
  end
235
114
 
236
115
  # Records a page view
@@ -238,97 +117,26 @@ module Segment
238
117
  # @see https://segment.com/docs/sources/server/ruby/#page
239
118
  #
240
119
  # @param [Hash] attrs
241
- # @option attrs [String] :anonymous_id ID for a user when you don't know
242
- # who they are yet. (optional but you must provide either an
243
- # `anonymous_id` or `user_id`)
244
- # @option attrs [String] :category The page category (optional)
245
- # @option attrs [Hash] :context ({})
246
- # @option attrs [Hash] :integrations What integrations this event
247
- # goes to (optional)
120
+ #
248
121
  # @option attrs [String] :name Name of the page
249
- # @option attrs [Hash] :options Options such as user traits (optional)
250
122
  # @option attrs [Hash] :properties Page properties (optional)
251
- # @option attrs [Time] :timestamp When the pageview occurred (optional)
252
- # @option attrs [String] :user_id The ID of the user viewing the page
253
- # @option attrs [String] :message_id ID that uniquely identifies a
254
- # message across the API. (optional)
123
+ # @macro common_attrs
255
124
  def page(attrs)
256
125
  symbolize_keys! attrs
257
- check_user_id! attrs
258
-
259
- name = attrs[:name].to_s
260
- properties = attrs[:properties] || {}
261
- timestamp = attrs[:timestamp] || Time.new
262
- context = attrs[:context] || {}
263
- message_id = attrs[:message_id].to_s if attrs[:message_id]
264
-
265
- raise ArgumentError, '.properties must be a hash' unless properties.is_a? Hash
266
- isoify_dates! properties
267
-
268
- check_timestamp! timestamp
269
- add_context context
270
-
271
- enqueue({
272
- :userId => attrs[:user_id],
273
- :anonymousId => attrs[:anonymous_id],
274
- :name => name,
275
- :category => attrs[:category],
276
- :properties => properties,
277
- :integrations => attrs[:integrations],
278
- :options => attrs[:options],
279
- :context => context,
280
- :messageId => message_id,
281
- :timestamp => datetime_in_iso8601(timestamp),
282
- :type => 'page'
283
- })
126
+ enqueue(FieldParser.parse_for_page(attrs))
284
127
  end
285
128
 
286
129
  # Records a screen view (for a mobile app)
287
130
  #
288
131
  # @param [Hash] attrs
289
- # @option attrs [String] :anonymous_id ID for a user when you don't know
290
- # who they are yet. (optional but you must provide either an
291
- # `anonymous_id` or `user_id`)
292
- # @option attrs [String] :category The screen category (optional)
293
- # @option attrs [Hash] :context ({})
294
- # @option attrs [Hash] :integrations What integrations this event
295
- # goes to (optional)
132
+ #
296
133
  # @option attrs [String] :name Name of the screen
297
- # @option attrs [Hash] :options Options such as user traits (optional)
298
- # @option attrs [Hash] :properties Page properties (optional)
299
- # @option attrs [Time] :timestamp When the pageview occurred (optional)
300
- # @option attrs [String] :user_id The ID of the user viewing the screen
301
- # @option attrs [String] :message_id ID that uniquely identifies a
302
- # message across the API. (optional)
134
+ # @option attrs [Hash] :properties Screen properties (optional)
135
+ # @option attrs [String] :category The screen category (optional)
136
+ # @macro common_attrs
303
137
  def screen(attrs)
304
138
  symbolize_keys! attrs
305
- check_user_id! attrs
306
-
307
- name = attrs[:name].to_s
308
- properties = attrs[:properties] || {}
309
- timestamp = attrs[:timestamp] || Time.new
310
- context = attrs[:context] || {}
311
- message_id = attrs[:message_id].to_s if attrs[:message_id]
312
-
313
- raise ArgumentError, '.properties must be a hash' unless properties.is_a? Hash
314
- isoify_dates! properties
315
-
316
- check_timestamp! timestamp
317
- add_context context
318
-
319
- enqueue({
320
- :userId => attrs[:user_id],
321
- :anonymousId => attrs[:anonymous_id],
322
- :name => name,
323
- :properties => properties,
324
- :category => attrs[:category],
325
- :options => attrs[:options],
326
- :integrations => attrs[:integrations],
327
- :context => context,
328
- :messageId => message_id,
329
- :timestamp => timestamp.iso8601,
330
- :type => 'screen'
331
- })
139
+ enqueue(FieldParser.parse_for_screen(attrs))
332
140
  end
333
141
 
334
142
  # @return [Fixnum] number of messages in the queue
@@ -336,6 +144,14 @@ module Segment
336
144
  @queue.length
337
145
  end
338
146
 
147
+ def test_queue
148
+ unless @test
149
+ raise 'Test queue only available when setting :test to true.'
150
+ end
151
+
152
+ @test_queue ||= TestQueue.new
153
+ end
154
+
339
155
  private
340
156
 
341
157
  # private: Enqueues the action.
@@ -345,6 +161,8 @@ module Segment
345
161
  # add our request id for tracing purposes
346
162
  action[:messageId] ||= uid
347
163
 
164
+ test_queue << action if @test
165
+
348
166
  if @queue.length < @max_queue_size
349
167
  @queue << action
350
168
  ensure_worker_running
@@ -360,53 +178,11 @@ module Segment
360
178
  end
361
179
  end
362
180
 
363
- # private: Ensures that a string is non-empty
364
- #
365
- # obj - String|Number that must be non-blank
366
- # name - Name of the validated value
367
- #
368
- def check_presence!(obj, name)
369
- if obj.nil? || (obj.is_a?(String) && obj.empty?)
370
- raise ArgumentError, "#{name} must be given"
371
- end
372
- end
373
-
374
- # private: Adds contextual information to the call
375
- #
376
- # context - Hash of call context
377
- def add_context(context)
378
- context[:library] = { :name => 'analytics-ruby', :version => Segment::Analytics::VERSION.to_s }
379
- end
380
-
381
181
  # private: Checks that the write_key is properly initialized
382
182
  def check_write_key!
383
183
  raise ArgumentError, 'Write key must be initialized' if @write_key.nil?
384
184
  end
385
185
 
386
- # private: Checks the timstamp option to make sure it is a Time.
387
- def check_timestamp!(timestamp)
388
- raise ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time
389
- end
390
-
391
- def event(attrs)
392
- symbolize_keys! attrs
393
-
394
- {
395
- :userId => user_id,
396
- :name => name,
397
- :properties => properties,
398
- :context => context,
399
- :timestamp => datetime_in_iso8601(timestamp),
400
- :type => 'screen'
401
- }
402
- end
403
-
404
- def check_user_id!(attrs)
405
- unless attrs[:user_id] || attrs[:anonymous_id]
406
- raise ArgumentError, 'Must supply either user_id or anonymous_id'
407
- end
408
- end
409
-
410
186
  def ensure_worker_running
411
187
  return if worker_running?
412
188
  @worker_mutex.synchronize do
@@ -0,0 +1,192 @@
1
+ module Segment
2
+ class Analytics
3
+ # Handles parsing fields according to the Segment Spec
4
+ #
5
+ # @see https://segment.com/docs/spec/
6
+ class FieldParser
7
+ class << self
8
+ include Segment::Analytics::Utils
9
+
10
+ # In addition to the common fields, track accepts:
11
+ #
12
+ # - "event"
13
+ # - "properties"
14
+ def parse_for_track(fields)
15
+ common = parse_common_fields(fields)
16
+
17
+ event = fields[:event]
18
+ properties = fields[:properties] || {}
19
+
20
+ check_presence!(event, 'event')
21
+ check_is_hash!(properties, 'properties')
22
+
23
+ isoify_dates! properties
24
+
25
+ common.merge({
26
+ :type => 'track',
27
+ :event => event.to_s,
28
+ :properties => properties
29
+ })
30
+ end
31
+
32
+ # In addition to the common fields, identify accepts:
33
+ #
34
+ # - "traits"
35
+ def parse_for_identify(fields)
36
+ common = parse_common_fields(fields)
37
+
38
+ traits = fields[:traits] || {}
39
+ check_is_hash!(traits, 'traits')
40
+ isoify_dates! traits
41
+
42
+ common.merge({
43
+ :type => 'identify',
44
+ :traits => traits
45
+ })
46
+ end
47
+
48
+ # In addition to the common fields, alias accepts:
49
+ #
50
+ # - "previous_id"
51
+ def parse_for_alias(fields)
52
+ common = parse_common_fields(fields)
53
+
54
+ previous_id = fields[:previous_id]
55
+ check_presence!(previous_id, 'previous_id')
56
+
57
+ common.merge({
58
+ :type => 'alias',
59
+ :previousId => previous_id
60
+ })
61
+ end
62
+
63
+ # In addition to the common fields, group accepts:
64
+ #
65
+ # - "group_id"
66
+ # - "traits"
67
+ def parse_for_group(fields)
68
+ common = parse_common_fields(fields)
69
+
70
+ group_id = fields[:group_id]
71
+ traits = fields[:traits] || {}
72
+
73
+ check_presence!(group_id, 'group_id')
74
+ check_is_hash!(traits, 'traits')
75
+
76
+ isoify_dates! traits
77
+
78
+ common.merge({
79
+ :type => 'group',
80
+ :groupId => group_id,
81
+ :traits => traits
82
+ })
83
+ end
84
+
85
+ # In addition to the common fields, page accepts:
86
+ #
87
+ # - "name"
88
+ # - "properties"
89
+ def parse_for_page(fields)
90
+ common = parse_common_fields(fields)
91
+
92
+ name = fields[:name] || ''
93
+ properties = fields[:properties] || {}
94
+
95
+ check_is_hash!(properties, 'properties')
96
+
97
+ isoify_dates! properties
98
+
99
+ common.merge({
100
+ :type => 'page',
101
+ :name => name.to_s,
102
+ :properties => properties
103
+ })
104
+ end
105
+
106
+ # In addition to the common fields, screen accepts:
107
+ #
108
+ # - "name"
109
+ # - "properties"
110
+ # - "category" (Not in spec, retained for backward compatibility"
111
+ def parse_for_screen(fields)
112
+ common = parse_common_fields(fields)
113
+
114
+ name = fields[:name]
115
+ properties = fields[:properties] || {}
116
+ category = fields[:category]
117
+
118
+ check_presence!(name, 'name')
119
+ check_is_hash!(properties, 'properties')
120
+
121
+ isoify_dates! properties
122
+
123
+ parsed = common.merge({
124
+ :type => 'screen',
125
+ :name => name,
126
+ :properties => properties
127
+ })
128
+
129
+ parsed[:category] = category if category
130
+
131
+ parsed
132
+ end
133
+
134
+ private
135
+
136
+ def parse_common_fields(fields)
137
+ timestamp = fields[:timestamp] || Time.new
138
+ message_id = fields[:message_id].to_s if fields[:message_id]
139
+ context = fields[:context] || {}
140
+
141
+ check_user_id! fields
142
+ check_timestamp! timestamp
143
+
144
+ add_context! context
145
+
146
+ parsed = {
147
+ :context => context,
148
+ :messageId => message_id,
149
+ :timestamp => datetime_in_iso8601(timestamp)
150
+ }
151
+
152
+ parsed[:userId] = fields[:user_id] if fields[:user_id]
153
+ parsed[:anonymousId] = fields[:anonymous_id] if fields[:anonymous_id]
154
+ parsed[:integrations] = fields[:integrations] if fields[:integrations]
155
+
156
+ # Not in spec, retained for backward compatibility
157
+ parsed[:options] = fields[:options] if fields[:options]
158
+
159
+ parsed
160
+ end
161
+
162
+ def check_user_id!(fields)
163
+ unless fields[:user_id] || fields[:anonymous_id]
164
+ raise ArgumentError, 'Must supply either user_id or anonymous_id'
165
+ end
166
+ end
167
+
168
+ def check_timestamp!(timestamp)
169
+ raise ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time
170
+ end
171
+
172
+ def add_context!(context)
173
+ context[:library] = { :name => 'analytics-ruby', :version => Segment::Analytics::VERSION.to_s }
174
+ end
175
+
176
+ # private: Ensures that a string is non-empty
177
+ #
178
+ # obj - String|Number that must be non-blank
179
+ # name - Name of the validated value
180
+ def check_presence!(obj, name)
181
+ if obj.nil? || (obj.is_a?(String) && obj.empty?)
182
+ raise ArgumentError, "#{name} must be given"
183
+ end
184
+ end
185
+
186
+ def check_is_hash!(obj, name)
187
+ raise ArgumentError, "#{name} must be a Hash" unless obj.is_a? Hash
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -5,6 +5,8 @@ module Segment
5
5
  class Analytics
6
6
  # A batch of `Message`s to be sent to the API
7
7
  class MessageBatch
8
+ class JSONGenerationError < StandardError; end
9
+
8
10
  extend Forwardable
9
11
  include Segment::Analytics::Logging
10
12
  include Segment::Analytics::Defaults::MessageBatch
@@ -16,8 +18,13 @@ module Segment
16
18
  end
17
19
 
18
20
  def <<(message)
19
- message_json_size = message.to_json.bytesize
21
+ begin
22
+ message_json = message.to_json
23
+ rescue StandardError => e
24
+ raise JSONGenerationError, "Serialization error: #{e}"
25
+ end
20
26
 
27
+ message_json_size = message_json.bytesize
21
28
  if message_too_big?(message_json_size)
22
29
  logger.error('a message exceeded the maximum allowed size')
23
30
  else
@@ -0,0 +1,56 @@
1
+ module Segment
2
+ class Analytics
3
+ class TestQueue
4
+ attr_reader :messages
5
+
6
+ def initialize
7
+ reset!
8
+ end
9
+
10
+ def [](key)
11
+ all[key]
12
+ end
13
+
14
+ def count
15
+ all.count
16
+ end
17
+
18
+ def <<(message)
19
+ all << message
20
+ send(message[:type]) << message
21
+ end
22
+
23
+ def alias
24
+ messages[:alias] ||= []
25
+ end
26
+
27
+ def all
28
+ messages[:all] ||= []
29
+ end
30
+
31
+ def group
32
+ messages[:group] ||= []
33
+ end
34
+
35
+ def identify
36
+ messages[:identify] ||= []
37
+ end
38
+
39
+ def page
40
+ messages[:page] ||= []
41
+ end
42
+
43
+ def screen
44
+ messages[:screen] ||= []
45
+ end
46
+
47
+ def track
48
+ messages[:track] ||= []
49
+ end
50
+
51
+ def reset!
52
+ @messages = {}
53
+ end
54
+ end
55
+ end
56
+ end
@@ -9,13 +9,11 @@ require 'json'
9
9
 
10
10
  module Segment
11
11
  class Analytics
12
- class Request
12
+ class Transport
13
13
  include Segment::Analytics::Defaults::Request
14
14
  include Segment::Analytics::Utils
15
15
  include Segment::Analytics::Logging
16
16
 
17
- # public: Creates a new request object to send analytics batch
18
- #
19
17
  def initialize(options = {})
20
18
  options[:host] ||= HOST
21
19
  options[:port] ||= PORT
@@ -34,10 +32,10 @@ module Segment
34
32
  @http = http
35
33
  end
36
34
 
37
- # public: Posts the write key and batch of messages to the API.
35
+ # Sends a batch of messages to the API
38
36
  #
39
- # returns - Response of the status and error if it exists
40
- def post(write_key, batch)
37
+ # @return [Response] API response
38
+ def send(write_key, batch)
41
39
  logger.debug("Sending request for #{batch.length} items")
42
40
 
43
41
  last_response, exception = retry_with_backoff(@retries) do
@@ -59,6 +57,11 @@ module Segment
59
57
  end
60
58
  end
61
59
 
60
+ # Closes a persistent connection if it exists
61
+ def shutdown
62
+ @http.finish if @http.started?
63
+ end
64
+
62
65
  private
63
66
 
64
67
  def should_retry_request?(status_code, body)
@@ -113,10 +116,11 @@ module Segment
113
116
 
114
117
  if self.class.stub
115
118
  logger.debug "stubbed request to #{@path}: " \
116
- "write key = #{write_key}, batch = JSON.generate(#{batch})"
119
+ "write key = #{write_key}, batch = #{JSON.generate(batch)}"
117
120
 
118
121
  [200, '{}']
119
122
  else
123
+ @http.start unless @http.started? # Maintain a persistent connection
120
124
  response = @http.request(request, payload)
121
125
  [response.code.to_i, response.body]
122
126
  end
@@ -64,12 +64,8 @@ module Segment
64
64
  end
65
65
  end
66
66
 
67
- def time_in_iso8601(time, fraction_digits = 3)
68
- fraction = if fraction_digits > 0
69
- ('.%06i' % time.usec)[0, fraction_digits + 1]
70
- end
71
-
72
- "#{time.strftime('%Y-%m-%dT%H:%M:%S')}#{fraction}#{formatted_offset(time, true, 'Z')}"
67
+ def time_in_iso8601(time)
68
+ "#{time.strftime('%Y-%m-%dT%H:%M:%S.%6N')}#{formatted_offset(time, true, 'Z')}"
73
69
  end
74
70
 
75
71
  def date_in_iso8601(date)
@@ -1,5 +1,5 @@
1
1
  module Segment
2
2
  class Analytics
3
- VERSION = '2.2.6'
3
+ VERSION = '2.3.1'
4
4
  end
5
5
  end
@@ -1,6 +1,6 @@
1
1
  require 'segment/analytics/defaults'
2
2
  require 'segment/analytics/message_batch'
3
- require 'segment/analytics/request'
3
+ require 'segment/analytics/transport'
4
4
  require 'segment/analytics/utils'
5
5
 
6
6
  module Segment
@@ -29,6 +29,7 @@ module Segment
29
29
  batch_size = options[:batch_size] || Defaults::MessageBatch::MAX_SIZE
30
30
  @batch = MessageBatch.new(batch_size)
31
31
  @lock = Mutex.new
32
+ @transport = Transport.new
32
33
  end
33
34
 
34
35
  # public: Continuously runs the loop to check for new events
@@ -38,15 +39,16 @@ module Segment
38
39
  return if @queue.empty?
39
40
 
40
41
  @lock.synchronize do
41
- @batch << @queue.pop until @batch.full? || @queue.empty?
42
+ consume_message_from_queue! until @batch.full? || @queue.empty?
42
43
  end
43
44
 
44
- res = Request.new.post @write_key, @batch
45
-
45
+ res = @transport.send @write_key, @batch
46
46
  @on_error.call(res.status, res.error) unless res.status == 200
47
47
 
48
48
  @lock.synchronize { @batch.clear }
49
49
  end
50
+ ensure
51
+ @transport.shutdown
50
52
  end
51
53
 
52
54
  # public: Check whether we have outstanding requests.
@@ -54,6 +56,14 @@ module Segment
54
56
  def is_requesting?
55
57
  @lock.synchronize { !@batch.empty? }
56
58
  end
59
+
60
+ private
61
+
62
+ def consume_message_from_queue!
63
+ @batch << @queue.pop
64
+ rescue MessageBatch::JSONGenerationError => e
65
+ @on_error.call(-1, e.to_s)
66
+ end
57
67
  end
58
68
  end
59
69
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: analytics-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.6
4
+ version: 2.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Segment.io
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-03 00:00:00.000000000 Z
11
+ date: 2021-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: commander
@@ -136,10 +136,12 @@ files:
136
136
  - lib/segment/analytics/backoff_policy.rb
137
137
  - lib/segment/analytics/client.rb
138
138
  - lib/segment/analytics/defaults.rb
139
+ - lib/segment/analytics/field_parser.rb
139
140
  - lib/segment/analytics/logging.rb
140
141
  - lib/segment/analytics/message_batch.rb
141
- - lib/segment/analytics/request.rb
142
142
  - lib/segment/analytics/response.rb
143
+ - lib/segment/analytics/test_queue.rb
144
+ - lib/segment/analytics/transport.rb
143
145
  - lib/segment/analytics/utils.rb
144
146
  - lib/segment/analytics/version.rb
145
147
  - lib/segment/analytics/worker.rb
@@ -155,14 +157,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
155
157
  requirements:
156
158
  - - ">="
157
159
  - !ruby/object:Gem::Version
158
- version: '0'
160
+ version: '2.0'
159
161
  required_rubygems_version: !ruby/object:Gem::Requirement
160
162
  requirements:
161
163
  - - ">="
162
164
  - !ruby/object:Gem::Version
163
165
  version: '0'
164
166
  requirements: []
165
- rubygems_version: 3.0.3
167
+ rubygems_version: 3.0.8
166
168
  signing_key:
167
169
  specification_version: 4
168
170
  summary: Segment.io analytics library