segment 2.2.5

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