segment 2.2.5

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.
@@ -0,0 +1,33 @@
1
+ require File.expand_path('../lib/segment/analytics/version', __FILE__)
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'segment'
5
+ spec.version = Segment::Analytics::VERSION
6
+ spec.files = Dir.glob('**/*')
7
+ spec.require_paths = ['lib']
8
+ spec.bindir = 'bin'
9
+ spec.executables = ['analytics']
10
+ spec.summary = 'Segment analytics library'
11
+ spec.description = 'The Segment ruby analytics library'
12
+ spec.authors = ['Segment']
13
+ spec.email = 'friends@segment.com'
14
+ spec.homepage = 'https://github.com/segmentio/analytics-ruby'
15
+ spec.license = 'MIT'
16
+
17
+ # Ruby 1.8 requires json
18
+ spec.add_dependency 'json', ['~> 1.7'] if RUBY_VERSION < "1.9"
19
+ spec.add_dependency 'commander', '~> 4.4'
20
+
21
+ spec.add_development_dependency 'rake', '~> 10.3'
22
+ spec.add_development_dependency 'rspec', '~> 3.0'
23
+ spec.add_development_dependency 'tzinfo', '1.2.1'
24
+ spec.add_development_dependency 'activesupport', '~> 4.1.11'
25
+ spec.add_development_dependency 'faraday', '~> 0.13'
26
+ spec.add_development_dependency 'pmap', '~> 1.1'
27
+
28
+ if RUBY_VERSION >= "2.1"
29
+ spec.add_development_dependency 'rubocop', '~> 0.51.0'
30
+ end
31
+
32
+ spec.add_development_dependency 'codecov', '~> 0.1.4'
33
+ end
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'segment/analytics'
4
+ require 'rubygems'
5
+ require 'commander/import'
6
+ require 'time'
7
+ require 'json'
8
+
9
+ program :name, 'simulator.rb'
10
+ program :version, '0.0.1'
11
+ program :description, 'scripting simulator'
12
+
13
+ def json_hash(str)
14
+ if str
15
+ return JSON.parse(str)
16
+ end
17
+ end
18
+
19
+ # analytics -method=<method> -segment-write-key=<segmentWriteKey> [options]
20
+
21
+ default_command :send
22
+
23
+ command :send do |c|
24
+ c.description = 'send a segment message'
25
+
26
+ c.option '--writeKey=<writeKey>', String, 'the Segment writeKey'
27
+ c.option '--type=<type>', String, 'The Segment message type'
28
+
29
+ c.option '--userId=<userId>', String, 'the user id to send the event as'
30
+ c.option '--anonymousId=<anonymousId>', String, 'the anonymous user id to send the event as'
31
+ c.option '--context=<context>', 'additional context for the event (JSON-encoded)'
32
+
33
+ c.option '--event=<event>', String, 'the event name to send with the event'
34
+ c.option '--properties=<properties>', 'the event properties to send (JSON-encoded)'
35
+
36
+ c.option '--name=<name>', 'name of the screen or page to send with the message'
37
+
38
+ c.option '--traits=<traits>', 'the identify/group traits to send (JSON-encoded)'
39
+
40
+ c.option '--groupId=<groupId>', String, 'the group id'
41
+
42
+ c.action do |args, options|
43
+ Analytics = Segment::Analytics.new({
44
+ write_key: options.writeKey,
45
+ on_error: Proc.new { |status, msg| print msg }
46
+ })
47
+
48
+ case options.type
49
+ when "track"
50
+ Analytics.track({
51
+ user_id: options.userId,
52
+ event: options.event,
53
+ anonymous_id: options.anonymousId,
54
+ properties: json_hash(options.properties),
55
+ context: json_hash(options.context)
56
+ })
57
+ when "page"
58
+ Analytics.page({
59
+ user_id: options.userId,
60
+ anonymous_id: options.anonymousId,
61
+ name: options.name,
62
+ properties: json_hash(options.properties),
63
+ context: json_hash(options.context)
64
+ })
65
+ when "screen"
66
+ Analytics.screen({
67
+ user_id: options.userId,
68
+ anonymous_id: options.anonymousId,
69
+ name: option.name,
70
+ traits: json_hash(options.traits),
71
+ properties: json_hash(option.properties)
72
+ })
73
+ when "identify"
74
+ Analytics.identify({
75
+ user_id: options.userId,
76
+ anonymous_id: options.anonymousId,
77
+ traits: json_hash(options.traits),
78
+ context: json_hash(options.context)
79
+ })
80
+ when "group"
81
+ Analytics.group({
82
+ user_id: options.userId,
83
+ anonymous_id: options.anonymousId,
84
+ group_id: options.groupId,
85
+ traits: json_hash(options.traits),
86
+ context: json_hash(options.context)
87
+ })
88
+ else
89
+ raise "Invalid Message Type #{options.type}"
90
+ end
91
+ Analytics.flush
92
+ end
93
+ end
@@ -0,0 +1,2 @@
1
+ ignore:
2
+ - "spec/**/*"
@@ -0,0 +1 @@
1
+ require 'segment'
@@ -0,0 +1 @@
1
+ require 'segment/analytics'
@@ -0,0 +1,38 @@
1
+ require 'segment/analytics/version'
2
+ require 'segment/analytics/defaults'
3
+ require 'segment/analytics/utils'
4
+ require 'segment/analytics/client'
5
+ require 'segment/analytics/worker'
6
+ require 'segment/analytics/request'
7
+ require 'segment/analytics/response'
8
+ require 'segment/analytics/logging'
9
+
10
+ module Segment
11
+ class Analytics
12
+ # Initializes a new instance of {Segment::Analytics::Client}, to which all
13
+ # method calls are proxied.
14
+ #
15
+ # @param options includes options that are passed down to
16
+ # {Segment::Analytics::Client#initialize}
17
+ # @option options [Boolean] :stub (false) If true, requests don't hit the
18
+ # server and are stubbed to be successful.
19
+ def initialize(options = {})
20
+ Request.stub = options[:stub] if options.has_key?(:stub)
21
+ @client = Segment::Analytics::Client.new options
22
+ end
23
+
24
+ def method_missing(message, *args, &block)
25
+ if @client.respond_to? message
26
+ @client.send message, *args, &block
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def respond_to_missing?(method_name, include_private = false)
33
+ @client.respond_to?(method_name) || super
34
+ end
35
+
36
+ include Logging
37
+ end
38
+ end
@@ -0,0 +1,49 @@
1
+ require 'segment/analytics/defaults'
2
+
3
+ module Segment
4
+ class Analytics
5
+ class BackoffPolicy
6
+ include Segment::Analytics::Defaults::BackoffPolicy
7
+
8
+ # @param [Hash] opts
9
+ # @option opts [Numeric] :min_timeout_ms The minimum backoff timeout
10
+ # @option opts [Numeric] :max_timeout_ms The maximum backoff timeout
11
+ # @option opts [Numeric] :multiplier The value to multiply the current
12
+ # interval with for each retry attempt
13
+ # @option opts [Numeric] :randomization_factor The randomization factor
14
+ # to use to create a range around the retry interval
15
+ def initialize(opts = {})
16
+ @min_timeout_ms = opts[:min_timeout_ms] || MIN_TIMEOUT_MS
17
+ @max_timeout_ms = opts[:max_timeout_ms] || MAX_TIMEOUT_MS
18
+ @multiplier = opts[:multiplier] || MULTIPLIER
19
+ @randomization_factor = opts[:randomization_factor] || RANDOMIZATION_FACTOR
20
+
21
+ @attempts = 0
22
+ end
23
+
24
+ # @return [Numeric] the next backoff interval, in milliseconds.
25
+ def next_interval
26
+ interval = @min_timeout_ms * (@multiplier**@attempts)
27
+ interval = add_jitter(interval, @randomization_factor)
28
+
29
+ @attempts += 1
30
+
31
+ [interval, @max_timeout_ms].min
32
+ end
33
+
34
+ private
35
+
36
+ def add_jitter(base, randomization_factor)
37
+ random_number = rand
38
+ max_deviation = base * randomization_factor
39
+ deviation = random_number * max_deviation
40
+
41
+ if random_number < 0.5
42
+ base - deviation
43
+ else
44
+ base + deviation
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,425 @@
1
+ require 'thread'
2
+ require 'time'
3
+
4
+ require 'segment/analytics/defaults'
5
+ require 'segment/analytics/logging'
6
+ require 'segment/analytics/utils'
7
+ require 'segment/analytics/worker'
8
+
9
+ module Segment
10
+ class Analytics
11
+ class Client
12
+ include Segment::Analytics::Utils
13
+ include Segment::Analytics::Logging
14
+
15
+ # @param [Hash] opts
16
+ # @option opts [String] :write_key Your project's write_key
17
+ # @option opts [FixNum] :max_queue_size Maximum number of calls to be
18
+ # remain queued.
19
+ # @option opts [Proc] :on_error Handles error calls from the API.
20
+ def initialize(opts = {})
21
+ symbolize_keys!(opts)
22
+
23
+ @queue = Queue.new
24
+ @write_key = opts[:write_key]
25
+ @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
26
+ @options = opts
27
+ @worker_mutex = Mutex.new
28
+ @worker = Worker.new(@queue, @write_key, @options)
29
+
30
+ check_write_key!
31
+
32
+ at_exit { @worker_thread && @worker_thread[:should_exit] = true }
33
+ end
34
+
35
+ # Synchronously waits until the worker has flushed the queue.
36
+ #
37
+ # Use only for scripts which are not long-running, and will specifically
38
+ # exit
39
+ def flush
40
+ while !@queue.empty? || @worker.is_requesting?
41
+ ensure_worker_running
42
+ sleep(0.1)
43
+ end
44
+ end
45
+
46
+ # Tracks an event
47
+ #
48
+ # @see https://segment.com/docs/sources/server/ruby/#track
49
+ #
50
+ # @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 ({})
55
+ # @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
+ # @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)
65
+ def track(attrs)
66
+ 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
+ })
98
+ end
99
+
100
+ # Identifies a user
101
+ #
102
+ # @see https://segment.com/docs/sources/server/ruby/#identify
103
+ #
104
+ # @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)
113
+ # @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)
118
+ def identify(attrs)
119
+ 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
+ })
145
+ end
146
+
147
+ # Aliases a user from one id to another
148
+ #
149
+ # @see https://segment.com/docs/sources/server/ruby/#alias
150
+ #
151
+ # @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)
156
+ # @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)
161
+ def alias(attrs)
162
+ 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
+ })
185
+ end
186
+
187
+ # Associates a user identity with a group.
188
+ #
189
+ # @see https://segment.com/docs/sources/server/ruby/#group
190
+ #
191
+ # @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 ({})
196
+ # @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)
205
+ def group(attrs)
206
+ 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
+ })
234
+ end
235
+
236
+ # Records a page view
237
+ #
238
+ # @see https://segment.com/docs/sources/server/ruby/#page
239
+ #
240
+ # @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)
248
+ # @option attrs [String] :name Name of the page
249
+ # @option attrs [Hash] :options Options such as user traits (optional)
250
+ # @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)
255
+ def page(attrs)
256
+ 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
+ })
284
+ end
285
+
286
+ # Records a screen view (for a mobile app)
287
+ #
288
+ # @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)
296
+ # @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)
303
+ def screen(attrs)
304
+ 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
+ })
332
+ end
333
+
334
+ # @return [Fixnum] number of messages in the queue
335
+ def queued_messages
336
+ @queue.length
337
+ end
338
+
339
+ private
340
+
341
+ # private: Enqueues the action.
342
+ #
343
+ # returns Boolean of whether the item was added to the queue.
344
+ def enqueue(action)
345
+ # add our request id for tracing purposes
346
+ action[:messageId] ||= uid
347
+
348
+ if @queue.length < @max_queue_size
349
+ @queue << action
350
+ ensure_worker_running
351
+
352
+ true
353
+ else
354
+ logger.warn(
355
+ 'Queue is full, dropping events. The :max_queue_size ' \
356
+ 'configuration parameter can be increased to prevent this from ' \
357
+ 'happening.'
358
+ )
359
+ false
360
+ end
361
+ end
362
+
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
+ # private: Checks that the write_key is properly initialized
382
+ def check_write_key!
383
+ raise ArgumentError, 'Write key must be initialized' if @write_key.nil?
384
+ end
385
+
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
+ def ensure_worker_running
411
+ return if worker_running?
412
+ @worker_mutex.synchronize do
413
+ return if worker_running?
414
+ @worker_thread = Thread.new do
415
+ @worker.run
416
+ end
417
+ end
418
+ end
419
+
420
+ def worker_running?
421
+ @worker_thread && @worker_thread.alive?
422
+ end
423
+ end
424
+ end
425
+ end