diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2fbcf4bbed6b..8fe14cd3ac16 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -31,6 +31,7 @@ export type { Mechanism } from './mechanism'; export type { ExtractedNodeRequestData, Primitive, WorkerLocation } from './misc'; export type { ClientOptions, Options } from './options'; export type { Package } from './package'; +export type { PolymorphicEvent } from './polymorphics'; export type { QueryParams, Request } from './request'; export type { Runtime } from './runtime'; export type { CaptureContext, Scope, ScopeContext } from './scope'; diff --git a/packages/types/src/polymorphics.ts b/packages/types/src/polymorphics.ts new file mode 100644 index 000000000000..0274aedfb2b4 --- /dev/null +++ b/packages/types/src/polymorphics.ts @@ -0,0 +1,11 @@ +/** + * Event-like interface that's usable in browser and node. + * + * Property availability taken from https://developer.mozilla.org/en-US/docs/Web/API/Event#browser_compatibility + */ +export interface PolymorphicEvent { + [key: string]: unknown; + readonly type: string; + readonly target?: unknown; + readonly currentTarget?: unknown; +} diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index 4981359968c9..81ae28349a0a 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { Primitive } from '@sentry/types'; +import { PolymorphicEvent, Primitive } from '@sentry/types'; // eslint-disable-next-line @typescript-eslint/unbound-method const objectToString = Object.prototype.toString; @@ -101,7 +101,7 @@ export function isPlainObject(wat: unknown): wat is Record { * @param wat A value to be checked. * @returns A boolean representing the result. */ -export function isEvent(wat: unknown): boolean { +export function isEvent(wat: unknown): wat is PolymorphicEvent { return typeof Event !== 'undefined' && isInstanceOf(wat, Event); } diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index 51566a2125a7..f0899f47b4c0 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -1,6 +1,6 @@ import { Primitive } from '@sentry/types'; -import { isError, isEvent, isNaN, isSyntheticEvent } from './is'; +import { isNaN, isSyntheticEvent } from './is'; import { memoBuilder, MemoFunc } from './memo'; import { convertToPlainObject } from './object'; import { getFunctionName } from './stacktrace'; @@ -117,7 +117,7 @@ function visit( // Before we begin, convert`Error` and`Event` instances into plain objects, since some of each of their relevant // properties are non-enumerable and otherwise would get missed. - const visitable = (isError(value) || isEvent(value) ? convertToPlainObject(value) : value) as ObjOrArray; + const visitable = convertToPlainObject(value as ObjOrArray); for (const visitKey in visitable) { // Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration. diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index cc401d2fbdfe..d4974cecbf44 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ExtendedError, WrappedFunction } from '@sentry/types'; +import { WrappedFunction } from '@sentry/types'; import { htmlTreeAsString } from './browser'; import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive } from './is'; @@ -92,50 +92,59 @@ export function urlEncode(object: { [key: string]: any }): string { } /** - * Transforms any object into an object literal with all its attributes - * attached to it. + * 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. */ -export function convertToPlainObject(value: unknown): { - [key: string]: unknown; -} { - let newObj = value as { - [key: string]: unknown; - }; - +export function convertToPlainObject( + value: V, +): + | { + [ownProps: string]: unknown; + type: string; + target: string; + currentTarget: string; + detail?: unknown; + } + | { + [ownProps: string]: unknown; + message: string; + name: string; + stack?: string; + } + | V { if (isError(value)) { - newObj = { + return { message: value.message, name: value.name, stack: value.stack, - ...getOwnProperties(value as ExtendedError), + ...getOwnProperties(value), }; } else if (isEvent(value)) { - /** - * Event-like interface that's usable in browser and node - */ - interface SimpleEvent { - [key: string]: unknown; + const newObj: { + [ownProps: string]: unknown; type: string; - target?: unknown; - currentTarget?: unknown; - } - - const event = value as SimpleEvent; - - newObj = { - type: event.type, - target: serializeEventTarget(event.target), - currentTarget: serializeEventTarget(event.currentTarget), - ...getOwnProperties(event), + target: string; + currentTarget: string; + detail?: unknown; + } = { + type: value.type, + target: serializeEventTarget(value.target), + currentTarget: serializeEventTarget(value.currentTarget), + ...getOwnProperties(value), }; if (typeof CustomEvent !== 'undefined' && isInstanceOf(value, CustomEvent)) { - newObj.detail = event.detail; + newObj.detail = value.detail; } + + return newObj; + } else { + return value; } - return newObj; } /** Creates a string representation of the target of an `Event` object */ @@ -148,14 +157,18 @@ function serializeEventTarget(target: unknown): string { } /** Filters out all but an object's own properties */ -function getOwnProperties(obj: { [key: string]: unknown }): { [key: string]: unknown } { - const extractedProps: { [key: string]: unknown } = {}; - for (const property in obj) { - if (Object.prototype.hasOwnProperty.call(obj, property)) { - extractedProps[property] = obj[property]; +function getOwnProperties(obj: unknown): { [key: string]: unknown } { + if (typeof obj === 'object' && obj !== null) { + const extractedProps: { [key: string]: unknown } = {}; + for (const property in obj) { + if (Object.prototype.hasOwnProperty.call(obj, property)) { + extractedProps[property] = (obj as Record)[property]; + } } + return extractedProps; + } else { + return {}; } - return extractedProps; } /** @@ -163,8 +176,7 @@ function getOwnProperties(obj: { [key: string]: unknown }): { [key: string]: unk * and truncated list that will be used inside the event message. * eg. `Non-error exception captured with keys: foo, bar, baz` */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function extractExceptionKeysForMessage(exception: any, maxLength: number = 40): string { +export function extractExceptionKeysForMessage(exception: Record, maxLength: number = 40): string { const keys = Object.keys(convertToPlainObject(exception)); keys.sort();