analytics-ruby 2.2.6 → 2.3.1

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,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