analytics-ruby 2.0.5 → 2.4.0
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/bin/analytics +108 -0
- data/lib/analytics-ruby.rb +1 -0
- data/lib/segment/analytics/backoff_policy.rb +49 -0
- data/lib/segment/analytics/client.rb +121 -256
- data/lib/segment/analytics/defaults.rb +20 -4
- data/lib/segment/analytics/field_parser.rb +192 -0
- data/lib/segment/analytics/logging.rb +36 -11
- data/lib/segment/analytics/message_batch.rb +72 -0
- data/lib/segment/analytics/response.rb +0 -1
- data/lib/segment/analytics/test_queue.rb +56 -0
- data/lib/segment/analytics/transport.rb +138 -0
- data/lib/segment/analytics/utils.rb +20 -19
- data/lib/segment/analytics/version.rb +1 -1
- data/lib/segment/analytics/worker.rb +20 -10
- data/lib/segment/analytics.rb +15 -6
- metadata +101 -55
- data/Gemfile +0 -2
- data/Gemfile.lock +0 -60
- data/History.md +0 -124
- data/Makefile +0 -8
- data/README.md +0 -39
- data/Rakefile +0 -7
- data/analytics-ruby.gemspec +0 -23
- data/lib/segment/analytics/request.rb +0 -84
- data/spec/segment/analytics/client_spec.rb +0 -299
- data/spec/segment/analytics/worker_spec.rb +0 -96
- data/spec/segment/analytics_spec.rb +0 -103
- data/spec/spec_helper.rb +0 -81
@@ -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
|
@@ -2,24 +2,49 @@ require 'logger'
|
|
2
2
|
|
3
3
|
module Segment
|
4
4
|
class Analytics
|
5
|
+
# Wraps an existing logger and adds a prefix to all messages
|
6
|
+
class PrefixedLogger
|
7
|
+
def initialize(logger, prefix)
|
8
|
+
@logger = logger
|
9
|
+
@prefix = prefix
|
10
|
+
end
|
11
|
+
|
12
|
+
def debug(msg)
|
13
|
+
@logger.debug("#{@prefix} #{msg}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def info(msg)
|
17
|
+
@logger.info("#{@prefix} #{msg}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def warn(msg)
|
21
|
+
@logger.warn("#{@prefix} #{msg}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def error(msg)
|
25
|
+
@logger.error("#{@prefix} #{msg}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
5
29
|
module Logging
|
6
30
|
class << self
|
7
31
|
def logger
|
8
|
-
@logger
|
9
|
-
Rails.logger
|
10
|
-
else
|
11
|
-
logger = Logger.new STDOUT
|
12
|
-
logger.progname = 'Segment::Analytics'
|
13
|
-
logger
|
14
|
-
end
|
15
|
-
end
|
32
|
+
return @logger if @logger
|
16
33
|
|
17
|
-
|
18
|
-
|
34
|
+
base_logger = if defined?(Rails)
|
35
|
+
Rails.logger
|
36
|
+
else
|
37
|
+
logger = Logger.new STDOUT
|
38
|
+
logger.progname = 'Segment::Analytics'
|
39
|
+
logger
|
40
|
+
end
|
41
|
+
@logger = PrefixedLogger.new(base_logger, '[analytics-ruby]')
|
19
42
|
end
|
43
|
+
|
44
|
+
attr_writer :logger
|
20
45
|
end
|
21
46
|
|
22
|
-
def self.included
|
47
|
+
def self.included(base)
|
23
48
|
class << base
|
24
49
|
def logger
|
25
50
|
Logging.logger
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'segment/analytics/logging'
|
3
|
+
|
4
|
+
module Segment
|
5
|
+
class Analytics
|
6
|
+
# A batch of `Message`s to be sent to the API
|
7
|
+
class MessageBatch
|
8
|
+
class JSONGenerationError < StandardError; end
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
include Segment::Analytics::Logging
|
12
|
+
include Segment::Analytics::Defaults::MessageBatch
|
13
|
+
|
14
|
+
def initialize(max_message_count)
|
15
|
+
@messages = []
|
16
|
+
@max_message_count = max_message_count
|
17
|
+
@json_size = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(message)
|
21
|
+
begin
|
22
|
+
message_json = message.to_json
|
23
|
+
rescue StandardError => e
|
24
|
+
raise JSONGenerationError, "Serialization error: #{e}"
|
25
|
+
end
|
26
|
+
|
27
|
+
message_json_size = message_json.bytesize
|
28
|
+
if message_too_big?(message_json_size)
|
29
|
+
logger.error('a message exceeded the maximum allowed size')
|
30
|
+
else
|
31
|
+
@messages << message
|
32
|
+
@json_size += message_json_size + 1 # One byte for the comma
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def full?
|
37
|
+
item_count_exhausted? || size_exhausted?
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear
|
41
|
+
@messages.clear
|
42
|
+
@json_size = 0
|
43
|
+
end
|
44
|
+
|
45
|
+
def_delegators :@messages, :to_json
|
46
|
+
def_delegators :@messages, :empty?
|
47
|
+
def_delegators :@messages, :length
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def item_count_exhausted?
|
52
|
+
@messages.length >= @max_message_count
|
53
|
+
end
|
54
|
+
|
55
|
+
def message_too_big?(message_json_size)
|
56
|
+
message_json_size > Defaults::Message::MAX_BYTES
|
57
|
+
end
|
58
|
+
|
59
|
+
# We consider the max size here as just enough to leave room for one more
|
60
|
+
# message of the largest size possible. This is a shortcut that allows us
|
61
|
+
# to use a native Ruby `Queue` that doesn't allow peeking. The tradeoff
|
62
|
+
# here is that we might fit in less messages than possible into a batch.
|
63
|
+
#
|
64
|
+
# The alternative is to use our own `Queue` implementation that allows
|
65
|
+
# peeking, and to consider the next message size when calculating whether
|
66
|
+
# the message can be accomodated in this batch.
|
67
|
+
def size_exhausted?
|
68
|
+
@json_size >= (MAX_BYTES - Defaults::Message::MAX_BYTES)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -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
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'segment/analytics/defaults'
|
2
|
+
require 'segment/analytics/utils'
|
3
|
+
require 'segment/analytics/response'
|
4
|
+
require 'segment/analytics/logging'
|
5
|
+
require 'segment/analytics/backoff_policy'
|
6
|
+
require 'net/http'
|
7
|
+
require 'net/https'
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
module Segment
|
11
|
+
class Analytics
|
12
|
+
class Transport
|
13
|
+
include Segment::Analytics::Defaults::Request
|
14
|
+
include Segment::Analytics::Utils
|
15
|
+
include Segment::Analytics::Logging
|
16
|
+
|
17
|
+
def initialize(options = {})
|
18
|
+
options[:host] ||= HOST
|
19
|
+
options[:port] ||= PORT
|
20
|
+
options[:ssl] ||= SSL
|
21
|
+
@headers = options[:headers] || HEADERS
|
22
|
+
@path = options[:path] || PATH
|
23
|
+
@retries = options[:retries] || RETRIES
|
24
|
+
@backoff_policy =
|
25
|
+
options[:backoff_policy] || Segment::Analytics::BackoffPolicy.new
|
26
|
+
|
27
|
+
http = Net::HTTP.new(options[:host], options[:port])
|
28
|
+
http.use_ssl = options[:ssl]
|
29
|
+
http.read_timeout = 8
|
30
|
+
http.open_timeout = 4
|
31
|
+
|
32
|
+
@http = http
|
33
|
+
end
|
34
|
+
|
35
|
+
# Sends a batch of messages to the API
|
36
|
+
#
|
37
|
+
# @return [Response] API response
|
38
|
+
def send(write_key, batch)
|
39
|
+
logger.debug("Sending request for #{batch.length} items")
|
40
|
+
|
41
|
+
last_response, exception = retry_with_backoff(@retries) do
|
42
|
+
status_code, body = send_request(write_key, batch)
|
43
|
+
error = JSON.parse(body)['error']
|
44
|
+
should_retry = should_retry_request?(status_code, body)
|
45
|
+
logger.debug("Response status code: #{status_code}")
|
46
|
+
logger.debug("Response error: #{error}") if error
|
47
|
+
|
48
|
+
[Response.new(status_code, error), should_retry]
|
49
|
+
end
|
50
|
+
|
51
|
+
if exception
|
52
|
+
logger.error(exception.message)
|
53
|
+
exception.backtrace.each { |line| logger.error(line) }
|
54
|
+
Response.new(-1, exception.to_s)
|
55
|
+
else
|
56
|
+
last_response
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Closes a persistent connection if it exists
|
61
|
+
def shutdown
|
62
|
+
@http.finish if @http.started?
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def should_retry_request?(status_code, body)
|
68
|
+
if status_code >= 500
|
69
|
+
true # Server error
|
70
|
+
elsif status_code == 429
|
71
|
+
true # Rate limited
|
72
|
+
elsif status_code >= 400
|
73
|
+
logger.error(body)
|
74
|
+
false # Client error. Do not retry, but log
|
75
|
+
else
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Takes a block that returns [result, should_retry].
|
81
|
+
#
|
82
|
+
# Retries upto `retries_remaining` times, if `should_retry` is false or
|
83
|
+
# an exception is raised. `@backoff_policy` is used to determine the
|
84
|
+
# duration to sleep between attempts
|
85
|
+
#
|
86
|
+
# Returns [last_result, raised_exception]
|
87
|
+
def retry_with_backoff(retries_remaining, &block)
|
88
|
+
result, caught_exception = nil
|
89
|
+
should_retry = false
|
90
|
+
|
91
|
+
begin
|
92
|
+
result, should_retry = yield
|
93
|
+
return [result, nil] unless should_retry
|
94
|
+
rescue StandardError => e
|
95
|
+
should_retry = true
|
96
|
+
caught_exception = e
|
97
|
+
end
|
98
|
+
|
99
|
+
if should_retry && (retries_remaining > 1)
|
100
|
+
logger.debug("Retrying request, #{retries_remaining} retries left")
|
101
|
+
sleep(@backoff_policy.next_interval.to_f / 1000)
|
102
|
+
retry_with_backoff(retries_remaining - 1, &block)
|
103
|
+
else
|
104
|
+
[result, caught_exception]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Sends a request for the batch, returns [status_code, body]
|
109
|
+
def send_request(write_key, batch)
|
110
|
+
payload = JSON.generate(
|
111
|
+
:sentAt => datetime_in_iso8601(Time.now),
|
112
|
+
:batch => batch
|
113
|
+
)
|
114
|
+
request = Net::HTTP::Post.new(@path, @headers)
|
115
|
+
request.basic_auth(write_key, nil)
|
116
|
+
|
117
|
+
if self.class.stub
|
118
|
+
logger.debug "stubbed request to #{@path}: " \
|
119
|
+
"write key = #{write_key}, batch = #{JSON.generate(batch)}"
|
120
|
+
|
121
|
+
[200, '{}']
|
122
|
+
else
|
123
|
+
@http.start unless @http.started? # Maintain a persistent connection
|
124
|
+
response = @http.request(request, payload)
|
125
|
+
[response.code.to_i, response.body]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class << self
|
130
|
+
attr_writer :stub
|
131
|
+
|
132
|
+
def stub
|
133
|
+
@stub || ENV['STUB']
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -8,7 +8,9 @@ module Segment
|
|
8
8
|
# public: Return a new hash with keys converted from strings to symbols
|
9
9
|
#
|
10
10
|
def symbolize_keys(hash)
|
11
|
-
hash.
|
11
|
+
hash.each_with_object({}) do |(k, v), memo|
|
12
|
+
memo[k.to_sym] = v
|
13
|
+
end
|
12
14
|
end
|
13
15
|
|
14
16
|
# public: Convert hash keys from strings to symbols in place
|
@@ -20,17 +22,18 @@ module Segment
|
|
20
22
|
# public: Return a new hash with keys as strings
|
21
23
|
#
|
22
24
|
def stringify_keys(hash)
|
23
|
-
hash.
|
25
|
+
hash.each_with_object({}) do |(k, v), memo|
|
26
|
+
memo[k.to_s] = v
|
27
|
+
end
|
24
28
|
end
|
25
29
|
|
26
30
|
# public: Returns a new hash with all the date values in the into iso8601
|
27
31
|
# strings
|
28
32
|
#
|
29
33
|
def isoify_dates(hash)
|
30
|
-
hash.
|
34
|
+
hash.each_with_object({}) do |(k, v), memo|
|
31
35
|
memo[k] = datetime_in_iso8601(v)
|
32
|
-
|
33
|
-
}
|
36
|
+
end
|
34
37
|
end
|
35
38
|
|
36
39
|
# public: Converts all the date values in the into iso8601 strings in place
|
@@ -42,16 +45,18 @@ module Segment
|
|
42
45
|
# public: Returns a uid string
|
43
46
|
#
|
44
47
|
def uid
|
45
|
-
arr = SecureRandom.random_bytes(16).unpack(
|
48
|
+
arr = SecureRandom.random_bytes(16).unpack('NnnnnN')
|
46
49
|
arr[2] = (arr[2] & 0x0fff) | 0x4000
|
47
50
|
arr[3] = (arr[3] & 0x3fff) | 0x8000
|
48
|
-
|
51
|
+
'%08x-%04x-%04x-%04x-%04x%08x' % arr
|
49
52
|
end
|
50
53
|
|
51
|
-
def datetime_in_iso8601
|
54
|
+
def datetime_in_iso8601(datetime)
|
52
55
|
case datetime
|
53
|
-
when Time
|
54
|
-
|
56
|
+
when Time
|
57
|
+
time_in_iso8601 datetime
|
58
|
+
when DateTime
|
59
|
+
time_in_iso8601 datetime.to_time
|
55
60
|
when Date
|
56
61
|
date_in_iso8601 datetime
|
57
62
|
else
|
@@ -59,19 +64,15 @@ module Segment
|
|
59
64
|
end
|
60
65
|
end
|
61
66
|
|
62
|
-
def time_in_iso8601
|
63
|
-
|
64
|
-
(".%06i" % time.usec)[0, fraction_digits + 1]
|
65
|
-
end
|
66
|
-
|
67
|
-
"#{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')}"
|
68
69
|
end
|
69
70
|
|
70
|
-
def date_in_iso8601
|
71
|
-
date.strftime(
|
71
|
+
def date_in_iso8601(date)
|
72
|
+
date.strftime('%F')
|
72
73
|
end
|
73
74
|
|
74
|
-
def formatted_offset
|
75
|
+
def formatted_offset(time, colon = true, alternate_utc_string = nil)
|
75
76
|
time.utc? && alternate_utc_string || seconds_to_utc_offset(time.utc_offset, colon)
|
76
77
|
end
|
77
78
|
|
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'segment/analytics/defaults'
|
2
|
+
require 'segment/analytics/message_batch'
|
3
|
+
require 'segment/analytics/transport'
|
2
4
|
require 'segment/analytics/utils'
|
3
|
-
require 'segment/analytics/defaults'
|
4
|
-
require 'segment/analytics/request'
|
5
5
|
|
6
6
|
module Segment
|
7
7
|
class Analytics
|
8
8
|
class Worker
|
9
9
|
include Segment::Analytics::Utils
|
10
10
|
include Segment::Analytics::Defaults
|
11
|
+
include Segment::Analytics::Logging
|
11
12
|
|
12
13
|
# public: Creates a new worker
|
13
14
|
#
|
@@ -24,10 +25,11 @@ module Segment
|
|
24
25
|
symbolize_keys! options
|
25
26
|
@queue = queue
|
26
27
|
@write_key = write_key
|
27
|
-
@
|
28
|
-
|
29
|
-
@batch =
|
28
|
+
@on_error = options[:on_error] || proc { |status, error| }
|
29
|
+
batch_size = options[:batch_size] || Defaults::MessageBatch::MAX_SIZE
|
30
|
+
@batch = MessageBatch.new(batch_size)
|
30
31
|
@lock = Mutex.new
|
32
|
+
@transport = Transport.new(options)
|
31
33
|
end
|
32
34
|
|
33
35
|
# public: Continuously runs the loop to check for new events
|
@@ -37,16 +39,16 @@ module Segment
|
|
37
39
|
return if @queue.empty?
|
38
40
|
|
39
41
|
@lock.synchronize do
|
40
|
-
until @batch.
|
41
|
-
@batch << @queue.pop
|
42
|
-
end
|
42
|
+
consume_message_from_queue! until @batch.full? || @queue.empty?
|
43
43
|
end
|
44
44
|
|
45
|
-
res =
|
46
|
-
@on_error.call
|
45
|
+
res = @transport.send @write_key, @batch
|
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
|