amplitude-experiment 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|