peekinside 0.0.0-stage → 1.2.1
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.
- package/LICENSE +21 -0
- package/README.md +45 -2
- package/bin/peekinside.js +8 -0
- package/index.d.ts +389 -0
- package/index.js +3 -0
- package/package.json +77 -4
- package/src/PeekInside.js +731 -0
- package/src/capture/body-capture.js +86 -0
- package/src/capture/body-decoder.js +188 -0
- package/src/capture/exchange-recorder.js +263 -0
- package/src/capture/http-message.js +138 -0
- package/src/capture/ignored-paths.js +49 -0
- package/src/capture/request-body-tap.js +40 -0
- package/src/capture/response-tap.js +83 -0
- package/src/cli.js +254 -0
- package/src/config.js +183 -0
- package/src/crypto.js +189 -0
- package/src/deployment.js +183 -0
- package/src/exceptions/exception-builder.js +339 -0
- package/src/exceptions/exception-capture.js +407 -0
- package/src/exceptions/exception-record.js +118 -0
- package/src/exceptions/fingerprint.js +60 -0
- package/src/exceptions/frame-classifier.js +114 -0
- package/src/exceptions/limits.js +95 -0
- package/src/exceptions/process-monitor.js +192 -0
- package/src/exceptions/rate-limiter.js +50 -0
- package/src/exceptions/request-context.js +337 -0
- package/src/exceptions/safe-json.js +186 -0
- package/src/exceptions/source-context.js +144 -0
- package/src/exceptions/source-maps.js +313 -0
- package/src/exceptions/stack-parser.js +179 -0
- package/src/hub/brand/peekinside-span-core-mark-dark.svg +7 -0
- package/src/hub/brand/peekinside-span-core-mark-light.svg +7 -0
- package/src/hub/client.js +184 -0
- package/src/hub/entry.js +4 -0
- package/src/hub/exception-state.js +97 -0
- package/src/hub/ingest-payload.js +362 -0
- package/src/hub/process.js +175 -0
- package/src/hub/project-log.js +559 -0
- package/src/hub/segment-log.js +282 -0
- package/src/hub/server.js +561 -0
- package/src/hub/spool-ingester.js +110 -0
- package/src/hub/store.js +404 -0
- package/src/instrumentation/console.js +83 -0
- package/src/instrumentation/http.js +146 -0
- package/src/instrumentation/index.js +234 -0
- package/src/instrumentation/jobs.js +186 -0
- package/src/instrumentation/loggers.js +135 -0
- package/src/instrumentation/mongodb.js +221 -0
- package/src/instrumentation/mysql2.js +161 -0
- package/src/instrumentation/operation.js +70 -0
- package/src/instrumentation/patch.js +72 -0
- package/src/instrumentation/pg.js +218 -0
- package/src/instrumentation/prisma.js +181 -0
- package/src/integrations/express-errors.js +135 -0
- package/src/integrations/express.js +57 -0
- package/src/integrations/fastify.js +72 -0
- package/src/integrations/framework-adapter.js +53 -0
- package/src/integrations/http-server.js +32 -0
- package/src/integrations/koa.js +72 -0
- package/src/integrations/server-binder.js +120 -0
- package/src/jobs/job-context.js +40 -0
- package/src/jobs/job-run-record.js +104 -0
- package/src/jobs/job-runner.js +173 -0
- package/src/logs/log-capture.js +143 -0
- package/src/logs/log-record.js +74 -0
- package/src/project.js +76 -0
- package/src/record.js +181 -0
- package/src/release.js +157 -0
- package/src/runtime/loop-monitor.js +162 -0
- package/src/runtime/sampler.js +93 -0
- package/src/spool.js +195 -0
- package/src/timeline/caller.js +129 -0
- package/src/timeline/execution-hash.js +110 -0
- package/src/timeline/lru.js +50 -0
- package/src/timeline/masking.js +266 -0
- package/src/timeline/shape.js +77 -0
- package/src/timeline/sql.js +396 -0
- package/src/timeline/timeline.js +642 -0
- package/src/transport/batch.js +324 -0
- package/src/transport/cloud.js +363 -0
- package/src/transport/local.js +354 -0
- package/src/user.js +83 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small least-recently-used cache, for work that repeats with the same input: analyzing the
|
|
3
|
+
* same SQL statement on every execution, mostly.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map with a size bound: past `max` entries, the least recently used one goes.
|
|
8
|
+
*/
|
|
9
|
+
export class LruCache {
|
|
10
|
+
/**
|
|
11
|
+
* @param {number} max - Entries kept
|
|
12
|
+
*/
|
|
13
|
+
constructor(max) {
|
|
14
|
+
this.max = max;
|
|
15
|
+
this._map = new Map();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {*} key
|
|
20
|
+
* @returns {*} The value, or undefined; a hit makes the entry the most recently used
|
|
21
|
+
*/
|
|
22
|
+
get(key) {
|
|
23
|
+
const value = this._map.get(key);
|
|
24
|
+
if (value !== undefined) {
|
|
25
|
+
this._map.delete(key);
|
|
26
|
+
this._map.set(key, value);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {*} key
|
|
33
|
+
* @param {*} value - Not undefined
|
|
34
|
+
* @returns {void}
|
|
35
|
+
*/
|
|
36
|
+
set(key, value) {
|
|
37
|
+
this._map.delete(key);
|
|
38
|
+
this._map.set(key, value);
|
|
39
|
+
if (this._map.size > this.max) this._map.delete(this._map.keys().next().value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Number of entries.
|
|
44
|
+
*
|
|
45
|
+
* @type {number}
|
|
46
|
+
*/
|
|
47
|
+
get size() {
|
|
48
|
+
return this._map.size;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Masking of the values the timeline records: query parameters, MongoDB and Prisma arguments,
|
|
3
|
+
* outgoing URLs. Masking happens when the value is captured, so a masked value never leaves the
|
|
4
|
+
* process, not even encrypted.
|
|
5
|
+
*
|
|
6
|
+
* Two rules decide what is masked:
|
|
7
|
+
* - names: a value under a sensitive name (a password column, an `apiKey` field, a `token` query
|
|
8
|
+
* parameter) whatever it holds;
|
|
9
|
+
* - shapes: a string that looks like a secret whatever its name: a JWT, a password hash.
|
|
10
|
+
*
|
|
11
|
+
* Then every value is made small and JSON-safe: long strings are cut, binary data and large
|
|
12
|
+
* objects are described rather than kept.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { types } from 'util';
|
|
16
|
+
|
|
17
|
+
// The same family of names the dashboard masks in headers.
|
|
18
|
+
const SENSITIVE_NAME = /passw|secret|token|api[-_]?key|session|authorization|cookie|credential|private[-_]?key|cvv|ssn|card[-_]?number/i;
|
|
19
|
+
// header.payload.signature (the signature is empty for unsigned tokens)
|
|
20
|
+
const JWT = /^eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
|
|
21
|
+
// bcrypt ($2a$, $2b$, $2y$), argon2 ($argon2id$...), scrypt
|
|
22
|
+
const PASSWORD_HASH = /^\$(?:2[aby]\$|argon2|scrypt\$)/;
|
|
23
|
+
|
|
24
|
+
export const MASKED = '[masked]';
|
|
25
|
+
const MAX_STRING = 256;
|
|
26
|
+
const MAX_JSON = 1024;
|
|
27
|
+
const MAX_URL = 2048;
|
|
28
|
+
const MAX_DEPTH = 8;
|
|
29
|
+
// Objects with more nodes than this are only measured, not copied.
|
|
30
|
+
const MAX_NODES = 1000;
|
|
31
|
+
const TOO_BIG = Symbol('tooBig');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Masking and size rules for captured values.
|
|
35
|
+
*/
|
|
36
|
+
export class Masker {
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} name - A column, field, key or query parameter name
|
|
39
|
+
* @returns {boolean} Whether values under this name are masked
|
|
40
|
+
*/
|
|
41
|
+
static isSensitiveName(name) {
|
|
42
|
+
return typeof name === 'string' && SENSITIVE_NAME.test(name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {*} value
|
|
47
|
+
* @returns {boolean} Whether the value is a string that looks like a secret (a JWT, a password
|
|
48
|
+
* hash)
|
|
49
|
+
*/
|
|
50
|
+
static isSecretString(value) {
|
|
51
|
+
return typeof value === 'string' && value.length >= 10 && (JWT.test(value) || PASSWORD_HASH.test(value));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* SQL parameters, masked: values bound to sensitive columns, and secret-looking strings.
|
|
56
|
+
*
|
|
57
|
+
* @param {Array} values - As the app passed them
|
|
58
|
+
* @param {Set<number>} sensitive - Indexes bound to sensitive columns (see SqlText)
|
|
59
|
+
* @param {number} [max=50] - Values kept
|
|
60
|
+
* @returns {Array}
|
|
61
|
+
*/
|
|
62
|
+
static sqlParams(values, sensitive, max = 50) {
|
|
63
|
+
const out = [];
|
|
64
|
+
const count = Math.min(values.length, max);
|
|
65
|
+
for (let i = 0; i < count; i++) {
|
|
66
|
+
out.push(sensitive.has(i) ? MASKED : Masker.value(values[i]));
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The fields of an object (MongoDB command fields, Prisma arguments, named SQL parameters),
|
|
73
|
+
* masked by name at any depth. Each field follows the value rules on its own, so one large field
|
|
74
|
+
* doesn't hide the others.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} fields
|
|
77
|
+
* @param {number} [max=50] - Fields kept
|
|
78
|
+
* @returns {Object}
|
|
79
|
+
*/
|
|
80
|
+
static fields(fields, max = 50) {
|
|
81
|
+
const out = {};
|
|
82
|
+
let count = 0;
|
|
83
|
+
for (const key of safeKeys(fields)) {
|
|
84
|
+
if (count++ >= max) break;
|
|
85
|
+
define(out, key, SENSITIVE_NAME.test(key) ? MASKED : Masker.value(safeGet(fields, key)));
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* One value, after the rules: strings over 256 characters are cut, secret-looking strings are
|
|
92
|
+
* masked, binary data becomes "<binary N bytes>", Dates ISO strings, bigints strings, functions
|
|
93
|
+
* and symbols "<function>"/"<symbol>", undefined null; objects and arrays (with sensitive keys
|
|
94
|
+
* masked at any depth) are kept when their JSON is 1 KB or less, else "<json 3.2 KB>".
|
|
95
|
+
*
|
|
96
|
+
* @param {*} value
|
|
97
|
+
* @returns {*} JSON-safe
|
|
98
|
+
*/
|
|
99
|
+
static value(value) {
|
|
100
|
+
if (value === null || typeof value !== 'object' || isBinary(value) || types.isDate(value)) {
|
|
101
|
+
return leaf(value);
|
|
102
|
+
}
|
|
103
|
+
const state = { nodes: 0 };
|
|
104
|
+
const masked = deep(value, 0, state, []);
|
|
105
|
+
if (masked === TOO_BIG) return describeJson(jsonSize(value));
|
|
106
|
+
if (masked !== null && typeof masked === 'object') {
|
|
107
|
+
const size = Buffer.byteLength(JSON.stringify(masked));
|
|
108
|
+
if (size > MAX_JSON) return describeJson(size);
|
|
109
|
+
}
|
|
110
|
+
return masked;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* An outgoing URL, safe to keep: without credentials, with the values of sensitive query
|
|
115
|
+
* parameters (and secret-looking values) masked, at most 2048 characters.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} url
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
static url(url) {
|
|
121
|
+
let text = String(url).replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#@]*@/i, '$1');
|
|
122
|
+
const query = text.indexOf('?');
|
|
123
|
+
if (query !== -1) {
|
|
124
|
+
const hash = text.indexOf('#', query);
|
|
125
|
+
const end = hash === -1 ? text.length : hash;
|
|
126
|
+
const masked = text.slice(query + 1, end).split('&').map((part) => {
|
|
127
|
+
const equals = part.indexOf('=');
|
|
128
|
+
if (equals === -1) return part;
|
|
129
|
+
const key = part.slice(0, equals);
|
|
130
|
+
const value = part.slice(equals + 1);
|
|
131
|
+
return SENSITIVE_NAME.test(decode(key)) || Masker.isSecretString(decode(value)) ? `${key}=${MASKED}` : part;
|
|
132
|
+
}).join('&');
|
|
133
|
+
text = `${text.slice(0, query + 1)}${masked}${text.slice(end)}`;
|
|
134
|
+
}
|
|
135
|
+
return text.length > MAX_URL ? text.slice(0, MAX_URL) : text;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function leaf(value) {
|
|
140
|
+
switch (typeof value) {
|
|
141
|
+
case 'string':
|
|
142
|
+
if (Masker.isSecretString(value)) return MASKED;
|
|
143
|
+
return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…` : value;
|
|
144
|
+
case 'number':
|
|
145
|
+
return Number.isFinite(value) ? value : String(value);
|
|
146
|
+
case 'boolean':
|
|
147
|
+
return value;
|
|
148
|
+
case 'bigint':
|
|
149
|
+
return value.toString();
|
|
150
|
+
case 'function':
|
|
151
|
+
return '<function>';
|
|
152
|
+
case 'symbol':
|
|
153
|
+
return '<symbol>';
|
|
154
|
+
case 'undefined':
|
|
155
|
+
return null;
|
|
156
|
+
default:
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
if (value === null) return null;
|
|
160
|
+
if (isBinary(value)) return `<binary ${byteLength(value)} bytes>`;
|
|
161
|
+
if (types.isDate(value)) {
|
|
162
|
+
const time = Date.prototype.getTime.call(value);
|
|
163
|
+
return Number.isNaN(time) ? null : Date.prototype.toISOString.call(value);
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Copies an object or array with sensitive keys masked and every leaf after the rules. Returns
|
|
169
|
+
// TOO_BIG past MAX_NODES nodes: such a value ends up described by its size anyway.
|
|
170
|
+
function deep(value, depth, state, ancestors) {
|
|
171
|
+
if (value === null || typeof value !== 'object' || isBinary(value) || types.isDate(value)) return leaf(value);
|
|
172
|
+
if (++state.nodes > MAX_NODES) return TOO_BIG;
|
|
173
|
+
if (depth >= MAX_DEPTH || ancestors.includes(value)) return '…';
|
|
174
|
+
|
|
175
|
+
// Driver types (ObjectId, Decimal128, Prisma's Decimal) say what they are through toJSON(), as
|
|
176
|
+
// JSON.stringify would ask them.
|
|
177
|
+
if (!Array.isArray(value)) {
|
|
178
|
+
const toJSON = safeGet(value, 'toJSON');
|
|
179
|
+
if (typeof toJSON === 'function') {
|
|
180
|
+
let json;
|
|
181
|
+
try {
|
|
182
|
+
json = toJSON.call(value);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return '<object>';
|
|
185
|
+
}
|
|
186
|
+
if (json !== value) return deep(json, depth, state, ancestors);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
ancestors.push(value);
|
|
191
|
+
try {
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
const out = [];
|
|
194
|
+
for (let i = 0; i < value.length; i++) {
|
|
195
|
+
const item = deep(safeGet(value, i), depth + 1, state, ancestors);
|
|
196
|
+
if (item === TOO_BIG) return TOO_BIG;
|
|
197
|
+
out.push(item);
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
const out = {};
|
|
202
|
+
for (const key of safeKeys(value)) {
|
|
203
|
+
if (SENSITIVE_NAME.test(key)) {
|
|
204
|
+
define(out, key, MASKED);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const item = deep(safeGet(value, key), depth + 1, state, ancestors);
|
|
208
|
+
if (item === TOO_BIG) return TOO_BIG;
|
|
209
|
+
define(out, key, item);
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
} finally {
|
|
213
|
+
ancestors.pop();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function describeJson(bytes) {
|
|
218
|
+
return `<json ${(bytes / 1024).toFixed(1)} KB>`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// The JSON size of a value too large to copy. Values JSON can't serialize count as large.
|
|
222
|
+
function jsonSize(value) {
|
|
223
|
+
try {
|
|
224
|
+
const json = JSON.stringify(value, (key, item) => (typeof item === 'bigint' ? item.toString() : item));
|
|
225
|
+
return json === undefined ? 0 : Buffer.byteLength(json);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return MAX_JSON + 1;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isBinary(value) {
|
|
232
|
+
return value !== null && typeof value === 'object' && (types.isAnyArrayBuffer(value) || ArrayBuffer.isView(value));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function byteLength(value) {
|
|
236
|
+
return typeof value.byteLength === 'number' ? value.byteLength : 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function safeKeys(value) {
|
|
240
|
+
try {
|
|
241
|
+
return Object.keys(value);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function safeGet(target, key) {
|
|
248
|
+
try {
|
|
249
|
+
return target[key];
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Keys from the app's data (and from the network) may be "__proto__".
|
|
256
|
+
function define(target, key, value) {
|
|
257
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function decode(text) {
|
|
261
|
+
try {
|
|
262
|
+
return decodeURIComponent(text.replace(/\+/g, ' '));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return text;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value-free shapes of query arguments, for statements that aren't SQL: MongoDB commands
|
|
3
|
+
* (`orders.find({ customerId: ?, status: { $in: ? } })`) and Prisma operations
|
|
4
|
+
* (`User.findMany({ where: { id: ? }, include: { posts: ? } })`).
|
|
5
|
+
*
|
|
6
|
+
* Every value becomes `?` and only the structure stays, so the shape is the same for every
|
|
7
|
+
* execution of the same call: it is both the statement's text and what its fingerprint is
|
|
8
|
+
* computed from.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const IDENTIFIER = /^[$A-Za-z_][\w$]*$/;
|
|
12
|
+
const MAX_DEPTH = 6;
|
|
13
|
+
// Arrays of objects ($or branches, pipeline stages) list at most this many entries.
|
|
14
|
+
const MAX_ITEMS = 20;
|
|
15
|
+
const MAX_LENGTH = 4096;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Writes shapes.
|
|
19
|
+
*/
|
|
20
|
+
export class Shape {
|
|
21
|
+
/**
|
|
22
|
+
* The shape of a value: objects keep their keys, arrays of objects their entries, everything
|
|
23
|
+
* else is `?`.
|
|
24
|
+
*
|
|
25
|
+
* @param {*} value
|
|
26
|
+
* @returns {string} At most 4096 characters
|
|
27
|
+
*/
|
|
28
|
+
static of(value) {
|
|
29
|
+
const text = write(value, 0);
|
|
30
|
+
return text.length > MAX_LENGTH ? `${text.slice(0, MAX_LENGTH - 1)}…` : text;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function write(value, depth) {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
// Values ($in: [1, 2, 3]) are one value; objects (pipeline stages, $or branches) are structure.
|
|
37
|
+
if (value.length === 0 || !value.every(isStructure)) return '?';
|
|
38
|
+
if (depth >= MAX_DEPTH) return '[…]';
|
|
39
|
+
const items = value.slice(0, MAX_ITEMS).map((item) => write(item, depth + 1));
|
|
40
|
+
if (value.length > MAX_ITEMS) items.push('…');
|
|
41
|
+
return `[${items.join(', ')}]`;
|
|
42
|
+
}
|
|
43
|
+
if (!isStructure(value)) return '?';
|
|
44
|
+
const keys = safeKeys(value);
|
|
45
|
+
if (keys.length === 0) return '{}';
|
|
46
|
+
if (depth >= MAX_DEPTH) return '{ … }';
|
|
47
|
+
const fields = keys.map((key) => `${IDENTIFIER.test(key) ? key : JSON.stringify(key)}: ${write(safeGet(value, key), depth + 1)}`);
|
|
48
|
+
return `{ ${fields.join(', ')} }`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Plain objects carry structure; driver values (ObjectId, Date, Decimal, Buffer) are values.
|
|
52
|
+
function isStructure(value) {
|
|
53
|
+
if (value === null || typeof value !== 'object') return false;
|
|
54
|
+
let proto;
|
|
55
|
+
try {
|
|
56
|
+
proto = Object.getPrototypeOf(value);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return proto === Object.prototype || proto === null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function safeKeys(value) {
|
|
64
|
+
try {
|
|
65
|
+
return Object.keys(value);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function safeGet(target, key) {
|
|
72
|
+
try {
|
|
73
|
+
return target[key];
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|