From 8f58e1d8c036b8255dd64a1240fa15ce999abac6 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 24 Nov 2023 20:15:19 +0100 Subject: [PATCH 01/10] update changelog --- CHANGELOG.md | 6 ++ src/js/integrations/debugsymbolicator.ts | 112 +++++++++++++---------- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b635eeac6..0037e7e0c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Symbolicate message and non-Error stacktraces locally in debug mode ([#3420](https://github.com/getsentry/sentry-react-native/pull/3420)) + ## 5.14.1 ### Fixes diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 4d1ee69f25..1c8fc1bd2a 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -44,28 +44,37 @@ export class DebugSymbolicator implements Integration { * @inheritDoc */ public setupOnce(): void { - addGlobalEventProcessor(async (event: Event, hint?: EventHint) => { + addGlobalEventProcessor(async (event: Event, hint: EventHint) => { const self = getCurrentHub().getIntegration(DebugSymbolicator); - if (!self || hint === undefined || hint.originalException === undefined) { + if (!self) { return event; } - const reactError = hint.originalException as ReactNativeError; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); - - let stack; - try { - stack = parseErrorStack(reactError); - } catch (e) { - // In RN 0.64 `parseErrorStack` now only takes a string - stack = parseErrorStack(reactError.stack); + if (event.exception + && hint.originalException + && typeof hint.originalException === 'object' + && 'stack' in hint.originalException + && typeof hint.originalException.stack === 'string') { + // originalException is ErrorLike object + const symbolicatedFrames = await this._symbolicate(hint.originalException.stack); + symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames); + } else if (hint.syntheticException + && typeof hint.syntheticException === 'object' + && 'stack' in hint.syntheticException + && typeof hint.syntheticException.stack === 'string') { + // syntheticException is Error object + const symbolicatedFrames = await this._symbolicate(hint.syntheticException.stack); + + if (event.exception) { + symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames); + } else if (event.threads) { + // RN JS doesn't have threads + // syntheticException is used for Sentry.captureMessage() threads + symbolicatedFrames && this._replaceThreadFramesInEvent(event, symbolicatedFrames); + } } - await self._symbolicate(event, stack); - return event; }); } @@ -74,36 +83,38 @@ export class DebugSymbolicator implements Integration { * Symbolicates the stack on the device talking to local dev server. * Mutates the passed event. */ - private async _symbolicate(event: Event, stack: string | undefined): Promise { + private async _symbolicate(rawStack: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); + const parsedStack = parseErrorStack(rawStack); + try { // eslint-disable-next-line @typescript-eslint/no-var-requires const symbolicateStackTrace = require('react-native/Libraries/Core/Devtools/symbolicateStackTrace'); - const prettyStack = await symbolicateStackTrace(stack); + const prettyStack = await symbolicateStackTrace(parsedStack); - if (prettyStack) { - let newStack = prettyStack; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (prettyStack.stack) { - // This has been changed in an react-native version so stack is contained in here - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - newStack = prettyStack.stack; - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const stackWithoutInternalCallsites = newStack.filter( - (frame: { file?: string }) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, - ); - - const symbolicatedFrames = await this._convertReactNativeFramesToSentryFrames(stackWithoutInternalCallsites); - this._replaceFramesInEvent(event, symbolicatedFrames); - } else { - logger.error('The stack is null'); + if (!prettyStack) { + logger.error('React Native DevServer could not symbolicate the stack trace.'); + return null; } + + // This has been changed in an react-native version so stack is contained in here + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const newStack = prettyStack.stack || prettyStack; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const stackWithoutInternalCallsites = newStack.filter( + (frame: { file?: string }) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, + ); + + return await this._convertReactNativeFramesToSentryFrames(stackWithoutInternalCallsites); } catch (error) { if (error instanceof Error) { logger.warn(`Unable to symbolicate stack trace: ${error.message}`); } + return null; } } @@ -135,17 +146,6 @@ export class DebugSymbolicator implements Integration { in_app: inApp, }; - // The upstream `react-native@0.61` delegates parsing of stacks to `stacktrace-parser`, which is buggy and - // leaves a trailing `(address at` in the function name. - // `react-native@0.62` seems to have custom logic to parse hermes frames specially. - // Anyway, all we do here is throw away the bogus suffix. - if (newFrame.function) { - const addressAtPos = newFrame.function.indexOf('(address at'); - if (addressAtPos >= 0) { - newFrame.function = newFrame.function.substring(0, addressAtPos).trim(); - } - } - if (inApp) { await this._addSourceContext(newFrame, getDevServer); } @@ -160,7 +160,7 @@ export class DebugSymbolicator implements Integration { * @param event Event * @param frames StackFrame[] */ - private _replaceFramesInEvent(event: Event, frames: StackFrame[]): void { + private _replaceExceptionFramesInEvent(event: Event, frames: StackFrame[]): void { if ( event.exception && event.exception.values && @@ -171,6 +171,22 @@ export class DebugSymbolicator implements Integration { } } + /** + * Replaces the frames in the thread of a message. + * @param event Event + * @param frames StackFrame[] + */ + private _replaceThreadFramesInEvent(event: Event, frames: StackFrame[]): void { + if ( + event.threads && + event.threads.values && + event.threads.values[0] && + event.threads.values[0].stacktrace + ) { + event.threads.values[0].stacktrace.frames = frames.reverse(); + } + } + /** * This tries to add source context for in_app Frames * From 0b4490d3ffec5c50b11ee2ece56917441762f5c3 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 27 Nov 2023 09:58:46 +0100 Subject: [PATCH 02/10] fix lint --- src/js/integrations/debugsymbolicator.ts | 29 ++++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 1c8fc1bd2a..3849c59450 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -51,18 +51,22 @@ export class DebugSymbolicator implements Integration { return event; } - if (event.exception - && hint.originalException - && typeof hint.originalException === 'object' - && 'stack' in hint.originalException - && typeof hint.originalException.stack === 'string') { + if ( + event.exception && + hint.originalException && + typeof hint.originalException === 'object' && + 'stack' in hint.originalException && + typeof hint.originalException.stack === 'string' + ) { // originalException is ErrorLike object const symbolicatedFrames = await this._symbolicate(hint.originalException.stack); symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames); - } else if (hint.syntheticException - && typeof hint.syntheticException === 'object' - && 'stack' in hint.syntheticException - && typeof hint.syntheticException.stack === 'string') { + } else if ( + hint.syntheticException && + typeof hint.syntheticException === 'object' && + 'stack' in hint.syntheticException && + typeof hint.syntheticException.stack === 'string' + ) { // syntheticException is Error object const symbolicatedFrames = await this._symbolicate(hint.syntheticException.stack); @@ -177,12 +181,7 @@ export class DebugSymbolicator implements Integration { * @param frames StackFrame[] */ private _replaceThreadFramesInEvent(event: Event, frames: StackFrame[]): void { - if ( - event.threads && - event.threads.values && - event.threads.values[0] && - event.threads.values[0].stacktrace - ) { + if (event.threads && event.threads.values && event.threads.values[0] && event.threads.values[0].stacktrace) { event.threads.values[0].stacktrace.frames = frames.reverse(); } } From 8079b139506aaa54260bf4f501f5d2400ae55a6b Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 27 Nov 2023 12:34:17 +0100 Subject: [PATCH 03/10] fix(promise-tracking): Remove Sentry frames from synthetic errors --- CHANGELOG.md | 1 + src/js/integrations/debugsymbolicator.ts | 23 +++++++++++++--- .../integrations/reactnativeerrorhandlers.ts | 2 ++ src/js/utils/error.ts | 26 +++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 src/js/utils/error.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0037e7e0c3..8e11ff8c8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Symbolicate message and non-Error stacktraces locally in debug mode ([#3420](https://github.com/getsentry/sentry-react-native/pull/3420)) +- Remove Sentry SDK frames from rejected promise SyntheticError stack ([#3423](https://github.com/getsentry/sentry-react-native/pull/3423)) ## 5.14.1 diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 3849c59450..ffb81389e0 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -2,6 +2,8 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import type { Event, EventHint, Integration, StackFrame } from '@sentry/types'; import { addContextToFrame, logger } from '@sentry/utils'; +import { getFramesToPop } from '../utils/error'; + const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|')); interface GetDevServer { @@ -59,7 +61,10 @@ export class DebugSymbolicator implements Integration { typeof hint.originalException.stack === 'string' ) { // originalException is ErrorLike object - const symbolicatedFrames = await this._symbolicate(hint.originalException.stack); + const symbolicatedFrames = await this._symbolicate( + hint.originalException.stack, + getFramesToPop(hint.originalException as Error), + ); symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames); } else if ( hint.syntheticException && @@ -68,7 +73,10 @@ export class DebugSymbolicator implements Integration { typeof hint.syntheticException.stack === 'string' ) { // syntheticException is Error object - const symbolicatedFrames = await this._symbolicate(hint.syntheticException.stack); + const symbolicatedFrames = await this._symbolicate( + hint.syntheticException.stack, + getFramesToPop(hint.syntheticException), + ); if (event.exception) { symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames); @@ -87,7 +95,7 @@ export class DebugSymbolicator implements Integration { * Symbolicates the stack on the device talking to local dev server. * Mutates the passed event. */ - private async _symbolicate(rawStack: string): Promise { + private async _symbolicate(rawStack: string, skipFirstFrames: number = 0): Promise { // eslint-disable-next-line @typescript-eslint/no-var-requires const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); const parsedStack = parseErrorStack(rawStack); @@ -106,8 +114,15 @@ export class DebugSymbolicator implements Integration { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const newStack = prettyStack.stack || prettyStack; + // https://github.com/getsentry/sentry-javascript/blob/739d904342aaf9327312f409952f14ceff4ae1ab/packages/utils/src/stacktrace.ts#L23 + // Match SentryParser which counts lines of stack (-1 for first line with the Error message) + const skipFirstAdjustedToSentryStackParser = Math.max(skipFirstFrames - 1, 0); + const stackWithoutPoppedFrames = skipFirstAdjustedToSentryStackParser + ? (newStack as Array).slice(skipFirstAdjustedToSentryStackParser) + : newStack; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const stackWithoutInternalCallsites = newStack.filter( + const stackWithoutInternalCallsites = stackWithoutPoppedFrames.filter( (frame: { file?: string }) => // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, diff --git a/src/js/integrations/reactnativeerrorhandlers.ts b/src/js/integrations/reactnativeerrorhandlers.ts index 9a6a493def..ebf22521a5 100644 --- a/src/js/integrations/reactnativeerrorhandlers.ts +++ b/src/js/integrations/reactnativeerrorhandlers.ts @@ -3,6 +3,7 @@ import type { EventHint, Integration, SeverityLevel } from '@sentry/types'; import { addExceptionMechanism, logger } from '@sentry/utils'; import type { ReactNativeClient } from '../client'; +import { createSyntheticError } from '../utils/error'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; /** ReactNativeErrorHandlers Options */ @@ -131,6 +132,7 @@ export class ReactNativeErrorHandlers implements Integration { getCurrentHub().captureException(error, { data: { id }, originalException: error, + syntheticException: createSyntheticError(), }); }, onHandled: (id: string) => { diff --git a/src/js/utils/error.ts b/src/js/utils/error.ts new file mode 100644 index 0000000000..0888cbe976 --- /dev/null +++ b/src/js/utils/error.ts @@ -0,0 +1,26 @@ +export interface ExtendedError extends Error { + framesToPop?: number | undefined; +} + +// Sentry Stack Parser is skipping lines not frames +// https://github.com/getsentry/sentry-javascript/blob/739d904342aaf9327312f409952f14ceff4ae1ab/packages/utils/src/stacktrace.ts#L23 +// 1 for first line with the Error message +const SENTRY_STACK_PARSER_OFFSET = 1; +const REMOVE_ERROR_CREATION_FRAMES = 2 + SENTRY_STACK_PARSER_OFFSET; + +/** + * Creates synthetic trace. By default pops 2 frames - `createSyntheticError` and the caller + */ +export function createSyntheticError(framesToPop: number = 0): ExtendedError { + const error: ExtendedError = new Error(); + error.framesToPop = framesToPop + REMOVE_ERROR_CREATION_FRAMES; // Skip createSyntheticError's own stack frame. + return error; +} + +/** + * Returns the number of frames to pop from the stack trace. + * @param error ExtendedError + */ +export function getFramesToPop(error: ExtendedError): number { + return error.framesToPop !== undefined ? error.framesToPop : 0; +} From 9d7c3bac9051f03ce791301b9137cdfe74c6cb7d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 27 Nov 2023 16:20:39 +0100 Subject: [PATCH 04/10] More refactor and add tests --- src/js/integrations/debugsymbolicator.ts | 140 +++++----- src/js/vendor/index.ts | 1 + src/js/vendor/react-native/index.ts | 55 ++++ test/integrations/debugsymbolicator.test.ts | 268 ++++++++++++++++++++ 4 files changed, 405 insertions(+), 59 deletions(-) create mode 100644 src/js/vendor/react-native/index.ts create mode 100644 test/integrations/debugsymbolicator.test.ts diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 3849c59450..ef29bd9a3d 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -1,23 +1,9 @@ -import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import type { Event, EventHint, Integration, StackFrame } from '@sentry/types'; +import type { Event, EventHint, EventProcessor, Hub, Integration, StackFrame as SentryStackFrame } from '@sentry/types'; import { addContextToFrame, logger } from '@sentry/utils'; -const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|')); - -interface GetDevServer { - (): { url: string }; -} +import type * as ReactNative from '../vendor/react-native'; -/** - * React Native Stack Frame - */ -interface ReactNativeFrame { - // arguments: [] - column: number; - file: string; - lineNumber: number; - methodName: string; -} +const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|')); /** * React Native Error @@ -43,7 +29,7 @@ export class DebugSymbolicator implements Integration { /** * @inheritDoc */ - public setupOnce(): void { + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor(async (event: Event, hint: EventHint) => { const self = getCurrentHub().getIntegration(DebugSymbolicator); @@ -87,30 +73,21 @@ export class DebugSymbolicator implements Integration { * Symbolicates the stack on the device talking to local dev server. * Mutates the passed event. */ - private async _symbolicate(rawStack: string): Promise { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); - const parsedStack = parseErrorStack(rawStack); + private async _symbolicate(rawStack: string): Promise { + const parsedStack = this._parseErrorStack(rawStack); try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const symbolicateStackTrace = require('react-native/Libraries/Core/Devtools/symbolicateStackTrace'); - const prettyStack = await symbolicateStackTrace(parsedStack); - + const prettyStack = await this._symbolicateStackTrace(parsedStack); if (!prettyStack) { logger.error('React Native DevServer could not symbolicate the stack trace.'); return null; } // This has been changed in an react-native version so stack is contained in here - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const newStack = prettyStack.stack || prettyStack; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const stackWithoutInternalCallsites = newStack.filter( - (frame: { file?: string }) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, + (frame: { file?: string }) => frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, ); return await this._convertReactNativeFramesToSentryFrames(stackWithoutInternalCallsites); @@ -126,15 +103,9 @@ export class DebugSymbolicator implements Integration { * Converts ReactNativeFrames to frames in the Sentry format * @param frames ReactNativeFrame[] */ - private async _convertReactNativeFramesToSentryFrames(frames: ReactNativeFrame[]): Promise { - let getDevServer: GetDevServer; - try { - getDevServer = require('react-native/Libraries/Core/Devtools/getDevServer'); - } catch (_oO) { - // We can't load devserver URL - } + private async _convertReactNativeFramesToSentryFrames(frames: ReactNative.StackFrame[]): Promise { return Promise.all( - frames.map(async (frame: ReactNativeFrame): Promise => { + frames.map(async (frame: ReactNative.StackFrame): Promise => { let inApp = !!frame.column && !!frame.lineNumber; inApp = inApp && @@ -142,7 +113,7 @@ export class DebugSymbolicator implements Integration { !frame.file.includes('node_modules') && !frame.file.includes('native code'); - const newFrame: StackFrame = { + const newFrame: SentryStackFrame = { lineno: frame.lineNumber, colno: frame.column, filename: frame.file, @@ -151,7 +122,7 @@ export class DebugSymbolicator implements Integration { }; if (inApp) { - await this._addSourceContext(newFrame, getDevServer); + await this._addSourceContext(newFrame); } return newFrame; @@ -164,7 +135,7 @@ export class DebugSymbolicator implements Integration { * @param event Event * @param frames StackFrame[] */ - private _replaceExceptionFramesInEvent(event: Event, frames: StackFrame[]): void { + private _replaceExceptionFramesInEvent(event: Event, frames: SentryStackFrame[]): void { if ( event.exception && event.exception.values && @@ -180,7 +151,7 @@ export class DebugSymbolicator implements Integration { * @param event Event * @param frames StackFrame[] */ - private _replaceThreadFramesInEvent(event: Event, frames: StackFrame[]): void { + private _replaceThreadFramesInEvent(event: Event, frames: SentryStackFrame[]): void { if (event.threads && event.threads.values && event.threads.values[0] && event.threads.values[0].stacktrace) { event.threads.values[0].stacktrace.frames = frames.reverse(); } @@ -192,30 +163,81 @@ export class DebugSymbolicator implements Integration { * @param frame StackFrame * @param getDevServer function from RN to get DevServer URL */ - private async _addSourceContext(frame: StackFrame, getDevServer?: GetDevServer): Promise { - let response; + private async _addSourceContext(frame: SentryStackFrame): Promise { + let sourceContext: string | null = null; const segments = frame.filename?.split('/') ?? []; - if (getDevServer) { - for (const idx in segments) { - if (Object.prototype.hasOwnProperty.call(segments, idx)) { - response = await fetch(`${getDevServer().url}${segments.slice(-idx).join('/')}`, { - method: 'GET', - }); + const serverUrl = this._getDevServer()?.url; + if (!serverUrl) { + return; + } - if (response.ok) { - break; - } - } + for (const idx in segments) { + if (!Object.prototype.hasOwnProperty.call(segments, idx)) { + continue; } + + sourceContext = await this._fetchSourceContext(serverUrl, segments, -idx); + if (sourceContext) { + break; + } + } + + if (!sourceContext) { + return; + } + + const lines = sourceContext.split('\n'); + addContextToFrame(lines, frame); + } + + /** + * Get source context for segment + */ + private async _fetchSourceContext(url: string, segments: Array, start: number): Promise { + const response = await fetch(`${url}${segments.slice(start).join('/')}`, { + method: 'GET', + }); + + if (response.ok) { + return response.text(); } + return null; + } - if (response && response.ok) { - const content = await response.text(); - const lines = content.split('\n'); + /** + * Loads and calls RN Core Devtools parseErrorStack function. + */ + private _parseErrorStack(errorStack: string): Array { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); + return parseErrorStack(errorStack); + } - addContextToFrame(lines, frame); + /** + * Loads and calls RN Core Devtools symbolicateStackTrace function. + */ + private _symbolicateStackTrace( + stack: Array, + extraData?: Record, + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const symbolicateStackTrace = require('react-native/Libraries/Core/Devtools/symbolicateStackTrace'); + return symbolicateStackTrace(stack, extraData); + } + + /** + * Loads and returns the RN DevServer URL. + */ + private _getDevServer(): ReactNative.DevServerInfo | undefined { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const getDevServer = require('react-native/Libraries/Core/Devtools/getDevServer'); + return getDevServer(); + } catch (_oO) { + // We can't load devserver URL } + return undefined; } } diff --git a/src/js/vendor/index.ts b/src/js/vendor/index.ts index 80d5ec4054..c26529aed0 100644 --- a/src/js/vendor/index.ts +++ b/src/js/vendor/index.ts @@ -1 +1,2 @@ export { utf8ToBytes } from './buffer'; +export * from './react-native'; diff --git a/src/js/vendor/react-native/index.ts b/src/js/vendor/react-native/index.ts new file mode 100644 index 0000000000..d3affa091c --- /dev/null +++ b/src/js/vendor/react-native/index.ts @@ -0,0 +1,55 @@ +// MIT License + +// Copyright (c) Meta Platforms, Inc. and affiliates. + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Adapted from https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/Libraries/Core/NativeExceptionsManager.js#L17 +export type StackFrame = { + column?: number; + file?: string; + lineNumber?: number; + methodName: string; + collapse?: boolean; +}; + +// Adapted from https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js#L17 +export type CodeFrame = Readonly<{ + content: string; + location?: { + [key: string]: unknown; + row: number; + column: number; + }; + fileName: string; +}>; + +// Adapted from https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js#L27 +export type SymbolicatedStackTrace = Readonly<{ + stack: Array; + codeFrame?: CodeFrame; +}>; + +// Adapted from https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/Libraries/Core/Devtools/getDevServer.js#L17 +export type DevServerInfo = { + [key: string]: unknown; + url: string; + fullBundleUrl?: string; + bundleLoadedFromServer: boolean; +}; diff --git a/test/integrations/debugsymbolicator.test.ts b/test/integrations/debugsymbolicator.test.ts new file mode 100644 index 0000000000..34cebc688d --- /dev/null +++ b/test/integrations/debugsymbolicator.test.ts @@ -0,0 +1,268 @@ +import type { Event, EventHint, Hub, Integration, StackFrame } from '@sentry/types'; + +import { DebugSymbolicator } from '../../src/js/integrations/debugsymbolicator'; +import type * as ReactNative from '../../src/js/vendor/react-native'; + +interface MockDebugSymbolicator extends Integration { + _parseErrorStack: jest.Mock, [string]>; + _symbolicateStackTrace: jest.Mock< + Promise, + [Array, Record | undefined] + >; + _getDevServer: jest.Mock; + _fetchSourceContext: jest.Mock, [string, Array, number]>; +} + +describe('Debug Symbolicator Integration', () => { + let integration: MockDebugSymbolicator; + const mockGetCurrentHub = () => + ({ + getIntegration: () => integration, + } as unknown as Hub); + + beforeEach(() => { + integration = new DebugSymbolicator() as unknown as MockDebugSymbolicator; + integration._parseErrorStack = jest.fn().mockReturnValue([]); + integration._symbolicateStackTrace = jest.fn().mockReturnValue( + Promise.resolve({ + stack: [], + }), + ); + integration._getDevServer = jest.fn().mockReturnValue({ + url: 'http://localhost:8081', + }); + integration._fetchSourceContext = jest.fn().mockReturnValue(Promise.resolve(null)); + }); + + describe('parse stack', () => { + const mockRawStack = `Error: This is mocked error stack trace + at foo (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:1:1) + at bar (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2:2) + at baz (native) +`; + + const mockSentryParsedFrames: Array = [ + { + function: '[native] baz', + }, + { + function: 'bar', + filename: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2:2', + lineno: 2, + colno: 2, + }, + { + function: 'foo', + filename: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:1:1', + lineno: 1, + colno: 1, + }, + ]; + + beforeEach(() => { + integration._parseErrorStack = jest.fn().mockReturnValue(>[ + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: 1, + column: 1, + methodName: 'foo', + }, + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: 2, + column: 2, + methodName: 'bar', + }, + ]); + + integration._symbolicateStackTrace = jest.fn().mockReturnValue( + Promise.resolve({ + stack: [ + { + file: '/User/project/foo.js', + lineNumber: 1, + column: 1, + methodName: 'foo', + }, + { + file: '/User/project/node_modules/bar/bar.js', + lineNumber: 2, + column: 2, + methodName: 'bar', + }, + ], + }), + ); + }); + + it('should symbolicate errors stack trace', async () => { + const symbolicatedEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: mockSentryParsedFrames, + }, + }, + ], + }, + }, + { + originalException: { + stack: mockRawStack, + }, + }, + ); + + expect(symbolicatedEvent).toStrictEqual({ + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: [ + { + function: 'bar', + filename: '/User/project/node_modules/bar/bar.js', + lineno: 2, + colno: 2, + in_app: false, + }, + { + function: 'foo', + filename: '/User/project/foo.js', + lineno: 1, + colno: 1, + in_app: true, + }, + ], + }, + }, + ], + }, + }); + }); + + it('should symbolicate synthetic error stack trace for exception', async () => { + const symbolicatedEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: [], + }, + }, + ], + }, + }, + { + originalException: 'Error: test', + syntheticException: { + stack: mockRawStack, + } as unknown as Error, + }, + ); + + expect(symbolicatedEvent).toStrictEqual({ + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: [ + { + function: 'bar', + filename: '/User/project/node_modules/bar/bar.js', + lineno: 2, + colno: 2, + in_app: false, + }, + { + function: 'foo', + filename: '/User/project/foo.js', + lineno: 1, + colno: 1, + in_app: true, + }, + ], + }, + }, + ], + }, + }); + }); + + it('should symbolicate synthetic error stack trace for message', async () => { + const symbolicatedEvent = await executeIntegrationFor( + { + threads: { + values: [ + { + stacktrace: { + frames: mockSentryParsedFrames, + }, + }, + ], + }, + }, + { + syntheticException: { + stack: mockRawStack, + } as unknown as Error, + }, + ); + + expect(symbolicatedEvent).toStrictEqual({ + threads: { + values: [ + { + stacktrace: { + frames: [ + { + function: 'bar', + filename: '/User/project/node_modules/bar/bar.js', + lineno: 2, + colno: 2, + in_app: false, + }, + { + function: 'foo', + filename: '/User/project/foo.js', + lineno: 1, + colno: 1, + in_app: true, + }, + ], + }, + }, + ], + }, + }); + }); + }); + + function executeIntegrationFor(mockedEvent: Event, hint: EventHint): Promise { + return new Promise((resolve, reject) => { + if (!integration) { + throw new Error('Setup integration before executing the test.'); + } + + integration.setupOnce(async eventProcessor => { + try { + const processedEvent = await eventProcessor(mockedEvent, hint); + resolve(processedEvent); + } catch (e) { + reject(e); + } + }, mockGetCurrentHub); + }); + } +}); From e2a4735f7d011f52beff2104c278eb4b10482aa9 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 27 Nov 2023 16:44:04 +0100 Subject: [PATCH 05/10] Add skip frames tests --- test/integrations/debugsymbolicator.test.ts | 92 +++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/test/integrations/debugsymbolicator.test.ts b/test/integrations/debugsymbolicator.test.ts index 34cebc688d..ab1465d8a0 100644 --- a/test/integrations/debugsymbolicator.test.ts +++ b/test/integrations/debugsymbolicator.test.ts @@ -247,6 +247,98 @@ describe('Debug Symbolicator Integration', () => { }, }); }); + + it('skips first frame (callee) for exception', async () => { + const symbolicatedEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: mockSentryParsedFrames, + }, + }, + ], + }, + }, + { + originalException: { + stack: mockRawStack, + framesToPop: 2, + // The current behavior matches https://github.com/getsentry/sentry-javascript/blob/739d904342aaf9327312f409952f14ceff4ae1ab/packages/utils/src/stacktrace.ts#L23 + // 2 for first line with the Error message + }, + }, + ); + + expect(symbolicatedEvent).toStrictEqual({ + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: [ + { + function: 'bar', + filename: '/User/project/node_modules/bar/bar.js', + lineno: 2, + colno: 2, + in_app: false, + }, + ], + }, + }, + ], + }, + }); + }); + + it('skips first frame (callee) for message', async () => { + const symbolicatedEvent = await executeIntegrationFor( + { + threads: { + values: [ + { + stacktrace: { + frames: mockSentryParsedFrames, + }, + }, + ], + }, + }, + { + syntheticException: { + stack: mockRawStack, + framesToPop: 2, + // The current behavior matches https://github.com/getsentry/sentry-javascript/blob/739d904342aaf9327312f409952f14ceff4ae1ab/packages/utils/src/stacktrace.ts#L23 + // 2 for first line with the Error message + } as unknown as Error, + }, + ); + + expect(symbolicatedEvent).toStrictEqual({ + threads: { + values: [ + { + stacktrace: { + frames: [ + { + function: 'bar', + filename: '/User/project/node_modules/bar/bar.js', + lineno: 2, + colno: 2, + in_app: false, + }, + ], + }, + }, + ], + }, + }); + }); }); function executeIntegrationFor(mockedEvent: Event, hint: EventHint): Promise { From c33bd32c85d2ef6ef8115a41db2b4a3f7c0d7c82 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 27 Nov 2023 18:12:54 +0100 Subject: [PATCH 06/10] Add test --- .../integrations/reactnativeerrorhandlers.ts | 17 ++++-- .../reactnativeerrorhandlers.test.ts | 52 ++++++++++++++++++- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/js/integrations/reactnativeerrorhandlers.ts b/src/js/integrations/reactnativeerrorhandlers.ts index ebf22521a5..f465f34628 100644 --- a/src/js/integrations/reactnativeerrorhandlers.ts +++ b/src/js/integrations/reactnativeerrorhandlers.ts @@ -101,11 +101,7 @@ export class ReactNativeErrorHandlers implements Integration { * Attach the unhandled rejection handler */ private _attachUnhandledRejectionHandler(): void { - const tracking: { - disable: () => void; - enable: (arg: unknown) => void; - // eslint-disable-next-line import/no-extraneous-dependencies,@typescript-eslint/no-var-requires - } = require('promise/setimmediate/rejection-tracking'); + const tracking = this._loadRejectionTracking(); const promiseRejectionTrackingOptions: PromiseRejectionTrackingOptions = { onUnhandled: (id, rejection = {}) => { @@ -253,4 +249,15 @@ export class ReactNativeErrorHandlers implements Integration { }); } } + + /** + * Loads and returns rejection tracking module + */ + private _loadRejectionTracking(): { + disable: () => void; + enable: (arg: unknown) => void; + } { + // eslint-disable-next-line @typescript-eslint/no-var-requires,import/no-extraneous-dependencies + return require('promise/setimmediate/rejection-tracking'); + } } diff --git a/test/integrations/reactnativeerrorhandlers.test.ts b/test/integrations/reactnativeerrorhandlers.test.ts index 6a7067eb44..2fc33a7056 100644 --- a/test/integrations/reactnativeerrorhandlers.test.ts +++ b/test/integrations/reactnativeerrorhandlers.test.ts @@ -6,6 +6,8 @@ const mockBrowserClient: BrowserClient = new BrowserClient({ transport: jest.fn(), }); +let mockHubCaptureException: jest.Mock; + jest.mock('@sentry/core', () => { const core = jest.requireActual('@sentry/core'); @@ -23,8 +25,11 @@ jest.mock('@sentry/core', () => { getClient: () => client, getScope: () => scope, captureEvent: jest.fn(), + captureException: jest.fn(), }; + mockHubCaptureException = hub.captureException; + return { ...core, addGlobalEventProcessor: jest.fn(), @@ -45,10 +50,26 @@ jest.mock('@sentry/utils', () => { }); import { getCurrentHub } from '@sentry/core'; -import type { Event, EventHint, SeverityLevel } from '@sentry/types'; +import type { Event, EventHint, ExtendedError, Integration, SeverityLevel } from '@sentry/types'; import { ReactNativeErrorHandlers } from '../../src/js/integrations/reactnativeerrorhandlers'; +interface MockTrackingOptions { + allRejections: boolean; + onUnhandled: jest.Mock; + onHandled: jest.Mock; +} + +interface MockedReactNativeErrorHandlers extends Integration { + _loadRejectionTracking: jest.Mock< + { + disable: jest.Mock; + enable: jest.Mock; + }, + [] + >; +} + describe('ReactNativeErrorHandlers', () => { beforeEach(() => { ErrorUtils.getGlobalHandler = () => jest.fn(); @@ -103,6 +124,35 @@ describe('ReactNativeErrorHandlers', () => { expect(hint).toEqual(expect.objectContaining({ originalException: new Error('Test Error') })); }); }); + + describe('onUnhandledRejection', () => { + test('unhandled rejected promise is captured with synthetical error', async () => { + mockHubCaptureException.mockClear(); + const integration = new ReactNativeErrorHandlers(); + const mockDisable = jest.fn(); + const mockEnable = jest.fn(); + (integration as unknown as MockedReactNativeErrorHandlers)._loadRejectionTracking = jest.fn(() => ({ + disable: mockDisable, + enable: mockEnable, + })); + integration.setupOnce(); + + const [actualTrackingOptions] = mockEnable.mock.calls[0] || []; + actualTrackingOptions?.onUnhandled?.(1, 'Test Error'); + const actualSyntheticError = mockHubCaptureException.mock.calls[0][1].syntheticException; + + expect(mockDisable).not.toHaveBeenCalled(); + expect(mockEnable).toHaveBeenCalledWith( + expect.objectContaining({ + allRejections: true, + onUnhandled: expect.any(Function), + onHandled: expect.any(Function), + }), + ); + expect(mockEnable).toHaveBeenCalledTimes(1); + expect((actualSyntheticError as ExtendedError).framesToPop).toBe(3); + }); + }); }); function getActualCaptureEventArgs() { From 66aeba63bef4e58aec9a51a64e8be091af12da6c Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 27 Nov 2023 18:46:27 +0100 Subject: [PATCH 07/10] add isErrorLike and only create synthetic error when needed --- src/js/integrations/debugsymbolicator.ts | 17 +++--------- .../integrations/reactnativeerrorhandlers.ts | 6 ++--- src/js/utils/error.ts | 7 +++++ test/error.test.ts | 25 +++++++++++++++++ .../reactnativeerrorhandlers.test.ts | 27 +++++++++++++++++++ 5 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 test/error.test.ts diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 72821cdaed..3a4b98bfa7 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -1,7 +1,7 @@ import type { Event, EventHint, EventProcessor, Hub, Integration, StackFrame as SentryStackFrame } from '@sentry/types'; import { addContextToFrame, logger } from '@sentry/utils'; -import { getFramesToPop } from '../utils/error'; +import { getFramesToPop, isErrorLike } from '../utils/error'; import type * as ReactNative from '../vendor/react-native'; const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|')); @@ -38,25 +38,14 @@ export class DebugSymbolicator implements Integration { return event; } - if ( - event.exception && - hint.originalException && - typeof hint.originalException === 'object' && - 'stack' in hint.originalException && - typeof hint.originalException.stack === 'string' - ) { + if (event.exception && isErrorLike(hint.originalException)) { // originalException is ErrorLike object const symbolicatedFrames = await this._symbolicate( hint.originalException.stack, getFramesToPop(hint.originalException as Error), ); symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames); - } else if ( - hint.syntheticException && - typeof hint.syntheticException === 'object' && - 'stack' in hint.syntheticException && - typeof hint.syntheticException.stack === 'string' - ) { + } else if (hint.syntheticException && isErrorLike(hint.syntheticException)) { // syntheticException is Error object const symbolicatedFrames = await this._symbolicate( hint.syntheticException.stack, diff --git a/src/js/integrations/reactnativeerrorhandlers.ts b/src/js/integrations/reactnativeerrorhandlers.ts index f465f34628..158a9f143b 100644 --- a/src/js/integrations/reactnativeerrorhandlers.ts +++ b/src/js/integrations/reactnativeerrorhandlers.ts @@ -3,7 +3,7 @@ import type { EventHint, Integration, SeverityLevel } from '@sentry/types'; import { addExceptionMechanism, logger } from '@sentry/utils'; import type { ReactNativeClient } from '../client'; -import { createSyntheticError } from '../utils/error'; +import { createSyntheticError, isErrorLike } from '../utils/error'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; /** ReactNativeErrorHandlers Options */ @@ -120,7 +120,7 @@ export class ReactNativeErrorHandlers implements Integration { tracking.enable({ allRejections: true, - onUnhandled: (id: string, error: Error) => { + onUnhandled: (id: string, error: unknown) => { if (__DEV__) { promiseRejectionTrackingOptions.onUnhandled(id, error); } @@ -128,7 +128,7 @@ export class ReactNativeErrorHandlers implements Integration { getCurrentHub().captureException(error, { data: { id }, originalException: error, - syntheticException: createSyntheticError(), + syntheticException: isErrorLike(error) ? undefined : createSyntheticError(), }); }, onHandled: (id: string) => { diff --git a/src/js/utils/error.ts b/src/js/utils/error.ts index 0888cbe976..7afedd1e93 100644 --- a/src/js/utils/error.ts +++ b/src/js/utils/error.ts @@ -24,3 +24,10 @@ export function createSyntheticError(framesToPop: number = 0): ExtendedError { export function getFramesToPop(error: ExtendedError): number { return error.framesToPop !== undefined ? error.framesToPop : 0; } + +/** + * Check if `wat` is an object with string stack property. + */ +export function isErrorLike(wat: unknown): wat is { stack: string } { + return wat !== null && typeof wat === 'object' && 'stack' in wat && typeof wat.stack === 'string'; +} diff --git a/test/error.test.ts b/test/error.test.ts new file mode 100644 index 0000000000..85fc3ee7ea --- /dev/null +++ b/test/error.test.ts @@ -0,0 +1,25 @@ +import { isErrorLike } from '../src/js/utils/error'; + +describe('error', () => { + describe('isErrorLike', () => { + test('returns true for Error object', () => { + expect(isErrorLike(new Error('test'))).toBe(true); + }); + + test('returns true for ErrorLike object', () => { + expect(isErrorLike({ stack: 'test' })).toBe(true); + }); + + test('returns false for non object', () => { + expect(isErrorLike('test')).toBe(false); + }); + + test('returns false for object without stack', () => { + expect(isErrorLike({})).toBe(false); + }); + + test('returns false for object with non string stack', () => { + expect(isErrorLike({ stack: 1 })).toBe(false); + }); + }); +}); diff --git a/test/integrations/reactnativeerrorhandlers.test.ts b/test/integrations/reactnativeerrorhandlers.test.ts index 2fc33a7056..3a4285f3e0 100644 --- a/test/integrations/reactnativeerrorhandlers.test.ts +++ b/test/integrations/reactnativeerrorhandlers.test.ts @@ -152,6 +152,33 @@ describe('ReactNativeErrorHandlers', () => { expect(mockEnable).toHaveBeenCalledTimes(1); expect((actualSyntheticError as ExtendedError).framesToPop).toBe(3); }); + + test('error like unhandled rejected promise is captured without synthetical error', async () => { + mockHubCaptureException.mockClear(); + const integration = new ReactNativeErrorHandlers(); + const mockDisable = jest.fn(); + const mockEnable = jest.fn(); + (integration as unknown as MockedReactNativeErrorHandlers)._loadRejectionTracking = jest.fn(() => ({ + disable: mockDisable, + enable: mockEnable, + })); + integration.setupOnce(); + + const [actualTrackingOptions] = mockEnable.mock.calls[0] || []; + actualTrackingOptions?.onUnhandled?.(1, new Error('Test Error')); + const actualSyntheticError = mockHubCaptureException.mock.calls[0][1].syntheticException; + + expect(mockDisable).not.toHaveBeenCalled(); + expect(mockEnable).toHaveBeenCalledWith( + expect.objectContaining({ + allRejections: true, + onUnhandled: expect.any(Function), + onHandled: expect.any(Function), + }), + ); + expect(mockEnable).toHaveBeenCalledTimes(1); + expect(actualSyntheticError).toBeUndefined(); + }); }); }); From da6cb09a68544e14083096ed64092761e743cdfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Wold=C5=99ich?= <31292499+krystofwoldrich@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:40:20 +0100 Subject: [PATCH 08/10] Update src/js/utils/error.ts Co-authored-by: LucasZF --- src/js/utils/error.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/js/utils/error.ts b/src/js/utils/error.ts index 7afedd1e93..37083bbec6 100644 --- a/src/js/utils/error.ts +++ b/src/js/utils/error.ts @@ -26,8 +26,10 @@ export function getFramesToPop(error: ExtendedError): number { } /** - * Check if `wat` is an object with string stack property. + * Check if `potentialError` is an object with string stack property. */ -export function isErrorLike(wat: unknown): wat is { stack: string } { - return wat !== null && typeof wat === 'object' && 'stack' in wat && typeof wat.stack === 'string'; +Suggestion +```suggestion +export function isErrorLike(potentialError : unknown): potentialError is { stack: string } { + return potentialError !== null && typeof potentialError === 'object' && 'stack' in potentialError && typeof potentialError .stack === 'string'; } From f73343323aea499cc6c1d516f01ef55878188b15 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 30 Nov 2023 15:00:33 +0100 Subject: [PATCH 09/10] fix gh suggestion --- src/js/utils/error.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/js/utils/error.ts b/src/js/utils/error.ts index 37083bbec6..ec02e60c65 100644 --- a/src/js/utils/error.ts +++ b/src/js/utils/error.ts @@ -28,8 +28,6 @@ export function getFramesToPop(error: ExtendedError): number { /** * Check if `potentialError` is an object with string stack property. */ -Suggestion -```suggestion export function isErrorLike(potentialError : unknown): potentialError is { stack: string } { return potentialError !== null && typeof potentialError === 'object' && 'stack' in potentialError && typeof potentialError .stack === 'string'; } From b0902879635963aa25972115159626c0e7e425ab Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 30 Nov 2023 15:50:32 +0100 Subject: [PATCH 10/10] fix prettier --- src/js/utils/error.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/js/utils/error.ts b/src/js/utils/error.ts index ec02e60c65..044e3a8516 100644 --- a/src/js/utils/error.ts +++ b/src/js/utils/error.ts @@ -28,6 +28,11 @@ export function getFramesToPop(error: ExtendedError): number { /** * Check if `potentialError` is an object with string stack property. */ -export function isErrorLike(potentialError : unknown): potentialError is { stack: string } { - return potentialError !== null && typeof potentialError === 'object' && 'stack' in potentialError && typeof potentialError .stack === 'string'; +export function isErrorLike(potentialError: unknown): potentialError is { stack: string } { + return ( + potentialError !== null && + typeof potentialError === 'object' && + 'stack' in potentialError && + typeof potentialError.stack === 'string' + ); }