analytics-ruby 2.0.5 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 51f3f02afed05c9c8461b454e0a3067962a75b8efde0930db0098eebf4a62829
4
+ data.tar.gz: e7d486d6c6186535e9a005c4d950e6e29d2dd9937f9b6da625191d786f76846a
5
+ SHA512:
6
+ metadata.gz: c2ce09d450de65620eb99f9fae86aa5ce57709d53aa4a93c73c2d3db0b173afad532383a50fe4dcb953df72171b731a263ff7308aed79b646a25bc202c555e4d
7
+ data.tar.gz: b3625484ebca6117ae79fc42f03a595dff861b8c5ca02a6ef12ea9e933229c1479dad40b4db1d85e61ddf585c7c5a072d68e9cd21ced6b2482db338f45073707
data/bin/analytics ADDED
@@ -0,0 +1,108 @@
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
+ c.option '--integrations=<integrations>', 'additional integrations for the event (JSON-encoded)'
33
+
34
+ c.option '--event=<event>', String, 'the event name to send with the event'
35
+ c.option '--properties=<properties>', 'the event properties to send (JSON-encoded)'
36
+
37
+ c.option '--name=<name>', 'name of the screen or page to send with the message'
38
+
39
+ c.option '--traits=<traits>', 'the identify/group traits to send (JSON-encoded)'
40
+
41
+ c.option '--groupId=<groupId>', String, 'the group id'
42
+ c.option '--previousId=<previousId>', String, 'the previous id'
43
+
44
+ c.action do |args, options|
45
+ Analytics = Segment::Analytics.new({
46
+ write_key: options.writeKey,
47
+ on_error: Proc.new { |status, msg| print msg }
48
+ })
49
+
50
+ case options.type
51
+ when "track"
52
+ Analytics.track({
53
+ user_id: options.userId,
54
+ event: options.event,
55
+ anonymous_id: options.anonymousId,
56
+ properties: json_hash(options.properties),
57
+ context: json_hash(options.context),
58
+ integrations: json_hash(options.integrations)
59
+ })
60
+ when "page"
61
+ Analytics.page({
62
+ user_id: options.userId,
63
+ anonymous_id: options.anonymousId,
64
+ name: options.name,
65
+ properties: json_hash(options.properties),
66
+ context: json_hash(options.context),
67
+ integrations: json_hash(options.integrations)
68
+ })
69
+ when "screen"
70
+ Analytics.screen({
71
+ user_id: options.userId,
72
+ anonymous_id: options.anonymousId,
73
+ name: options.name,
74
+ properties: json_hash(options.properties),
75
+ context: json_hash(options.context),
76
+ integrations: json_hash(options.integrations)
77
+ })
78
+ when "identify"
79
+ Analytics.identify({
80
+ user_id: options.userId,
81
+ anonymous_id: options.anonymousId,
82
+ traits: json_hash(options.traits),
83
+ context: json_hash(options.context),
84
+ integrations: json_hash(options.integrations)
85
+ })
86
+ when "group"
87
+ Analytics.group({
88
+ user_id: options.userId,
89
+ anonymous_id: options.anonymousId,
90
+ group_id: options.groupId,
91
+ traits: json_hash(options.traits),
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)
102
+ })
103
+ else
104
+ raise "Invalid Message Type #{options.type}"
105
+ end
106
+ Analytics.flush
107
+ end
108
+ end
@@ -0,0 +1 @@
1
+ require 'segment'
@@ -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
@@ -1,39 +1,42 @@
1
1
  require 'thread'
2
2
  require 'time'
3
+
4
+ require 'segment/analytics/defaults'
5
+ require 'segment/analytics/logging'
3
6
  require 'segment/analytics/utils'
4
7
  require 'segment/analytics/worker'
5
- require 'segment/analytics/defaults'
6
8
 
7
9
  module Segment
8
10
  class Analytics
9
11
  class Client
