events-sdk-ruby 0.0.1
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/hightouch/analytics/backoff_policy.rb +51 -0
- data/lib/hightouch/analytics/client.rb +204 -0
- data/lib/hightouch/analytics/defaults.rb +38 -0
- data/lib/hightouch/analytics/field_parser.rb +197 -0
- data/lib/hightouch/analytics/logging.rb +62 -0
- data/lib/hightouch/analytics/message_batch.rb +74 -0
- data/lib/hightouch/analytics/response.rb +17 -0
- data/lib/hightouch/analytics/test_queue.rb +58 -0
- data/lib/hightouch/analytics/transport.rb +140 -0
- data/lib/hightouch/analytics/utils.rb +89 -0
- data/lib/hightouch/analytics/version.rb +7 -0
- data/lib/hightouch/analytics/worker.rb +71 -0
- data/lib/hightouch/analytics.rb +42 -0
- data/lib/hightouch.rb +3 -0
- metadata +170 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dcf2a40935376b273b429ab73d49f299cfa056d6b85d0de15695e4cc11422707
|
4
|
+
data.tar.gz: 41c229206e54bdf563bbbc21408031d3fb61b1e7270919528266a602ca89ebe8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a1340235d5dd7c589558bb1d51c68d56a74e7aa5a08375acbeb8367d2d83772f2cb7764c243897760811ca6e82d050faf479d0973aa3d510299adbf60319ac08
|
7
|
+
data.tar.gz: d9904ab645177fbac8dbf2581651b37f1809225802ccdcc97e4acce613246202de7e82cb2172deb4860889e460ea229f5660b56f1d23e1f4828aa33ca1d36eef
|
data/bin/analytics
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'hightouch/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> -hightouch-write-key=<hightouchWriteKey> [options]
|
20
|
+
|
21
|
+
default_command :send
|
22
|
+
|
23
|
+
command :send do |c|
|
24
|
+
c.description = 'send a hightouch message'
|
25
|
+
|
26
|
+
c.option '--writeKey=<writeKey>', String, 'the Hightouch writeKey'
|
27
|
+
c.option '--type=<type>', String, 'The Hightouch message type'
|
28
|
+
|
29
|
+
c.option '--userId=<userId>', String, 'the user id to send the event as'
|
30
|
+
c.option '--anonymousId=<anonymousId>', String, 'the anonymous user id to send the event as'
|
31
|
+
c.option '--context=<context>', 'additional context for the event (JSON-encoded)'
|
32
|
+
c.option '--integrations=<integrations>', 'additional integrations for the event (JSON-encoded)'
|
33
|
+
|
34
|
+
c.option '--event=<event>', String, 'the event name to send with the event'
|
35
|
+
c.option '--properties=<properties>', 'the event properties to send (JSON-encoded)'
|
36
|
+
|
37
|
+
c.option '--name=<name>', 'name of the screen or page to send with the message'
|
38
|
+
|
39
|
+
c.option '--traits=<traits>', 'the identify/group traits to send (JSON-encoded)'
|
40
|
+
|
41
|
+
c.option '--groupId=<groupId>', String, 'the group id'
|
42
|
+
c.option '--previousId=<previousId>', String, 'the previous id'
|
43
|
+
|
44
|
+
c.action do |args, options|
|
45
|
+
Analytics = Hightouch::Analytics.new({
|
46
|
+
:write_key => options.writeKey,
|
47
|
+
:on_error => Proc.new { |status, msg| print msg }
|
48
|
+
})
|
49
|
+
|
50
|
+
case options.type
|
51
|
+
when "track"
|
52
|
+
Analytics.track({
|
53
|
+
:user_id => options.userId,
|
54
|
+
:event => options.event,
|
55
|
+
:anonymous_id => options.anonymousId,
|
56
|
+
:properties => json_hash(options.properties),
|
57
|
+
:context => json_hash(options.context),
|
58
|
+
:integrations => json_hash(options.integrations)
|
59
|
+
})
|
60
|
+
when "page"
|
61
|
+
Analytics.page({
|
62
|
+
:user_id => options.userId,
|
63
|
+
:anonymous_id => options.anonymousId,
|
64
|
+
:name => options.name,
|
65
|
+
:properties => json_hash(options.properties),
|
66
|
+
:context => json_hash(options.context),
|
67
|
+
:integrations => json_hash(options.integrations)
|
68
|
+
})
|
69
|
+
when "screen"
|
70
|
+
Analytics.screen({
|
71
|
+
:user_id => options.userId,
|
72
|
+
:anonymous_id => options.anonymousId,
|
73
|
+
:name => options.name,
|
74
|
+
:properties => json_hash(options.properties),
|
75
|
+
:context => json_hash(options.context),
|
76
|
+
:integrations => json_hash(options.integrations)
|
77
|
+
})
|
78
|
+
when "identify"
|
79
|
+
Analytics.identify({
|
80
|
+
:user_id => options.userId,
|
81
|
+
:anonymous_id => options.anonymousId,
|
82
|
+
:traits => json_hash(options.traits),
|
83
|
+
:context => json_hash(options.context),
|
84
|
+
:integrations => json_hash(options.integrations)
|
85
|
+
})
|
86
|
+
when "group"
|
87
|
+
Analytics.group({
|
88
|
+
:user_id => options.userId,
|
89
|
+
:anonymous_id => options.anonymousId,
|
90
|
+
:group_id => options.groupId,
|
91
|
+
:traits => json_hash(options.traits),
|
92
|
+
:context => json_hash(options.context),
|
93
|
+
:integrations => json_hash(options.integrations)
|
94
|
+
})
|
95
|
+
when "alias"
|
96
|
+
Analytics.alias({
|
97
|
+
:previous_id => options.previousId,
|
98
|
+
:user_id => options.userId,
|
99
|
+
:anonymous_id => options.anonymousId,
|
100
|
+
:context => json_hash(options.context),
|
101
|
+
:integrations => json_hash(options.integrations)
|
102
|
+
})
|
103
|
+
else
|
104
|
+
raise "Invalid Message Type #{options.type}"
|
105
|
+
end
|
106
|
+
Analytics.flush
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hightouch/analytics/defaults'
|
4
|
+
|
5
|
+
module Hightouch
|
6
|
+
class Analytics
|
7
|
+
class BackoffPolicy
|
8
|
+
include Hightouch::Analytics::Defaults::BackoffPolicy
|
9
|
+
|
10
|
+
# @param [Hash] opts
|
11
|
+
# @option opts [Numeric] :min_timeout_ms The minimum backoff timeout
|
12
|
+
# @option opts [Numeric] :max_timeout_ms The maximum backoff timeout
|
13
|
+
# @option opts [Numeric] :multiplier The value to multiply the current
|
14
|
+
# interval with for each retry attempt
|
15
|
+
# @option opts [Numeric] :randomization_factor The randomization factor
|
16
|
+
# to use to create a range around the retry interval
|
17
|
+
def initialize(opts = {})
|
18
|
+
@min_timeout_ms = opts[:min_timeout_ms] || MIN_TIMEOUT_MS
|
19
|
+
@max_timeout_ms = opts[:max_timeout_ms] || MAX_TIMEOUT_MS
|
20
|
+
@multiplier = opts[:multiplier] || MULTIPLIER
|
21
|
+
@randomization_factor = opts[:randomization_factor] || RANDOMIZATION_FACTOR
|
22
|
+
|
23
|
+
@attempts = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Numeric] the next backoff interval, in milliseconds.
|
27
|
+
def next_interval
|
28
|
+
interval = @min_timeout_ms * (@multiplier**@attempts)
|
29
|
+
interval = add_jitter(interval, @randomization_factor)
|
30
|
+
|
31
|
+
@attempts += 1
|
32
|
+
|
33
|
+
[interval, @max_timeout_ms].min
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def add_jitter(base, randomization_factor)
|
39
|
+
random_number = rand
|
40
|
+
max_deviation = base * randomization_factor
|
41
|
+
deviation = random_number * max_deviation
|
42
|
+
|
43
|
+
if random_number < 0.5
|
44
|
+
base - deviation
|
45
|
+
else
|
46
|
+
base + deviation
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
require 'hightouch/analytics/defaults'
|
7
|
+
require 'hightouch/analytics/logging'
|
8
|
+
require 'hightouch/analytics/utils'
|
9
|
+
require 'hightouch/analytics/worker'
|
10
|
+
|
11
|
+
module Hightouch
|
12
|
+
class Analytics
|
13
|
+
class Client
|
14
|
+
include Hightouch::Analytics::Utils
|
15
|
+
include Hightouch::Analytics::Logging
|
16
|
+
|
17
|
+
# @param [Hash] opts
|
18
|
+
# @option opts [String] :write_key Your project's write_key
|
19
|
+
# @option opts [FixNum] :max_queue_size Maximum number of calls to be
|
20
|
+
# remain queued.
|
21
|
+
# @option opts [Proc] :on_error Handles error calls from the API.
|
22
|
+
def initialize(opts = {})
|
23
|
+
symbolize_keys!(opts)
|
24
|
+
|
25
|
+
@queue = Queue.new
|
26
|
+
@test = opts[:test]
|
27
|
+
@write_key = opts[:write_key]
|
28
|
+
@max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
|
29
|
+
@worker_mutex = Mutex.new
|
30
|
+
@worker = Worker.new(@queue, @write_key, opts)
|
31
|
+
@worker_thread = nil
|
32
|
+
|
33
|
+
check_write_key!
|
34
|
+
|
35
|
+
at_exit { @worker_thread && @worker_thread[:should_exit] = true }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Synchronously waits until the worker has flushed the queue.
|
39
|
+
#
|
40
|
+
# Use only for scripts which are not long-running, and will specifically
|
41
|
+
# exit
|
42
|
+
def flush
|
43
|
+
while !@queue.empty? || @worker.is_requesting?
|
44
|
+
ensure_worker_running
|
45
|
+
sleep(0.1)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# @!macro common_attrs
|
50
|
+
# @option attrs [String] :anonymous_id ID for a user when you don't know
|
51
|
+
# who they are yet. (optional but you must provide either an
|
52
|
+
# `anonymous_id` or `user_id`)
|
53
|
+
# @option attrs [Hash] :context ({})
|
54
|
+
# @option attrs [Hash] :integrations What integrations this event
|
55
|
+
# goes to (optional)
|
56
|
+
# @option attrs [String] :message_id ID that uniquely
|
57
|
+
# identifies a message across the API. (optional)
|
58
|
+
# @option attrs [Time] :timestamp When the event occurred (optional)
|
59
|
+
# @option attrs [String] :user_id The ID for this user in your database
|
60
|
+
# (optional but you must provide either an `anonymous_id` or `user_id`)
|
61
|
+
# @option attrs [Hash] :options Options such as user traits (optional)
|
62
|
+
|
63
|
+
# Tracks an event
|
64
|
+
#
|
65
|
+
# @see https://hightouch.com/docs/sources/server/ruby/#track
|
66
|
+
#
|
67
|
+
# @param [Hash] attrs
|
68
|
+
#
|
69
|
+
# @option attrs [String] :event Event name
|
70
|
+
# @option attrs [Hash] :properties Event properties (optional)
|
71
|
+
# @macro common_attrs
|
72
|
+
def track(attrs)
|
73
|
+
symbolize_keys! attrs
|
74
|
+
enqueue(FieldParser.parse_for_track(attrs))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Identifies a user
|
78
|
+
#
|
79
|
+
# @see https://hightouch.com/docs/sources/server/ruby/#identify
|
80
|
+
#
|
81
|
+
# @param [Hash] attrs
|
82
|
+
#
|
83
|
+
# @option attrs [Hash] :traits User traits (optional)
|
84
|
+
# @macro common_attrs
|
85
|
+
def identify(attrs)
|
86
|
+
symbolize_keys! attrs
|
87
|
+
enqueue(FieldParser.parse_for_identify(attrs))
|
88
|
+
end
|
89
|
+
|
90
|
+
# Aliases a user from one id to another
|
91
|
+
#
|
92
|
+
# @see https://hightouch.com/docs/sources/server/ruby/#alias
|
93
|
+
#
|
94
|
+
# @param [Hash] attrs
|
95
|
+
#
|
96
|
+
# @option attrs [String] :previous_id The ID to alias from
|
97
|
+
# @macro common_attrs
|
98
|
+
def alias(attrs)
|
99
|
+
symbolize_keys! attrs
|
100
|
+
enqueue(FieldParser.parse_for_alias(attrs))
|
101
|
+
end
|
102
|
+
|
103
|
+
# Associates a user identity with a group.
|
104
|
+
#
|
105
|
+
# @see https://hightouch.com/docs/sources/server/ruby/#group
|
106
|
+
#
|
107
|
+
# @param [Hash] attrs
|
108
|
+
#
|
109
|
+
# @option attrs [String] :group_id The ID of the group
|
110
|
+
# @option attrs [Hash] :traits User traits (optional)
|
111
|
+
# @macro common_attrs
|
112
|
+
def group(attrs)
|
113
|
+
symbolize_keys! attrs
|
114
|
+
enqueue(FieldParser.parse_for_group(attrs))
|
115
|
+
end
|
116
|
+
|
117
|
+
# Records a page view
|
118
|
+
#
|
119
|
+
# @see https://hightouch.com/docs/sources/server/ruby/#page
|
120
|
+
#
|
121
|
+
# @param [Hash] attrs
|
122
|
+
#
|
123
|
+
# @option attrs [String] :name Name of the page
|
124
|
+
# @option attrs [Hash] :properties Page properties (optional)
|
125
|
+
# @macro common_attrs
|
126
|
+
def page(attrs)
|
127
|
+
symbolize_keys! attrs
|
128
|
+
enqueue(FieldParser.parse_for_page(attrs))
|
129
|
+
end
|
130
|
+
|
131
|
+
# Records a screen view (for a mobile app)
|
132
|
+
#
|
133
|
+
# @param [Hash] attrs
|
134
|
+
#
|
135
|
+
# @option attrs [String] :name Name of the screen
|
136
|
+
# @option attrs [Hash] :properties Screen properties (optional)
|
137
|
+
# @option attrs [String] :category The screen category (optional)
|
138
|
+
# @macro common_attrs
|
139
|
+
def screen(attrs)
|
140
|
+
symbolize_keys! attrs
|
141
|
+
enqueue(FieldParser.parse_for_screen(attrs))
|
142
|
+
end
|
143
|
+
|
144
|
+
# @return [Fixnum] number of messages in the queue
|
145
|
+
def queued_messages
|
146
|
+
@queue.length
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_queue
|
150
|
+
unless @test
|
151
|
+
raise 'Test queue only available when setting :test to true.'
|
152
|
+
end
|
153
|
+
|
154
|
+
@test_queue ||= TestQueue.new
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
# private: Enqueues the action.
|
160
|
+
#
|
161
|
+
# returns Boolean of whether the item was added to the queue.
|
162
|
+
def enqueue(action)
|
163
|
+
# add our request id for tracing purposes
|
164
|
+
action[:messageId] ||= uid
|
165
|
+
|
166
|
+
if @test
|
167
|
+
test_queue << action
|
168
|
+
return true
|
169
|
+
end
|
170
|
+
|
171
|
+
if @queue.length < @max_queue_size
|
172
|
+
@queue << action
|
173
|
+
ensure_worker_running
|
174
|
+
|
175
|
+
true
|
176
|
+
else
|
177
|
+
logger.warn(
|
178
|
+
'Queue is full, dropping events. The :max_queue_size configuration parameter can be increased to prevent this from happening.'
|
179
|
+
)
|
180
|
+
false
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# private: Checks that the write_key is properly initialized
|
185
|
+
def check_write_key!
|
186
|
+
raise ArgumentError, 'Write key must be initialized' if @write_key.nil?
|
187
|
+
end
|
188
|
+
|
189
|
+
def ensure_worker_running
|
190
|
+
return if worker_running?
|
191
|
+
@worker_mutex.synchronize do
|
192
|
+
return if worker_running?
|
193
|
+
@worker_thread = Thread.new do
|
194
|
+
@worker.run
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def worker_running?
|
200
|
+
@worker_thread && @worker_thread.alive?
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hightouch
|
4
|
+
class Analytics
|
5
|
+
module Defaults
|
6
|
+
module Request
|
7
|
+
HOST = 'events.us-east-1.hightouch.com'
|
8
|
+
PORT = 443
|
9
|
+
PATH = '/v1/batch'
|
10
|
+
SSL = true
|
11
|
+
HEADERS = { 'Accept' => 'application/json',
|
12
|
+
'Content-Type' => 'application/json',
|
13
|
+
'User-Agent' => "events-sdk-ruby/#{Analytics::VERSION}" }
|
14
|
+
RETRIES = 10
|
15
|
+
end
|
16
|
+
|
17
|
+
module Queue
|
18
|
+
MAX_SIZE = 10000
|
19
|
+
end
|
20
|
+
|
21
|
+
module Message
|
22
|
+
MAX_BYTES = 32768 # 32Kb
|
23
|
+
end
|
24
|
+
|
25
|
+
module MessageBatch
|
26
|
+
MAX_BYTES = 512_000 # 500Kb
|
27
|
+
MAX_SIZE = 100
|
28
|
+
end
|
29
|
+
|
30
|
+
module BackoffPolicy
|
31
|
+
MIN_TIMEOUT_MS = 100
|
32
|
+
MAX_TIMEOUT_MS = 10000
|
33
|
+
MULTIPLIER = 1.5
|
34
|
+
RANDOMIZATION_FACTOR = 0.5
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hightouch
|
4
|
+
class Analytics
|
5
|
+
# Handles parsing fields according to the Hightouch Spec
|
6
|
+
#
|
7
|
+
# @see https://hightouch.com/docs/spec/
|
8
|
+
class FieldParser
|
9
|
+
class << self
|
10
|
+
include Hightouch::Analytics::Utils
|
11
|
+
|
12
|
+
# In addition to the common fields, track accepts:
|
13
|
+
#
|
14
|
+
# - "event"
|
15
|
+
# - "properties"
|
16
|
+
def parse_for_track(fields)
|
17
|
+
common = parse_common_fields(fields)
|
18
|
+
|
19
|
+
event = fields[:event]
|
20
|
+
properties = fields[:properties] || {}
|
21
|
+
|
22
|
+
check_presence!(event, 'event')
|
23
|
+
check_is_hash!(properties, 'properties')
|
24
|
+
|
25
|
+
isoify_dates! properties
|
26
|
+
|
27
|
+
common.merge({
|
28
|
+
:type => 'track',
|
29
|
+
:event => event.to_s,
|
30
|
+
:properties => properties
|
31
|
+
})
|
32
|
+
end
|
33
|
+
|
34
|
+
# In addition to the common fields, identify accepts:
|
35
|
+
#
|
36
|
+
# - "traits"
|
37
|
+
def parse_for_identify(fields)
|
38
|
+
common = parse_common_fields(fields)
|
39
|
+
|
40
|
+
traits = fields[:traits] || {}
|
41
|
+
check_is_hash!(traits, 'traits')
|
42
|
+
isoify_dates! traits
|
43
|
+
|
44
|
+
common.merge({
|
45
|
+
:type => 'identify',
|
46
|
+
:traits => traits
|
47
|
+
})
|
48
|
+
end
|
49
|
+
|
50
|
+
# In addition to the common fields, alias accepts:
|
51
|
+
#
|
52
|
+
# - "previous_id"
|
53
|
+
def parse_for_alias(fields)
|
54
|
+
common = parse_common_fields(fields)
|
55
|
+
|
56
|
+
previous_id = fields[:previous_id]
|
57
|
+
check_presence!(previous_id, 'previous_id')
|
58
|
+
|
59
|
+
common.merge({
|
60
|
+
:type => 'alias',
|
61
|
+
:previousId => previous_id
|
62
|
+
})
|
63
|
+
end
|
64
|
+
|
65
|
+
# In addition to the common fields, group accepts:
|
66
|
+
#
|
67
|
+
# - "group_id"
|
68
|
+
# - "traits"
|
69
|
+
def parse_for_group(fields)
|
70
|
+
common = parse_common_fields(fields)
|
71
|
+
|
72
|
+
group_id = fields[:group_id]
|
73
|
+
traits = fields[:traits] || {}
|
74
|
+
|
75
|
+
check_presence!(group_id, 'group_id')
|
76
|
+
check_is_hash!(traits, 'traits')
|
77
|
+
|
78
|
+
isoify_dates! traits
|
79
|
+
|
80
|
+
common.merge({
|
81
|
+
:type => 'group',
|
82
|
+
:groupId => group_id,
|
83
|
+
:traits => traits
|
84
|
+
})
|
85
|
+
end
|
86
|
+
|
87
|
+
# In addition to the common fields, page accepts:
|
88
|
+
#
|
89
|
+
# - "name"
|
90
|
+
# - "properties"
|
91
|
+
def parse_for_page(fields)
|
92
|
+
common = parse_common_fields(fields)
|
93
|
+
|
94
|
+
name = fields[:name] || ''
|
95
|
+
properties = fields[:properties] || {}
|
96
|
+
|
97
|
+
check_is_hash!(properties, 'properties')
|
98
|
+
|
99
|
+
isoify_dates! properties
|
100
|
+
|
101
|
+
common.merge({
|
102
|
+
:type => 'page',
|
103
|
+
:name => name.to_s,
|
104
|
+
:properties => properties
|
105
|
+
})
|
106
|
+
end
|
107
|
+
|
108
|
+
# In addition to the common fields, screen accepts:
|
109
|
+
#
|
110
|
+
# - "name"
|
111
|
+
# - "properties"
|
112
|
+
# - "category" (Not in spec, retained for backward compatibility"
|
113
|
+
def parse_for_screen(fields)
|
114
|
+
common = parse_common_fields(fields)
|
115
|
+
|
116
|
+
name = fields[:name]
|
117
|
+
properties = fields[:properties] || {}
|
118
|
+
category = fields[:category]
|
119
|
+
|
120
|
+
check_presence!(name, 'name')
|
121
|
+
check_is_hash!(properties, 'properties')
|
122
|
+
|
123
|
+
isoify_dates! properties
|
124
|
+
|
125
|
+
parsed = common.merge({
|
126
|
+
:type => 'screen',
|
127
|
+
:name => name,
|
128
|
+
:properties => properties
|
129
|
+
})
|
130
|
+
|
131
|
+
parsed[:category] = category if category
|
132
|
+
|
133
|
+
parsed
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def parse_common_fields(fields)
|
139
|
+
timestamp = fields[:timestamp] || Time.new
|
140
|
+
message_id = fields[:message_id].to_s if fields[:message_id]
|
141
|
+
context = fields[:context] || {}
|
142
|
+
|
143
|
+
check_user_id! fields
|
144
|
+
check_timestamp! timestamp
|
145
|
+
|
146
|
+
add_context! context
|
147
|
+
|
148
|
+
parsed = {
|
149
|
+
:context => context,
|
150
|
+
:messageId => message_id,
|
151
|
+
:timestamp => datetime_in_iso8601(timestamp)
|
152
|
+
}
|
153
|
+
|
154
|
+
parsed[:userId] = fields[:user_id] if fields[:user_id]
|
155
|
+
parsed[:anonymousId] = fields[:anonymous_id] if fields[:anonymous_id]
|
156
|
+
parsed[:integrations] = fields[:integrations] if fields[:integrations]
|
157
|
+
|
158
|
+
# Not in spec, retained for backward compatibility
|
159
|
+
parsed[:options] = fields[:options] if fields[:options]
|
160
|
+
|
161
|
+
parsed
|
162
|
+
end
|
163
|
+
|
164
|
+
def check_user_id!(fields)
|
165
|
+
return unless blank?(fields[:user_id])
|
166
|
+
return unless blank?(fields[:anonymous_id])
|
167
|
+
|
168
|
+
raise ArgumentError, 'Must supply either user_id or anonymous_id'
|
169
|
+
end
|
170
|
+
|
171
|
+
def check_timestamp!(timestamp)
|
172
|
+
raise ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time
|
173
|
+
end
|
174
|
+
|
175
|
+
def add_context!(context)
|
176
|
+
context[:library] = { :name => 'events-sdk-ruby', :version => Hightouch::Analytics::VERSION.to_s }
|
177
|
+
end
|
178
|
+
|
179
|
+
# private: Ensures that a string is non-empty
|
180
|
+
#
|
181
|
+
# obj - String|Number that must be non-blank
|
182
|
+
# name - Name of the validated value
|
183
|
+
def check_presence!(obj, name)
|
184
|
+
raise ArgumentError, "#{name} must be given" if blank?(obj)
|
185
|
+
end
|
186
|
+
|
187
|
+
def blank?(obj)
|
188
|
+
obj.nil? || (obj.is_a?(String) && obj.empty?)
|
189
|
+
end
|
190
|
+
|
191
|
+
def check_is_hash!(obj, name)
|
192
|
+
raise ArgumentError, "#{name} must be a Hash" unless obj.is_a? Hash
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Hightouch
|
6
|
+
class Analytics
|
7
|
+
# Wraps an existing logger and adds a prefix to all messages
|
8
|
+
class PrefixedLogger
|
9
|
+
def initialize(logger, prefix)
|
10
|
+
@logger = logger
|
11
|
+
@prefix = prefix
|
12
|
+
end
|
13
|
+
|
14
|
+
def debug(msg)
|
15
|
+
@logger.debug("#{@prefix} #{msg}")
|
16
|
+
end
|
17
|
+
|
18
|
+
def info(msg)
|
19
|
+
@logger.info("#{@prefix} #{msg}")
|
20
|
+
end
|
21
|
+
|
22
|
+
def warn(msg)
|
23
|
+
@logger.warn("#{@prefix} #{msg}")
|
24
|
+
end
|
25
|
+
|
26
|
+
def error(msg)
|
27
|
+
@logger.error("#{@prefix} #{msg}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Logging
|
32
|
+
class << self
|
33
|
+
def logger
|
34
|
+
return @logger if @logger
|
35
|
+
|
36
|
+
base_logger = if defined?(Rails)
|
37
|
+
Rails.logger
|
38
|
+
else
|
39
|
+
logger = Logger.new STDOUT
|
40
|
+
logger.progname = 'Hightouch::Analytics'
|
41
|
+
logger
|
42
|
+
end
|
43
|
+
@logger = PrefixedLogger.new(base_logger, '[events-sdk-ruby]')
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_writer :logger
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.included(base)
|
50
|
+
class << base
|
51
|
+
def logger
|
52
|
+
Logging.logger
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def logger
|
58
|
+
Logging.logger
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'hightouch/analytics/logging'
|
5
|
+
|
6
|
+
module Hightouch
|
7
|
+
class Analytics
|
8
|
+
# A batch of `Message`s to be sent to the API
|
9
|
+
class MessageBatch
|
10
|
+
class JSONGenerationError < StandardError; end
|
11
|
+
|
12
|
+
extend Forwardable
|
13
|
+
include Hightouch::Analytics::Logging
|
14
|
+
include Hightouch::Analytics::Defaults::MessageBatch
|
15
|
+
|
16
|
+
def initialize(max_message_count)
|
17
|
+
@messages = []
|
18
|
+
@max_message_count = max_message_count
|
19
|
+
@json_size = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(message)
|
23
|
+
begin
|
24
|
+
message_json = message.to_json
|
25
|
+
rescue StandardError => e
|
26
|
+
raise JSONGenerationError, "Serialization error: #{e}"
|
27
|
+
end
|
28
|
+
|
29
|
+
message_json_size = message_json.bytesize
|
30
|
+
if message_too_big?(message_json_size)
|
31
|
+
logger.error('a message exceeded the maximum allowed size')
|
32
|
+
else
|
33
|
+
@messages << message
|
34
|
+
@json_size += message_json_size + 1 # One byte for the comma
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def full?
|
39
|
+
item_count_exhausted? || size_exhausted?
|
40
|
+
end
|
41
|
+
|
42
|
+
def clear
|
43
|
+
@messages.clear
|
44
|
+
@json_size = 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def_delegators :@messages, :to_json
|
48
|
+
def_delegators :@messages, :empty?
|
49
|
+
def_delegators :@messages, :length
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def item_count_exhausted?
|
54
|
+
@messages.length >= @max_message_count
|
55
|
+
end
|
56
|
+
|
57
|
+
def message_too_big?(message_json_size)
|
58
|
+
message_json_size > Defaults::Message::MAX_BYTES
|
59
|
+
end
|
60
|
+
|
61
|
+
# We consider the max size here as just enough to leave room for one more
|
62
|
+
# message of the largest size possible. This is a shortcut that allows us
|
63
|
+
# to use a native Ruby `Queue` that doesn't allow peeking. The tradeoff
|
64
|
+
# here is that we might fit in less messages than possible into a batch.
|
65
|
+
#
|
66
|
+
# The alternative is to use our own `Queue` implementation that allows
|
67
|
+
# peeking, and to consider the next message size when calculating whether
|
68
|
+
# the message can be accomodated in this batch.
|
69
|
+
def size_exhausted?
|
70
|
+
@json_size >= (MAX_BYTES - Defaults::Message::MAX_BYTES)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hightouch
|
4
|
+
class Analytics
|
5
|
+
class Response
|
6
|
+
attr_reader :status, :error
|
7
|
+
|
8
|
+
# public: Simple class to wrap responses from the API
|
9
|
+
#
|
10
|
+
#
|
11
|
+
def initialize(status = 200, error = nil)
|
12
|
+
@status = status
|
13
|
+
@error = error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hightouch
|
4
|
+
class Analytics
|
5
|
+
class TestQueue
|
6
|
+
attr_reader :messages
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
reset!
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
all[key]
|
14
|
+
end
|
15
|
+
|
16
|
+
def count
|
17
|
+
all.count
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(message)
|
21
|
+
all << message
|
22
|
+
send(message[:type]) << message
|
23
|
+
end
|
24
|
+
|
25
|
+
def alias
|
26
|
+
messages[:alias] ||= []
|
27
|
+
end
|
28
|
+
|
29
|
+
def all
|
30
|
+
messages[:all] ||= []
|
31
|
+
end
|
32
|
+
|
33
|
+
def group
|
34
|
+
messages[:group] ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
def identify
|
38
|
+
messages[:identify] ||= []
|
39
|
+
end
|
40
|
+
|
41
|
+
def page
|
42
|
+
messages[:page] ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
def screen
|
46
|
+
messages[:screen] ||= []
|
47
|
+
end
|
48
|
+
|
49
|
+
def track
|
50
|
+
messages[:track] ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
def reset!
|
54
|
+
@messages = {}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hightouch/analytics/defaults'
|
4
|
+
require 'hightouch/analytics/utils'
|
5
|
+
require 'hightouch/analytics/response'
|
6
|
+
require 'hightouch/analytics/logging'
|
7
|
+
require 'hightouch/analytics/backoff_policy'
|
8
|
+
require 'net/http'
|
9
|
+
require 'net/https'
|
10
|
+
require 'json'
|
11
|
+
|
12
|
+
module Hightouch
|
13
|
+
class Analytics
|
14
|
+
class Transport
|
15
|
+
include Hightouch::Analytics::Defaults::Request
|
16
|
+
include Hightouch::Analytics::Utils
|
17
|
+
include Hightouch::Analytics::Logging
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
options[:host] ||= HOST
|
21
|
+
options[:port] ||= PORT
|
22
|
+
options[:ssl] ||= SSL
|
23
|
+
@headers = options[:headers] || HEADERS
|
24
|
+
@path = options[:path] || PATH
|
25
|
+
@retries = options[:retries] || RETRIES
|
26
|
+
@backoff_policy =
|
27
|
+
options[:backoff_policy] || Hightouch::Analytics::BackoffPolicy.new
|
28
|
+
|
29
|
+
http = Net::HTTP.new(options[:host], options[:port])
|
30
|
+
http.use_ssl = options[:ssl]
|
31
|
+
http.read_timeout = 8
|
32
|
+
http.open_timeout = 4
|
33
|
+
|
34
|
+
@http = http
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sends a batch of messages to the API
|
38
|
+
#
|
39
|
+
# @return [Response] API response
|
40
|
+
def send(write_key, batch)
|
41
|
+
logger.debug("Sending request for #{batch.length} items")
|
42
|
+
|
43
|
+
last_response, exception = retry_with_backoff(@retries) do
|
44
|
+
status_code, body = send_request(write_key, batch)
|
45
|
+
error = JSON.parse(body)['error']
|
46
|
+
should_retry = should_retry_request?(status_code, body)
|
47
|
+
logger.debug("Response status code: #{status_code}")
|
48
|
+
logger.debug("Response error: #{error}") if error
|
49
|
+
|
50
|
+
[Response.new(status_code, error), should_retry]
|
51
|
+
end
|
52
|
+
|
53
|
+
if exception
|
54
|
+
logger.error(exception.message)
|
55
|
+
exception.backtrace.each { |line| logger.error(line) }
|
56
|
+
Response.new(-1, exception.to_s)
|
57
|
+
else
|
58
|
+
last_response
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Closes a persistent connection if it exists
|
63
|
+
def shutdown
|
64
|
+
@http.finish if @http.started?
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def should_retry_request?(status_code, body)
|
70
|
+
if status_code >= 500
|
71
|
+
true # Server error
|
72
|
+
elsif status_code == 429
|
73
|
+
true # Rate limited
|
74
|
+
elsif status_code >= 400
|
75
|
+
logger.error(body)
|
76
|
+
false # Client error. Do not retry, but log
|
77
|
+
else
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Takes a block that returns [result, should_retry].
|
83
|
+
#
|
84
|
+
# Retries upto `retries_remaining` times, if `should_retry` is false or
|
85
|
+
# an exception is raised. `@backoff_policy` is used to determine the
|
86
|
+
# duration to sleep between attempts
|
87
|
+
#
|
88
|
+
# Returns [last_result, raised_exception]
|
89
|
+
def retry_with_backoff(retries_remaining, &block)
|
90
|
+
result, caught_exception = nil
|
91
|
+
should_retry = false
|
92
|
+
|
93
|
+
begin
|
94
|
+
result, should_retry = yield
|
95
|
+
return [result, nil] unless should_retry
|
96
|
+
rescue StandardError => e
|
97
|
+
should_retry = true
|
98
|
+
caught_exception = e
|
99
|
+
end
|
100
|
+
|
101
|
+
if should_retry && (retries_remaining > 1)
|
102
|
+
logger.debug("Retrying request, #{retries_remaining} retries left")
|
103
|
+
sleep(@backoff_policy.next_interval.to_f / 1000)
|
104
|
+
retry_with_backoff(retries_remaining - 1, &block)
|
105
|
+
else
|
106
|
+
[result, caught_exception]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Sends a request for the batch, returns [status_code, body]
|
111
|
+
def send_request(write_key, batch)
|
112
|
+
payload = JSON.generate(
|
113
|
+
:sentAt => datetime_in_iso8601(Time.now),
|
114
|
+
:batch => batch
|
115
|
+
)
|
116
|
+
request = Net::HTTP::Post.new(@path, @headers)
|
117
|
+
request.basic_auth(write_key, nil)
|
118
|
+
|
119
|
+
if self.class.stub
|
120
|
+
logger.debug "stubbed request to #{@path}: " \
|
121
|
+
"write key = #{write_key}, batch = #{JSON.generate(batch)}"
|
122
|
+
|
123
|
+
[200, '{}']
|
124
|
+
else
|
125
|
+
@http.start unless @http.started? # Maintain a persistent connection
|
126
|
+
response = @http.request(request, payload)
|
127
|
+
[response.code.to_i, response.body]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class << self
|
132
|
+
attr_writer :stub
|
133
|
+
|
134
|
+
def stub
|
135
|
+
@stub || ENV['STUB']
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Hightouch
|
6
|
+
class Analytics
|
7
|
+
module Utils
|
8
|
+
extend self
|
9
|
+
|
10
|
+
# public: Return a new hash with keys converted from strings to symbols
|
11
|
+
#
|
12
|
+
def symbolize_keys(hash)
|
13
|
+
hash.each_with_object({}) do |(k, v), memo|
|
14
|
+
memo[k.to_sym] = v
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# public: Convert hash keys from strings to symbols in place
|
19
|
+
#
|
20
|
+
def symbolize_keys!(hash)
|
21
|
+
hash.replace symbolize_keys hash
|
22
|
+
end
|
23
|
+
|
24
|
+
# public: Return a new hash with keys as strings
|
25
|
+
#
|
26
|
+
def stringify_keys(hash)
|
27
|
+
hash.each_with_object({}) do |(k, v), memo|
|
28
|
+
memo[k.to_s] = v
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# public: Returns a new hash with all the date values in the into iso8601
|
33
|
+
# strings
|
34
|
+
#
|
35
|
+
def isoify_dates(hash)
|
36
|
+
hash.each_with_object({}) do |(k, v), memo|
|
37
|
+
memo[k] = datetime_in_iso8601(v)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# public: Converts all the date values in the into iso8601 strings in place
|
42
|
+
#
|
43
|
+
def isoify_dates!(hash)
|
44
|
+
hash.replace isoify_dates hash
|
45
|
+
end
|
46
|
+
|
47
|
+
# public: Returns a uid string
|
48
|
+
#
|
49
|
+
def uid
|
50
|
+
arr = SecureRandom.random_bytes(16).unpack('NnnnnN')
|
51
|
+
arr[2] = (arr[2] & 0x0fff) | 0x4000
|
52
|
+
arr[3] = (arr[3] & 0x3fff) | 0x8000
|
53
|
+
'%08x-%04x-%04x-%04x-%04x%08x' % arr
|
54
|
+
end
|
55
|
+
|
56
|
+
def datetime_in_iso8601(datetime)
|
57
|
+
case datetime
|
58
|
+
when Time
|
59
|
+
time_in_iso8601 datetime
|
60
|
+
when DateTime
|
61
|
+
time_in_iso8601 datetime.to_time
|
62
|
+
when Date
|
63
|
+
date_in_iso8601 datetime
|
64
|
+
else
|
65
|
+
datetime
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def time_in_iso8601(time)
|
70
|
+
"#{time.strftime('%Y-%m-%dT%H:%M:%S.%3N')}#{formatted_offset(time, true, 'Z')}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def date_in_iso8601(date)
|
74
|
+
date.strftime('%F')
|
75
|
+
end
|
76
|
+
|
77
|
+
def formatted_offset(time, colon = true, alternate_utc_string = nil)
|
78
|
+
time.utc? && alternate_utc_string || seconds_to_utc_offset(time.utc_offset, colon)
|
79
|
+
end
|
80
|
+
|
81
|
+
def seconds_to_utc_offset(seconds, colon = true)
|
82
|
+
(colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON) % [(seconds < 0 ? '-' : '+'), (seconds.abs / 3600), ((seconds.abs % 3600) / 60)]
|
83
|
+
end
|
84
|
+
|
85
|
+
UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
|
86
|
+
UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hightouch/analytics/defaults'
|
4
|
+
require 'hightouch/analytics/message_batch'
|
5
|
+
require 'hightouch/analytics/transport'
|
6
|
+
require 'hightouch/analytics/utils'
|
7
|
+
|
8
|
+
module Hightouch
|
9
|
+
class Analytics
|
10
|
+
class Worker
|
11
|
+
include Hightouch::Analytics::Utils
|
12
|
+
include Hightouch::Analytics::Defaults
|
13
|
+
include Hightouch::Analytics::Logging
|
14
|
+
|
15
|
+
# public: Creates a new worker
|
16
|
+
#
|
17
|
+
# The worker continuously takes messages off the queue
|
18
|
+
# and makes requests to the hightouch api
|
19
|
+
#
|
20
|
+
# queue - Queue synchronized between client and worker
|
21
|
+
# write_key - String of the project's Write key
|
22
|
+
# options - Hash of worker options
|
23
|
+
# batch_size - Fixnum of how many items to send in a batch
|
24
|
+
# on_error - Proc of what to do on an error
|
25
|
+
#
|
26
|
+
def initialize(queue, write_key, options = {})
|
27
|
+
symbolize_keys! options
|
28
|
+
@queue = queue
|
29
|
+
@write_key = write_key
|
30
|
+
@on_error = options[:on_error] || proc { |status, error| }
|
31
|
+
batch_size = options[:batch_size] || Defaults::MessageBatch::MAX_SIZE
|
32
|
+
@batch = MessageBatch.new(batch_size)
|
33
|
+
@lock = Mutex.new
|
34
|
+
@transport = Transport.new(options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# public: Continuously runs the loop to check for new events
|
38
|
+
#
|
39
|
+
def run
|
40
|
+
until Thread.current[:should_exit]
|
41
|
+
return if @queue.empty?
|
42
|
+
|
43
|
+
@lock.synchronize do
|
44
|
+
consume_message_from_queue! until @batch.full? || @queue.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
res = @transport.send @write_key, @batch
|
48
|
+
@on_error.call(res.status, res.error) unless res.status == 200
|
49
|
+
|
50
|
+
@lock.synchronize { @batch.clear }
|
51
|
+
end
|
52
|
+
ensure
|
53
|
+
@transport.shutdown
|
54
|
+
end
|
55
|
+
|
56
|
+
# public: Check whether we have outstanding requests.
|
57
|
+
#
|
58
|
+
def is_requesting?
|
59
|
+
@lock.synchronize { !@batch.empty? }
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def consume_message_from_queue!
|
65
|
+
@batch << @queue.pop
|
66
|
+
rescue MessageBatch::JSONGenerationError => e
|
67
|
+
@on_error.call(-1, e.to_s)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hightouch/analytics/version'
|
4
|
+
require 'hightouch/analytics/defaults'
|
5
|
+
require 'hightouch/analytics/utils'
|
6
|
+
require 'hightouch/analytics/field_parser'
|
7
|
+
require 'hightouch/analytics/client'
|
8
|
+
require 'hightouch/analytics/worker'
|
9
|
+
require 'hightouch/analytics/transport'
|
10
|
+
require 'hightouch/analytics/response'
|
11
|
+
require 'hightouch/analytics/logging'
|
12
|
+
require 'hightouch/analytics/test_queue'
|
13
|
+
|
14
|
+
module Hightouch
|
15
|
+
class Analytics
|
16
|
+
# Initializes a new instance of {Hightouch::Analytics::Client}, to which all
|
17
|
+
# method calls are proxied.
|
18
|
+
#
|
19
|
+
# @param options includes options that are passed down to
|
20
|
+
# {Hightouch::Analytics::Client#initialize}
|
21
|
+
# @option options [Boolean] :stub (false) If true, requests don't hit the
|
22
|
+
# server and are stubbed to be successful.
|
23
|
+
def initialize(options = {})
|
24
|
+
Transport.stub = options[:stub] if options.has_key?(:stub)
|
25
|
+
@client = Hightouch::Analytics::Client.new options
|
26
|
+
end
|
27
|
+
|
28
|
+
def method_missing(message, *args, &block)
|
29
|
+
if @client.respond_to? message
|
30
|
+
@client.send message, *args, &block
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def respond_to_missing?(method_name, include_private = false)
|
37
|
+
@client.respond_to?(method_name) || super
|
38
|
+
end
|
39
|
+
|
40
|
+
include Logging
|
41
|
+
end
|
42
|
+
end
|
data/lib/hightouch.rb
ADDED
metadata
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: events-sdk-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- HT-SDKS
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-09-12 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: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
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'
|
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'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 5.2.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 5.2.0
|
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: '1.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.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.6'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.6'
|
125
|
+
description: Hightouch Events SDK
|
126
|
+
email: engineering@hightouch.com
|
127
|
+
executables:
|
128
|
+
- analytics
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- bin/analytics
|
133
|
+
- lib/hightouch.rb
|
134
|
+
- lib/hightouch/analytics.rb
|
135
|
+
- lib/hightouch/analytics/backoff_policy.rb
|
136
|
+
- lib/hightouch/analytics/client.rb
|
137
|
+
- lib/hightouch/analytics/defaults.rb
|
138
|
+
- lib/hightouch/analytics/field_parser.rb
|
139
|
+
- lib/hightouch/analytics/logging.rb
|
140
|
+
- lib/hightouch/analytics/message_batch.rb
|
141
|
+
- lib/hightouch/analytics/response.rb
|
142
|
+
- lib/hightouch/analytics/test_queue.rb
|
143
|
+
- lib/hightouch/analytics/transport.rb
|
144
|
+
- lib/hightouch/analytics/utils.rb
|
145
|
+
- lib/hightouch/analytics/version.rb
|
146
|
+
- lib/hightouch/analytics/worker.rb
|
147
|
+
homepage: https://github.com/ht-sdks/events-sdk-ruby
|
148
|
+
licenses:
|
149
|
+
- MIT
|
150
|
+
metadata: {}
|
151
|
+
post_install_message:
|
152
|
+
rdoc_options: []
|
153
|
+
require_paths:
|
154
|
+
- lib
|
155
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '2.0'
|
160
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
requirements: []
|
166
|
+
rubygems_version: 3.1.6
|
167
|
+
signing_key:
|
168
|
+
specification_version: 4
|
169
|
+
summary: Hightouch Events SDK
|
170
|
+
test_files: []
|