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.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +89 -0
- data/History.md +222 -0
- data/Makefile +17 -0
- data/README.md +84 -0
- data/RELEASING.md +9 -0
- data/Rakefile +23 -0
- data/analytics-ruby.gemspec +33 -0
- data/bin/analytics +93 -0
- data/codecov.yml +2 -0
- data/lib/analytics-ruby.rb +1 -0
- data/lib/segment.rb +1 -0
- data/lib/segment/analytics.rb +38 -0
- data/lib/segment/analytics/backoff_policy.rb +49 -0
- data/lib/segment/analytics/client.rb +425 -0
- data/lib/segment/analytics/defaults.rb +36 -0
- data/lib/segment/analytics/logging.rb +33 -0
- data/lib/segment/analytics/message.rb +26 -0
- data/lib/segment/analytics/message_batch.rb +59 -0
- data/lib/segment/analytics/request.rb +134 -0
- data/lib/segment/analytics/response.rb +15 -0
- data/lib/segment/analytics/utils.rb +91 -0
- data/lib/segment/analytics/version.rb +5 -0
- data/lib/segment/analytics/worker.rb +61 -0
- data/spec/helpers/runscope_client.rb +38 -0
- data/spec/segment/analytics/backoff_policy_spec.rb +92 -0
- data/spec/segment/analytics/client_spec.rb +328 -0
- data/spec/segment/analytics/e2e_spec.rb +48 -0
- data/spec/segment/analytics/message_batch_spec.rb +49 -0
- data/spec/segment/analytics/message_spec.rb +35 -0
- data/spec/segment/analytics/request_spec.rb +244 -0
- data/spec/segment/analytics/response_spec.rb +30 -0
- data/spec/segment/analytics/worker_spec.rb +110 -0
- data/spec/segment/analytics_spec.rb +120 -0
- data/spec/spec_helper.rb +128 -0
- metadata +205 -0
@@ -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
|
data/bin/analytics
ADDED
@@ -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
|
data/codecov.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'segment'
|
data/lib/segment.rb
ADDED
@@ -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
|