UNPKG

posthog-node

Version:
250 lines (210 loc) 7.34 kB
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry // Licensed under the MIT License import { isError, isErrorEvent, isEvent, isPlainObject } from './type-checking' import { ErrorProperties, EventHint, Exception, Mechanism, StackFrame, StackParser } from './types' import { addSourceContext } from './context-lines' export async function propertiesFromUnknownInput( stackParser: StackParser, input: unknown, hint?: EventHint ): Promise<ErrorProperties> { const providedMechanism = hint && hint.mechanism const mechanism = providedMechanism || { handled: true, type: 'generic', } const error = getError(mechanism, input, hint) const exception = await exceptionFromError(stackParser, error) exception.value = exception.value || '' exception.type = exception.type || 'Error' exception.mechanism = mechanism const properties = { $exception_list: [exception] } return properties } function getError(mechanism: Mechanism, exception: unknown, hint?: EventHint): Error { if (isError(exception)) { return exception } mechanism.synthetic = true if (isPlainObject(exception)) { const errorFromProp = getErrorPropertyFromObject(exception) if (errorFromProp) { return errorFromProp } const message = getMessageForObject(exception) const ex = hint?.syntheticException || new Error(message) ex.message = message return ex } // This handles when someone does: `throw "something awesome";` // We use synthesized Error here so we can extract a (rough) stack trace. const ex = hint?.syntheticException || new Error(exception as string) ex.message = `${exception}` return ex } /** If a plain object has a property that is an `Error`, return this error. */ function getErrorPropertyFromObject(obj: Record<string, unknown>): Error | undefined { for (const prop in obj) { if (Object.prototype.hasOwnProperty.call(obj, prop)) { const value = obj[prop] if (value instanceof Error) { return value } } } return undefined } function getMessageForObject(exception: Record<string, unknown>): string { if ('name' in exception && typeof exception.name === 'string') { let message = `'${exception.name}' captured as exception` if ('message' in exception && typeof exception.message === 'string') { message += ` with message '${exception.message}'` } return message } else if ('message' in exception && typeof exception.message === 'string') { return exception.message } const keys = extractExceptionKeysForMessage(exception) // Some ErrorEvent instances do not have an `error` property, which is why they are not handled before // We still want to try to get a decent message for these cases if (isErrorEvent(exception)) { return `Event \`ErrorEvent\` captured as exception with message \`${exception.message}\`` } const className = getObjectClassName(exception) return `${className && className !== 'Object' ? `'${className}'` : 'Object'} captured as exception with keys: ${keys}` } function getObjectClassName(obj: unknown): string | undefined | void { try { const prototype: unknown | null = Object.getPrototypeOf(obj) return prototype ? prototype.constructor.name : undefined } catch (e) { // ignore errors here } } /** * 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: Record<string, unknown>, maxLength: number = 40): string { 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 '' } function truncate(str: string, max: number = 0): string { if (typeof str !== 'string' || max === 0) { return str } return str.length <= max ? str : `${str.slice(0, max)}...` } /** * 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<V>(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)) { return { message: value.message, name: value.name, stack: value.stack, ...getOwnProperties(value), } } else if (isEvent(value)) { const newObj: { [ownProps: string]: unknown type: string target: string currentTarget: string detail?: unknown } = { type: value.type, target: serializeEventTarget(value.target), currentTarget: serializeEventTarget(value.currentTarget), ...getOwnProperties(value), } // TODO: figure out why this fails typing (I think CustomEvent is only supported in Node 19 onwards) // if (typeof CustomEvent !== 'undefined' && isInstanceOf(value, CustomEvent)) { // newObj.detail = (value as unknown as CustomEvent).detail // } return newObj } else { return value } } /** Filters out all but an object's own properties */ 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 {} } } /** Creates a string representation of the target of an `Event` object */ function serializeEventTarget(target: unknown): string { try { return Object.prototype.toString.call(target) } catch (_oO) { return '<unknown>' } } /** * Extracts stack frames from the error and builds an Exception */ async function exceptionFromError(stackParser: StackParser, error: Error): Promise<Exception> { const exception: Exception = { type: error.name || error.constructor.name, value: error.message, } const frames = await addSourceContext(parseStackFrames(stackParser, error)) if (frames.length) { exception.stacktrace = { frames, type: 'raw' } } return exception } /** * Extracts stack frames from the error.stack string */ function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] { return stackParser(error.stack || '', 1) }