diff --git a/CHANGELOG.md b/CHANGELOG.md index abda7ed70..60ca2416d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased](https://github.com/Instabug/Instabug-React-Native/compare/v15.0.1...dev) + +### Added + +- Add support for chaining errors . ([#1417](https://github.com/Instabug/Instabug-React-Native/pull/1417)) + ## [15.0.1](https://github.com/Instabug/Instabug-React-Native/compare/v14.3.0...v15.0.1) ### Added diff --git a/examples/default/src/screens/CrashReportingScreen.tsx b/examples/default/src/screens/CrashReportingScreen.tsx index 397565ecd..f4cbf67b4 100644 --- a/examples/default/src/screens/CrashReportingScreen.tsx +++ b/examples/default/src/screens/CrashReportingScreen.tsx @@ -62,6 +62,32 @@ export const CrashReportingScreen: React.FC = () => { throw error; } } + + function throwUnhandledChainingException(error: Error, isPromise: boolean = false) { + const appName = 'Instabug Test App'; + const rejectionType = isPromise ? 'Promise Rejection ' : ''; + const errorMessage = `Unhandled ${rejectionType}${error.name} from ${appName}`; + + if (!error.message) { + console.log(`IBG-CRSH | Error message: ${error.message}`); + error.message = errorMessage; + } + + if (isPromise) { + console.log('IBG-CRSH | Promise'); + Promise.reject(error).then(() => + Alert.alert(`Promise Rejection Crash report for ${error.name} is Sent!`), + ); + } else { + try { + throw ReferenceError(); + } catch (e) { + error.cause = e; + throw error; + } + } + } + const [isEnabled, setIsEnabled] = useState(false); const [userAttributeKey, setUserAttributeKey] = useState(''); @@ -216,6 +242,10 @@ export const CrashReportingScreen: React.FC = () => { title="Throw Unhandled Syntax Exception" onPress={() => throwUnhandledException(new SyntaxError())} /> + throwUnhandledChainingException(new SyntaxError('level 1 SyntaxError'))} + /> throwUnhandledException(new RangeError())} diff --git a/src/native/NativeCrashReporting.ts b/src/native/NativeCrashReporting.ts index 7793624d9..b9188c843 100644 --- a/src/native/NativeCrashReporting.ts +++ b/src/native/NativeCrashReporting.ts @@ -11,10 +11,20 @@ export interface CrashData { os: (typeof Platform)['OS']; platform: 'react_native'; exception: StackFrame[]; + cause_crash?: CauseCrashData; +} + +export interface CauseCrashData { + message: string; + e_message: string; + e_name: string; + exception: StackFrame[]; + cause_crash?: CauseCrashData; } export interface CrashReportingNativeModule extends NativeModule { setEnabled(isEnabled: boolean): void; + sendJSCrash(data: CrashData | string): Promise; sendHandledJSCrash( @@ -23,6 +33,7 @@ export interface CrashReportingNativeModule extends NativeModule { fingerprint?: string | null, nonFatalExceptionLevel?: NonFatalErrorLevel | null, ): Promise; + setNDKCrashesEnabled(isEnabled: boolean): Promise; } diff --git a/src/utils/InstabugUtils.ts b/src/utils/InstabugUtils.ts index 2a619f31d..39d2959d4 100644 --- a/src/utils/InstabugUtils.ts +++ b/src/utils/InstabugUtils.ts @@ -7,7 +7,7 @@ import parseErrorStackLib, { import type { NavigationState as NavigationStateV5, PartialState } from '@react-navigation/native'; import type { NavigationState as NavigationStateV4 } from 'react-navigation'; -import type { CrashData } from '../native/NativeCrashReporting'; +import type { CauseCrashData, CrashData } from '../native/NativeCrashReporting'; import { NativeCrashReporting } from '../native/NativeCrashReporting'; import type { NetworkData } from './XhrNetworkInterceptor'; import { NativeInstabug } from '../native/NativeInstabug'; @@ -59,6 +59,23 @@ export const getCrashDataFromError = (error: Error) => { platform: 'react_native', exception: jsStackTrace, }; + // Recursively attach inner_crash objects (up to 3 levels) + let currentError: any = error; + let level = 0; + let parentCrash: CauseCrashData | CrashData = jsonObject; + while (currentError.cause && level < 3) { + const cause = currentError.cause as Error; + const innerCrash: CauseCrashData = { + message: `${cause.name} - ${cause.message}`, + e_message: cause.message, + e_name: cause.name, + exception: getStackTrace(cause), + }; + parentCrash.cause_crash = innerCrash; + parentCrash = innerCrash; + currentError = cause; + level++; + } return jsonObject; }; diff --git a/test/utils/InstabugUtils.spec.ts b/test/utils/InstabugUtils.spec.ts index 11a5c8d1a..7d05de3e0 100644 --- a/test/utils/InstabugUtils.spec.ts +++ b/test/utils/InstabugUtils.spec.ts @@ -243,6 +243,65 @@ describe('Instabug Utils', () => { NonFatalErrorLevel.error, ); }); + it('getCrashDataFromError should include one level of cause crash', () => { + const causeError = new TypeError('Cause error'); + const rootError = new Error('Root error'); + (rootError as any).cause = causeError; + + const crashData = InstabugUtils.getCrashDataFromError(rootError); + const jsStackTraceRootError = InstabugUtils.getStackTrace(rootError); + const jsStackTraceCauseError = InstabugUtils.getStackTrace(causeError); + + expect(crashData.message).toBe('Error - Root error'); + expect(crashData.e_message).toBe('Root error'); + expect(crashData.e_name).toBe('Error'); + expect(crashData.platform).toBe('react_native'); + expect(crashData.exception).toEqual(jsStackTraceRootError); + expect(crashData.cause_crash).toBeDefined(); + expect(crashData.cause_crash?.message).toBe('TypeError - Cause error'); + expect(crashData.cause_crash?.e_name).toBe('TypeError'); + expect(crashData.cause_crash?.exception).toEqual(jsStackTraceCauseError); + }); + + it('getCrashDataFromError should include up to 3 levels of cause crash', () => { + const errorLevel3 = new Error('Third level error'); + const errorLevel2 = new Error('Second level error'); + const errorLevel1 = new Error('First level error'); + const rootError = new Error('Root error'); + + (errorLevel2 as any).cause = errorLevel3; + (errorLevel1 as any).cause = errorLevel2; + (rootError as any).cause = errorLevel1; + + const crashData = InstabugUtils.getCrashDataFromError(rootError); + + expect(crashData.message).toBe('Error - Root error'); + expect(crashData.cause_crash?.message).toBe('Error - First level error'); + expect(crashData.cause_crash?.cause_crash?.message).toBe('Error - Second level error'); + expect(crashData.cause_crash?.cause_crash?.cause_crash?.message).toBe( + 'Error - Third level error', + ); + expect(crashData.cause_crash?.cause_crash?.cause_crash?.cause_crash).toBeUndefined(); + }); + + it('getCrashDataFromError should stop at 3 levels even if more causes exist', () => { + const errorLevel4 = new Error('Fourth level error'); + const errorLevel3 = new Error('Third level error'); + const errorLevel2 = new Error('Second level error'); + const errorLevel1 = new Error('First level error'); + const rootError = new Error('Root error'); + + (errorLevel3 as any).cause = errorLevel4; + (errorLevel2 as any).cause = errorLevel3; + (errorLevel1 as any).cause = errorLevel2; + (rootError as any).cause = errorLevel1; + + const crashData = InstabugUtils.getCrashDataFromError(rootError); + + const thirdLevel = crashData.cause_crash?.cause_crash?.cause_crash; + expect(thirdLevel?.message).toBe('Error - Third level error'); + expect(thirdLevel?.cause_crash).toBeUndefined(); // should not include 4th level + }); }); describe('reportNetworkLog', () => {