10
12
  include Segment::Analytics::Utils
13
+ include Segment::Analytics::Logging
11
14
 
12
- # public: Creates a new client
13
- #
14
- # options - Hash
15
- # :write_key - String of your project's write_key
16
- # :max_queue_size - Fixnum of the max calls to remain queued (optional)
17
- # :on_error - Proc which handles error calls from the API
18
- def initialize options = {}
19
- symbolize_keys! options
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)
20
22
 
21
23
  @queue = Queue.new
22
- @write_key = options[:write_key]
23
- @max_queue_size = options[:max_queue_size] || Defaults::Queue::MAX_SIZE
24
- @options = options
24
+ @test = opts[:test]
25
+ @write_key = opts[:write_key]
26
+ @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
25
27
  @worker_mutex = Mutex.new
26
- @worker = Worker.new @queue, @write_key, @options
28
+ @worker = Worker.new(@queue, @write_key, opts)
29
+ @worker_thread = nil
27
30
 
28
31
  check_write_key!
29
32
 
30
33
  at_exit { @worker_thread && @worker_thread[:should_exit] = true }
31
34
  end
32
35
 
33
- # public: Synchronously waits until the worker has flushed the queue.
34
- # Use only for scripts which are not long-running, and will
35
- # specifically exit
36
+ # Synchronously waits until the worker has flushed the queue.
36
37
  #
38
+ # Use only for scripts which are not long-running, and will specifically
39
+ # exit
37
40
  def flush
38
41
  while !@queue.empty? || @worker.is_requesting?
39
42
  ensure_worker_running
@@ -41,222 +44,114 @@ module Segment
41
44
  end
42
45
  end
43
46
 
44
- # public: Tracks an event
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
+
61
+ # Tracks an event
45
62
  #
46
- # options - Hash
47
- # :event - String of event name.
48
- # :user_id - String of the user id.
49
- # :properties - Hash of event properties. (optional)
50
- # :timestamp - Time of when the event occurred. (optional)
51
- # :context - Hash of context. (optional)
52
- def track options
53
- symbolize_keys! options
54
- check_user_id! options
55
-
56
- event = options[:event]
57
- properties = options[:properties] || {}
58
- timestamp = options[:timestamp] || Time.new
59
- context = options[:context] || {}
60
-
61
- check_timestamp! timestamp
62
-
63
- if event.nil? || event.empty?
64
- fail ArgumentError, 'Must supply event as a non-empty string'
65
- end
66
-
67
- fail ArgumentError, 'Properties must be a Hash' unless properties.is_a? Hash
68
- isoify_dates! properties
69
-
70
- add_context context
71
-
72
- enqueue({
73
- :event => event,
74
- :userId => options[:user_id],
75
- :anonymousId => options[:anonymous_id],
76
- :context => context,
77
- :integrations => options[:integrations],
78
- :properties => properties,
79
- :timestamp => datetime_in_iso8601(timestamp),
80
- :type => 'track'
81
- })
63
+ # @see https://segment.com/docs/sources/server/ruby/#track
64
+ #
65
+ # @param [Hash] attrs
66
+ #
67
+ # @option attrs [String] :event Event name
68
+ # @option attrs [Hash] :properties Event properties (optional)
69
+ # @macro common_attrs
70
+ def track(attrs)
71
+ symbolize_keys! attrs
72
+ enqueue(FieldParser.parse_for_track(attrs))
82
73
  end
83
74
 
84
- # public: Identifies a user
75
+ # Identifies a user
85
76
  #
