/
object.ts
268 lines (232 loc) · 8.02 KB
/
object.ts
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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
import { ExtendedError, WrappedFunction } from '@sentry/types';
import { isError, isPrimitive, isSyntheticEvent } from './is';
import { Memo } from './memo';
import { truncate } from './string';
/**
* Wrap a given object method with a higher-order function
*
* @param source An object that contains a method to be wrapped.
* @param name A name of method to be wrapped.
* @param replacement A function that should be used to wrap a given method.
* @returns void
*/
export function fill(source: { [key: string]: any }, name: string, replacement: (...args: any[]) => any): void {
if (!(name in source) || (source[name] as WrappedFunction).__sentry__) {
return;
}
const original = source[name] as () => any;
const wrapped = replacement(original) as WrappedFunction;
// Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work
// otherwise it'll throw "TypeError: Object.defineProperties called on non-object"
// tslint:disable-next-line:strict-type-predicates
if (typeof wrapped === 'function') {
wrapped.prototype = wrapped.prototype || {};
Object.defineProperties(wrapped, {
__sentry__: {
enumerable: false,
value: true,
},
__sentry_original__: {
enumerable: false,
value: original,
},
__sentry_wrapped__: {
enumerable: false,
value: wrapped,
},
});
}
source[name] = wrapped;
}
/**
* Encodes given object into url-friendly format
*
* @param object An object that contains serializable values
* @returns string Encoded
*/
export function urlEncode(object: { [key: string]: any }): string {
return Object.keys(object)
.map(
// tslint:disable-next-line:no-unsafe-any
key => `${encodeURIComponent(key)}=${encodeURIComponent(object[key])}`,
)
.join('&');
}
/**
* Transforms Error object into an object literal with all it's attributes
* attached to it.
*
* Based on: https://github.com/ftlabs/js-abbreviate/blob/fa709e5f139e7770a71827b1893f22418097fbda/index.js#L95-L106
*
* @param error An Error containing all relevant information
* @returns An object with all error properties
*/
function objectifyError(error: ExtendedError): object {
// These properties are implemented as magical getters and don't show up in `for-in` loop
const err: {
stack: string | undefined;
message: string;
name: string;
[key: string]: any;
} = {
message: error.message,
name: error.name,
stack: error.stack,
};
for (const i in error) {
if (Object.prototype.hasOwnProperty.call(error, i)) {
err[i] = error[i];
}
}
return err;
}
/** Calculates bytes size of input string */
function utf8Length(value: string): number {
// tslint:disable-next-line:no-bitwise
return ~-encodeURI(value).split(/%..|./).length;
}
/** Calculates bytes size of input object */
function jsonSize(value: any): number {
return utf8Length(JSON.stringify(value));
}
/** JSDoc */
export function normalizeToSize<T>(
object: { [key: string]: any },
// Default Node.js REPL depth
depth: number = 3,
// 100kB, as 200kB is max payload size, so half sounds reasonable
maxSize: number = 100 * 1024,
): T {
const serialized = normalize(object, depth);
if (jsonSize(serialized) > maxSize) {
return normalizeToSize(object, depth - 1, maxSize);
}
return serialized as T;
}
/** Transforms any input value into a string form, either primitive value or a type of the input */
function serializeValue(value: any): any {
const type = Object.prototype.toString.call(value);
// Node.js REPL notation
if (typeof value === 'string') {
return truncate(value, 40);
} else if (type === '[object Object]') {
return '[Object]';
} else if (type === '[object Array]') {
return '[Array]';
} else {
const normalized = normalizeValue(value);
return isPrimitive(normalized) ? normalized : type;
}
}
/**
* normalizeValue()
*
* Takes unserializable input and make it serializable friendly
*
* - translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
* - serializes Error objects
* - filter global objects
*/
function normalizeValue<T>(value: T, key?: any): T | string {
if (key === 'domain' && typeof value === 'object' && ((value as unknown) as { _events: any })._events) {
return '[Domain]';
}
if (key === 'domainEmitter') {
return '[DomainEmitter]';
}
if (typeof (global as any) !== 'undefined' && (value as unknown) === global) {
return '[Global]';
}
if (typeof (window as any) !== 'undefined' && (value as unknown) === window) {
return '[Window]';
}
if (typeof (document as any) !== 'undefined' && (value as unknown) === document) {
return '[Document]';
}
// tslint:disable-next-line:strict-type-predicates
if (typeof Event !== 'undefined' && value instanceof Event) {
return Object.getPrototypeOf(value) ? value.constructor.name : 'Event';
}
// React's SyntheticEvent thingy
if (isSyntheticEvent(value)) {
return '[SyntheticEvent]';
}
if (Number.isNaN((value as unknown) as number)) {
return '[NaN]';
}
if (value === void 0) {
return '[undefined]';
}
if (typeof value === 'function') {
return `[Function: ${value.name || '<unknown-function-name>'}]`;
}
return value;
}
/**
* Walks an object to perform a normalization on it
*
* @param key of object that's walked in current iteration
* @param value object to be walked
* @param depth Optional number indicating how deep should walking be performed
* @param memo Optional Memo class handling decycling
*/
export function walk(key: string, value: any, depth: number = +Infinity, memo: Memo = new Memo()): any {
// If we reach the maximum depth, serialize whatever has left
if (depth === 0) {
return serializeValue(value);
}
// If value implements `toJSON` method, call it and return early
// tslint:disable:no-unsafe-any
if (value !== null && value !== undefined && typeof value.toJSON === 'function') {
return value.toJSON();
}
// tslint:enable:no-unsafe-any
// If normalized value is a primitive, there are no branches left to walk, so we can just bail out, as theres no point in going down that branch any further
const normalized = normalizeValue(value, key);
if (isPrimitive(normalized)) {
return normalized;
}
// Create source that we will use for next itterations, either objectified error object (Error type with extracted keys:value pairs) or the input itself
const source = (isError(value) ? objectifyError(value as Error) : value) as {
[key: string]: any;
};
// Create an accumulator that will act as a parent for all future itterations of that branch
const acc = Array.isArray(value) ? [] : {};
// If we already walked that branch, bail out, as it's circular reference
if (memo.memoize(value)) {
return '[Circular ~]';
}
// Walk all keys of the source
for (const innerKey in source) {
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
if (!Object.prototype.hasOwnProperty.call(source, innerKey)) {
continue;
}
// Recursively walk through all the child nodes
(acc as { [key: string]: any })[innerKey] = walk(innerKey, source[innerKey], depth - 1, memo);
}
// Once walked through all the branches, remove the parent from memo storage
memo.unmemoize(value);
// Return accumulated values
return acc;
}
/**
* normalize()
*
* - Creates a copy to prevent original input mutation
* - Skip non-enumerablers
* - Calls `toJSON` if implemented
* - Removes circular references
* - Translates non-serializeable values (undefined/NaN/Functions) to serializable format
* - Translates known global objects/Classes to a string representations
* - Takes care of Error objects serialization
* - Optionally limit depth of final output
*/
export function normalize(input: any, depth?: number): any {
try {
// tslint:disable-next-line:no-unsafe-any
return JSON.parse(JSON.stringify(input, (key: string, value: any) => walk(key, value, depth)));
} catch (_oO) {
return '**non-serializable**';
}
}