Skip to content

Commit

Permalink
ref(utils): Clean up dangerous type casts in object helper file (#5047)
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst authored May 9, 2022
1 parent 8b16378 commit 8d475b1
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 42 deletions.
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
11 changes: 11 additions & 0 deletions packages/types/src/polymorphics.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 2 additions & 2 deletions packages/utils/src/is.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -101,7 +101,7 @@ export function isPlainObject(wat: unknown): wat is Record<string, unknown> {
* @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);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/normalize.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<unknown>;
const visitable = convertToPlainObject(value as ObjOrArray<unknown>);

for (const visitKey in visitable) {
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
Expand Down
88 changes: 50 additions & 38 deletions packages/utils/src/object.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<V extends unknown>(
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 */
Expand All @@ -148,23 +157,26 @@ 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<string, unknown>)[property];
}
}
return extractedProps;
} else {
return {};
}
return extractedProps;
}

/**
* 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`
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function extractExceptionKeysForMessage(exception: any, maxLength: number = 40): string {
export function extractExceptionKeysForMessage(exception: Record<string, unknown>, maxLength: number = 40): string {
const keys = Object.keys(convertToPlainObject(exception));
keys.sort();

Expand Down

0 comments on commit 8d475b1

Please sign in to comment.