86
- # options - Hash
87
- # :user_id - String of the user id
88
- # :traits - Hash of user traits. (optional)
89
- # :timestamp - Time of when the event occurred. (optional)
90
- # :context - Hash of context. (optional)
91
- def identify options
92
- symbolize_keys! options
93
- check_user_id! options
94
-
95
- traits = options[:traits] || {}
96
- timestamp = options[:timestamp] || Time.new
97
- context = options[:context] || {}
98
-
99
- check_timestamp! timestamp
100
-
101
- fail ArgumentError, 'Must supply traits as a hash' unless traits.is_a? Hash
102
- isoify_dates! traits
103
-
104
- add_context context
105
-
106
- enqueue({
107
- :userId => options[:user_id],
108
- :anonymousId => options[:anonymous_id],
109
- :integrations => options[:integrations],
110
- :context => context,
111
- :traits => traits,
112
- :timestamp => datetime_in_iso8601(timestamp),
113
- :type => 'identify'
114
- })
77
+ # @see https://segment.com/docs/sources/server/ruby/#identify
78
+ #
79
+ # @param [Hash] attrs
80
+ #
81
+ # @option attrs [Hash] :traits User traits (optional)
82
+ # @macro common_attrs
83
+ def identify(attrs)
84
+ symbolize_keys! attrs
85
+ enqueue(FieldParser.parse_for_identify(attrs))
115
86
  end
116
87
 
117
- # public: Aliases a user from one id to another
88
+ # Aliases a user from one id to another
118
89
  #
119
- # options - Hash
120
- # :previous_id - String of the id to alias from
121
- # :user_id - String of the id to alias to
122
- # :timestamp - Time of when the alias occured (optional)
123
- # :context - Hash of context (optional)
124
- def alias(options)
125
- symbolize_keys! options
126
-
127
- from = options[:previous_id]
128
- to = options[:user_id]
129
- timestamp = options[:timestamp] || Time.new
130
- context = options[:context] || {}
131
-
132
- check_presence! from, 'previous_id'
133
- check_presence! to, 'user_id'
134
- check_timestamp! timestamp
135
- add_context context
136
-
137
- enqueue({
138
- :previousId => from,
139
- :userId => to,
140
- :integrations => options[:integrations],
141
- :context => context,
142
- :timestamp => datetime_in_iso8601(timestamp),
143
- :type => 'alias'
144
- })
90
+ # @see https://segment.com/docs/sources/server/ruby/#alias
91
+ #
92
+ # @param [Hash] attrs
93
+ #
94
+ # @option attrs [String] :previous_id The ID to alias from
95
+ # @macro common_attrs
96
+ def alias(attrs)
97
+ symbolize_keys! attrs
98
+ enqueue(FieldParser.parse_for_alias(attrs))
145
99
  end
146
100
 
147
- # public: Associates a user identity with a group.
101
+ # Associates a user identity with a group.
148
102
  #
149
- # options - Hash
150
- # :previous_id - String of the id to alias from
151
- # :user_id - String of the id to alias to
152
- # :timestamp - Time of when the alias occured (optional)
153
- # :context - Hash of context (optional)
154
- def group(options)
155
- symbolize_keys! options
156
- check_user_id! options
157
-
158
- group_id = options[:group_id]
159
- user_id = options[:user_id]
160
- traits = options[:traits] || {}
161
- timestamp = options[:timestamp] || Time.new
162
- context = options[:context] || {}
163
-
164
- fail ArgumentError, '.traits must be a hash' unless traits.is_a? Hash
165
- isoify_dates! traits
166
-
167
- check_presence! group_id, 'group_id'
168
- check_timestamp! timestamp
169
- add_context context
170
-
171
- enqueue({
172
- :groupId => group_id,
173
- :userId => user_id,
174
- :traits => traits,
175
- :integrations => options[:integrations],
176
- :context => context,
177
- :timestamp => datetime_in_iso8601(timestamp),
178
- :type => 'group'
179
- })
103
+ # @see https://segment.com/docs/sources/server/ruby/#group
104
+ #
105
+ # @param [Hash] attrs
106
+ #
107
+ # @option attrs [String] :group_id The ID of the group
108
+ # @option attrs [Hash] :traits User traits (optional)
109
+ # @macro common_attrs
110
+ def group(attrs)
111
+ symbolize_keys! attrs
112
+ enqueue(FieldParser.parse_for_group(attrs))
180
113
  end
