/
helpers.rb
229 lines (200 loc) · 6.42 KB
/
helpers.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
require 'uri'
require 'set'
require 'json'
module Bugsnag
module Helpers # rubocop:todo Metrics/ModuleLength
MAX_STRING_LENGTH = 3072
MAX_PAYLOAD_LENGTH = 512000
MAX_ARRAY_LENGTH = 80
MAX_TRIM_STACK_FRAMES = 30
RAW_DATA_TYPES = [Numeric, TrueClass, FalseClass]
##
# Trim the size of value if the serialized JSON value is longer than is
# accepted by Bugsnag
def self.trim_if_needed(value)
value = "" if value.nil?
return value unless payload_too_long?(value)
# Truncate exception messages
reduced_value = truncate_exception_messages(value)
return reduced_value unless payload_too_long?(reduced_value)
# Trim metadata
reduced_value = trim_metadata(reduced_value)
return reduced_value unless payload_too_long?(reduced_value)
# Trim code from stacktrace
reduced_value = trim_stacktrace_code(reduced_value)
return reduced_value unless payload_too_long?(reduced_value)
# Remove metadata
reduced_value = remove_metadata_from_events(reduced_value)
return reduced_value unless payload_too_long?(reduced_value)
# Remove oldest functions in stacktrace
trim_stacktrace_functions(reduced_value)
end
##
# Merges r_hash into l_hash recursively, favouring the values in r_hash.
#
# Returns a new array consisting of the merged values
def self.deep_merge(l_hash, r_hash)
l_hash.merge(r_hash) do |key, l_val, r_val|
if l_val.is_a?(Hash) && r_val.is_a?(Hash)
deep_merge(l_val, r_val)
elsif l_val.is_a?(Array) && r_val.is_a?(Array)
l_val.concat(r_val)
else
r_val
end
end
end
##
# Merges r_hash into l_hash recursively, favouring the values in r_hash.
#
# Overwrites the values in the existing l_hash
def self.deep_merge!(l_hash, r_hash)
l_hash.merge!(r_hash) do |key, l_val, r_val|
if l_val.is_a?(Hash) && r_val.is_a?(Hash)
deep_merge(l_val, r_val)
elsif l_val.is_a?(Array) && r_val.is_a?(Array)
l_val.concat(r_val)
else
r_val
end
end
end
private
TRUNCATION_INFO = '[TRUNCATED]'
##
# Truncate exception messages
def self.truncate_exception_messages(payload)
extract_exception(payload) do |exception|
exception[:message] = trim_as_string(exception[:message])
end
payload
end
##
# Remove all code from stacktraces
def self.trim_stacktrace_code(payload)
extract_exception(payload) do |exception|
exception[:stacktrace].each do |frame|
frame.delete(:code)
end
end
payload
end
##
# Truncate stacktraces
def self.trim_stacktrace_functions(payload)
extract_exception(payload) do |exception|
stack = exception[:stacktrace]
exception[:stacktrace] = stack.take(MAX_TRIM_STACK_FRAMES)
end
payload
end
##
# Wrapper for trimming stacktraces
def self.extract_exception(payload, &block)
valid_payload = payload.is_a?(Hash) && payload[:events].respond_to?(:map)
return unless valid_payload && block_given?
payload[:events].each do |event|
event[:exceptions].each(&block)
end
end
##
# Take the metadata from the events and trim it down
def self.trim_metadata(payload)
return payload unless payload.is_a?(Hash) and payload[:events].respond_to?(:map)
payload[:events].map do |event|
event[:metaData] = truncate_arrays_in_value(event[:metaData])
event[:metaData] = trim_strings_in_value(event[:metaData])
end
payload
end
##
# Check if a value is a raw type which should not be trimmed, truncated
# or converted to a string
def self.is_json_raw_type?(value)
RAW_DATA_TYPES.detect {|klass| value.is_a?(klass)} != nil
end
##
# Shorten array until it fits within the payload size limit when serialized
def self.truncate_array(array)
return [] unless array.respond_to?(:slice)
array.slice(0, MAX_ARRAY_LENGTH).map do |item|
truncate_arrays_in_value(item)
end
end
##
# Trim all strings to be less than the maximum allowed string length
def self.trim_strings_in_value(value)
return value if is_json_raw_type?(value)
case value
when Hash
trim_strings_in_hash(value)
when Array, Set
trim_strings_in_array(value)
else
trim_as_string(value)
end
end
##
# Validate that the serialized JSON string value is below maximum payload
# length
def self.payload_too_long?(value)
get_payload_length(value) >= MAX_PAYLOAD_LENGTH
end
def self.get_payload_length(value)
if value.is_a?(String)
value.length
else
::JSON.dump(value).length
end
end
def self.trim_strings_in_hash(hash)
return {} unless hash.is_a?(Hash)
hash.each_with_object({}) do |(key, value), reduced_hash|
if reduced_value = trim_strings_in_value(value)
reduced_hash[key] = reduced_value
end
end
end
# If possible, convert the provided object to a string and trim to the
# maximum allowed string length
def self.trim_as_string(text)
return "" unless text.respond_to? :to_s
text = text.to_s
if text.length > MAX_STRING_LENGTH
length = MAX_STRING_LENGTH - TRUNCATION_INFO.length
text = text.slice(0, length) + TRUNCATION_INFO
end
text
end
def self.trim_strings_in_array(collection)
return [] unless collection.respond_to?(:map)
collection.map {|value| trim_strings_in_value(value)}
end
def self.truncate_arrays_in_value(value)
case value
when Hash
truncate_arrays_in_hash(value)
when Array, Set
truncate_array(value)
else
value
end
end
# Remove `metaData` from array of `events` within object
def self.remove_metadata_from_events(object)
return {} unless object.is_a?(Hash) and object[:events].respond_to?(:map)
object[:events].map do |event|
event.delete(:metaData) if object.is_a?(Hash)
end
object
end
def self.truncate_arrays_in_hash(hash)
return {} unless hash.is_a?(Hash)
hash.each_with_object({}) do |(key, value), reduced_hash|
if reduced_value = truncate_arrays_in_value(value)
reduced_hash[key] = reduced_value
end
end
end
end
end