@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
299 lines (258 loc) • 9.52 kB
JavaScript
import { htmlTreeAsString } from './browser.js';
import { DEBUG_BUILD } from './debug-build.js';
import { isError, isEvent, isInstanceOf, isPrimitive, isElement } 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.
*
* If the method on the passed object is not a function, the wrapper will not be applied.
*
* @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;
}
// explicitly casting to unknown because we don't know the type of the method initially at all
const original = source[name] ;
if (typeof original !== 'function') {
return;
}
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);
}
try {
source[name] = wrapped;
} catch {
DEBUG_BUILD && logger.log(`Failed to replace method "${name}" in object`, source);
}
}
/**
* 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.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
function getOriginalFunction(func) {
return func.__sentry_original__;
}
/**
* 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 argument 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();
const firstKey = keys[0];
if (!firstKey) {
return '[object has no keys]';
}
if (firstKey.length >= maxLength) {
return truncate(firstKey, 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.
*
* @deprecated This function is no longer used by the SDK and will be removed in a future major version.
*/
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) {
// Early return for primitive values
if (inputValue === null || typeof inputValue !== 'object') {
return inputValue;
}
// Check memo map first for all object types
const memoVal = memoizationMap.get(inputValue);
if (memoVal !== undefined) {
return memoVal ;
}
// handle arrays
if (Array.isArray(inputValue)) {
const returnValue = [];
// Store mapping to handle circular references
memoizationMap.set(inputValue, returnValue);
inputValue.forEach(value => {
returnValue.push(_dropUndefinedKeys(value, memoizationMap));
});
return returnValue ;
}
if (isPojo(inputValue)) {
const returnValue = {};
// Store mapping to handle circular references
memoizationMap.set(inputValue, returnValue);
const keys = Object.keys(inputValue);
keys.forEach(key => {
const val = inputValue[key];
if (val !== undefined) {
returnValue[key] = _dropUndefinedKeys(val, memoizationMap);
}
});
return returnValue ;
}
// For other object types, return as is
return inputValue;
}
function isPojo(input) {
// Plain objects have Object as constructor or no constructor
const constructor = (input ).constructor;
return constructor === Object || constructor === undefined;
}
/**
* 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) {
// this will catch both undefined and null
case wat == undefined:
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 };
//# sourceMappingURL=object.js.map