181
114
 
182
- # public: Records a page view
115
+ # Records a page view
183
116
  #
184
- # options - Hash
185
- # :user_id - String of the id to alias from
186
- # :name - String name of the page
187
- # :properties - Hash of page properties (optional)
188
- # :timestamp - Time of when the pageview occured (optional)
189
- # :context - Hash of context (optional)
190
- def page(options)
191
- symbolize_keys! options
192
- check_user_id! options
193
-
194
- name = options[:name].to_s
195
- properties = options[:properties] || {}
196
- timestamp = options[:timestamp] || Time.new
197
- context = options[:context] || {}
198
-
199
- fail ArgumentError, '.name must be a string' unless !name.empty?
200
- fail ArgumentError, '.properties must be a hash' unless properties.is_a? Hash
201
- isoify_dates! properties
202
-
203
- check_timestamp! timestamp
204
- add_context context
205
-
206
- enqueue({
207
- :userId => options[:user_id],
208
- :anonymousId => options[:anonymous_id],
209
- :name => name,
210
- :properties => properties,
211
- :integrations => options[:integrations],
212
- :context => context,
213
- :timestamp => datetime_in_iso8601(timestamp),
214
- :type => 'page'
215
- })
216
- end
217
- # public: Records a screen view (for a mobile app)
117
+ # @see https://segment.com/docs/sources/server/ruby/#page
218
118
  #
219
- # options - Hash
220
- # :user_id - String of the id to alias from
221
- # :name - String name of the screen
222
- # :properties - Hash of screen properties (optional)
223
- # :timestamp - Time of when the screen occured (optional)
224
- # :context - Hash of context (optional)
225
- def screen(options)
226
- symbolize_keys! options
227
- check_user_id! options
228
-
229
- name = options[:name].to_s
230
- properties = options[:properties] || {}
231
- timestamp = options[:timestamp] || Time.new
232
- context = options[:context] || {}
233
-
234
- fail ArgumentError, '.name must be a string' if name.empty?
235
- fail ArgumentError, '.properties must be a hash' unless properties.is_a? Hash
236
- isoify_dates! properties
237
-
238
- check_timestamp! timestamp
239
- add_context context
240
-
241
- enqueue({
242
- :userId => options[:user_id],
243
- :anonymousId => options[:anonymous_id],
244
- :name => name,
245
- :properties => properties,
246
- :integrations => options[:integrations],
247
- :context => context,
248
- :timestamp => timestamp.iso8601,
249
- :type => 'screen'
250
- })
119
+ # @param [Hash] attrs
120
+ #
121
+ # @option attrs [String] :name Name of the page
122
+ # @option attrs [Hash] :properties Page properties (optional)
123
+ # @macro common_attrs
124
+ def page(attrs)
125
+ symbolize_keys! attrs
126
+ enqueue(FieldParser.parse_for_page(attrs))
251
127
  end
252
128
 
253
- # public: Returns the number of queued messages
129
+ # Records a screen view (for a mobile app)
130
+ #
131
+ # @param [Hash] attrs
254
132
  #
255
- # returns Fixnum of messages in the queue
133
+ # @option attrs [String] :name Name of the screen
134
+ # @option attrs [Hash] :properties Screen properties (optional)
135
+ # @option attrs [String] :category The screen category (optional)
136
+ # @macro common_attrs
137
+ def screen(attrs)
138
+ symbolize_keys! attrs
139
+ enqueue(FieldParser.parse_for_screen(attrs))
140
+ end
141
+
142
+ # @return [Fixnum] number of messages in the queue
256
143
  def queued_messages
257
144
  @queue.length
258
145
  end
259
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
+
260
155
  private
261
156
 
