june-analytics-ruby 2.4.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/analytics +106 -0
- data/lib/analytics-ruby.rb +1 -0
- data/lib/june/analytics/backoff_policy.rb +49 -0
- data/lib/june/analytics/client.rb +202 -0
- data/lib/june/analytics/defaults.rb +36 -0
- data/lib/june/analytics/field_parser.rb +195 -0
- data/lib/june/analytics/logging.rb +60 -0
- data/lib/june/analytics/message_batch.rb +72 -0
- data/lib/june/analytics/response.rb +15 -0
- data/lib/june/analytics/test_queue.rb +56 -0
- data/lib/june/analytics/transport.rb +138 -0
- data/lib/june/analytics/utils.rb +87 -0
- data/lib/june/analytics/version.rb +5 -0
- data/lib/june/analytics/worker.rb +69 -0
- data/lib/june/analytics.rb +40 -0
- data/lib/june.rb +1 -0
- metadata +171 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9d9ccf08aaa08d1cddeb97d6e87a256743953b77650ec26c5bc014b02d1a856f
|
4
|
+
data.tar.gz: fc9bed4bcd7a9ce0d94664912676c3511ddb3a65c0da237877b1d9e42f285859
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ca13d9568607e89050448e51b2ab8c6bff1b20dd936d15bac04c628bec946cd0bbb6260bf321544303df8d10a7cff8cbcbff4798ad07d9a4d71f6bcd02d7e2ba
|
7
|
+
data.tar.gz: 846d286ef6513e2956d1d53bb45fb90a3166f44a624da64fb55080f0b5c3b92c59926cb1953fa8638f2a81809b0b83b985e59d8e63c542d5af7c54be131935c2
|
data/bin/analytics
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'june/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
|
+
default_command :send
|
20
|
+
|
21
|
+
command :send do |c|
|
22
|
+
c.description = 'send a june message'
|
23
|
+
|
24
|
+
c.option '--writeKey=<writeKey>', String, 'the June writeKey'
|
25
|
+
c.option '--type=<type>', String, 'The June message type'
|
26
|
+
|
27
|
+
c.option '--userId=<userId>', String, 'the user id to send the event as'
|
28
|
+
c.option '--anonymousId=<anonymousId>', String, 'the anonymous user id to send the event as'
|
29
|
+
c.option '--context=<context>', 'additional context for the event (JSON-encoded)'
|
30
|
+
c.option '--integrations=<integrations>', 'additional integrations for the event (JSON-encoded)'
|
31
|
+
|
32
|
+
c.option '--event=<event>', String, 'the event name to send with the event'
|
33
|
+
c.option '--properties=<properties>', 'the event properties to send (JSON-encoded)'
|
34
|
+
|
35
|
+
c.option '--name=<name>', 'name of the screen or page to send with the message'
|
36
|
+
|
37
|
+
c.option '--traits=<traits>', 'the identify/group traits to send (JSON-encoded)'
|
38
|
+
|
39
|
+
c.option '--groupId=<groupId>', String, 'the group id'
|
40
|
+
c.option '--previousId=<previousId>', String, 'the previous id'
|
41
|
+
|
42
|
+
c.action do |args, options|
|
43
|
+
Analytics = June::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
|
+
integrations: json_hash(options.integrations)
|
57
|
+
})
|
58
|
+
when "page"
|
59
|
+
Analytics.page({
|
60
|
+
user_id: options.userId,
|
61
|
+
anonymous_id: options.anonymousId,
|
62
|
+
name: options.name,
|
63
|
+
properties: json_hash(options.properties),
|
64
|
+
context: json_hash(options.context),
|
65
|
+
integrations: json_hash(options.integrations)
|
66
|
+
})
|
67
|
+
when "screen"
|
68
|
+
Analytics.screen({
|
69
|
+
user_id: options.userId,
|
70
|
+
anonymous_id: options.anonymousId,
|
71
|
+
name: options.name,
|
72
|
+
properties: json_hash(options.properties),
|
73
|
+
context: json_hash(options.context),
|
74
|
+
integrations: json_hash(options.integrations)
|
75
|
+
})
|
76
|
+
when "identify"
|
77
|
+
Analytics.identify({
|
78
|
+
user_id: options.userId,
|
79
|
+
anonymous_id: options.anonymousId,
|
80
|
+
traits: json_hash(options.traits),
|
81
|
+
context: json_hash(options.context),
|
82
|
+
integrations: json_hash(options.integrations)
|
83
|
+
})
|
84
|
+
when "group"
|
85
|
+
Analytics.group({
|
86
|
+
user_id: options.userId,
|
87
|
+
anonymous_id: options.anonymousId,
|
88
|
+
group_id: options.groupId,
|
89
|
+
traits: json_hash(options.traits),
|
90
|
+
context: json_hash(options.context),
|
91
|
+
integrations: json_hash(options.integrations)
|
92
|
+
})
|
93
|
+
when "alias"
|
94
|
+
Analytics.alias({
|
95
|
+
previous_id: options.previousId,
|
96
|
+
user_id: options.userId,
|
97
|
+
anonymous_id: options.anonymousId,
|
98
|
+
context: json_hash(options.context),
|
99
|
+
integrations: json_hash(options.integrations)
|
100
|
+
})
|
101
|
+
else
|
102
|
+
raise "Invalid Message Type #{options.type}"
|
103
|
+
end
|
104
|
+
Analytics.flush
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'june'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'june/analytics/defaults'
|
2
|
+
|
3
|
+
module June
|
4
|
+
class Analytics
|
5
|
+
class BackoffPolicy
|
6
|
+
include June::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,202 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
require 'june/analytics/defaults'
|
5
|
+
require 'june/analytics/logging'
|
6
|
+
require 'june/analytics/utils'
|
7
|
+
require 'june/analytics/worker'
|
8
|
+
|
9
|
+
module June
|
10
|
+
class Analytics
|
11
|
+
class Client
|
12
|
+
include June::Analytics::Utils
|
13
|
+
include June::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
|
+
@test = opts[:test]
|
25
|
+
@write_key = opts[:write_key]
|
26
|
+
@max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
|
27
|
+
@worker_mutex = Mutex.new
|
28
|
+
@worker = Worker.new(@queue, @write_key, opts)
|
29
|
+
@worker_thread = nil
|
30
|
+
|
31
|
+
check_write_key!
|
32
|
+
|
33
|
+
at_exit { @worker_thread && @worker_thread[:should_exit] = true }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Synchronously waits until the worker has flushed the queue.
|
37
|
+
#
|
38
|
+
# Use only for scripts which are not long-running, and will specifically
|
39
|
+
# exit
|
40
|
+
def flush
|
41
|
+
while !@queue.empty? || @worker.is_requesting?
|
42
|
+
ensure_worker_running
|
43
|
+
sleep(0.1)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
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
|
62
|
+
#
|
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))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Identifies a user
|
76
|
+
#
|
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))
|
86
|
+
end
|
87
|
+
|
88
|
+
# Aliases a user from one id to another
|
89
|
+
#
|
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))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Associates a user identity with a group.
|
102
|
+
#
|
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))
|
113
|
+
end
|
114
|
+
|
115
|
+
# Records a page view
|
116
|
+
#
|
117
|
+
# @see https://segment.com/docs/sources/server/ruby/#page
|
118
|
+
#
|
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))
|
127
|
+
end
|
128
|
+
|
129
|
+
# Records a screen view (for a mobile app)
|
130
|
+
#
|
131
|
+
# @param [Hash] attrs
|
132
|
+
#
|
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
|
143
|
+
def queued_messages
|
144
|
+
@queue.length
|
145
|
+
end
|
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
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
# private: Enqueues the action.
|
158
|
+
#
|
159
|
+
# returns Boolean of whether the item was added to the queue.
|
160
|
+
def enqueue(action)
|
161
|
+
# add our request id for tracing purposes
|
162
|
+
action[:messageId] ||= uid
|
163
|
+
|
164
|
+
if @test
|
165
|
+
test_queue << action
|
166
|
+
return true
|
167
|
+
end
|
168
|
+
|
169
|
+
if @queue.length < @max_queue_size
|
170
|
+
@queue << action
|
171
|
+
ensure_worker_running
|
172
|
+
|
173
|
+
true
|
174
|
+
else
|
175
|
+
logger.warn(
|
176
|
+
'Queue is full, dropping events. The :max_queue_size configuration parameter can be increased to prevent this from happening.'
|
177
|
+
)
|
178
|
+
false
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# private: Checks that the write_key is properly initialized
|
183
|
+
def check_write_key!
|
184
|
+
raise ArgumentError, 'Write key must be initialized' if @write_key.nil?
|
185
|
+
end
|
186
|
+
|
187
|
+
def ensure_worker_running
|
188
|
+
return if worker_running?
|
189
|
+
@worker_mutex.synchronize do
|
190
|
+
return if worker_running?
|
191
|
+
@worker_thread = Thread.new do
|
192
|
+
@worker.run
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def worker_running?
|
198
|
+
@worker_thread && @worker_thread.alive?
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module June
|
2
|
+
class Analytics
|
3
|
+
module Defaults
|
4
|
+
module Request
|
5
|
+
HOST = 'api.june.so'
|
6
|
+
PORT = 443
|
7
|
+
PATH = '/sdk/batch'
|
8
|
+
SSL = true
|
9
|
+
HEADERS = { 'Accept' => 'application/json',
|
10
|
+
'Content-Type' => 'application/json',
|
11
|
+
'User-Agent' => "analytics-ruby/#{Analytics::VERSION}" }
|
12
|
+
RETRIES = 10
|
13
|
+
end
|
14
|
+
|
15
|
+
module Queue
|
16
|
+
MAX_SIZE = 10000
|
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
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
module June
|
2
|
+
class Analytics
|
3
|
+
# Handles parsing fields according to the June Spec
|
4
|
+
#
|
5
|
+
# @see https://june.so/docs/ruby
|
6
|
+
class FieldParser
|
7
|
+
class << self
|
8
|
+
include June::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
|
+
return unless blank?(fields[:user_id])
|
164
|
+
return unless blank?(fields[:anonymous_id])
|
165
|
+
|
166
|
+
raise ArgumentError, 'Must supply either user_id or anonymous_id'
|
167
|
+
end
|
168
|
+
|
169
|
+
def check_timestamp!(timestamp)
|
170
|
+
raise ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time
|
171
|
+
end
|
172
|
+
|
173
|
+
def add_context!(context)
|
174
|
+
context[:library] = { :name => 'analytics-ruby', :version => June::Analytics::VERSION.to_s }
|
175
|
+
end
|
176
|
+
|
177
|
+
# private: Ensures that a string is non-empty
|
178
|
+
#
|
179
|
+
# obj - String|Number that must be non-blank
|
180
|
+
# name - Name of the validated value
|
181
|
+
def check_presence!(obj, name)
|
182
|
+
raise ArgumentError, "#{name} must be given" if blank?(obj)
|
183
|
+
end
|
184
|
+
|
185
|
+
def blank?(obj)
|
186
|
+
obj.nil? || (obj.is_a?(String) && obj.empty?)
|
187
|
+
end
|
188
|
+
|
189
|
+
def check_is_hash!(obj, name)
|
190
|
+
raise ArgumentError, "#{name} must be a Hash" unless obj.is_a? Hash
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module June
|
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
|
+
|
29
|
+
module Logging
|
30
|
+
class << self
|
31
|
+
def logger
|
32
|
+
return @logger if @logger
|
33
|
+
|
34
|
+
base_logger = if defined?(Rails)
|
35
|
+
Rails.logger
|
36
|
+
else
|
37
|
+
logger = Logger.new STDOUT
|
38
|
+
logger.progname = 'June::Analytics'
|
39
|
+
logger
|
40
|
+
end
|
41
|
+
@logger = PrefixedLogger.new(base_logger, '[analytics-ruby]')
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_writer :logger
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.included(base)
|
48
|
+
class << base
|
49
|
+
def logger
|
50
|
+
Logging.logger
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def logger
|
56
|
+
Logging.logger
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'june/analytics/logging'
|
3
|
+
|
4
|
+
module June
|
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 June::Analytics::Logging
|
12
|
+
include June::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,15 @@
|
|
1
|
+
module June
|
2
|
+
class Analytics
|
3
|
+
class Response
|
4
|
+
attr_reader :status, :error
|
5
|
+
|
6
|
+
# public: Simple class to wrap responses from the API
|
7
|
+
#
|
8
|
+
#
|
9
|
+
def initialize(status = 200, error = nil)
|
10
|
+
@status = status
|
11
|
+
@error = error
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module June
|
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 'june/analytics/defaults'
|
2
|
+
require 'june/analytics/utils'
|
3
|
+
require 'june/analytics/response'
|
4
|
+
require 'june/analytics/logging'
|
5
|
+
require 'june/analytics/backoff_policy'
|
6
|
+
require 'net/http'
|
7
|
+
require 'net/https'
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
module June
|
11
|
+
class Analytics
|
12
|
+
class Transport
|
13
|
+
include June::Analytics::Defaults::Request
|
14
|
+
include June::Analytics::Utils
|
15
|
+
include June::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] || June::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
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module June
|
4
|
+
class Analytics
|
5
|
+
module Utils
|
6
|
+
extend self
|
7
|
+
|
8
|
+
# public: Return a new hash with keys converted from strings to symbols
|
9
|
+
#
|
10
|
+
def symbolize_keys(hash)
|
11
|
+
hash.each_with_object({}) do |(k, v), memo|
|
12
|
+
memo[k.to_sym] = v
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# public: Convert hash keys from strings to symbols in place
|
17
|
+
#
|
18
|
+
def symbolize_keys!(hash)
|
19
|
+
hash.replace symbolize_keys hash
|
20
|
+
end
|
21
|
+
|
22
|
+
# public: Return a new hash with keys as strings
|
23
|
+
#
|
24
|
+
def stringify_keys(hash)
|
25
|
+
hash.each_with_object({}) do |(k, v), memo|
|
26
|
+
memo[k.to_s] = v
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# public: Returns a new hash with all the date values in the into iso8601
|
31
|
+
# strings
|
32
|
+
#
|
33
|
+
def isoify_dates(hash)
|
34
|
+
hash.each_with_object({}) do |(k, v), memo|
|
35
|
+
memo[k] = datetime_in_iso8601(v)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# public: Converts all the date values in the into iso8601 strings in place
|
40
|
+
#
|
41
|
+
def isoify_dates!(hash)
|
42
|
+
hash.replace isoify_dates hash
|
43
|
+
end
|
44
|
+
|
45
|
+
# public: Returns a uid string
|
46
|
+
#
|
47
|
+
def uid
|
48
|
+
arr = SecureRandom.random_bytes(16).unpack('NnnnnN')
|
49
|
+
arr[2] = (arr[2] & 0x0fff) | 0x4000
|
50
|
+
arr[3] = (arr[3] & 0x3fff) | 0x8000
|
51
|
+
'%08x-%04x-%04x-%04x-%04x%08x' % arr
|
52
|
+
end
|
53
|
+
|
54
|
+
def datetime_in_iso8601(datetime)
|
55
|
+
case datetime
|
56
|
+
when Time
|
57
|
+
time_in_iso8601 datetime
|
58
|
+
when DateTime
|
59
|
+
time_in_iso8601 datetime.to_time
|
60
|
+
when Date
|
61
|
+
date_in_iso8601 datetime
|
62
|
+
else
|
63
|
+
datetime
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def time_in_iso8601(time)
|
68
|
+
"#{time.strftime('%Y-%m-%dT%H:%M:%S.%3N')}#{formatted_offset(time, true, 'Z')}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def date_in_iso8601(date)
|
72
|
+
date.strftime('%F')
|
73
|
+
end
|
74
|
+
|
75
|
+
def formatted_offset(time, colon = true, alternate_utc_string = nil)
|
76
|
+
time.utc? && alternate_utc_string || seconds_to_utc_offset(time.utc_offset, colon)
|
77
|
+
end
|
78
|
+
|
79
|
+
def seconds_to_utc_offset(seconds, colon = true)
|
80
|
+
(colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON) % [(seconds < 0 ? '-' : '+'), (seconds.abs / 3600), ((seconds.abs % 3600) / 60)]
|
81
|
+
end
|
82
|
+
|
83
|
+
UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
|
84
|
+
UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'june/analytics/defaults'
|
2
|
+
require 'june/analytics/message_batch'
|
3
|
+
require 'june/analytics/transport'
|
4
|
+
require 'june/analytics/utils'
|
5
|
+
|
6
|
+
module June
|
7
|
+
class Analytics
|
8
|
+
class Worker
|
9
|
+
include June::Analytics::Utils
|
10
|
+
include June::Analytics::Defaults
|
11
|
+
include June::Analytics::Logging
|
12
|
+
|
13
|
+
# public: Creates a new worker
|
14
|
+
#
|
15
|
+
# The worker continuously takes messages off the queue
|
16
|
+
# and makes requests to the june.so api
|
17
|
+
#
|
18
|
+
# queue - Queue synchronized between client and worker
|
19
|
+
# write_key - String of the project's Write key
|
20
|
+
# options - Hash of worker options
|
21
|
+
# batch_size - Fixnum of how many items to send in a batch
|
22
|
+
# on_error - Proc of what to do on an error
|
23
|
+
#
|
24
|
+
def initialize(queue, write_key, options = {})
|
25
|
+
symbolize_keys! options
|
26
|
+
@queue = queue
|
27
|
+
@write_key = write_key
|
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)
|
31
|
+
@lock = Mutex.new
|
32
|
+
@transport = Transport.new(options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# public: Continuously runs the loop to check for new events
|
36
|
+
#
|
37
|
+
def run
|
38
|
+
until Thread.current[:should_exit]
|
39
|
+
return if @queue.empty?
|
40
|
+
|
41
|
+
@lock.synchronize do
|
42
|
+
consume_message_from_queue! until @batch.full? || @queue.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
res = @transport.send @write_key, @batch
|
46
|
+
@on_error.call(res.status, res.error) unless res.status == 200
|
47
|
+
|
48
|
+
@lock.synchronize { @batch.clear }
|
49
|
+
end
|
50
|
+
ensure
|
51
|
+
@transport.shutdown
|
52
|
+
end
|
53
|
+
|
54
|
+
# public: Check whether we have outstanding requests.
|
55
|
+
#
|
56
|
+
def is_requesting?
|
57
|
+
@lock.synchronize { !@batch.empty? }
|
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
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'june/analytics/version'
|
2
|
+
require 'june/analytics/defaults'
|
3
|
+
require 'june/analytics/utils'
|
4
|
+
require 'june/analytics/field_parser'
|
5
|
+
require 'june/analytics/client'
|
6
|
+
require 'june/analytics/worker'
|
7
|
+
require 'june/analytics/transport'
|
8
|
+
require 'june/analytics/response'
|
9
|
+
require 'june/analytics/logging'
|
10
|
+
require 'june/analytics/test_queue'
|
11
|
+
|
12
|
+
module June
|
13
|
+
class Analytics
|
14
|
+
# Initializes a new instance of {June::Analytics::Client}, to which all
|
15
|
+
# method calls are proxied.
|
16
|
+
#
|
17
|
+
# @param options includes options that are passed down to
|
18
|
+
# {June::Analytics::Client#initialize}
|
19
|
+
# @option options [Boolean] :stub (false) If true, requests don't hit the
|
20
|
+
# server and are stubbed to be successful.
|
21
|
+
def initialize(options = {})
|
22
|
+
Transport.stub = options[:stub] if options.has_key?(:stub)
|
23
|
+
@client = June::Analytics::Client.new options
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(message, *args, &block)
|
27
|
+
if @client.respond_to? message
|
28
|
+
@client.send message, *args, &block
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def respond_to_missing?(method_name, include_private = false)
|
35
|
+
@client.respond_to?(method_name) || super
|
36
|
+
end
|
37
|
+
|
38
|
+
include Logging
|
39
|
+
end
|
40
|
+
end
|
data/lib/june.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'june/analytics'
|
metadata
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: june-analytics-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.4.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- June.so
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-07-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: commander
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.4'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: tzinfo
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.2.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.2.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 4.1.11
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 4.1.11
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: oj
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.6.2
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.6.2
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.51.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.51.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: codecov
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.1.4
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.1.4
|
125
|
+
description: The June.so ruby analytics library
|
126
|
+
email: eng@june.so
|
127
|
+
executables:
|
128
|
+
- analytics
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- bin/analytics
|
133
|
+
- lib/analytics-ruby.rb
|
134
|
+
- lib/june.rb
|
135
|
+
- lib/june/analytics.rb
|
136
|
+
- lib/june/analytics/backoff_policy.rb
|
137
|
+
- lib/june/analytics/client.rb
|
138
|
+
- lib/june/analytics/defaults.rb
|
139
|
+
- lib/june/analytics/field_parser.rb
|
140
|
+
- lib/june/analytics/logging.rb
|
141
|
+
- lib/june/analytics/message_batch.rb
|
142
|
+
- lib/june/analytics/response.rb
|
143
|
+
- lib/june/analytics/test_queue.rb
|
144
|
+
- lib/june/analytics/transport.rb
|
145
|
+
- lib/june/analytics/utils.rb
|
146
|
+
- lib/june/analytics/version.rb
|
147
|
+
- lib/june/analytics/worker.rb
|
148
|
+
homepage: https://github.com/juneHQ/analytics-ruby
|
149
|
+
licenses:
|
150
|
+
- MIT
|
151
|
+
metadata: {}
|
152
|
+
post_install_message:
|
153
|
+
rdoc_options: []
|
154
|
+
require_paths:
|
155
|
+
- lib
|
156
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - ">="
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '2.0'
|
161
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
requirements: []
|
167
|
+
rubygems_version: 3.2.22
|
168
|
+
signing_key:
|
169
|
+
specification_version: 4
|
170
|
+
summary: June.so analytics library
|
171
|
+
test_files: []
|