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 +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
|