262
157
  # private: Enqueues the action.
@@ -264,57 +159,28 @@ module Segment
264
159
  # returns Boolean of whether the item was added to the queue.
265
160
  def enqueue(action)
266
161
  # add our request id for tracing purposes
267
- action[:messageId] = uid
268
- unless queue_full = @queue.length >= @max_queue_size
269
- ensure_worker_running
162
+ action[:messageId] ||= uid
163
+
164
+ test_queue << action if @test
165
+
166
+ if @queue.length < @max_queue_size
270
167
  @queue << action
271
- end
272
- !queue_full
273
- end
168
+ ensure_worker_running
274
169
 
275
- # private: Ensures that a string is non-empty
276
- #
277
- # obj - String|Number that must be non-blank
278
- # name - Name of the validated value
279
- #
280
- def check_presence!(obj, name)
281
- if obj.nil? || (obj.is_a?(String) && obj.empty?)
282
- fail ArgumentError, "#{name} must be given"
170
+ true
171
+ else
172
+ logger.warn(
173
+ 'Queue is full, dropping events. The :max_queue_size ' \
174
+ 'configuration parameter can be increased to prevent this from ' \
175
+ 'happening.'
176
+ )
177
+ false
283
178
  end
284
179
  end
285
180
 
286
- # private: Adds contextual information to the call
287
- #
288
- # context - Hash of call context
289
- def add_context(context)
290
- context[:library] = { :name => "analytics-ruby", :version => Segment::Analytics::VERSION.to_s }
291
- end
292
-
293
181
  # private: Checks that the write_key is properly initialized
294
182
  def check_write_key!
295
- fail ArgumentError, 'Write key must be initialized' if @write_key.nil?
296
- end
297
-
298
- # private: Checks the timstamp option to make sure it is a Time.
299
- def check_timestamp!(timestamp)
300
- fail ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time
301
- end
302
-
303
- def event attrs
304
- symbolize_keys! attrs
305
-
306
- {
307
- :userId => user_id,
308
- :name => name,
309
- :properties => properties,
310
- :context => context,
311
- :timestamp => datetime_in_iso8601(timestamp),
312
- :type => 'screen'
313
- }
314
- end
315
-
316
- def check_user_id! options
317
- fail ArgumentError, 'Must supply either user_id or anonymous_id' unless options[:user_id] || options[:anonymous_id]
183
+ raise ArgumentError, 'Write key must be initialized' if @write_key.nil?
318
184
  end
319
185
 
320
186
  def ensure_worker_running
@@ -333,4 +199,3 @@ module Segment
333
199
  end
334
200
  end
335
201
  end
336
-
@@ -6,15 +6,31 @@ module Segment
6
6
  PORT = 443
7
7
  PATH = '/v1/import'
8
8
  SSL = true
9
- HEADERS = { :accept => 'application/json' }
10
- RETRIES = 4
11
- BACKOFF = 30.0
9
+ HEADERS = { 'Accept' => 'application/json',
10
+ 'Content-Type' => 'application/json',
11
+ 'User-Agent' => "analytics-ruby/#{Analytics::VERSION}" }
12
+ RETRIES = 10
12
13
  end
13
14
 
14
15
  module Queue
15
- BATCH_SIZE = 100
16
16
  MAX_SIZE = 10000
17
17
  end
18
+
19
+ module Message
20
+ MAX_BYTES = 32768 # 32Kb
21
+ end
22
+
23
+ module MessageBatch
24
+ MAX_BYTES = 512_000 # 500Kb
25
+ MAX_SIZE = 100
26
+ end
27
+
28
+ module BackoffPolicy
29
+ MIN_TIMEOUT_MS = 100
30
+ MAX_TIMEOUT_MS = 10000
31
+ MULTIPLIER = 1.5
32
+ RANDOMIZATION_FACTOR = 0.5
33
+ end
18
34
  end
19
35
  end
20
36
  end