298 lines
9.3 KiB
JavaScript
298 lines
9.3 KiB
JavaScript
import { htmlTreeAsString } from './browser.js';
|
|
import { DEBUG_BUILD } from './debug-build.js';
|
|
import { isError, isEvent, isInstanceOf, isElement, isPlainObject, isPrimitive } from './is.js';
|
|
import { logger } from './logger.js';
|
|
import { truncate } from './string.js';
|
|
|
|
/**
|
|
* Replace a method in an object with a wrapped version of itself.
|
|
*
|
|
* @param source An object that contains a method to be wrapped.
|
|
* @param name The name of the method to be wrapped.
|
|
* @param replacementFactory A higher-order function that takes the original version of the given method and returns a
|
|
* wrapped version. Note: The function returned by `replacementFactory` needs to be a non-arrow function, in order to
|
|
* preserve the correct value of `this`, and the original method must be called using `origMethod.call(this, <other
|
|
* args>)` or `origMethod.apply(this, [<other args>])` (rather than being called directly), again to preserve `this`.
|
|
* @returns void
|
|
*/
|
|
function fill(source, name, replacementFactory) {
|
|
if (!(name in source)) {
|
|
return;
|
|
}
|
|
|
|
const original = source[name] ;
|
|
const wrapped = replacementFactory(original) ;
|
|
|
|
// 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"
|
|
if (typeof wrapped === 'function') {
|
|
markFunctionWrapped(wrapped, original);
|
|
}
|
|
|
|
source[name] = wrapped;
|
|
}
|
|
|
|
/**
|
|
* Defines a non-enumerable property on the given object.
|
|
*
|
|
* @param obj The object on which to set the property
|
|
* @param name The name of the property to be set
|
|
* @param value The value to which to set the property
|
|
*/
|
|
function addNonEnumerableProperty(obj, name, value) {
|
|
try {
|
|
Object.defineProperty(obj, name, {
|
|
// enumerable: false, // the default, so we can save on bundle size by not explicitly setting it
|
|
value: value,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
} catch (o_O) {
|
|
DEBUG_BUILD && logger.log(`Failed to add non-enumerable property "${name}" to object`, obj);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remembers the original function on the wrapped function and
|
|
* patches up the prototype.
|
|
*
|
|
* @param wrapped the wrapper function
|
|
* @param original the original function that gets wrapped
|
|
*/
|
|
function markFunctionWrapped(wrapped, original) {
|
|
try {
|
|
const proto = original.prototype || {};
|
|
wrapped.prototype = original.prototype = proto;
|
|
addNonEnumerableProperty(wrapped, '__sentry_original__', original);
|
|
} catch (o_O) {} // eslint-disable-line no-empty
|
|
}
|
|
|
|
/**
|
|
* This extracts the original function if available. See
|
|
* `markFunctionWrapped` for more information.
|
|
*
|
|
* @param func the function to unwrap
|
|
* @returns the unwrapped version of the function if available.
|
|
*/
|
|
function getOriginalFunction(func) {
|
|
return func.__sentry_original__;
|
|
}
|
|
|
|
/**
|
|
* Encodes given object into url-friendly format
|
|
*
|
|
* @param object An object that contains serializable values
|
|
* @returns string Encoded
|
|
*/
|
|
function urlEncode(object) {
|
|
return Object.keys(object)
|
|
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(object[key])}`)
|
|
.join('&');
|
|
}
|
|
|
|
/**
|
|
* Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their
|
|
* non-enumerable properties attached.
|
|
*
|
|
* @param value Initial source that we have to transform in order for it to be usable by the serializer
|
|
* @returns An Event or Error turned into an object - or the value argurment itself, when value is neither an Event nor
|
|
* an Error.
|
|
*/
|
|
function convertToPlainObject(
|
|
value,
|
|
)
|
|
|
|
{
|
|
if (isError(value)) {
|
|
return {
|
|
message: value.message,
|
|
name: value.name,
|
|
stack: value.stack,
|
|
...getOwnProperties(value),
|
|
};
|
|
} else if (isEvent(value)) {
|
|
const newObj
|
|
|
|
= {
|
|
type: value.type,
|
|
target: serializeEventTarget(value.target),
|
|
currentTarget: serializeEventTarget(value.currentTarget),
|
|
...getOwnProperties(value),
|
|
};
|
|
|
|
if (typeof CustomEvent !== 'undefined' && isInstanceOf(value, CustomEvent)) {
|
|
newObj.detail = value.detail;
|
|
}
|
|
|
|
return newObj;
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/** Creates a string representation of the target of an `Event` object */
|
|
function serializeEventTarget(target) {
|
|
try {
|
|
return isElement(target) ? htmlTreeAsString(target) : Object.prototype.toString.call(target);
|
|
} catch (_oO) {
|
|
return '<unknown>';
|
|
}
|
|
}
|
|
|
|
/** Filters out all but an object's own properties */
|
|
function getOwnProperties(obj) {
|
|
if (typeof obj === 'object' && obj !== null) {
|
|
const extractedProps = {};
|
|
for (const property in obj) {
|
|
if (Object.prototype.hasOwnProperty.call(obj, property)) {
|
|
extractedProps[property] = (obj )[property];
|
|
}
|
|
}
|
|
return extractedProps;
|
|
} else {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given any captured exception, extract its keys and create a sorted
|
|
* and truncated list that will be used inside the event message.
|
|
* eg. `Non-error exception captured with keys: foo, bar, baz`
|
|
*/
|
|
function extractExceptionKeysForMessage(exception, maxLength = 40) {
|
|
const keys = Object.keys(convertToPlainObject(exception));
|
|
keys.sort();
|
|
|
|
if (!keys.length) {
|
|
return '[object has no keys]';
|
|
}
|
|
|
|
if (keys[0].length >= maxLength) {
|
|
return truncate(keys[0], maxLength);
|
|
}
|
|
|
|
for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) {
|
|
const serialized = keys.slice(0, includedKeys).join(', ');
|
|
if (serialized.length > maxLength) {
|
|
continue;
|
|
}
|
|
if (includedKeys === keys.length) {
|
|
return serialized;
|
|
}
|
|
return truncate(serialized, maxLength);
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Given any object, return a new object having removed all fields whose value was `undefined`.
|
|
* Works recursively on objects and arrays.
|
|
*
|
|
* Attention: This function keeps circular references in the returned object.
|
|
*/
|
|
function dropUndefinedKeys(inputValue) {
|
|
// This map keeps track of what already visited nodes map to.
|
|
// Our Set - based memoBuilder doesn't work here because we want to the output object to have the same circular
|
|
// references as the input object.
|
|
const memoizationMap = new Map();
|
|
|
|
// This function just proxies `_dropUndefinedKeys` to keep the `memoBuilder` out of this function's API
|
|
return _dropUndefinedKeys(inputValue, memoizationMap);
|
|
}
|
|
|
|
function _dropUndefinedKeys(inputValue, memoizationMap) {
|
|
if (isPojo(inputValue)) {
|
|
// If this node has already been visited due to a circular reference, return the object it was mapped to in the new object
|
|
const memoVal = memoizationMap.get(inputValue);
|
|
if (memoVal !== undefined) {
|
|
return memoVal ;
|
|
}
|
|
|
|
const returnValue = {};
|
|
// Store the mapping of this value in case we visit it again, in case of circular data
|
|
memoizationMap.set(inputValue, returnValue);
|
|
|
|
for (const key of Object.keys(inputValue)) {
|
|
if (typeof inputValue[key] !== 'undefined') {
|
|
returnValue[key] = _dropUndefinedKeys(inputValue[key], memoizationMap);
|
|
}
|
|
}
|
|
|
|
return returnValue ;
|
|
}
|
|
|
|
if (Array.isArray(inputValue)) {
|
|
// If this node has already been visited due to a circular reference, return the array it was mapped to in the new object
|
|
const memoVal = memoizationMap.get(inputValue);
|
|
if (memoVal !== undefined) {
|
|
return memoVal ;
|
|
}
|
|
|
|
const returnValue = [];
|
|
// Store the mapping of this value in case we visit it again, in case of circular data
|
|
memoizationMap.set(inputValue, returnValue);
|
|
|
|
inputValue.forEach((item) => {
|
|
returnValue.push(_dropUndefinedKeys(item, memoizationMap));
|
|
});
|
|
|
|
return returnValue ;
|
|
}
|
|
|
|
return inputValue;
|
|
}
|
|
|
|
function isPojo(input) {
|
|
if (!isPlainObject(input)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const name = (Object.getPrototypeOf(input) ).constructor.name;
|
|
return !name || name === 'Object';
|
|
} catch (e) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure that something is an object.
|
|
*
|
|
* Turns `undefined` and `null` into `String`s and all other primitives into instances of their respective wrapper
|
|
* classes (String, Boolean, Number, etc.). Acts as the identity function on non-primitives.
|
|
*
|
|
* @param wat The subject of the objectification
|
|
* @returns A version of `wat` which can safely be used with `Object` class methods
|
|
*/
|
|
function objectify(wat) {
|
|
let objectified;
|
|
switch (true) {
|
|
case wat === undefined || wat === null:
|
|
objectified = new String(wat);
|
|
break;
|
|
|
|
// Though symbols and bigints do have wrapper classes (`Symbol` and `BigInt`, respectively), for whatever reason
|
|
// those classes don't have constructors which can be used with the `new` keyword. We therefore need to cast each as
|
|
// an object in order to wrap it.
|
|
case typeof wat === 'symbol' || typeof wat === 'bigint':
|
|
objectified = Object(wat);
|
|
break;
|
|
|
|
// this will catch the remaining primitives: `String`, `Number`, and `Boolean`
|
|
case isPrimitive(wat):
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
objectified = new (wat ).constructor(wat);
|
|
break;
|
|
|
|
// by process of elimination, at this point we know that `wat` must already be an object
|
|
default:
|
|
objectified = wat;
|
|
break;
|
|
}
|
|
return objectified;
|
|
}
|
|
|
|
export { addNonEnumerableProperty, convertToPlainObject, dropUndefinedKeys, extractExceptionKeysForMessage, fill, getOriginalFunction, markFunctionWrapped, objectify, urlEncode };
|
|
//# sourceMappingURL=object.js.map
|