amplitude-experiment 1.1.5 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/amplitude-experiment.gemspec +1 -0
- data/lib/amplitude/client.rb +54 -0
- data/lib/amplitude/config.rb +78 -0
- data/lib/amplitude/constants.rb +45 -0
- data/lib/amplitude/event.rb +244 -0
- data/lib/amplitude/exception.rb +15 -0
- data/lib/amplitude/http_client.rb +161 -0
- data/lib/amplitude/plugin.rb +131 -0
- data/lib/amplitude/processor.rb +100 -0
- data/lib/amplitude/storage.rb +146 -0
- data/lib/amplitude/timeline.rb +98 -0
- data/lib/amplitude/utils.rb +29 -0
- data/lib/amplitude/workers.rb +101 -0
- data/lib/amplitude-experiment.rb +6 -0
- data/lib/amplitude.rb +12 -0
- data/lib/experiment/local/assignment/assignment.rb +21 -0
- data/lib/experiment/local/assignment/assignment_config.rb +12 -0
- data/lib/experiment/local/assignment/assignment_filter.rb +15 -0
- data/lib/experiment/local/assignment/assignment_service.rb +48 -0
- data/lib/experiment/local/client.rb +19 -6
- data/lib/experiment/local/config.rb +6 -1
- data/lib/experiment/remote/client.rb +1 -3
- data/lib/experiment/util/hash.rb +15 -0
- data/lib/experiment/util/lru_cache.rb +107 -0
- data/lib/experiment/version.rb +1 -1
- metadata +35 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f404cf631d8d1063d98e1cb1d84ac87775e6fad2569d1cbdbb4cb09c9d07f788
|
4
|
+
data.tar.gz: 6f0583a52122d0c07aa5087817524f7dd1b7a1e669231242ba3938ca96891be1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5a42786a515308365e9e1b8796a5caca4bd67c00c059e094ef87bca6891169ffbf456238b257eaa9abf3fd947e4f963e445782e9fabcb552ea55bf13e4f5c13
|
7
|
+
data.tar.gz: 01af1ba4be8773a21bf31614dd26aef15151eccebaa17c64d326c45d95ff1785623d89422fff43b8c963845a280907d82ddc2071e8ba0ae6c898ea03f27b5897
|
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
spec.extra_rdoc_files = ['README.md']
|
21
21
|
|
22
|
+
spec.add_development_dependency 'concurrent-ruby', '~> 1.2.2'
|
22
23
|
spec.add_development_dependency 'psych', '~> 4.0'
|
23
24
|
spec.add_development_dependency 'rake', '~> 13.0'
|
24
25
|
spec.add_development_dependency 'rdoc', '= 6.4'
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module AmplitudeAnalytics
|
2
|
+
# Amplitude
|
3
|
+
class Amplitude
|
4
|
+
attr_reader :configuration
|
5
|
+
|
6
|
+
def initialize(api_key, configuration: nil)
|
7
|
+
@configuration = configuration || Config.new
|
8
|
+
@configuration.api_key = api_key
|
9
|
+
@timeline = Timeline.new
|
10
|
+
@timeline.setup(self)
|
11
|
+
add(AmplitudeDestinationPlugin.new)
|
12
|
+
add(ContextPlugin.new)
|
13
|
+
register_on_exit
|
14
|
+
end
|
15
|
+
|
16
|
+
def track(event)
|
17
|
+
@timeline.process(event)
|
18
|
+
end
|
19
|
+
|
20
|
+
def flush
|
21
|
+
@timeline.flush
|
22
|
+
end
|
23
|
+
|
24
|
+
def add(plugin)
|
25
|
+
@timeline.add(plugin)
|
26
|
+
plugin.setup(self)
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove(plugin)
|
31
|
+
@timeline.remove(plugin)
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def shutdown
|
36
|
+
@configuration.opt_out = true
|
37
|
+
@timeline.shutdown
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def register_on_exit
|
43
|
+
if Thread.respond_to?(:_at_exit)
|
44
|
+
begin
|
45
|
+
at_exit { shutdown }
|
46
|
+
rescue StandardError
|
47
|
+
@configuration.logger.warning('register for exit fail')
|
48
|
+
end
|
49
|
+
else
|
50
|
+
at_exit { shutdown }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'logger'
|
2
|
+
module AmplitudeAnalytics
|
3
|
+
# Config
|
4
|
+
class Config
|
5
|
+
attr_accessor :api_key, :flush_interval_millis, :flush_max_retries,
|
6
|
+
:logger, :min_id_length, :callback, :server_zone, :use_batch,
|
7
|
+
:storage_provider, :opt_out, :ingestion_metadata
|
8
|
+
|
9
|
+
def initialize(api_key: nil, flush_queue_size: FLUSH_QUEUE_SIZE,
|
10
|
+
flush_interval_millis: FLUSH_INTERVAL_MILLIS,
|
11
|
+
flush_max_retries: FLUSH_MAX_RETRIES,
|
12
|
+
logger: Logger.new($stdout, progname: LOGGER_NAME, level: Logger::ERROR),
|
13
|
+
min_id_length: nil, callback: nil, server_zone: DEFAULT_ZONE,
|
14
|
+
use_batch: false, server_url: nil,
|
15
|
+
storage_provider: InMemoryStorageProvider.new, ingestion_metadata: nil)
|
16
|
+
@api_key = api_key
|
17
|
+
@flush_queue_size = flush_queue_size
|
18
|
+
@flush_size_divider = 1
|
19
|
+
@flush_interval_millis = flush_interval_millis
|
20
|
+
@flush_max_retries = flush_max_retries
|
21
|
+
@logger = logger
|
22
|
+
@min_id_length = min_id_length
|
23
|
+
@callback = callback
|
24
|
+
@server_zone = server_zone
|
25
|
+
@use_batch = use_batch
|
26
|
+
@server_url = server_url
|
27
|
+
@storage_provider = storage_provider
|
28
|
+
@opt_out = false
|
29
|
+
@ingestion_metadata = ingestion_metadata
|
30
|
+
end
|
31
|
+
|
32
|
+
def storage
|
33
|
+
@storage_provider.storage
|
34
|
+
end
|
35
|
+
|
36
|
+
def valid?
|
37
|
+
@api_key && @flush_queue_size > 0 && @flush_interval_millis > 0 && min_id_length_valid?
|
38
|
+
end
|
39
|
+
|
40
|
+
def min_id_length_valid?
|
41
|
+
@min_id_length.nil? || (@min_id_length.is_a?(Integer) && @min_id_length > 0)
|
42
|
+
end
|
43
|
+
|
44
|
+
def flush_queue_size
|
45
|
+
[@flush_queue_size / @flush_size_divider, 1].max
|
46
|
+
end
|
47
|
+
|
48
|
+
def flush_queue_size=(size)
|
49
|
+
@flush_queue_size = size
|
50
|
+
@flush_size_divider = 1
|
51
|
+
end
|
52
|
+
|
53
|
+
def server_url
|
54
|
+
@url || (
|
55
|
+
if use_batch
|
56
|
+
SERVER_URL[@server_zone][BATCH]
|
57
|
+
else
|
58
|
+
SERVER_URL[@server_zone][HTTP_V2]
|
59
|
+
end)
|
60
|
+
end
|
61
|
+
|
62
|
+
def server_url=(url)
|
63
|
+
@url = url
|
64
|
+
end
|
65
|
+
|
66
|
+
def options
|
67
|
+
{ 'min_id_length' => @min_id_length } if min_id_length_valid? && @min_id_length
|
68
|
+
end
|
69
|
+
|
70
|
+
def increase_flush_divider
|
71
|
+
@flush_size_divider += 1
|
72
|
+
end
|
73
|
+
|
74
|
+
def reset_flush_divider
|
75
|
+
@flush_size_divider = 1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module AmplitudeAnalytics
|
2
|
+
SDK_LIBRARY = 'amplitude-experiment-ruby'.freeze
|
3
|
+
SDK_VERSION = '1.1.5'.freeze
|
4
|
+
|
5
|
+
EU_ZONE = 'EU'.freeze
|
6
|
+
DEFAULT_ZONE = 'US'.freeze
|
7
|
+
BATCH = 'batch'.freeze
|
8
|
+
HTTP_V2 = 'v2'.freeze
|
9
|
+
SERVER_URL = {
|
10
|
+
EU_ZONE => {
|
11
|
+
BATCH => 'https://api.eu.amplitude.com/batch',
|
12
|
+
HTTP_V2 => 'https://api.eu.amplitude.com/2/httpapi'
|
13
|
+
},
|
14
|
+
DEFAULT_ZONE => {
|
15
|
+
BATCH => 'https://api2.amplitude.com/batch',
|
16
|
+
HTTP_V2 => 'https://api2.amplitude.com/2/httpapi'
|
17
|
+
}
|
18
|
+
}.freeze
|
19
|
+
LOGGER_NAME = 'amplitude'.freeze
|
20
|
+
|
21
|
+
MAX_PROPERTY_KEYS = 1024
|
22
|
+
MAX_STRING_LENGTH = 1024
|
23
|
+
FLUSH_QUEUE_SIZE = 200
|
24
|
+
FLUSH_INTERVAL_MILLIS = 10_000
|
25
|
+
FLUSH_MAX_RETRIES = 12
|
26
|
+
CONNECTION_TIMEOUT = 10.0 # seconds float
|
27
|
+
MAX_BUFFER_CAPACITY = 20_000
|
28
|
+
|
29
|
+
# PluginType
|
30
|
+
class PluginType
|
31
|
+
BEFORE = 0
|
32
|
+
ENRICHMENT = 1
|
33
|
+
DESTINATION = 2
|
34
|
+
|
35
|
+
def self.name(value)
|
36
|
+
mapping = {
|
37
|
+
BEFORE => 'BEFORE',
|
38
|
+
ENRICHMENT => 'ENRICHMENT',
|
39
|
+
DESTINATION => 'DESTINATION'
|
40
|
+
}
|
41
|
+
|
42
|
+
mapping[value]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module AmplitudeAnalytics
|
4
|
+
# IngestionMetadata
|
5
|
+
class IngestionMetadata
|
6
|
+
INGESTION_METADATA_KEY_MAPPING = {
|
7
|
+
'source_name' => ['source_name', String],
|
8
|
+
'source_version' => ['source_version', String]
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
attr_accessor :source_name, :source_version
|
12
|
+
|
13
|
+
def initialize(source_name: nil, source_version: nil)
|
14
|
+
@source_name = source_name
|
15
|
+
@source_version = source_version
|
16
|
+
end
|
17
|
+
|
18
|
+
def body
|
19
|
+
result = {}
|
20
|
+
INGESTION_METADATA_KEY_MAPPING.each do |key, mapping|
|
21
|
+
next unless instance_variable_defined?("@#{key}") && !instance_variable_get("@#{key}").nil?
|
22
|
+
|
23
|
+
value = instance_variable_get("@#{key}")
|
24
|
+
if value.is_a?(mapping[1])
|
25
|
+
result[mapping[0]] = value
|
26
|
+
else
|
27
|
+
puts "#{self.class}.#{key} expected #{mapping[1]} but received #{value.class}."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
result
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# EventOptions
|
35
|
+
class EventOptions
|
36
|
+
EVENT_KEY_MAPPING = {
|
37
|
+
'user_id' => ['user_id', String],
|
38
|
+
'device_id' => ['device_id', String],
|
39
|
+
'event_type' => ['event_type', String],
|
40
|
+
'time' => ['time', Integer],
|
41
|
+
'event_properties' => ['event_properties', Hash],
|
42
|
+
'user_properties' => ['user_properties', Hash],
|
43
|
+
'groups' => ['groups', Hash],
|
44
|
+
'app_version' => ['app_version', String],
|
45
|
+
'platform' => ['platform', String],
|
46
|
+
'os_name' => ['os_name', String],
|
47
|
+
'os_version' => ['os_version', String],
|
48
|
+
'device_brand' => ['device_brand', String],
|
49
|
+
'device_manufacturer' => ['device_manufacturer', String],
|
50
|
+
'device_model' => ['device_model', String],
|
51
|
+
'carrier' => ['carrier', String],
|
52
|
+
'country' => ['country', String],
|
53
|
+
'region' => ['region', String],
|
54
|
+
'city' => ['city', String],
|
55
|
+
'dma' => ['dma', String],
|
56
|
+
'language' => ['language', String],
|
57
|
+
'price' => ['price', Float],
|
58
|
+
'quantity' => ['quantity', Integer],
|
59
|
+
'revenue' => ['revenue', Float],
|
60
|
+
'product_id' => ['productId', String],
|
61
|
+
'revenue_type' => ['revenueType', String],
|
62
|
+
'location_lat' => ['location_lat', Float],
|
63
|
+
'location_lng' => ['location_lng', Float],
|
64
|
+
'ip' => ['ip', String],
|
65
|
+
'idfa' => ['idfa', String],
|
66
|
+
'idfv' => ['idfv', String],
|
67
|
+
'adid' => ['adid', String],
|
68
|
+
'android_id' => ['android_id', String],
|
69
|
+
'event_id' => ['event_id', Integer],
|
70
|
+
'session_id' => ['session_id', Integer],
|
71
|
+
'insert_id' => ['insert_id', String],
|
72
|
+
'library' => ['library', String],
|
73
|
+
'ingestion_metadata' => ['ingestion_metadata', IngestionMetadata],
|
74
|
+
'group_properties' => ['group_properties', Hash],
|
75
|
+
'partner_id' => ['partner_id', String],
|
76
|
+
'version_name' => ['version_name', String]
|
77
|
+
}.freeze
|
78
|
+
|
79
|
+
attr_accessor :user_id, :device_id, :event_type, :time, :event_properties, :user_properties,
|
80
|
+
:groups, :app_version, :platform, :os_name, :os_version, :device_brand,
|
81
|
+
:device_manufacturer, :device_model, :carrier, :country, :region, :city,
|
82
|
+
:dma, :language, :price, :quantity, :revenue, :product_id, :revenue_type,
|
83
|
+
:location_lat, :location_lng, :ip, :idfa, :idfv, :adid, :android_id,
|
84
|
+
:event_id, :session_id, :insert_id, :library, :ingestion_metadata,
|
85
|
+
:group_properties, :partner_id, :version_name, :retry
|
86
|
+
|
87
|
+
def initialize(user_id: nil, device_id: nil, time: nil, event_properties: nil,
|
88
|
+
user_properties: nil, groups: nil, app_version: nil, platform: nil, os_name: nil,
|
89
|
+
os_version: nil, device_brand: nil, device_manufacturer: nil, device_model: nil,
|
90
|
+
carrier: nil, country: nil, region: nil, city: nil, dma: nil, language: nil,
|
91
|
+
price: nil, quantity: nil, revenue: nil, product_id: nil, revenue_type: nil,
|
92
|
+
location_lat: nil, location_lng: nil, ip: nil, idfa: nil, idfv: nil, adid: nil,
|
93
|
+
android_id: nil, event_id: nil, session_id: nil, insert_id: nil,
|
94
|
+
ingestion_metadata: nil, group_properties: nil, partner_id: nil, version_name: nil,
|
95
|
+
callback: nil)
|
96
|
+
@user_id = user_id
|
97
|
+
@device_id = device_id
|
98
|
+
@event_type = event_type
|
99
|
+
@time = time
|
100
|
+
@event_properties = event_properties
|
101
|
+
@user_properties = user_properties
|
102
|
+
@groups = groups
|
103
|
+
@app_version = app_version
|
104
|
+
@platform = platform
|
105
|
+
@os_name = os_name
|
106
|
+
@os_version = os_version
|
107
|
+
@device_brand = device_brand
|
108
|
+
@device_manufacturer = device_manufacturer
|
109
|
+
@device_model = device_model
|
110
|
+
@carrier = carrier
|
111
|
+
@country = country
|
112
|
+
@region = region
|
113
|
+
@city = city
|
114
|
+
@dma = dma
|
115
|
+
@language = language
|
116
|
+
@price = price
|
117
|
+
@quantity = quantity
|
118
|
+
@revenue = revenue
|
119
|
+
@product_id = product_id
|
120
|
+
@revenue_type = revenue_type
|
121
|
+
@location_lat = location_lat
|
122
|
+
@location_lng = location_lng
|
123
|
+
@ip = ip
|
124
|
+
@idfa = idfa
|
125
|
+
@idfv = idfv
|
126
|
+
@adid = adid
|
127
|
+
@android_id = android_id
|
128
|
+
@event_id = event_id
|
129
|
+
@session_id = session_id
|
130
|
+
@insert_id = insert_id
|
131
|
+
@ingestion_metadata = ingestion_metadata
|
132
|
+
@group_properties = group_properties
|
133
|
+
@partner_id = partner_id
|
134
|
+
@version_name = version_name
|
135
|
+
@event_callback = callback
|
136
|
+
@retry = 0
|
137
|
+
end
|
138
|
+
|
139
|
+
def [](key)
|
140
|
+
instance_variable_get("@#{key}")
|
141
|
+
end
|
142
|
+
|
143
|
+
def []=(key, value)
|
144
|
+
send("#{key}=", value) if verify_property(key, value)
|
145
|
+
end
|
146
|
+
|
147
|
+
def include?(key)
|
148
|
+
instance_variable_defined?("@#{key}") && !instance_variable_get("@#{key}").nil?
|
149
|
+
end
|
150
|
+
|
151
|
+
def valid_properties?(key, value)
|
152
|
+
return false unless key.is_a?(String)
|
153
|
+
|
154
|
+
if value.is_a?(Array)
|
155
|
+
result = true
|
156
|
+
value.each do |element|
|
157
|
+
return false if element.is_a?(Array)
|
158
|
+
|
159
|
+
if element.is_a?(Hash)
|
160
|
+
result &&= valid_object?(element)
|
161
|
+
elsif !element.is_a?(Numeric) && !element.is_a?(String) && !element.is_a?(TrueClass) && !element.is_a?(FalseClass)
|
162
|
+
result = false
|
163
|
+
end
|
164
|
+
break unless result
|
165
|
+
end
|
166
|
+
return result
|
167
|
+
end
|
168
|
+
|
169
|
+
return valid_object?(value) if value.is_a?(Hash)
|
170
|
+
|
171
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
|
172
|
+
value.is_a?(Numeric) || value.is_a?(String) || value.is_a?(Symbol)
|
173
|
+
end
|
174
|
+
|
175
|
+
def valid_object?(obj)
|
176
|
+
obj.each do |key, value|
|
177
|
+
return false unless valid_properties?(key, value)
|
178
|
+
end
|
179
|
+
true
|
180
|
+
end
|
181
|
+
|
182
|
+
def verify_property(key, value)
|
183
|
+
return true if value.nil?
|
184
|
+
|
185
|
+
unless instance_variable_defined?("@#{key}")
|
186
|
+
AmplitudeAnalytics.logger.error("Unexpected event property key: #{key}")
|
187
|
+
return false
|
188
|
+
end
|
189
|
+
|
190
|
+
expected_type = EVENT_KEY_MAPPING[key][1]
|
191
|
+
unless value.is_a?(expected_type)
|
192
|
+
AmplitudeAnalytics.logger.error("Event property #{key} expected #{expected_type} but received #{value.class}.")
|
193
|
+
return false
|
194
|
+
end
|
195
|
+
|
196
|
+
return valid_object?(value) if value.is_a?(Hash)
|
197
|
+
|
198
|
+
true
|
199
|
+
end
|
200
|
+
|
201
|
+
def event_body
|
202
|
+
event_body = {}
|
203
|
+
EVENT_KEY_MAPPING.each do |key, mapping|
|
204
|
+
next unless include?(key) && self[key]
|
205
|
+
|
206
|
+
value = self[key]
|
207
|
+
if value.is_a?(mapping[1])
|
208
|
+
event_body[mapping[0]] = value
|
209
|
+
else
|
210
|
+
puts "#{self.class}.#{key} expected #{mapping[1]} but received #{value.class}."
|
211
|
+
end
|
212
|
+
end
|
213
|
+
event_body['ingestion_metadata'] = @ingestion_metadata.body if @ingestion_metadata.respond_to?(:body)
|
214
|
+
%w[user_properties event_properties group_properties].each do |properties|
|
215
|
+
next unless event_body[properties]
|
216
|
+
end
|
217
|
+
AmplitudeAnalytics.truncate(event_body.sort.to_h)
|
218
|
+
end
|
219
|
+
|
220
|
+
def callback(status_code, message = nil)
|
221
|
+
@event_callback.call(self, status_code, message) if @event_callback.respond_to?(:call)
|
222
|
+
end
|
223
|
+
|
224
|
+
def to_s
|
225
|
+
JSON.generate(event_body)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# BaseEvent
|
230
|
+
class BaseEvent < EventOptions
|
231
|
+
def initialize(event_type, **kwargs)
|
232
|
+
@event_type = event_type
|
233
|
+
super(**kwargs)
|
234
|
+
end
|
235
|
+
|
236
|
+
def load_event_options(event_options)
|
237
|
+
return if event_options.nil?
|
238
|
+
|
239
|
+
EVENT_KEY_MAPPING.each_key do |key|
|
240
|
+
self[key] = Marshal.load(Marshal.dump(event_options[key])) if event_options.include?(key)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module AmplitudeAnalytics
|
2
|
+
# InvalidEventError
|
3
|
+
class InvalidEventError < StandardError
|
4
|
+
def initialize(message = 'Invalid event.')
|
5
|
+
super(message)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# InvalidAPIKeyError
|
10
|
+
class InvalidAPIKeyError < StandardError
|
11
|
+
def initialize(message = 'Invalid API key.')
|
12
|
+
super(message)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module AmplitudeAnalytics
|
6
|
+
JSON_HEADER = {
|
7
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
8
|
+
Accept: '*/*'
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
# HttpStatus
|
12
|
+
class HttpStatus
|
13
|
+
attr_reader :value
|
14
|
+
|
15
|
+
def initialize(value)
|
16
|
+
@value = value
|
17
|
+
end
|
18
|
+
|
19
|
+
SUCCESS = new(200)
|
20
|
+
INVALID_REQUEST = new(400)
|
21
|
+
TIMEOUT = new(408)
|
22
|
+
PAYLOAD_TOO_LARGE = new(413)
|
23
|
+
TOO_MANY_REQUESTS = new(429)
|
24
|
+
FAILED = new(500)
|
25
|
+
UNKNOWN = new(-1)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Response
|
29
|
+
class Response
|
30
|
+
attr_accessor :status, :code, :body
|
31
|
+
|
32
|
+
def initialize(status: HttpStatus::UNKNOWN, body: nil)
|
33
|
+
@status = status
|
34
|
+
@code = status.value
|
35
|
+
@body = body || {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse(res)
|
39
|
+
res_body = JSON.parse(res.body)
|
40
|
+
@code = res_body['code']
|
41
|
+
@status = get_status(@code)
|
42
|
+
@body = res_body
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_status(code)
|
47
|
+
case code
|
48
|
+
when 200
|
49
|
+
HttpStatus::SUCCESS
|
50
|
+
when 400
|
51
|
+
HttpStatus::INVALID_REQUEST
|
52
|
+
when 408
|
53
|
+
HttpStatus::TIMEOUT
|
54
|
+
when 413
|
55
|
+
HttpStatus::PAYLOAD_TOO_LARGE
|
56
|
+
when 429
|
57
|
+
HttpStatus::TOO_MANY_REQUESTS
|
58
|
+
when 500
|
59
|
+
HttpStatus::FAILED
|
60
|
+
else
|
61
|
+
HttpStatus::UNKNOWN
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def error
|
66
|
+
@body['error'] if @body.key?('error')
|
67
|
+
end
|
68
|
+
|
69
|
+
def missing_field
|
70
|
+
@body['missing_field'] if @body.key?('missing_field')
|
71
|
+
end
|
72
|
+
|
73
|
+
def events_with_invalid_fields
|
74
|
+
@body['events_with_invalid_fields'] if @body.key?('events_with_invalid_fields')
|
75
|
+
end
|
76
|
+
|
77
|
+
def events_with_missing_fields
|
78
|
+
@body['events_with_missing_fields'] if @body.key?('events_with_missing_fields')
|
79
|
+
end
|
80
|
+
|
81
|
+
def events_with_invalid_id_lengths
|
82
|
+
@body['events_with_invalid_id_lengths'] if @body.key?('events_with_invalid_id_lengths')
|
83
|
+
end
|
84
|
+
|
85
|
+
def silenced_events
|
86
|
+
@body['silenced_events'] if @body.key?('silenced_events')
|
87
|
+
end
|
88
|
+
|
89
|
+
def throttled_events
|
90
|
+
@body['throttled_events'] if @body.key?('throttled_events')
|
91
|
+
end
|
92
|
+
|
93
|
+
def exceed_daily_quota(event)
|
94
|
+
return true if @body.key?('exceeded_daily_quota_users') && @body['exceeded_daily_quota_users'].include?(event.user_id)
|
95
|
+
return true if @body.key?('exceeded_daily_quota_devices') && @body['exceeded_daily_quota_devices'].include?(event.device_id)
|
96
|
+
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
def invalid_or_silenced_index
|
101
|
+
result = Set.new
|
102
|
+
result.merge(@body['events_with_missing_fields'].values.flatten) if @body.key?('events_with_missing_fields')
|
103
|
+
result.merge(@body['events_with_invalid_fields'].values.flatten) if @body.key?('events_with_invalid_fields')
|
104
|
+
result.merge(@body['events_with_invalid_id_lengths'].values.flatten) if @body.key?('events_with_invalid_id_lengths')
|
105
|
+
result.merge(@body['silenced_events']) if @body.key?('silenced_events')
|
106
|
+
result.to_a
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.get_status(code)
|
110
|
+
case code
|
111
|
+
when 200..299
|
112
|
+
HttpStatus::SUCCESS
|
113
|
+
when 429
|
114
|
+
HttpStatus::TOO_MANY_REQUESTS
|
115
|
+
when 413
|
116
|
+
HttpStatus::PAYLOAD_TOO_LARGE
|
117
|
+
when 408
|
118
|
+
HttpStatus::TIMEOUT
|
119
|
+
when 400..499
|
120
|
+
HttpStatus::INVALID_REQUEST
|
121
|
+
when 500..Float::INFINITY
|
122
|
+
HttpStatus::FAILED
|
123
|
+
else
|
124
|
+
HttpStatus::UNKNOWN
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# HttpClient
|
130
|
+
class HttpClient
|
131
|
+
def post(url, payload, header = nil)
|
132
|
+
result = Response.new
|
133
|
+
begin
|
134
|
+
uri = URI.parse(url)
|
135
|
+
http = Net::HTTP.new(uri.host || '', uri.port)
|
136
|
+
http.use_ssl = uri.scheme == 'https'
|
137
|
+
|
138
|
+
headers = header || JSON_HEADER
|
139
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
140
|
+
request.body = payload
|
141
|
+
res = http.request(request)
|
142
|
+
result.parse(res)
|
143
|
+
rescue Net::ReadTimeout
|
144
|
+
result.code = 408
|
145
|
+
result.status = HttpStatus::TIMEOUT
|
146
|
+
rescue Net::HTTPError => e
|
147
|
+
begin
|
148
|
+
result.parse(e)
|
149
|
+
rescue StandardError
|
150
|
+
result = Response.new
|
151
|
+
result.code = e.response.code.to_i
|
152
|
+
result.status = Response.get_status(e.response.code.to_i)
|
153
|
+
result.body = { 'error' => e.response.message }
|
154
|
+
end
|
155
|
+
rescue Net::OpenTimeout => e
|
156
|
+
result.body = { 'error' => e.message }
|
157
|
+
end
|
158
|
+
result
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|