From b2e8289574e37f384349d8ecced17760b475aa55 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 10 May 2024 12:46:07 -0400 Subject: [PATCH 1/7] feat(integrations): Update integration to new JS interface --- src/js/integrations/debugsymbolicator.ts | 350 +++++++---------- src/js/integrations/debugsymbolicatorutils.ts | 78 ++++ src/js/integrations/default.ts | 79 ++-- src/js/integrations/devicecontext.ts | 146 ++++--- src/js/integrations/eventorigin.ts | 31 +- src/js/integrations/expocontext.ts | 56 ++- src/js/integrations/index.ts | 20 +- src/js/integrations/modulesloader.ts | 63 ++-- src/js/integrations/nativelinkederrors.ts | 330 ++++++++-------- .../integrations/reactnativeerrorhandlers.ts | 357 ++++++------------ .../reactnativeerrorhandlersutils.ts | 93 +++++ src/js/integrations/reactnativeinfo.ts | 107 +++--- src/js/integrations/release.ts | 102 +++-- src/js/integrations/rewriteframes.ts | 4 +- src/js/integrations/screenshot.ts | 74 ++-- src/js/integrations/sdkinfo.ts | 87 +++-- src/js/integrations/spotlight.ts | 15 +- src/js/integrations/viewhierarchy.ts | 84 ++--- test/integrations/debugsymbolicator.test.ts | 68 ++-- test/integrations/devicecontext.test.ts | 121 +++--- test/integrations/eventorigin.test.ts | 31 +- test/integrations/expocontext.test.ts | 29 +- .../integrationsexecutionorder.test.ts | 4 +- test/integrations/modulesloader.test.ts | 27 +- test/integrations/nativelinkederrors.test.ts | 8 +- .../reactnativeerrorhandlers.test.ts | 60 ++- test/integrations/reactnativeinfo.test.ts | 19 +- test/integrations/release.test.ts | 126 ++----- test/integrations/sdkinfo.test.ts | 27 +- test/integrations/spotlight.test.ts | 30 +- test/integrations/viewhierarchy.test.ts | 30 +- test/touchevents.test.tsx | 20 +- 32 files changed, 1158 insertions(+), 1518 deletions(-) create mode 100644 src/js/integrations/debugsymbolicatorutils.ts create mode 100644 src/js/integrations/reactnativeerrorhandlersutils.ts diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 1174c79053..78100cdd6f 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -1,10 +1,9 @@ -import type { Event, EventHint, EventProcessor, Hub, Integration, StackFrame as SentryStackFrame } from '@sentry/types'; +import type { Event, EventHint, IntegrationFn, StackFrame as SentryStackFrame } from '@sentry/types'; import { addContextToFrame, logger } from '@sentry/utils'; import { getFramesToPop, isErrorLike } from '../utils/error'; -import { ReactNativeLibraries } from '../utils/rnlibraries'; -import { createStealthXhr, XHR_READYSTATE_DONE } from '../utils/xhr'; import type * as ReactNative from '../vendor/react-native'; +import { fetchSourceContext, getDevServer, parseErrorStack, symbolicateStackTrace } from './debugsymbolicatorutils'; // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|')); @@ -20,256 +19,163 @@ export type ReactNativeError = Error & { }; /** Tries to symbolicate the JS stack trace on the device. */ -export class DebugSymbolicator implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'DebugSymbolicator'; - /** - * @inheritDoc - */ - public name: string = DebugSymbolicator.id; - - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor(async (event: Event, hint: EventHint) => { - const self = getCurrentHub().getIntegration(DebugSymbolicator); - - if (!self) { - return event; - } - - 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 && isErrorLike(hint.syntheticException)) { - // syntheticException is Error object - const symbolicatedFrames = await this._symbolicate( - hint.syntheticException.stack, - getFramesToPop(hint.syntheticException), - ); +export const debugSymbolicatorIntegration: IntegrationFn = () => { + return { + name: 'DebugSymbolicator', + setupOnce: () => { + /* noop */ + }, + processEvent, + }; +}; - 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); - } - } +async function processEvent(event: Event, hint: EventHint): Promise { + if (event.exception && isErrorLike(hint.originalException)) { + // originalException is ErrorLike object + const symbolicatedFrames = await symbolicate( + hint.originalException.stack, + getFramesToPop(hint.originalException as Error), + ); + symbolicatedFrames && replaceExceptionFramesInEvent(event, symbolicatedFrames); + } else if (hint.syntheticException && isErrorLike(hint.syntheticException)) { + // syntheticException is Error object + const symbolicatedFrames = await symbolicate( + hint.syntheticException.stack, + getFramesToPop(hint.syntheticException), + ); - return event; - }); + if (event.exception) { + symbolicatedFrames && replaceExceptionFramesInEvent(event, symbolicatedFrames); + } else if (event.threads) { + // RN JS doesn't have threads + symbolicatedFrames && replaceThreadFramesInEvent(event, symbolicatedFrames); + } } - /** - * Symbolicates the stack on the device talking to local dev server. - * Mutates the passed event. - */ - private async _symbolicate(rawStack: string, skipFirstFrames: number = 0): Promise { - try { - const parsedStack = this._parseErrorStack(rawStack); - - 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 - 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.slice(skipFirstAdjustedToSentryStackParser) - : newStack; + return event; +} - const stackWithoutInternalCallsites = stackWithoutPoppedFrames.filter( - (frame: { file?: string }) => frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, - ); +/** + * Symbolicates the stack on the device talking to local dev server. + * Mutates the passed event. + */ +async function symbolicate(rawStack: string, skipFirstFrames: number = 0): Promise { + try { + const parsedStack = parseErrorStack(rawStack); - return await this._convertReactNativeFramesToSentryFrames(stackWithoutInternalCallsites); - } catch (error) { - if (error instanceof Error) { - logger.warn(`Unable to symbolicate stack trace: ${error.message}`); - } + const prettyStack = await symbolicateStackTrace(parsedStack); + if (!prettyStack) { + logger.error('React Native DevServer could not symbolicate the stack trace.'); return null; } - } - - /** - * Converts ReactNativeFrames to frames in the Sentry format - * @param frames ReactNativeFrame[] - */ - private async _convertReactNativeFramesToSentryFrames(frames: ReactNative.StackFrame[]): Promise { - return Promise.all( - frames.map(async (frame: ReactNative.StackFrame): Promise => { - let inApp = !!frame.column && !!frame.lineNumber; - inApp = - inApp && - frame.file !== undefined && - !frame.file.includes('node_modules') && - !frame.file.includes('native code'); - const newFrame: SentryStackFrame = { - lineno: frame.lineNumber, - colno: frame.column, - filename: frame.file, - function: frame.methodName, - in_app: inApp, - }; + // This has been changed in an react-native version so stack is contained in here + const newStack = prettyStack.stack || prettyStack; - if (inApp) { - await this._addSourceContext(newFrame); - } + // 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.slice(skipFirstAdjustedToSentryStackParser) + : newStack; - return newFrame; - }), + const stackWithoutInternalCallsites = stackWithoutPoppedFrames.filter( + (frame: { file?: string }) => frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, ); - } - /** - * Replaces the frames in the exception of a error. - * @param event Event - * @param frames StackFrame[] - */ - private _replaceExceptionFramesInEvent(event: Event, frames: SentryStackFrame[]): void { - if ( - event.exception && - event.exception.values && - event.exception.values[0] && - event.exception.values[0].stacktrace - ) { - event.exception.values[0].stacktrace.frames = frames.reverse(); + return await convertReactNativeFramesToSentryFrames(stackWithoutInternalCallsites); + } catch (error) { + if (error instanceof Error) { + logger.warn(`Unable to symbolicate stack trace: ${error.message}`); } + return null; } +} - /** - * Replaces the frames in the thread of a message. - * @param event Event - * @param frames StackFrame[] - */ - 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(); - } - } - - /** - * This tries to add source context for in_app Frames - * - * @param frame StackFrame - * @param getDevServer function from RN to get DevServer URL - */ - private async _addSourceContext(frame: SentryStackFrame): Promise { - let sourceContext: string | null = null; - - const segments = frame.filename?.split('/') ?? []; - - const serverUrl = this._getDevServer()?.url; - if (!serverUrl) { - return; - } - - for (const idx in segments) { - if (!Object.prototype.hasOwnProperty.call(segments, idx)) { - continue; - } +/** + * Converts ReactNativeFrames to frames in the Sentry format + * @param frames ReactNativeFrame[] + */ +async function convertReactNativeFramesToSentryFrames(frames: ReactNative.StackFrame[]): Promise { + return Promise.all( + frames.map(async (frame: ReactNative.StackFrame): Promise => { + let inApp = !!frame.column && !!frame.lineNumber; + inApp = + inApp && + frame.file !== undefined && + !frame.file.includes('node_modules') && + !frame.file.includes('native code'); + + const newFrame: SentryStackFrame = { + lineno: frame.lineNumber, + colno: frame.column, + filename: frame.file, + function: frame.methodName, + in_app: inApp, + }; - sourceContext = await this._fetchSourceContext(serverUrl, segments, -idx); - if (sourceContext) { - break; + if (inApp) { + await addSourceContext(newFrame); } - } - if (!sourceContext) { - return; - } + return newFrame; + }), + ); +} - const lines = sourceContext.split('\n'); - addContextToFrame(lines, frame); +/** + * Replaces the frames in the exception of a error. + * @param event Event + * @param frames StackFrame[] + */ +function replaceExceptionFramesInEvent(event: Event, frames: SentryStackFrame[]): void { + if (event.exception && event.exception.values && event.exception.values[0] && event.exception.values[0].stacktrace) { + event.exception.values[0].stacktrace.frames = frames.reverse(); } +} - /** - * Get source context for segment - */ - private async _fetchSourceContext(url: string, segments: Array, start: number): Promise { - return new Promise(resolve => { - const fullUrl = `${url}${segments.slice(start).join('/')}`; - - const xhr = createStealthXhr(); - if (!xhr) { - resolve(null); - return; - } +/** + * Replaces the frames in the thread of a message. + * @param event Event + * @param frames StackFrame[] + */ +function 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(); + } +} - xhr.open('GET', fullUrl, true); - xhr.send(); +/** + * This tries to add source context for in_app Frames + * + * @param frame StackFrame + * @param getDevServer function from RN to get DevServer URL + */ +async function addSourceContext(frame: SentryStackFrame): Promise { + let sourceContext: string | null = null; - xhr.onreadystatechange = (): void => { - if (xhr.readyState === XHR_READYSTATE_DONE) { - if (xhr.status !== 200) { - resolve(null); - } - const response = xhr.responseText; - if ( - typeof response !== 'string' || - // Expo Dev Server responses with status 200 and config JSON - // when web support not enabled and requested file not found - response.startsWith('{') - ) { - resolve(null); - } + const segments = frame.filename?.split('/') ?? []; - resolve(response); - } - }; - xhr.onerror = (): void => { - resolve(null); - }; - }); + const serverUrl = getDevServer()?.url; + if (!serverUrl) { + return; } - /** - * Loads and calls RN Core Devtools parseErrorStack function. - */ - private _parseErrorStack(errorStack: string): Array { - if (!ReactNativeLibraries.Devtools) { - throw new Error('React Native Devtools not available.'); + for (const idx in segments) { + if (!Object.prototype.hasOwnProperty.call(segments, idx)) { + continue; } - return ReactNativeLibraries.Devtools.parseErrorStack(errorStack); - } - /** - * Loads and calls RN Core Devtools symbolicateStackTrace function. - */ - private _symbolicateStackTrace( - stack: Array, - extraData?: Record, - ): Promise { - if (!ReactNativeLibraries.Devtools) { - throw new Error('React Native Devtools not available.'); + sourceContext = await fetchSourceContext(serverUrl, segments, -idx); + if (sourceContext) { + break; } - return ReactNativeLibraries.Devtools.symbolicateStackTrace(stack, extraData); } - /** - * Loads and returns the RN DevServer URL. - */ - private _getDevServer(): ReactNative.DevServerInfo | undefined { - try { - return ReactNativeLibraries.Devtools?.getDevServer(); - } catch (_oO) { - // We can't load devserver URL - } - return undefined; + if (!sourceContext) { + return; } + + const lines = sourceContext.split('\n'); + addContextToFrame(lines, frame); } diff --git a/src/js/integrations/debugsymbolicatorutils.ts b/src/js/integrations/debugsymbolicatorutils.ts new file mode 100644 index 0000000000..8bef6de82c --- /dev/null +++ b/src/js/integrations/debugsymbolicatorutils.ts @@ -0,0 +1,78 @@ +import { ReactNativeLibraries } from '../utils/rnlibraries'; +import { createStealthXhr, XHR_READYSTATE_DONE } from '../utils/xhr'; +import type * as ReactNative from '../vendor/react-native'; + +/** + * Get source context for segment + */ +export async function fetchSourceContext(url: string, segments: Array, start: number): Promise { + return new Promise(resolve => { + const fullUrl = `${url}${segments.slice(start).join('/')}`; + + const xhr = createStealthXhr(); + if (!xhr) { + resolve(null); + return; + } + + xhr.open('GET', fullUrl, true); + xhr.send(); + + xhr.onreadystatechange = (): void => { + if (xhr.readyState === XHR_READYSTATE_DONE) { + if (xhr.status !== 200) { + resolve(null); + } + const response = xhr.responseText; + if ( + typeof response !== 'string' || + // Expo Dev Server responses with status 200 and config JSON + // when web support not enabled and requested file not found + response.startsWith('{') + ) { + resolve(null); + } + + resolve(response); + } + }; + xhr.onerror = (): void => { + resolve(null); + }; + }); +} + +/** + * Loads and calls RN Core Devtools parseErrorStack function. + */ +export function parseErrorStack(errorStack: string): Array { + if (!ReactNativeLibraries.Devtools) { + throw new Error('React Native Devtools not available.'); + } + return ReactNativeLibraries.Devtools.parseErrorStack(errorStack); +} + +/** + * Loads and calls RN Core Devtools symbolicateStackTrace function. + */ +export function symbolicateStackTrace( + stack: Array, + extraData?: Record, +): Promise { + if (!ReactNativeLibraries.Devtools) { + throw new Error('React Native Devtools not available.'); + } + return ReactNativeLibraries.Devtools.symbolicateStackTrace(stack, extraData); +} + +/** + * Loads and returns the RN DevServer URL. + */ +export function getDevServer(): ReactNative.DevServerInfo | undefined { + try { + return ReactNativeLibraries.Devtools?.getDevServer(); + } catch (_oO) { + // We can't load devserver URL + } + return undefined; +} diff --git a/src/js/integrations/default.ts b/src/js/integrations/default.ts index 4dc16bfae1..877c14168b 100644 --- a/src/js/integrations/default.ts +++ b/src/js/integrations/default.ts @@ -1,25 +1,34 @@ -import { HttpClient } from '@sentry/integrations'; -import { Integrations as BrowserReactIntegrations } from '@sentry/react'; +import { httpClientIntegration } from '@sentry/integrations'; +import { + breadcrumbsIntegration, + browserApiErrorsIntegration, + dedupeIntegration, + functionToStringIntegration, + globalHandlersIntegration as browserGlobalHandlersIntegration, + httpContextIntegration, + inboundFiltersIntegration, + linkedErrorsIntegration as browserLinkedErrorsIntegration, +} from '@sentry/react'; import type { Integration } from '@sentry/types'; import type { ReactNativeClientOptions } from '../options'; import { HermesProfiling } from '../profiling/integration'; import { ReactNativeTracing } from '../tracing'; import { isExpoGo, notWeb } from '../utils/environment'; -import { DebugSymbolicator } from './debugsymbolicator'; -import { DeviceContext } from './devicecontext'; -import { EventOrigin } from './eventorigin'; -import { ExpoContext } from './expocontext'; -import { ModulesLoader } from './modulesloader'; -import { NativeLinkedErrors } from './nativelinkederrors'; -import { ReactNativeErrorHandlers } from './reactnativeerrorhandlers'; -import { ReactNativeInfo } from './reactnativeinfo'; -import { Release } from './release'; +import { debugSymbolicatorIntegration } from './debugsymbolicator'; +import { deviceContextIntegration } from './devicecontext'; +import { eventOriginIntegration } from './eventorigin'; +import { expoContextIntegration } from './expocontext'; +import { modulesLoaderIntegration } from './modulesloader'; +import { nativeLinkedErrorsIntegration } from './nativelinkederrors'; +import { reactNativeErrorHandlersIntegration } from './reactnativeerrorhandlers'; +import { reactNativeInfoIntegration } from './reactnativeinfo'; +import { nativeReleaseIntegration } from './release'; import { createReactNativeRewriteFrames } from './rewriteframes'; -import { Screenshot } from './screenshot'; -import { SdkInfo } from './sdkinfo'; +import { screenshotIntegration } from './screenshot'; +import { sdkInfoIntegration } from './sdkinfo'; import { Spotlight } from './spotlight'; -import { ViewHierarchy } from './viewhierarchy'; +import { viewHierarchyIntegration } from './viewhierarchy'; /** * Returns the default ReactNative integrations based on the current environment. @@ -33,44 +42,44 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ if (notWeb()) { integrations.push( - new ReactNativeErrorHandlers({ + reactNativeErrorHandlersIntegration({ patchGlobalPromise: options.patchGlobalPromise, }), ); - integrations.push(new NativeLinkedErrors()); + integrations.push(nativeLinkedErrorsIntegration()); } else { - integrations.push(new BrowserReactIntegrations.TryCatch()); - integrations.push(new BrowserReactIntegrations.GlobalHandlers()); - integrations.push(new BrowserReactIntegrations.LinkedErrors()); + integrations.push(browserApiErrorsIntegration()); + integrations.push(browserGlobalHandlersIntegration()); + integrations.push(browserLinkedErrorsIntegration()); } // @sentry/react default integrations - integrations.push(new BrowserReactIntegrations.InboundFilters()); - integrations.push(new BrowserReactIntegrations.FunctionToString()); - integrations.push(new BrowserReactIntegrations.Breadcrumbs()); - integrations.push(new BrowserReactIntegrations.Dedupe()); - integrations.push(new BrowserReactIntegrations.HttpContext()); + integrations.push(inboundFiltersIntegration()); + integrations.push(functionToStringIntegration()); + integrations.push(breadcrumbsIntegration()); + integrations.push(dedupeIntegration()); + integrations.push(httpContextIntegration()); // end @sentry/react-native default integrations - integrations.push(new Release()); - integrations.push(new EventOrigin()); - integrations.push(new SdkInfo()); - integrations.push(new ReactNativeInfo()); + integrations.push(nativeReleaseIntegration()); + integrations.push(eventOriginIntegration()); + integrations.push(sdkInfoIntegration()); + integrations.push(reactNativeInfoIntegration()); if (__DEV__ && notWeb()) { - integrations.push(new DebugSymbolicator()); + integrations.push(debugSymbolicatorIntegration()); } integrations.push(createReactNativeRewriteFrames()); if (options.enableNative) { - integrations.push(new DeviceContext()); - integrations.push(new ModulesLoader()); + integrations.push(deviceContextIntegration()); + integrations.push(modulesLoaderIntegration()); if (options.attachScreenshot) { - integrations.push(new Screenshot()); + integrations.push(screenshotIntegration()); } if (options.attachViewHierarchy) { - integrations.push(new ViewHierarchy()); + integrations.push(viewHierarchyIntegration()); } if (options._experiments && typeof options._experiments.profilesSampleRate === 'number') { integrations.push(new HermesProfiling()); @@ -88,11 +97,11 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ integrations.push(new ReactNativeTracing()); } if (options.enableCaptureFailedRequests) { - integrations.push(new HttpClient()); + integrations.push(httpClientIntegration()); } if (isExpoGo()) { - integrations.push(new ExpoContext()); + integrations.push(expoContextIntegration()); } if (options.enableSpotlight) { diff --git a/src/js/integrations/devicecontext.ts b/src/js/integrations/devicecontext.ts index df2834a727..801e4d1c5d 100644 --- a/src/js/integrations/devicecontext.ts +++ b/src/js/integrations/devicecontext.ts @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import type { Event, EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Event, IntegrationFn } from '@sentry/types'; import { logger, severityLevelFromString } from '@sentry/utils'; import { AppState } from 'react-native'; @@ -8,93 +8,81 @@ import type { NativeDeviceContextsResponse } from '../NativeRNSentry'; import { NATIVE } from '../wrapper'; /** Load device context from native. */ -export class DeviceContext implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'DeviceContext'; +export const deviceContextIntegration: IntegrationFn = () => { + return { + name: 'DeviceContext', + setupOnce: () => { + /* noop */ + }, + processEvent, + }; +}; - /** - * @inheritDoc - */ - public name: string = DeviceContext.id; - - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor(async (event: Event) => { - const self = getCurrentHub().getIntegration(DeviceContext); - if (!self) { - return event; - } - - let native: NativeDeviceContextsResponse | null = null; - try { - native = await NATIVE.fetchNativeDeviceContexts(); - } catch (e) { - logger.log(`Failed to get device context from native: ${e}`); - } - - if (!native) { - return event; - } +async function processEvent(event: Event): Promise { + let native: NativeDeviceContextsResponse | null = null; + try { + native = await NATIVE.fetchNativeDeviceContexts(); + } catch (e) { + logger.log(`Failed to get device context from native: ${e}`); + } - const nativeUser = native.user; - if (!event.user && nativeUser) { - event.user = nativeUser; - } + if (!native) { + return event; + } - let nativeContexts = native.contexts; - if (AppState.currentState !== 'unknown') { - nativeContexts = nativeContexts || {}; - nativeContexts.app = { - ...nativeContexts.app, - in_foreground: AppState.currentState === 'active', - }; - } - if (nativeContexts) { - event.contexts = { ...nativeContexts, ...event.contexts }; - if (nativeContexts.app) { - event.contexts.app = { ...nativeContexts.app, ...event.contexts.app }; - } - } + const nativeUser = native.user; + if (!event.user && nativeUser) { + event.user = nativeUser; + } - const nativeTags = native.tags; - if (nativeTags) { - event.tags = { ...nativeTags, ...event.tags }; - } + let nativeContexts = native.contexts; + if (AppState.currentState !== 'unknown') { + nativeContexts = nativeContexts || {}; + nativeContexts.app = { + ...nativeContexts.app, + in_foreground: AppState.currentState === 'active', + }; + } + if (nativeContexts) { + event.contexts = { ...nativeContexts, ...event.contexts }; + if (nativeContexts.app) { + event.contexts.app = { ...nativeContexts.app, ...event.contexts.app }; + } + } - const nativeExtra = native.extra; - if (nativeExtra) { - event.extra = { ...nativeExtra, ...event.extra }; - } + const nativeTags = native.tags; + if (nativeTags) { + event.tags = { ...nativeTags, ...event.tags }; + } - const nativeFingerprint = native.fingerprint; - if (nativeFingerprint) { - event.fingerprint = (event.fingerprint ?? []).concat( - nativeFingerprint.filter(item => (event.fingerprint ?? []).indexOf(item) < 0), - ); - } + const nativeExtra = native.extra; + if (nativeExtra) { + event.extra = { ...nativeExtra, ...event.extra }; + } - const nativeLevel = typeof native['level'] === 'string' ? severityLevelFromString(native['level']) : undefined; - if (!event.level && nativeLevel) { - event.level = nativeLevel; - } + const nativeFingerprint = native.fingerprint; + if (nativeFingerprint) { + event.fingerprint = (event.fingerprint ?? []).concat( + nativeFingerprint.filter(item => (event.fingerprint ?? []).indexOf(item) < 0), + ); + } - const nativeEnvironment = native['environment']; - if (!event.environment && nativeEnvironment) { - event.environment = nativeEnvironment; - } + const nativeLevel = typeof native['level'] === 'string' ? severityLevelFromString(native['level']) : undefined; + if (!event.level && nativeLevel) { + event.level = nativeLevel; + } - const nativeBreadcrumbs = Array.isArray(native['breadcrumbs']) - ? native['breadcrumbs'].map(breadcrumbFromObject) - : undefined; - if (nativeBreadcrumbs) { - event.breadcrumbs = nativeBreadcrumbs; - } + const nativeEnvironment = native['environment']; + if (!event.environment && nativeEnvironment) { + event.environment = nativeEnvironment; + } - return event; - }); + const nativeBreadcrumbs = Array.isArray(native['breadcrumbs']) + ? native['breadcrumbs'].map(breadcrumbFromObject) + : undefined; + if (nativeBreadcrumbs) { + event.breadcrumbs = nativeBreadcrumbs; } + + return event; } diff --git a/src/js/integrations/eventorigin.ts b/src/js/integrations/eventorigin.ts index 3b61e562f1..e066432036 100644 --- a/src/js/integrations/eventorigin.ts +++ b/src/js/integrations/eventorigin.ts @@ -1,28 +1,19 @@ -import type { EventProcessor, Integration } from '@sentry/types'; +import type { Event, Integration } from '@sentry/types'; /** Default EventOrigin instrumentation */ -export class EventOrigin implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'EventOrigin'; - - /** - * @inheritDoc - */ - public name: string = EventOrigin.id; - - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (e: EventProcessor) => void): void { - addGlobalEventProcessor(event => { +export const eventOriginIntegration = (): Integration => { + return { + name: 'EventOrigin', + setupOnce: () => { + // noop + }, + processEvent: (event: Event) => { event.tags = event.tags ?? {}; event.tags['event.origin'] = 'javascript'; event.tags['event.environment'] = 'javascript'; return event; - }); - } -} + }, + }; +}; diff --git a/src/js/integrations/expocontext.ts b/src/js/integrations/expocontext.ts index 04944b53bc..b28b8a5e9b 100644 --- a/src/js/integrations/expocontext.ts +++ b/src/js/integrations/expocontext.ts @@ -1,44 +1,32 @@ -import type { DeviceContext, Event, EventProcessor, Hub, Integration, OsContext } from '@sentry/types'; +import type { DeviceContext, Event, Integration, OsContext } from '@sentry/types'; import { getExpoDevice } from '../utils/expomodules'; /** Load device context from expo modules. */ -export class ExpoContext implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ExpoContext'; - - /** - * @inheritDoc - */ - public name: string = ExpoContext.id; - - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor(async (event: Event) => { - const self = getCurrentHub().getIntegration(ExpoContext); - if (!self) { - return event; - } - - const expoDeviceContext = getExpoDeviceContext(); - if (expoDeviceContext) { - event.contexts = event.contexts || {}; - event.contexts.device = { ...expoDeviceContext, ...event.contexts.device }; - } +export const expoContextIntegration = (): Integration => { + return { + name: 'ExpoContext', + setupOnce: () => { + // noop + }, + processEvent, + }; +}; - const expoOsContext = getExpoOsContext(); - if (expoOsContext) { - event.contexts = event.contexts || {}; - event.contexts.os = { ...expoOsContext, ...event.contexts.os }; - } +function processEvent(event: Event): Event { + const expoDeviceContext = getExpoDeviceContext(); + if (expoDeviceContext) { + event.contexts = event.contexts || {}; + event.contexts.device = { ...expoDeviceContext, ...event.contexts.device }; + } - return event; - }); + const expoOsContext = getExpoOsContext(); + if (expoOsContext) { + event.contexts = event.contexts || {}; + event.contexts.os = { ...expoOsContext, ...event.contexts.os }; } + + return event; } /** diff --git a/src/js/integrations/index.ts b/src/js/integrations/index.ts index 3a8ad303ae..37772e6927 100644 --- a/src/js/integrations/index.ts +++ b/src/js/integrations/index.ts @@ -1,10 +1,14 @@ -export { DebugSymbolicator } from './debugsymbolicator'; -export { DeviceContext } from './devicecontext'; -export { ReactNativeErrorHandlers } from './reactnativeerrorhandlers'; -export { Release } from './release'; -export { EventOrigin } from './eventorigin'; -export { SdkInfo } from './sdkinfo'; -export { ReactNativeInfo } from './reactnativeinfo'; -export { ModulesLoader } from './modulesloader'; +export { debugSymbolicatorIntegration } from './debugsymbolicator'; +export { deviceContextIntegration } from './devicecontext'; +export { reactNativeErrorHandlersIntegration } from './reactnativeerrorhandlers'; +export { nativeLinkedErrorsIntegration } from './nativelinkederrors'; +export { nativeReleaseIntegration } from './release'; +export { eventOriginIntegration } from './eventorigin'; +export { sdkInfoIntegration } from './sdkinfo'; +export { reactNativeInfoIntegration } from './reactnativeinfo'; +export { modulesLoaderIntegration } from './modulesloader'; export { HermesProfiling } from '../profiling/integration'; +export { screenshotIntegration } from './screenshot'; +export { viewHierarchyIntegration } from './viewhierarchy'; +export { expoContextIntegration } from './expocontext'; export { Spotlight } from './spotlight'; diff --git a/src/js/integrations/modulesloader.ts b/src/js/integrations/modulesloader.ts index b3f4da04cc..6bc25a33f5 100644 --- a/src/js/integrations/modulesloader.ts +++ b/src/js/integrations/modulesloader.ts @@ -1,43 +1,38 @@ -import type { Event, EventProcessor, Integration } from '@sentry/types'; +import type { Event, Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; import { NATIVE } from '../wrapper'; /** Loads runtime JS modules from prepared file. */ -export class ModulesLoader implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ModulesLoader'; +export const modulesLoaderIntegration = (): Integration => { + return { + name: 'ModulesLoader', + setupOnce: () => { + // noop + }, + processEvent: createProcessEvent(), + }; +}; - /** - * @inheritDoc - */ - public name: string = ModulesLoader.id; +function createProcessEvent(): (event: Event) => Promise { + let isSetup = false; + let modules: Record | null = null; - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (e: EventProcessor) => void): void { - let isSetup = false; - let modules: Record | null; - - addGlobalEventProcessor(async (event: Event) => { - if (!isSetup) { - try { - modules = await NATIVE.fetchModules(); - } catch (e) { - logger.log(`Failed to get modules from native: ${e}`); - } - isSetup = true; - } - if (modules) { - event.modules = { - ...modules, - ...event.modules, - }; + return async (event: Event) => { + if (!isSetup) { + try { + modules = await NATIVE.fetchModules(); + } catch (e) { + logger.log(`Failed to get modules from native: ${e}`); } - return event; - }); - } + isSetup = true; + } + if (modules) { + event.modules = { + ...modules, + ...event.modules, + }; + } + return event; + }; } diff --git a/src/js/integrations/nativelinkederrors.ts b/src/js/integrations/nativelinkederrors.ts index 8f4f5e0566..41d62f07dd 100644 --- a/src/js/integrations/nativelinkederrors.ts +++ b/src/js/integrations/nativelinkederrors.ts @@ -4,10 +4,8 @@ import type { DebugImage, Event, EventHint, - EventProcessor, Exception, ExtendedError, - Hub, Integration, StackFrame, StackParser, @@ -28,197 +26,171 @@ interface LinkedErrorsOptions { /** * Processes JS and RN native linked errors. */ -export class NativeLinkedErrors implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'NativeLinkedErrors'; - - /** - * @inheritDoc - */ - public name: string = NativeLinkedErrors.id; - - private readonly _key: LinkedErrorsOptions['key']; - private readonly _limit: LinkedErrorsOptions['limit']; - private _nativePackage: string | null = null; - - /** - * @inheritDoc - */ - public constructor(options: Partial = {}) { - this._key = options.key || DEFAULT_KEY; - this._limit = options.limit || DEFAULT_LIMIT; +export const nativeLinkedErrorsIntegration = (options: Partial = {}): Integration => { + const key = options.key || DEFAULT_KEY; + const limit = options.limit || DEFAULT_LIMIT; + + return { + name: 'NativeLinkedErrors', + setupOnce: (): void => { + // noop + }, + preprocessEvent: (event: Event, hint: EventHint, client: Client): void => + preprocessEvent(event, hint, client, limit, key), + }; +}; + +function preprocessEvent(event: Event, hint: EventHint | undefined, client: Client, limit: number, key: string): void { + if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { + return; } - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { - /* noop */ - } - - /** - * @inheritDoc - */ - public preprocessEvent(event: Event, hint: EventHint | undefined, client: Client): void { - if (this._nativePackage === null) { - this._nativePackage = this._fetchNativePackage(); - } + const parser = client.getOptions().stackParser; - this._handler(client.getOptions().stackParser, this._key, this._limit, event, hint); - } + const { exceptions: linkedErrors, debugImages } = walkErrorTree( + parser, + limit, + hint.originalException as ExtendedError, + key, + ); + event.exception.values = [...event.exception.values, ...linkedErrors]; - /** - * Enriches passed event with linked exceptions and native debug meta images. - */ - private _handler(parser: StackParser, key: string, limit: number, event: Event, hint?: EventHint): void { - if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { - return; - } - const { exceptions: linkedErrors, debugImages } = this._walkErrorTree( - parser, - limit, - hint.originalException as ExtendedError, - key, - ); - event.exception.values = [...event.exception.values, ...linkedErrors]; - - event.debug_meta = event.debug_meta || {}; - event.debug_meta.images = event.debug_meta.images || []; - event.debug_meta.images.push(...(debugImages || [])); - } + event.debug_meta = event.debug_meta || {}; + event.debug_meta.images = event.debug_meta.images || []; + event.debug_meta.images.push(...(debugImages || [])); +} - /** - * Walks linked errors and created Sentry exceptions chain. - * Collects debug images from native errors stack frames. - */ - private _walkErrorTree( - parser: StackParser, - limit: number, - error: ExtendedError, - key: string, - exceptions: Exception[] = [], - debugImages: DebugImage[] = [], - ): { - exceptions: Exception[]; - debugImages?: DebugImage[]; - } { - const linkedError = error[key]; - if (!linkedError || exceptions.length + 1 >= limit) { - return { - exceptions, - debugImages, - }; - } - - let exception: Exception; - let exceptionDebugImages: DebugImage[] | undefined; - if ('stackElements' in linkedError) { - // isJavaException - exception = this._exceptionFromJavaStackElements(linkedError); - } else if ('stackReturnAddresses' in linkedError) { - // isObjCException - const { appleException, appleDebugImages } = this._exceptionFromAppleStackReturnAddresses(linkedError); - exception = appleException; - exceptionDebugImages = appleDebugImages; - } else if (isInstanceOf(linkedError, Error)) { - exception = exceptionFromError(parser, error[key]); - } else if (isPlainObject(linkedError)) { - exception = { - type: typeof linkedError.name === 'string' ? linkedError.name : undefined, - value: typeof linkedError.message === 'string' ? linkedError.message : undefined, - }; - } else { - return { - exceptions, - debugImages, - }; - } - - return this._walkErrorTree( - parser, - limit, - linkedError, - key, - [...exceptions, exception], - [...debugImages, ...(exceptionDebugImages || [])], - ); +/** + * Walks linked errors and created Sentry exceptions chain. + * Collects debug images from native errors stack frames. + */ +function walkErrorTree( + parser: StackParser, + limit: number, + error: ExtendedError, + key: string, + exceptions: Exception[] = [], + debugImages: DebugImage[] = [], +): { + exceptions: Exception[]; + debugImages?: DebugImage[]; +} { + const linkedError = error[key]; + if (!linkedError || exceptions.length + 1 >= limit) { + return { + exceptions, + debugImages, + }; } - /** - * Converts a Java Throwable to an SentryException - */ - private _exceptionFromJavaStackElements(javaThrowable: { - name: string; - message: string; - stackElements: { - className: string; - fileName: string; - methodName: string; - lineNumber: number; - }[]; - }): Exception { + let exception: Exception; + let exceptionDebugImages: DebugImage[] | undefined; + if ('stackElements' in linkedError) { + // isJavaException + exception = exceptionFromJavaStackElements(linkedError); + } else if ('stackReturnAddresses' in linkedError) { + // isObjCException + const { appleException, appleDebugImages } = exceptionFromAppleStackReturnAddresses(linkedError); + exception = appleException; + exceptionDebugImages = appleDebugImages; + } else if (isInstanceOf(linkedError, Error)) { + exception = exceptionFromError(parser, error[key]); + } else if (isPlainObject(linkedError)) { + exception = { + type: typeof linkedError.name === 'string' ? linkedError.name : undefined, + value: typeof linkedError.message === 'string' ? linkedError.message : undefined, + }; + } else { return { - type: javaThrowable.name, - value: javaThrowable.message, - stacktrace: { - frames: javaThrowable.stackElements - .map( - stackElement => - { - platform: 'java', - module: stackElement.className, - filename: stackElement.fileName, - lineno: stackElement.lineNumber >= 0 ? stackElement.lineNumber : undefined, - function: stackElement.methodName, - in_app: - this._nativePackage !== null && stackElement.className.startsWith(this._nativePackage) - ? true - : undefined, - }, - ) - .reverse(), - }, + exceptions, + debugImages, }; } - /** - * Converts StackAddresses to a SentryException with DebugMetaImages - */ - private _exceptionFromAppleStackReturnAddresses(objCException: { - name: string; - message: string; - stackReturnAddresses: number[]; - }): { - appleException: Exception; - appleDebugImages: DebugImage[]; - } { - const nativeStackFrames = this._fetchNativeStackFrames(objCException.stackReturnAddresses); + return walkErrorTree( + parser, + limit, + linkedError, + key, + [...exceptions, exception], + [...debugImages, ...(exceptionDebugImages || [])], + ); +} - return { - appleException: { - type: objCException.name, - value: objCException.message, - stacktrace: { - frames: (nativeStackFrames && nativeStackFrames.frames.reverse()) || [], - }, +/** + * Converts a Java Throwable to an SentryException + */ +function exceptionFromJavaStackElements(javaThrowable: { + name: string; + message: string; + stackElements: { + className: string; + fileName: string; + methodName: string; + lineNumber: number; + }[]; +}): Exception { + const nativePackage = fetchNativePackage(); + return { + type: javaThrowable.name, + value: javaThrowable.message, + stacktrace: { + frames: javaThrowable.stackElements + .map( + stackElement => + { + platform: 'java', + module: stackElement.className, + filename: stackElement.fileName, + lineno: stackElement.lineNumber >= 0 ? stackElement.lineNumber : undefined, + function: stackElement.methodName, + in_app: nativePackage !== null && stackElement.className.startsWith(nativePackage) ? true : undefined, + }, + ) + .reverse(), + }, + }; +} + +/** + * Converts StackAddresses to a SentryException with DebugMetaImages + */ +function exceptionFromAppleStackReturnAddresses(objCException: { + name: string; + message: string; + stackReturnAddresses: number[]; +}): { + appleException: Exception; + appleDebugImages: DebugImage[]; +} { + const nativeStackFrames = fetchNativeStackFrames(objCException.stackReturnAddresses); + + return { + appleException: { + type: objCException.name, + value: objCException.message, + stacktrace: { + frames: (nativeStackFrames && nativeStackFrames.frames.reverse()) || [], }, - appleDebugImages: (nativeStackFrames && (nativeStackFrames.debugMetaImages as DebugImage[])) || [], - }; - } + }, + appleDebugImages: (nativeStackFrames && (nativeStackFrames.debugMetaImages as DebugImage[])) || [], + }; +} - /** - * Fetches the native package/image name from the native layer - */ - private _fetchNativePackage(): string | null { - return NATIVE.fetchNativePackageName(); +let nativePackage: string | null = null; +/** + * Fetches the native package/image name from the native layer + */ +function fetchNativePackage(): string | null { + if (nativePackage === null) { + nativePackage = NATIVE.fetchNativePackageName(); } + return nativePackage; +} - /** - * Fetches native debug image information on iOS - */ - private _fetchNativeStackFrames(instructionsAddr: number[]): NativeStackFrames | null { - return NATIVE.fetchNativeStackFramesBy(instructionsAddr); - } +/** + * Fetches native debug image information on iOS + */ +function fetchNativeStackFrames(instructionsAddr: number[]): NativeStackFrames | null { + return NATIVE.fetchNativeStackFramesBy(instructionsAddr); } diff --git a/src/js/integrations/reactnativeerrorhandlers.ts b/src/js/integrations/reactnativeerrorhandlers.ts index a5a4e7e487..3b95729b54 100644 --- a/src/js/integrations/reactnativeerrorhandlers.ts +++ b/src/js/integrations/reactnativeerrorhandlers.ts @@ -1,11 +1,10 @@ -import { getCurrentHub } from '@sentry/core'; -import type { EventHint, Integration } from '@sentry/types'; +import { captureException, getClient, getCurrentScope } from '@sentry/core'; +import type { EventHint, Integration, SeverityLevel } from '@sentry/types'; import { addExceptionMechanism, logger } from '@sentry/utils'; -import type { ReactNativeClient } from '../client'; import { createSyntheticError, isErrorLike } from '../utils/error'; -import { ReactNativeLibraries } from '../utils/rnlibraries'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; +import { checkPromiseAndWarn, polyfillPromise, requireRejectionTracking } from './reactnativeerrorhandlersutils'; /** ReactNativeErrorHandlers Options */ interface ReactNativeErrorHandlersOptions { @@ -20,262 +19,146 @@ interface PromiseRejectionTrackingOptions { } /** ReactNativeErrorHandlers Integration */ -export class ReactNativeErrorHandlers implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ReactNativeErrorHandlers'; - - /** - * @inheritDoc - */ - public name: string = ReactNativeErrorHandlers.id; - - /** ReactNativeOptions */ - private readonly _options: ReactNativeErrorHandlersOptions; +export const reactNativeErrorHandlersIntegration = ( + options: Partial = {}, +): Integration => { + return { + name: 'ReactNativeErrorHandlers', + setupOnce: () => + setup({ + onerror: options.onerror || true, + onunhandledrejection: options.onunhandledrejection || true, + patchGlobalPromise: options.patchGlobalPromise || true, + }), + }; +}; + +function setup(options: ReactNativeErrorHandlersOptions): void { + options.onunhandledrejection && setupUnhandledRejectionsTracking(options.patchGlobalPromise); + options.onerror && setupErrorUtilsGlobalHandler(); +} - /** Constructor */ - public constructor(options?: Partial) { - this._options = { - onerror: true, - onunhandledrejection: true, - patchGlobalPromise: true, - ...options, - }; +/** + * Setup unhandled promise rejection tracking + */ +function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void { + if (patchGlobalPromise) { + polyfillPromise(); } - /** - * @inheritDoc - */ - public setupOnce(): void { - this._handleUnhandledRejections(); - this._handleOnError(); - } + attachUnhandledRejectionHandler(); + checkPromiseAndWarn(); +} - /** - * Handle Promises - */ - private _handleUnhandledRejections(): void { - if (this._options.onunhandledrejection) { - if (this._options.patchGlobalPromise) { - this._polyfillPromise(); +function attachUnhandledRejectionHandler(): void { + const tracking = requireRejectionTracking(); + + const promiseRejectionTrackingOptions: PromiseRejectionTrackingOptions = { + onUnhandled: (id, rejection = {}) => { + // eslint-disable-next-line no-console + console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${rejection}`); + }, + onHandled: id => { + // eslint-disable-next-line no-console + console.warn( + `Promise Rejection Handled (id: ${id})\n` + + 'This means you can ignore any previous messages of the form ' + + `"Possible Unhandled Promise Rejection (id: ${id}):"`, + ); + }, + }; + + tracking.enable({ + allRejections: true, + onUnhandled: (id: string, error: unknown) => { + if (__DEV__) { + promiseRejectionTrackingOptions.onUnhandled(id, error); } - this._attachUnhandledRejectionHandler(); - this._checkPromiseAndWarn(); - } - } - /** - * Polyfill the global promise instance with one we can be sure that we can attach the tracking to. - * - * In newer RN versions >=0.63, the global promise is not the same reference as the one imported from the promise library. - * This is due to a version mismatch between promise versions. - * Originally we tried a solution where we would have you put a package resolution to ensure the promise instances match. However, - * - Using a package resolution requires the you to manually troubleshoot. - * - The package resolution fix no longer works with 0.67 on iOS Hermes. - */ - private _polyfillPromise(): void { - if (!ReactNativeLibraries.Utilities) { - logger.warn('Could not polyfill Promise. React Native Libraries Utilities not found.'); - return; - } - - const Promise = this._getPromisePolyfill(); - - // As of RN 0.67 only done and finally are used - // eslint-disable-next-line import/no-extraneous-dependencies - require('promise/setimmediate/done'); - // eslint-disable-next-line import/no-extraneous-dependencies - require('promise/setimmediate/finally'); - - ReactNativeLibraries.Utilities.polyfillGlobal('Promise', () => Promise); - } - - /** - * Single source of truth for the Promise implementation we want to use. - * This is important for verifying that the rejected promise tracing will work as expected. - */ - private _getPromisePolyfill(): unknown { - /* eslint-disable import/no-extraneous-dependencies,@typescript-eslint/no-var-requires */ - // Below, we follow the exact way React Native initializes its promise library, and we globally replace it. - return require('promise/setimmediate/es6-extensions'); - } - - /** - * Attach the unhandled rejection handler - */ - private _attachUnhandledRejectionHandler(): void { - const tracking = this._loadRejectionTracking(); - - const promiseRejectionTrackingOptions: PromiseRejectionTrackingOptions = { - onUnhandled: (id, rejection = {}) => { - // eslint-disable-next-line no-console - console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${rejection}`); - }, - onHandled: id => { - // eslint-disable-next-line no-console - console.warn( - `Promise Rejection Handled (id: ${id})\n` + - 'This means you can ignore any previous messages of the form ' + - `"Possible Unhandled Promise Rejection (id: ${id}):"`, - ); - }, - }; + captureException(error, { + data: { id }, + originalException: error, + syntheticException: isErrorLike(error) ? undefined : createSyntheticError(), + }); + }, + onHandled: (id: string) => { + promiseRejectionTrackingOptions.onHandled(id); + }, + }); +} - tracking.enable({ - allRejections: true, - onUnhandled: (id: string, error: unknown) => { - if (__DEV__) { - promiseRejectionTrackingOptions.onUnhandled(id, error); - } +function setupErrorUtilsGlobalHandler(): void { + let handlingFatal = false; - getCurrentHub().captureException(error, { - data: { id }, - originalException: error, - syntheticException: isErrorLike(error) ? undefined : createSyntheticError(), - }); - }, - onHandled: (id: string) => { - promiseRejectionTrackingOptions.onHandled(id); - }, - }); + const errorUtils = RN_GLOBAL_OBJ.ErrorUtils; + if (!errorUtils) { + logger.warn('ErrorUtils not found. Can be caused by different environment for example react-native-web.'); + return; } - /** - * Checks if the promise is the same one or not, if not it will warn the user - */ - private _checkPromiseAndWarn(): void { - try { - // `promise` package is a dependency of react-native, therefore it is always available. - // but it is possible that the user has installed a different version of promise - // or dependency that uses a different version. - // We have to check if the React Native Promise and the `promise` package Promise are using the same reference. - // If they are not, likely there are multiple versions of the `promise` package installed. - const ReactNativePromise = ReactNativeLibraries.Promise; - // eslint-disable-next-line @typescript-eslint/no-var-requires,import/no-extraneous-dependencies - const PromisePackagePromise = require('promise/setimmediate/es6-extensions'); - const UsedPromisePolyfill = this._getPromisePolyfill(); - - if (ReactNativePromise !== PromisePackagePromise) { - logger.warn( - 'You appear to have multiple versions of the "promise" package installed. ' + - 'This may cause unexpected behavior like undefined `Promise.allSettled`. ' + - 'Please install the `promise` package manually using the exact version as the React Native package. ' + - 'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.', - ); - } - // This only make sense if the user disabled the integration Polyfill - if (UsedPromisePolyfill !== RN_GLOBAL_OBJ.Promise) { - logger.warn( - 'Unhandled promise rejections will not be caught by Sentry. ' + - 'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.', - ); - } else { - logger.log('Unhandled promise rejections will be caught by Sentry.'); - } - } catch (e) { - // Do Nothing - logger.warn( - 'Unhandled promise rejections will not be caught by Sentry. ' + - 'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.', - ); - } - } - /** - * Handle errors - */ - private _handleOnError(): void { - if (this._options.onerror) { - let handlingFatal = false; + const defaultHandler = errorUtils.getGlobalHandler && errorUtils.getGlobalHandler(); - const errorUtils = RN_GLOBAL_OBJ.ErrorUtils; - if (!errorUtils) { - logger.warn('ErrorUtils not found. Can be caused by different environment for example react-native-web.'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + errorUtils.setGlobalHandler(async (error: any, isFatal?: boolean) => { + // We want to handle fatals, but only in production mode. + const shouldHandleFatal = isFatal && !__DEV__; + if (shouldHandleFatal) { + if (handlingFatal) { + logger.log('Encountered multiple fatals in a row. The latest:', error); return; } + handlingFatal = true; + } - const defaultHandler = errorUtils.getGlobalHandler && errorUtils.getGlobalHandler(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - errorUtils.setGlobalHandler(async (error: any, isFatal?: boolean) => { - // We want to handle fatals, but only in production mode. - const shouldHandleFatal = isFatal && !__DEV__; - if (shouldHandleFatal) { - if (handlingFatal) { - logger.log('Encountered multiple fatals in a row. The latest:', error); - return; - } - handlingFatal = true; - } - - const currentHub = getCurrentHub(); - const client = currentHub.getClient(); - const scope = currentHub.getScope(); - - if (!client) { - logger.error('Sentry client is missing, the error event might be lost.', error); + const client = getClient(); - // If there is no client something is fishy, anyway we call the default handler - defaultHandler(error, isFatal); + if (!client) { + logger.error('Sentry client is missing, the error event might be lost.', error); - return; - } + // If there is no client something is fishy, anyway we call the default handler + defaultHandler(error, isFatal); - const options = client.getOptions(); + return; + } - const hint: EventHint = { - originalException: error, - attachments: scope?.getAttachments(), - }; - const event = await client.eventFromException(error, hint); + const hint: EventHint = { + originalException: error, + attachments: getCurrentScope().getScopeData().attachments, + }; + const event = await client.eventFromException(error, hint); - if (isFatal) { - event.level = 'fatal'; + if (isFatal) { + event.level = 'fatal' as SeverityLevel; - addExceptionMechanism(event, { - handled: false, - type: 'onerror', - }); - } else { - event.level = 'error'; + addExceptionMechanism(event, { + handled: false, + type: 'onerror', + }); + } else { + event.level = 'error'; - addExceptionMechanism(event, { - handled: true, - type: 'generic', - }); - } + addExceptionMechanism(event, { + handled: true, + type: 'generic', + }); + } - currentHub.captureEvent(event, hint); + client.captureEvent(event, hint); - if (!__DEV__) { - void client.flush(options.shutdownTimeout || 2000).then( - () => { - defaultHandler(error, isFatal); - }, - (reason: unknown) => { - logger.error( - '[ReactNativeErrorHandlers] Error while flushing the event cache after uncaught error.', - reason, - ); - }, - ); - } else { - // If in dev, we call the default handler anyway and hope the error will be sent - // Just for a better dev experience - defaultHandler(error, isFatal); - } - }); + if (__DEV__) { + // If in dev, we call the default handler anyway and hope the error will be sent + // Just for a better dev experience + defaultHandler(error, isFatal); + return; } - } - /** - * 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'); - } + void client.flush(client.getOptions().shutdownTimeout || 2000).then( + () => { + defaultHandler(error, isFatal); + }, + (reason: unknown) => { + logger.error('[ReactNativeErrorHandlers] Error while flushing the event cache after uncaught error.', reason); + }, + ); + }); } diff --git a/src/js/integrations/reactnativeerrorhandlersutils.ts b/src/js/integrations/reactnativeerrorhandlersutils.ts new file mode 100644 index 0000000000..835f77b719 --- /dev/null +++ b/src/js/integrations/reactnativeerrorhandlersutils.ts @@ -0,0 +1,93 @@ +import { logger } from '@sentry/utils'; + +import { ReactNativeLibraries } from '../utils/rnlibraries'; +import { RN_GLOBAL_OBJ } from '../utils/worldwide'; + +/** + * Polyfill the global promise instance with one we can be sure that we can attach the tracking to. + * + * In newer RN versions >=0.63, the global promise is not the same reference as the one imported from the promise library. + * This is due to a version mismatch between promise versions. + * Originally we tried a solution where we would have you put a package resolution to ensure the promise instances match. However, + * - Using a package resolution requires the you to manually troubleshoot. + * - The package resolution fix no longer works with 0.67 on iOS Hermes. + */ +export function polyfillPromise(): void { + if (!ReactNativeLibraries.Utilities) { + logger.warn('Could not polyfill Promise. React Native Libraries Utilities not found.'); + return; + } + + const Promise = getPromisePolyfill(); + + // As of RN 0.67 only done and finally are used + // eslint-disable-next-line import/no-extraneous-dependencies + require('promise/setimmediate/done'); + // eslint-disable-next-line import/no-extraneous-dependencies + require('promise/setimmediate/finally'); + + ReactNativeLibraries.Utilities.polyfillGlobal('Promise', () => Promise); +} + +/** + * Single source of truth for the Promise implementation we want to use. + * This is important for verifying that the rejected promise tracing will work as expected. + */ +export function getPromisePolyfill(): unknown { + /* eslint-disable import/no-extraneous-dependencies,@typescript-eslint/no-var-requires */ + // Below, we follow the exact way React Native initializes its promise library, and we globally replace it. + return require('promise/setimmediate/es6-extensions'); +} + +/** + * Lazy require the rejection tracking module + */ +export function requireRejectionTracking(): { + 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'); +} + +/** + * Checks if the promise is the same one or not, if not it will warn the user + */ +export function checkPromiseAndWarn(): void { + try { + // `promise` package is a dependency of react-native, therefore it is always available. + // but it is possible that the user has installed a different version of promise + // or dependency that uses a different version. + // We have to check if the React Native Promise and the `promise` package Promise are using the same reference. + // If they are not, likely there are multiple versions of the `promise` package installed. + const ReactNativePromise = ReactNativeLibraries.Promise; + // eslint-disable-next-line @typescript-eslint/no-var-requires,import/no-extraneous-dependencies + const PromisePackagePromise = require('promise/setimmediate/es6-extensions'); + const UsedPromisePolyfill = getPromisePolyfill(); + + if (ReactNativePromise !== PromisePackagePromise) { + logger.warn( + 'You appear to have multiple versions of the "promise" package installed. ' + + 'This may cause unexpected behavior like undefined `Promise.allSettled`. ' + + 'Please install the `promise` package manually using the exact version as the React Native package. ' + + 'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.', + ); + } + + // This only make sense if the user disabled the integration Polyfill + if (UsedPromisePolyfill !== RN_GLOBAL_OBJ.Promise) { + logger.warn( + 'Unhandled promise rejections will not be caught by Sentry. ' + + 'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.', + ); + } else { + logger.log('Unhandled promise rejections will be caught by Sentry.'); + } + } catch (e) { + // Do Nothing + logger.warn( + 'Unhandled promise rejections will not be caught by Sentry. ' + + 'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.', + ); + } +} diff --git a/src/js/integrations/reactnativeinfo.ts b/src/js/integrations/reactnativeinfo.ts index dc03ccac42..73e05c488e 100644 --- a/src/js/integrations/reactnativeinfo.ts +++ b/src/js/integrations/reactnativeinfo.ts @@ -1,4 +1,4 @@ -import type { Context, Event, EventHint, EventProcessor, Integration } from '@sentry/types'; +import type { Context, Event, EventHint, Integration } from '@sentry/types'; import { getExpoGoVersion, @@ -26,71 +26,64 @@ export interface ReactNativeContext extends Context { } /** Loads React Native context at runtime */ -export class ReactNativeInfo implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ReactNativeInfo'; +export const reactNativeInfoIntegration = (): Integration => { + return { + name: 'ReactNativeInfo', + setupOnce: () => { + // noop + }, + processEvent, + }; +}; - /** - * @inheritDoc - */ - public name: string = ReactNativeInfo.id; +function processEvent(event: Event, hint: EventHint): Event { + const reactNativeError = hint?.originalException ? (hint?.originalException as ReactNativeError) : undefined; - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { - addGlobalEventProcessor(async (event: Event, hint?: EventHint) => { - const reactNativeError = hint?.originalException ? (hint?.originalException as ReactNativeError) : undefined; + const reactNativeContext: ReactNativeContext = { + turbo_module: isTurboModuleEnabled(), + fabric: isFabricEnabled(), + react_native_version: getReactNativeVersion(), + expo: isExpo(), + }; - const reactNativeContext: ReactNativeContext = { - turbo_module: isTurboModuleEnabled(), - fabric: isFabricEnabled(), - react_native_version: getReactNativeVersion(), - expo: isExpo(), - }; - - if (isHermesEnabled()) { - reactNativeContext.js_engine = 'hermes'; - const hermesVersion = getHermesVersion(); - if (hermesVersion) { - reactNativeContext.hermes_version = hermesVersion; - } - reactNativeContext.hermes_debug_info = !isEventWithHermesBytecodeFrames(event); - } else if (reactNativeError?.jsEngine) { - reactNativeContext.js_engine = reactNativeError.jsEngine; - } + if (isHermesEnabled()) { + reactNativeContext.js_engine = 'hermes'; + const hermesVersion = getHermesVersion(); + if (hermesVersion) { + reactNativeContext.hermes_version = hermesVersion; + } + reactNativeContext.hermes_debug_info = !isEventWithHermesBytecodeFrames(event); + } else if (reactNativeError?.jsEngine) { + reactNativeContext.js_engine = reactNativeError.jsEngine; + } - if (reactNativeContext.js_engine === 'hermes') { - event.tags = { - hermes: 'true', - ...event.tags, - }; - } + if (reactNativeContext.js_engine === 'hermes') { + event.tags = { + hermes: 'true', + ...event.tags, + }; + } - if (reactNativeError?.componentStack) { - reactNativeContext.component_stack = reactNativeError.componentStack; - } + if (reactNativeError?.componentStack) { + reactNativeContext.component_stack = reactNativeError.componentStack; + } - const expoGoVersion = getExpoGoVersion(); - if (expoGoVersion) { - reactNativeContext.expo_go_version = expoGoVersion; - } + const expoGoVersion = getExpoGoVersion(); + if (expoGoVersion) { + reactNativeContext.expo_go_version = expoGoVersion; + } - const expoSdkVersion = getExpoSdkVersion(); - if (expoSdkVersion) { - reactNativeContext.expo_sdk_version = expoSdkVersion; - } + const expoSdkVersion = getExpoSdkVersion(); + if (expoSdkVersion) { + reactNativeContext.expo_sdk_version = expoSdkVersion; + } - event.contexts = { - react_native_context: reactNativeContext, - ...event.contexts, - }; + event.contexts = { + react_native_context: reactNativeContext, + ...event.contexts, + }; - return event; - }); - } + return event; } /** diff --git a/src/js/integrations/release.ts b/src/js/integrations/release.ts index 0ca52c12e2..88b57e1b82 100644 --- a/src/js/integrations/release.ts +++ b/src/js/integrations/release.ts @@ -1,66 +1,58 @@ -import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import type { Event, Integration } from '@sentry/types'; +import type { BaseTransportOptions, Client, ClientOptions, Event, EventHint, Integration } from '@sentry/types'; import { NATIVE } from '../wrapper'; /** Release integration responsible to load release from file. */ -export class Release implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Release'; - /** - * @inheritDoc - */ - public name: string = Release.id; - - /** - * @inheritDoc - */ - public setupOnce(): void { - addGlobalEventProcessor(async (event: Event) => { - const self = getCurrentHub().getIntegration(Release); - if (!self) { - return event; - } - - const options = getCurrentHub().getClient()?.getOptions(); +export const nativeReleaseIntegration = (): Integration => { + return { + name: 'Release', + setupOnce: () => { + // noop + }, + processEvent, + }; +}; + +async function processEvent( + event: Event, + _: EventHint, + client: Client>, +): Promise { + const options = client.getOptions(); + + /* + __sentry_release and __sentry_dist is set by the user with setRelease and setDist. If this is used then this is the strongest. + Otherwise we check for the release and dist in the options passed on init, as this is stronger than the release/dist from the native build. + */ + if (typeof event.extra?.__sentry_release === 'string') { + event.release = `${event.extra.__sentry_release}`; + } else if (typeof options?.release === 'string') { + event.release = options.release; + } - /* - __sentry_release and __sentry_dist is set by the user with setRelease and setDist. If this is used then this is the strongest. - Otherwise we check for the release and dist in the options passed on init, as this is stronger than the release/dist from the native build. - */ - if (typeof event.extra?.__sentry_release === 'string') { - event.release = `${event.extra.__sentry_release}`; - } else if (typeof options?.release === 'string') { - event.release = options.release; - } + if (typeof event.extra?.__sentry_dist === 'string') { + event.dist = `${event.extra.__sentry_dist}`; + } else if (typeof options?.dist === 'string') { + event.dist = options.dist; + } - if (typeof event.extra?.__sentry_dist === 'string') { - event.dist = `${event.extra.__sentry_dist}`; - } else if (typeof options?.dist === 'string') { - event.dist = options.dist; - } + if (event.release && event.dist) { + return event; + } - if (event.release && event.dist) { - return event; + try { + const nativeRelease = await NATIVE.fetchNativeRelease(); + if (nativeRelease) { + if (!event.release) { + event.release = `${nativeRelease.id}@${nativeRelease.version}+${nativeRelease.build}`; } - - try { - const nativeRelease = await NATIVE.fetchNativeRelease(); - if (nativeRelease) { - if (!event.release) { - event.release = `${nativeRelease.id}@${nativeRelease.version}+${nativeRelease.build}`; - } - if (!event.dist) { - event.dist = `${nativeRelease.build}`; - } - } - } catch (_Oo) { - // Something went wrong, we just continue + if (!event.dist) { + event.dist = `${nativeRelease.build}`; } - - return event; - }); + } + } catch (_Oo) { + // Something went wrong, we just continue } + + return event; } diff --git a/src/js/integrations/rewriteframes.ts b/src/js/integrations/rewriteframes.ts index 844d55b221..04170d088a 100644 --- a/src/js/integrations/rewriteframes.ts +++ b/src/js/integrations/rewriteframes.ts @@ -1,4 +1,4 @@ -import { RewriteFrames } from '@sentry/integrations'; +import { rewriteFramesIntegration } from '@sentry/integrations'; import type { Integration, StackFrame } from '@sentry/types'; import { Platform } from 'react-native'; @@ -14,7 +14,7 @@ export const IOS_DEFAULT_BUNDLE_NAME = 'app:///main.jsbundle'; * and Expo bundle postfix. */ export function createReactNativeRewriteFrames(): Integration { - return new RewriteFrames({ + return rewriteFramesIntegration({ iteratee: (frame: StackFrame) => { if (frame.platform === 'java' || frame.platform === 'cocoa') { // Because platform is not required in StackFrame type diff --git a/src/js/integrations/screenshot.ts b/src/js/integrations/screenshot.ts index a52d15276f..b3544ccb00 100644 --- a/src/js/integrations/screenshot.ts +++ b/src/js/integrations/screenshot.ts @@ -1,62 +1,32 @@ -import { getClient } from '@sentry/core'; -import type { Event, EventHint, EventProcessor, Integration } from '@sentry/types'; -import { resolvedSyncPromise } from '@sentry/utils'; +import type { Event, EventHint, Integration } from '@sentry/types'; import type { ReactNativeClient } from '../client'; import type { Screenshot as ScreenshotAttachment } from '../wrapper'; import { NATIVE } from '../wrapper'; /** Adds screenshots to error events */ -export class Screenshot implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Screenshot'; - - /** - * @inheritDoc - */ - public name: string = Screenshot.id; - - /** - * If enabled attaches a screenshot to the event hint. - * - * @deprecated Screenshots are now added in global event processor. - */ - public static attachScreenshotToEventHint( - hint: EventHint, - { attachScreenshot }: { attachScreenshot?: boolean }, - ): PromiseLike { - if (!attachScreenshot) { - return resolvedSyncPromise(hint); - } - - return NATIVE.captureScreenshot().then(screenshots => { - if (screenshots !== null && screenshots.length > 0) { - hint.attachments = [...screenshots, ...(hint?.attachments || [])]; - } - return hint; - }); +export const screenshotIntegration = (): Integration => { + return { + name: 'Screenshot', + setupOnce: () => { + // noop + }, + processEvent, + }; +}; + +async function processEvent(event: Event, hint: EventHint, client: ReactNativeClient): Promise { + const options = client.getOptions(); + + const hasException = event.exception && event.exception.values && event.exception.values.length > 0; + if (!hasException || options?.beforeScreenshot?.(event, hint) === false) { + return event; } - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (e: EventProcessor) => void): void { - const options = getClient()?.getOptions(); - - addGlobalEventProcessor(async (event: Event, hint: EventHint) => { - const hasException = event.exception && event.exception.values && event.exception.values.length > 0; - if (!hasException || options?.beforeScreenshot?.(event, hint) === false) { - return event; - } - - const screenshots: ScreenshotAttachment[] | null = await NATIVE.captureScreenshot(); - if (screenshots && screenshots.length > 0) { - hint.attachments = [...screenshots, ...(hint?.attachments || [])]; - } - - return event; - }); + const screenshots: ScreenshotAttachment[] | null = await NATIVE.captureScreenshot(); + if (screenshots && screenshots.length > 0) { + hint.attachments = [...screenshots, ...(hint?.attachments || [])]; } + + return event; } diff --git a/src/js/integrations/sdkinfo.ts b/src/js/integrations/sdkinfo.ts index 85c8628291..65f5656856 100644 --- a/src/js/integrations/sdkinfo.ts +++ b/src/js/integrations/sdkinfo.ts @@ -1,4 +1,4 @@ -import type { EventProcessor, Integration, Package, SdkInfo as SdkInfoType } from '@sentry/types'; +import type { Event, Integration, Package, SdkInfo as SdkInfoType } from '@sentry/types'; import { logger } from '@sentry/utils'; import { isExpoGo, notWeb } from '../utils/environment'; @@ -19,50 +19,55 @@ export const defaultSdkInfo: DefaultSdkInfo = { }; /** Default SdkInfo instrumentation */ -export class SdkInfo implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'SdkInfo'; +export const sdkInfoIntegration = (): Integration => { + const fetchNativeSdkInfo = createCachedFetchNativeSdkInfo(); - /** - * @inheritDoc - */ - public name: string = SdkInfo.id; + return { + name: 'SdkInfo', + setupOnce: () => { + // noop + }, + processEvent: (event: Event) => processEvent(event, fetchNativeSdkInfo), + }; +}; - private _nativeSdkPackage: Package | null = null; +async function processEvent(event: Event, fetchNativeSdkInfo: () => Promise): Promise { + const nativeSdkPackage = await fetchNativeSdkInfo(); - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (e: EventProcessor) => void): void { - addGlobalEventProcessor(async event => { - // this._nativeSdkInfo should be defined a following time so this call won't always be awaited. - if (this._nativeSdkPackage === null) { - try { - this._nativeSdkPackage = await NATIVE.fetchNativeSdkInfo(); - } catch (e) { - // If this fails, go ahead as usual as we would rather have the event be sent with a package missing. - if (notWeb() && !isExpoGo()) { - logger.warn( - '[SdkInfo] Native SDK Info retrieval failed...something could be wrong with your Sentry installation:', - ); - logger.warn(e); - } - } - } + event.platform = event.platform || 'javascript'; + event.sdk = event.sdk || {}; + event.sdk.name = event.sdk.name || defaultSdkInfo.name; + event.sdk.version = event.sdk.version || defaultSdkInfo.version; + event.sdk.packages = [ + // default packages are added by baseclient and should not be added here + ...(event.sdk.packages || []), + ...((nativeSdkPackage && [nativeSdkPackage]) || []), + ]; - event.platform = event.platform || 'javascript'; - event.sdk = event.sdk || {}; - event.sdk.name = event.sdk.name || defaultSdkInfo.name; - event.sdk.version = event.sdk.version || defaultSdkInfo.version; - event.sdk.packages = [ - // default packages are added by baseclient and should not be added here - ...(event.sdk.packages || []), - ...((this._nativeSdkPackage && [this._nativeSdkPackage]) || []), - ]; + return event; +} - return event; - }); +function createCachedFetchNativeSdkInfo(): () => Promise { + if (!notWeb() || isExpoGo()) { + return () => { + return Promise.resolve(null); + }; } + + return async () => { + let isCached: boolean = false; + let nativeSdkPackageCache: Package | null = null; + if (isCached) { + return nativeSdkPackageCache; + } + + try { + nativeSdkPackageCache = await NATIVE.fetchNativeSdkInfo(); + isCached = true; + } catch (e) { + logger.warn('Could not fetch native sdk info.', e); + } + + return nativeSdkPackageCache; + }; } diff --git a/src/js/integrations/spotlight.ts b/src/js/integrations/spotlight.ts index 156c975490..ea1f51214a 100644 --- a/src/js/integrations/spotlight.ts +++ b/src/js/integrations/spotlight.ts @@ -1,4 +1,4 @@ -import type { Client, Envelope, EventProcessor, Integration } from '@sentry/types'; +import type { BaseTransportOptions, Client, ClientOptions, Envelope, Integration } from '@sentry/types'; import { logger, serializeEnvelope } from '@sentry/utils'; import { makeUtf8TextEncoder } from '../transports/TextEncoder'; @@ -28,13 +28,12 @@ export function Spotlight({ return { name: 'Spotlight', - setupOnce(_: (callback: EventProcessor) => void, getCurrentHub) { - const client = getCurrentHub().getClient(); - if (client) { - setup(client, sidecarUrl); - } else { - logger.warn('[Spotlight] Could not initialize Sidecar integration due to missing Client'); - } + setupOnce(): void { + // nothing to do here + }, + + setup(client: Client>): void { + setup(client, sidecarUrl); }, }; } diff --git a/src/js/integrations/viewhierarchy.ts b/src/js/integrations/viewhierarchy.ts index e84a113c63..490ed29598 100644 --- a/src/js/integrations/viewhierarchy.ts +++ b/src/js/integrations/viewhierarchy.ts @@ -1,55 +1,47 @@ -import type { Event, EventHint, EventProcessor, Integration } from '@sentry/types'; -import type { AttachmentType } from '@sentry/types/types/attachment'; +import type { Attachment, Event, EventHint, Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; import { NATIVE } from '../wrapper'; -/** Adds ViewHierarchy to error events */ -export class ViewHierarchy implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ViewHierarchy'; - - private static _fileName: string = 'view-hierarchy.json'; - private static _contentType: string = 'application/json'; - private static _attachmentType: AttachmentType = 'event.view_hierarchy' as AttachmentType; - - /** - * @inheritDoc - */ - public name: string = ViewHierarchy.id; - - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (e: EventProcessor) => void): void { - addGlobalEventProcessor(async (event: Event, hint: EventHint) => { - const hasException = event.exception && event.exception.values && event.exception.values.length > 0; - if (!hasException) { - return event; - } +const filename: string = 'view-hierarchy.json'; +const contentType: string = 'application/json'; +const attachmentType = 'event.view_hierarchy' as Attachment['attachmentType']; - let viewHierarchy: Uint8Array | null = null; - try { - viewHierarchy = await NATIVE.fetchViewHierarchy(); - } catch (e) { - logger.error('Failed to get view hierarchy from native.', e); - } +/** Adds ViewHierarchy to error events */ +export const viewHierarchyIntegration = (): Integration => { + return { + name: 'ViewHierarchy', + setupOnce: () => { + // noop + }, + processEvent, + }; +}; + +async function processEvent(event: Event, hint: EventHint): Promise { + const hasException = event.exception && event.exception.values && event.exception.values.length > 0; + if (!hasException) { + return event; + } - if (viewHierarchy) { - hint.attachments = [ - { - filename: ViewHierarchy._fileName, - contentType: ViewHierarchy._contentType, - attachmentType: ViewHierarchy._attachmentType, - data: viewHierarchy, - }, - ...(hint?.attachments || []), - ]; - } + let viewHierarchy: Uint8Array | null = null; + try { + viewHierarchy = await NATIVE.fetchViewHierarchy(); + } catch (e) { + logger.error('Failed to get view hierarchy from native.', e); + } - return event; - }); + if (viewHierarchy) { + hint.attachments = [ + { + filename, + contentType, + attachmentType, + data: viewHierarchy, + }, + ...(hint?.attachments || []), + ]; } + + return event; } diff --git a/test/integrations/debugsymbolicator.test.ts b/test/integrations/debugsymbolicator.test.ts index ab1465d8a0..035fa63fe2 100644 --- a/test/integrations/debugsymbolicator.test.ts +++ b/test/integrations/debugsymbolicator.test.ts @@ -1,37 +1,32 @@ -import type { Event, EventHint, Hub, Integration, StackFrame } from '@sentry/types'; +jest.mock('../../src/js/integrations/debugsymbolicatorutils'); -import { DebugSymbolicator } from '../../src/js/integrations/debugsymbolicator'; +import type { Client, Event, EventHint, StackFrame } from '@sentry/types'; + +import { debugSymbolicatorIntegration } from '../../src/js/integrations/debugsymbolicator'; +import { + fetchSourceContext, + getDevServer, + parseErrorStack, + symbolicateStackTrace, +} from '../../src/js/integrations/debugsymbolicatorutils'; 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]>; +async function processEvent(mockedEvent: Event, mockedHint: EventHint): Promise { + return debugSymbolicatorIntegration().processEvent!(mockedEvent, mockedHint, {} as Client); } 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( + (parseErrorStack as jest.Mock).mockReturnValue([]); + (symbolicateStackTrace as jest.Mock).mockReturnValue( Promise.resolve({ stack: [], }), ); - integration._getDevServer = jest.fn().mockReturnValue({ + (getDevServer as jest.Mock).mockReturnValue({ url: 'http://localhost:8081', }); - integration._fetchSourceContext = jest.fn().mockReturnValue(Promise.resolve(null)); + (fetchSourceContext as jest.Mock).mockReturnValue(Promise.resolve(null)); }); describe('parse stack', () => { @@ -60,7 +55,7 @@ describe('Debug Symbolicator Integration', () => { ]; beforeEach(() => { - integration._parseErrorStack = jest.fn().mockReturnValue(>[ + (parseErrorStack as jest.Mock).mockReturnValue(>[ { file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', lineNumber: 1, @@ -75,7 +70,7 @@ describe('Debug Symbolicator Integration', () => { }, ]); - integration._symbolicateStackTrace = jest.fn().mockReturnValue( + (symbolicateStackTrace as jest.Mock).mockReturnValue( Promise.resolve({ stack: [ { @@ -96,7 +91,7 @@ describe('Debug Symbolicator Integration', () => { }); it('should symbolicate errors stack trace', async () => { - const symbolicatedEvent = await executeIntegrationFor( + const symbolicatedEvent = await processEvent( { exception: { values: [ @@ -148,7 +143,7 @@ describe('Debug Symbolicator Integration', () => { }); it('should symbolicate synthetic error stack trace for exception', async () => { - const symbolicatedEvent = await executeIntegrationFor( + const symbolicatedEvent = await processEvent( { exception: { values: [ @@ -201,7 +196,7 @@ describe('Debug Symbolicator Integration', () => { }); it('should symbolicate synthetic error stack trace for message', async () => { - const symbolicatedEvent = await executeIntegrationFor( + const symbolicatedEvent = await processEvent( { threads: { values: [ @@ -249,7 +244,7 @@ describe('Debug Symbolicator Integration', () => { }); it('skips first frame (callee) for exception', async () => { - const symbolicatedEvent = await executeIntegrationFor( + const symbolicatedEvent = await processEvent( { exception: { values: [ @@ -297,7 +292,7 @@ describe('Debug Symbolicator Integration', () => { }); it('skips first frame (callee) for message', async () => { - const symbolicatedEvent = await executeIntegrationFor( + const symbolicatedEvent = await processEvent( { threads: { values: [ @@ -340,21 +335,4 @@ describe('Debug Symbolicator Integration', () => { }); }); }); - - 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); - }); - } }); diff --git a/test/integrations/devicecontext.test.ts b/test/integrations/devicecontext.test.ts index a2644ca58a..3bc120c7fa 100644 --- a/test/integrations/devicecontext.test.ts +++ b/test/integrations/devicecontext.test.ts @@ -1,7 +1,6 @@ -import type { Hub } from '@sentry/core'; -import type { Event, SeverityLevel } from '@sentry/types'; +import type { Client, Event, EventHint, SeverityLevel } from '@sentry/types'; -import { DeviceContext } from '../../src/js/integrations'; +import { deviceContextIntegration } from '../../src/js/integrations'; import type { NativeDeviceContextsResponse } from '../../src/js/NativeRNSentry'; import { NATIVE } from '../../src/js/wrapper'; @@ -15,20 +14,9 @@ jest.mock('react-native', () => ({ })); describe('Device Context Integration', () => { - let integration: DeviceContext; - - const mockGetCurrentHub = () => - ({ - getIntegration: () => integration, - } as unknown as Hub); - - beforeEach(() => { - integration = new DeviceContext(); - }); - it('add native user', async () => { ( - await executeIntegrationWith({ + await processEventWith({ nativeContexts: { user: { id: 'native-user' } }, }) ).expectEvent.toStrictEqualToNativeContexts(); @@ -36,7 +24,7 @@ describe('Device Context Integration', () => { it('do not overwrite event user', async () => { ( - await executeIntegrationWith({ + await processEventWith({ nativeContexts: { user: { id: 'native-user' } }, mockEvent: { user: { id: 'event-user' } }, }) @@ -45,7 +33,7 @@ describe('Device Context Integration', () => { it('do not overwrite event app context', async () => { ( - await executeIntegrationWith({ + await processEventWith({ nativeContexts: { app: { view_names: ['native view'] } }, mockEvent: { contexts: { app: { view_names: ['Home'] } } }, }) @@ -53,7 +41,7 @@ describe('Device Context Integration', () => { }); it('merge event context app', async () => { - const { processedEvent } = await executeIntegrationWith({ + const { processedEvent } = await processEventWith({ nativeContexts: { contexts: { app: { native: 'value' } } }, mockEvent: { contexts: { app: { event_app: 'value' } } }, }); @@ -68,7 +56,7 @@ describe('Device Context Integration', () => { }); it('merge event context app even when event app doesnt exist', async () => { - const { processedEvent } = await executeIntegrationWith({ + const { processedEvent } = await processEventWith({ nativeContexts: { contexts: { app: { native: 'value' } } }, mockEvent: { contexts: { keyContext: { key: 'value' } } }, }); @@ -85,7 +73,7 @@ describe('Device Context Integration', () => { }); it('merge event and native contexts', async () => { - const { processedEvent } = await executeIntegrationWith({ + const { processedEvent } = await processEventWith({ nativeContexts: { contexts: { duplicate: { context: 'native-value' }, native: { context: 'value' } } }, mockEvent: { contexts: { duplicate: { context: 'event-value' }, event: { context: 'value' } } }, }); @@ -99,7 +87,7 @@ describe('Device Context Integration', () => { }); it('merge native tags', async () => { - const { processedEvent } = await executeIntegrationWith({ + const { processedEvent } = await processEventWith({ nativeContexts: { tags: { duplicate: 'native-tag', native: 'tag' } }, mockEvent: { tags: { duplicate: 'event-tag', event: 'tag' } }, }); @@ -113,7 +101,7 @@ describe('Device Context Integration', () => { }); it('merge native extra', async () => { - const { processedEvent } = await executeIntegrationWith({ + const { processedEvent } = await processEventWith({ nativeContexts: { extra: { duplicate: 'native-extra', native: 'extra' } }, mockEvent: { extra: { duplicate: 'event-extra', event: 'extra' } }, }); @@ -127,7 +115,7 @@ describe('Device Context Integration', () => { }); it('merge fingerprints', async () => { - const { processedEvent } = await executeIntegrationWith({ + const { processedEvent } = await processEventWith({ nativeContexts: { fingerprint: ['duplicate-fingerprint', 'native-fingerprint'] }, mockEvent: { fingerprint: ['duplicate-fingerprint', 'event-fingerprint'] }, }); @@ -138,7 +126,7 @@ describe('Device Context Integration', () => { it('add native level', async () => { ( - await executeIntegrationWith({ + await processEventWith({ nativeContexts: { level: 'fatal' }, }) ).expectEvent.toStrictEqualToNativeContexts(); @@ -146,7 +134,7 @@ describe('Device Context Integration', () => { it('do not overwrite event level', async () => { ( - await executeIntegrationWith({ + await processEventWith({ nativeContexts: { level: 'native-level' }, mockEvent: { level: 'info' }, }) @@ -155,7 +143,7 @@ describe('Device Context Integration', () => { it('add native environment', async () => { ( - await executeIntegrationWith({ + await processEventWith({ nativeContexts: { environment: 'native-environment' }, }) ).expectEvent.toStrictEqualToNativeContexts(); @@ -163,7 +151,7 @@ describe('Device Context Integration', () => { it('do not overwrite event environment', async () => { ( - await executeIntegrationWith({ + await processEventWith({ nativeContexts: { environment: 'native-environment' }, mockEvent: { environment: 'event-environment' }, }) @@ -171,7 +159,7 @@ describe('Device Context Integration', () => { }); it('use only native breadcrumbs', async () => { - const { processedEvent } = await executeIntegrationWith({ + const { processedEvent } = await processEventWith({ nativeContexts: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'native-breadcrumb' }] }, mockEvent: { breadcrumbs: [{ message: 'duplicate-breadcrumb' }, { message: 'event-breadcrumb' }] }, }); @@ -182,7 +170,7 @@ describe('Device Context Integration', () => { it('adds in_foreground false to native app contexts', async () => { mockCurrentAppState = 'background'; - const { processedEvent } = await executeIntegrationWith({ + const { processedEvent } = await processEventWith({ nativeContexts: { contexts: { app: { native: 'value' } } }, }); expect(processedEvent).toStrictEqual({ @@ -197,7 +185,7 @@ describe('Device Context Integration', () => { it('adds in_foreground to native app contexts', async () => { mockCurrentAppState = 'active'; - const { processedEvent } = await executeIntegrationWith({ + const { processedEvent } = await processEventWith({ nativeContexts: { contexts: { app: { native: 'value' } } }, }); expect(processedEvent).toStrictEqual({ @@ -212,7 +200,7 @@ describe('Device Context Integration', () => { it('do not add in_foreground if unknown', async () => { mockCurrentAppState = 'unknown'; - const { processedEvent } = await executeIntegrationWith({ + const { processedEvent } = await processEventWith({ nativeContexts: { contexts: { app: { native: 'value' } } }, }); expect(processedEvent).toStrictEqual({ @@ -223,45 +211,36 @@ describe('Device Context Integration', () => { }, }); }); +}); - async function executeIntegrationWith({ - nativeContexts, - mockEvent, - }: { - nativeContexts: Record; - mockEvent?: Event; - }): Promise<{ - processedEvent: Event | null; +async function processEventWith({ + nativeContexts, + mockEvent, +}: { + nativeContexts: Record; + mockEvent?: Event; +}): Promise<{ + processedEvent: Event | null; + expectEvent: { + toStrictEqualToNativeContexts: () => void; + toStrictEqualMockEvent: () => void; + }; +}> { + (NATIVE.fetchNativeDeviceContexts as jest.MockedFunction).mockImplementation( + () => Promise.resolve(nativeContexts as NativeDeviceContextsResponse), + ); + const originalNativeContexts = { ...nativeContexts }; + const originalMockEvent = { ...mockEvent }; + const processedEvent = await processEvent(mockEvent ?? {}); + return { + processedEvent, expectEvent: { - toStrictEqualToNativeContexts: () => void; - toStrictEqualMockEvent: () => void; - }; - }> { - ( - NATIVE.fetchNativeDeviceContexts as jest.MockedFunction - ).mockImplementation(() => Promise.resolve(nativeContexts as NativeDeviceContextsResponse)); - const originalNativeContexts = { ...nativeContexts }; - const originalMockEvent = { ...mockEvent }; - const processedEvent = await executeIntegrationFor(mockEvent ?? {}); - return { - processedEvent, - expectEvent: { - toStrictEqualToNativeContexts: () => expect(processedEvent).toStrictEqual(originalNativeContexts), - toStrictEqualMockEvent: () => expect(processedEvent).toStrictEqual(originalMockEvent), - }, - }; - } - - function executeIntegrationFor(mockedEvent: Event): Promise { - return new Promise((resolve, reject) => { - integration.setupOnce(async eventProcessor => { - try { - const processedEvent = await eventProcessor(mockedEvent, {}); - resolve(processedEvent); - } catch (e) { - reject(e); - } - }, mockGetCurrentHub); - }); - } -}); + toStrictEqualToNativeContexts: () => expect(processedEvent).toStrictEqual(originalNativeContexts), + toStrictEqualMockEvent: () => expect(processedEvent).toStrictEqual(originalMockEvent), + }, + }; +} + +function processEvent(mockedEvent: Event): Event | null | PromiseLike { + return deviceContextIntegration().processEvent!(mockedEvent, {} as EventHint, {} as Client); +} diff --git a/test/integrations/eventorigin.test.ts b/test/integrations/eventorigin.test.ts index faae0f56e6..b359929fdb 100644 --- a/test/integrations/eventorigin.test.ts +++ b/test/integrations/eventorigin.test.ts @@ -1,30 +1,13 @@ -import type { Event } from '@sentry/types'; +import type { Client } from '@sentry/types'; -import { EventOrigin } from '../../src/js/integrations'; +import { eventOriginIntegration } from '../../src/js/integrations'; describe('Event Origin', () => { - it('Adds event.origin and event.environment javascript tags to events', done => { - const integration = new EventOrigin(); + it('Adds event.origin and event.environment javascript tags to events', async () => { + const integration = eventOriginIntegration(); - const mockEvent: Event = {}; - - integration.setupOnce(async eventProcessor => { - try { - const processedEvent = await eventProcessor(mockEvent, {}); - - expect(processedEvent).toBeDefined(); - if (processedEvent) { - expect(processedEvent.tags).toBeDefined(); - if (processedEvent.tags) { - expect(processedEvent.tags['event.origin']).toBe('javascript'); - expect(processedEvent.tags['event.environment']).toBe('javascript'); - } - } - - done(); - } catch (e) { - done(e); - } - }); + const processedEvent = await integration.processEvent!({}, {}, {} as Client); + expect(processedEvent?.tags?.['event.origin']).toBe('javascript'); + expect(processedEvent?.tags?.['event.environment']).toBe('javascript'); }); }); diff --git a/test/integrations/expocontext.test.ts b/test/integrations/expocontext.test.ts index 95f24095d5..7b449269f9 100644 --- a/test/integrations/expocontext.test.ts +++ b/test/integrations/expocontext.test.ts @@ -1,23 +1,11 @@ -import type { Hub } from '@sentry/core'; -import type { Event } from '@sentry/types'; +import type { Client, Event } from '@sentry/types'; -import { ExpoContext } from '../../src/js/integrations/expocontext'; +import { expoContextIntegration } from '../../src/js/integrations/expocontext'; import { getExpoDevice } from '../../src/js/utils/expomodules'; jest.mock('../../src/js/utils/expomodules'); describe('Expo Context Integration', () => { - let integration: ExpoContext; - - const mockGetCurrentHub = () => - ({ - getIntegration: () => integration, - } as unknown as Hub); - - beforeEach(() => { - integration = new ExpoContext(); - }); - it('does not add device context because expo device module is not available', async () => { (getExpoDevice as jest.Mock).mockReturnValue(undefined); const actualEvent = await executeIntegrationFor({}); @@ -112,16 +100,7 @@ describe('Expo Context Integration', () => { }); }); - function executeIntegrationFor(mockedEvent: Event): Promise { - return new Promise((resolve, reject) => { - integration.setupOnce(async eventProcessor => { - try { - const processedEvent = await eventProcessor(mockedEvent, {}); - resolve(processedEvent); - } catch (e) { - reject(e); - } - }, mockGetCurrentHub); - }); + function executeIntegrationFor(mockedEvent: Event): Event { + return expoContextIntegration().processEvent!(mockedEvent, {}, {} as Client) as Event; } }); diff --git a/test/integrations/integrationsexecutionorder.test.ts b/test/integrations/integrationsexecutionorder.test.ts index 7a9e9f5def..bd003eae7a 100644 --- a/test/integrations/integrationsexecutionorder.test.ts +++ b/test/integrations/integrationsexecutionorder.test.ts @@ -31,7 +31,7 @@ describe('Integration execution order', () => { client.setupIntegrations(); client.captureException(new Error('test')); - jest.runAllTimers(); + await client.flush(); expect(nativeLinkedErrors.preprocessEvent).toHaveBeenCalledBefore(rewriteFrames.processEvent!); }); @@ -56,7 +56,7 @@ describe('Integration execution order', () => { client.setupIntegrations(); client.captureException(new Error('test')); - jest.runAllTimers(); + await client.flush(); expect(linkedErrors.preprocessEvent).toHaveBeenCalledBefore(rewriteFrames.processEvent!); }); diff --git a/test/integrations/modulesloader.test.ts b/test/integrations/modulesloader.test.ts index f4315b8ad1..61edd558a8 100644 --- a/test/integrations/modulesloader.test.ts +++ b/test/integrations/modulesloader.test.ts @@ -1,17 +1,11 @@ -import type { Event, EventHint } from '@sentry/types'; +import type { Client, Event, EventHint } from '@sentry/types'; -import { ModulesLoader } from '../../src/js/integrations'; +import { modulesLoaderIntegration } from '../../src/js/integrations'; import { NATIVE } from '../../src/js/wrapper'; jest.mock('../../src/js/wrapper'); describe('Modules Loader', () => { - let integration: ModulesLoader; - - beforeEach(() => { - integration = new ModulesLoader(); - }); - it('integration event processor does not throw on native error', async () => { (NATIVE.fetchModules as jest.Mock).mockImplementation(() => { throw new Error('Test Error'); @@ -46,16 +40,11 @@ describe('Modules Loader', () => { }); }); - function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint = {}): Promise { - return new Promise((resolve, reject) => { - integration.setupOnce(async eventProcessor => { - try { - const processedEvent = await eventProcessor(mockedEvent, mockedHint); - resolve(processedEvent); - } catch (e) { - reject(e); - } - }); - }); + function executeIntegrationFor( + mockedEvent: Event, + mockedHint: EventHint = {}, + ): Event | null | PromiseLike { + const integration = modulesLoaderIntegration(); + return integration.processEvent!(mockedEvent, mockedHint, {} as Client); } }); diff --git a/test/integrations/nativelinkederrors.test.ts b/test/integrations/nativelinkederrors.test.ts index 9303255d43..3f1781fd40 100644 --- a/test/integrations/nativelinkederrors.test.ts +++ b/test/integrations/nativelinkederrors.test.ts @@ -1,7 +1,7 @@ import { defaultStackParser } from '@sentry/browser'; import type { Client, DebugImage, Event, EventHint, ExtendedError } from '@sentry/types'; -import { NativeLinkedErrors } from '../../src/js/integrations/nativelinkederrors'; +import { nativeLinkedErrorsIntegration } from '../../src/js/integrations/nativelinkederrors'; import type { NativeStackFrames } from '../../src/js/NativeRNSentry'; import { NATIVE } from '../../src/js/wrapper'; @@ -267,7 +267,7 @@ describe('NativeLinkedErrors', () => { }, ); - expect(NATIVE.fetchNativePackageName).toBeCalledTimes(1); + expect(NATIVE.fetchNativePackageName).toBeCalledTimes(0); // not need for iOS expect(NATIVE.fetchNativeStackFramesBy).toBeCalledTimes(1); expect(NATIVE.fetchNativeStackFramesBy).toBeCalledWith([6446871344, 6442783348, 4350761216]); expect(actualEvent).toEqual( @@ -346,8 +346,8 @@ function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint): Event }), } as unknown as Client; - const integration = new NativeLinkedErrors(); - integration.preprocessEvent(mockedEvent, mockedHint, mockedClient); + const integration = nativeLinkedErrorsIntegration(); + integration.preprocessEvent!(mockedEvent, mockedHint, mockedClient); return mockedEvent; } diff --git a/test/integrations/reactnativeerrorhandlers.test.ts b/test/integrations/reactnativeerrorhandlers.test.ts index 59dcd35c55..66249b3966 100644 --- a/test/integrations/reactnativeerrorhandlers.test.ts +++ b/test/integrations/reactnativeerrorhandlers.test.ts @@ -1,23 +1,24 @@ -import { setCurrentClient } from '@sentry/core'; -import type { ExtendedError, Integration, SeverityLevel } from '@sentry/types'; +jest.mock('../../src/js/integrations/reactnativeerrorhandlersutils'); -import { ReactNativeErrorHandlers } from '../../src/js/integrations/reactnativeerrorhandlers'; -import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; +import { type Hub, setCurrentClient } from '@sentry/core'; +import type { ExtendedError, SeverityLevel } from '@sentry/types'; -interface MockedReactNativeErrorHandlers extends Integration { - _loadRejectionTracking: jest.Mock< - { - disable: jest.Mock; - enable: jest.Mock; - }, - [] - >; -} +import { reactNativeErrorHandlersIntegration } from '../../src/js/integrations/reactnativeerrorhandlers'; +import { requireRejectionTracking } from '../../src/js/integrations/reactnativeerrorhandlersutils'; +import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; describe('ReactNativeErrorHandlers', () => { let client: TestClient; + let mockDisable: jest.Mock; + let mockEnable: jest.Mock; beforeEach(() => { + mockDisable = jest.fn(); + mockEnable = jest.fn(); + (requireRejectionTracking as jest.Mock).mockReturnValue({ + disable: mockDisable, + enable: mockEnable, + }); ErrorUtils.getGlobalHandler = () => jest.fn(); client = new TestClient(getDefaultTestClientOptions()); @@ -39,9 +40,12 @@ describe('ReactNativeErrorHandlers', () => { errorHandlerCallback = _callback as typeof errorHandlerCallback; }); - const integration = new ReactNativeErrorHandlers(); + const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!( + () => {}, + () => ({} as Hub), + ); expect(ErrorUtils.setGlobalHandler).toHaveBeenCalledWith(errorHandlerCallback); }); @@ -80,14 +84,11 @@ describe('ReactNativeErrorHandlers', () => { describe('onUnhandledRejection', () => { test('unhandled rejected promise is captured with synthetical error', async () => { - 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 integration = reactNativeErrorHandlersIntegration(); + integration.setupOnce!( + () => {}, + () => ({} as Hub), + ); const [actualTrackingOptions] = mockEnable.mock.calls[0] || []; actualTrackingOptions?.onUnhandled?.(1, 'Test Error'); @@ -108,14 +109,11 @@ describe('ReactNativeErrorHandlers', () => { }); test('error like unhandled rejected promise is captured without synthetical error', async () => { - 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 integration = reactNativeErrorHandlersIntegration(); + integration.setupOnce!( + () => {}, + () => ({} as Hub), + ); const [actualTrackingOptions] = mockEnable.mock.calls[0] || []; actualTrackingOptions?.onUnhandled?.(1, new Error('Test Error')); diff --git a/test/integrations/reactnativeinfo.test.ts b/test/integrations/reactnativeinfo.test.ts index 0c27f88f32..2b6819e152 100644 --- a/test/integrations/reactnativeinfo.test.ts +++ b/test/integrations/reactnativeinfo.test.ts @@ -1,8 +1,8 @@ -import type { Event, EventHint } from '@sentry/types'; +import type { Client, Event, EventHint } from '@sentry/types'; import type { ReactNativeError } from '../../src/js/integrations/debugsymbolicator'; import type { ReactNativeContext } from '../../src/js/integrations/reactnativeinfo'; -import { ReactNativeInfo } from '../../src/js/integrations/reactnativeinfo'; +import { reactNativeInfoIntegration } from '../../src/js/integrations/reactnativeinfo'; let mockedIsHermesEnabled: jest.Mock; let mockedIsTurboModuleEnabled: jest.Mock; @@ -269,16 +269,7 @@ function expectMocksToBeCalledOnce() { expect(mockedGetExpoSdkVersion).toBeCalledTimes(1); } -function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint): Promise { - const integration = new ReactNativeInfo(); - return new Promise((resolve, reject) => { - integration.setupOnce(async eventProcessor => { - try { - const processedEvent = await eventProcessor(mockedEvent, mockedHint); - resolve(processedEvent); - } catch (e) { - reject(e); - } - }); - }); +function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint): Event | null | PromiseLike { + const integration = reactNativeInfoIntegration(); + return integration.processEvent!(mockedEvent, mockedHint, {} as Client); } diff --git a/test/integrations/release.test.ts b/test/integrations/release.test.ts index fc543a9ad1..8a5a3e4240 100644 --- a/test/integrations/release.test.ts +++ b/test/integrations/release.test.ts @@ -1,30 +1,10 @@ -import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import type { EventProcessor } from '@sentry/types'; +import type { Client } from '@sentry/types'; -import { Release } from '../../src/js/integrations/release'; - -const mockRelease = Release; - -jest.mock('@sentry/core', () => { - const client = { - getOptions: jest.fn(), - }; - - const hub = { - getClient: () => client, - // out-of-scope variables have to be prefixed with `mock` caseSensitive - getIntegration: () => mockRelease, - }; - - return { - addGlobalEventProcessor: jest.fn(), - getCurrentHub: () => hub, - }; -}); +import { nativeReleaseIntegration } from '../../src/js/integrations/release'; jest.mock('../../src/js/wrapper', () => ({ NATIVE: { - fetchNativeRelease: async () => ({ + fetchNativeRelease: () => ({ build: 'native_build', id: 'native_id', version: 'native_version', @@ -34,116 +14,51 @@ jest.mock('../../src/js/wrapper', () => ({ describe('Tests the Release integration', () => { test('Uses release from native SDK if release/dist are not present in options.', async () => { - const releaseIntegration = new Release(); - - let eventProcessor: EventProcessor = () => null; - - // @ts-expect-error Mock - addGlobalEventProcessor.mockImplementation(e => (eventProcessor = e)); - releaseIntegration.setupOnce(); - - expect(addGlobalEventProcessor).toBeCalled(); + const releaseIntegration = nativeReleaseIntegration(); - const client = getCurrentHub().getClient(); - - // @ts-expect-error Mock - client.getOptions.mockImplementation(() => ({})); - - const event = await eventProcessor({}, {}); + const event = await releaseIntegration.processEvent!({}, {}, { getOptions: () => ({}) } as Client); expect(event?.release).toBe('native_id@native_version+native_build'); expect(event?.dist).toBe('native_build'); }); test('Uses release from native SDK if release is not present in options.', async () => { - const releaseIntegration = new Release(); - - let eventProcessor: EventProcessor = () => null; - - // @ts-expect-error Mock - addGlobalEventProcessor.mockImplementation(e => (eventProcessor = e)); - releaseIntegration.setupOnce(); + const releaseIntegration = nativeReleaseIntegration(); - const client = getCurrentHub().getClient(); - - // @ts-expect-error Mock - client.getOptions.mockImplementation(() => ({ - dist: 'options_dist', - })); - - const event = await eventProcessor({}, {}); + const event = await releaseIntegration.processEvent!({}, {}, { + getOptions: () => ({ dist: 'options_dist' }), + } as Client); expect(event?.release).toBe('native_id@native_version+native_build'); expect(event?.dist).toBe('options_dist'); }); test('Uses dist from native SDK if dist is not present in options.', async () => { - const releaseIntegration = new Release(); - - let eventProcessor: EventProcessor = () => null; + const releaseIntegration = nativeReleaseIntegration(); - // @ts-expect-error Mock - addGlobalEventProcessor.mockImplementation(e => (eventProcessor = e)); - releaseIntegration.setupOnce(); - - const client = getCurrentHub().getClient(); - - // @ts-expect-error Mock - client.getOptions.mockImplementation(() => ({ - release: 'options_release', - })); - - const event = await eventProcessor({}, {}); + const event = await releaseIntegration.processEvent!({}, {}, { + getOptions: () => ({ release: 'options_release' }), + } as Client); expect(event?.release).toBe('options_release'); expect(event?.dist).toBe('native_build'); }); test('Uses release and dist from options', async () => { - const releaseIntegration = new Release(); - - let eventProcessor: EventProcessor = () => null; - - // @ts-expect-error Mock - addGlobalEventProcessor.mockImplementation(e => (eventProcessor = e)); - releaseIntegration.setupOnce(); - - expect(addGlobalEventProcessor).toBeCalled(); - - const client = getCurrentHub().getClient(); + const releaseIntegration = nativeReleaseIntegration(); - // @ts-expect-error Mock - client.getOptions.mockImplementation(() => ({ - dist: 'options_dist', - release: 'options_release', - })); - - const event = await eventProcessor({}, {}); + const event = await releaseIntegration.processEvent!({}, {}, { + getOptions: () => ({ dist: 'options_dist', release: 'options_release' }), + } as Client); expect(event?.release).toBe('options_release'); expect(event?.dist).toBe('options_dist'); }); test('Uses __sentry_release and __sentry_dist over everything else.', async () => { - const releaseIntegration = new Release(); - - let eventProcessor: EventProcessor = () => null; - - // @ts-expect-error Mock - addGlobalEventProcessor.mockImplementation(e => (eventProcessor = e)); - releaseIntegration.setupOnce(); - - expect(addGlobalEventProcessor).toBeCalled(); + const releaseIntegration = nativeReleaseIntegration(); - const client = getCurrentHub().getClient(); - - // @ts-expect-error Mock - client.getOptions.mockImplementation(() => ({ - dist: 'options_dist', - release: 'options_release', - })); - - const event = await eventProcessor( + const event = await releaseIntegration.processEvent!( { extra: { __sentry_dist: 'sentry_dist', @@ -151,6 +66,9 @@ describe('Tests the Release integration', () => { }, }, {}, + { + getOptions: () => ({ dist: 'options_dist' }), + } as Client, ); expect(event?.release).toBe('sentry_release'); diff --git a/test/integrations/sdkinfo.test.ts b/test/integrations/sdkinfo.test.ts index c4eeff1386..38ee3f8097 100644 --- a/test/integrations/sdkinfo.test.ts +++ b/test/integrations/sdkinfo.test.ts @@ -1,7 +1,7 @@ import type { Event, EventHint, Package } from '@sentry/types'; import { SDK_NAME, SDK_VERSION } from '../../src/js'; -import { SdkInfo } from '../../src/js/integrations'; +import { sdkInfoIntegration } from '../../src/js/integrations'; import { NATIVE } from '../../src/js/wrapper'; let mockedFetchNativeSdkInfo: jest.Mock, []>; @@ -36,7 +36,7 @@ describe('Sdk Info', () => { it('Adds native package and javascript platform to event on iOS', async () => { mockedFetchNativeSdkInfo = jest.fn().mockResolvedValue(mockCocoaPackage); const mockEvent: Event = {}; - const processedEvent = await executeIntegrationFor(mockEvent); + const processedEvent = await processEvent(mockEvent); expect(processedEvent?.sdk?.packages).toEqual(expect.arrayContaining([mockCocoaPackage])); expect(processedEvent?.platform === 'javascript'); @@ -47,7 +47,7 @@ describe('Sdk Info', () => { NATIVE.platform = 'android'; mockedFetchNativeSdkInfo = jest.fn().mockResolvedValue(mockAndroidPackage); const mockEvent: Event = {}; - const processedEvent = await executeIntegrationFor(mockEvent); + const processedEvent = await processEvent(mockEvent); expect(processedEvent?.sdk?.packages).toEqual(expect.not.arrayContaining([mockCocoaPackage])); expect(processedEvent?.platform === 'javascript'); @@ -57,7 +57,7 @@ describe('Sdk Info', () => { it('Does not add any default non native packages', async () => { mockedFetchNativeSdkInfo = jest.fn().mockResolvedValue(null); const mockEvent: Event = {}; - const processedEvent = await executeIntegrationFor(mockEvent); + const processedEvent = await processEvent(mockEvent); expect(processedEvent?.sdk?.packages).toEqual([]); expect(processedEvent?.platform === 'javascript'); @@ -72,7 +72,7 @@ describe('Sdk Info', () => { version: '1.0.0', }, }; - const processedEvent = await executeIntegrationFor(mockEvent); + const processedEvent = await processEvent(mockEvent); expect(processedEvent?.sdk?.name).toEqual('test-sdk'); expect(processedEvent?.sdk?.version).toEqual('1.0.0'); @@ -81,23 +81,14 @@ describe('Sdk Info', () => { it('Does use default sdk name and version', async () => { mockedFetchNativeSdkInfo = jest.fn().mockResolvedValue(null); const mockEvent: Event = {}; - const processedEvent = await executeIntegrationFor(mockEvent); + const processedEvent = await processEvent(mockEvent); expect(processedEvent?.sdk?.name).toEqual(SDK_NAME); expect(processedEvent?.sdk?.version).toEqual(SDK_VERSION); }); }); -function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint = {}): Promise { - const integration = new SdkInfo(); - return new Promise((resolve, reject) => { - integration.setupOnce(async eventProcessor => { - try { - const processedEvent = await eventProcessor(mockedEvent, mockedHint); - resolve(processedEvent); - } catch (e) { - reject(e); - } - }); - }); +function processEvent(mockedEvent: Event, mockedHint: EventHint = {}): Event | null | PromiseLike { + const integration = sdkInfoIntegration(); + return integration.processEvent!(mockedEvent, mockedHint, {} as any); } diff --git a/test/integrations/spotlight.test.ts b/test/integrations/spotlight.test.ts index 4afe47891c..8c3f0c27a2 100644 --- a/test/integrations/spotlight.test.ts +++ b/test/integrations/spotlight.test.ts @@ -1,6 +1,6 @@ import type { HttpRequestEventMap } from '@mswjs/interceptors'; import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'; -import type { Envelope, Hub } from '@sentry/types'; +import type { Client, Envelope } from '@sentry/types'; import { XMLHttpRequest } from 'xmlhttprequest'; import { Spotlight } from '../../src/js/integrations/spotlight'; @@ -20,17 +20,12 @@ describe('spotlight', () => { }); it('should not change the original envelope', () => { - const mockHub = createMockHub(); + const mockClient = createMockClient(); const spotlight = Spotlight(); - spotlight.setupOnce( - () => {}, - () => mockHub as unknown as Hub, - ); + spotlight.setup?.(mockClient as unknown as Client); - const spotlightBeforeEnvelope = mockHub.getClient().on.mock.calls[0]?.[1] as - | ((envelope: Envelope) => void) - | undefined; + const spotlightBeforeEnvelope = mockClient.on.mock.calls[0]?.[1] as ((envelope: Envelope) => void) | undefined; const originalEnvelopeReference = createMockEnvelope(); spotlightBeforeEnvelope?.(originalEnvelopeReference); @@ -40,17 +35,12 @@ describe('spotlight', () => { }); it('should remove image attachments from spotlight envelope', async () => { - const mockHub = createMockHub(); + const mockClient = createMockClient(); const spotlight = Spotlight(); - spotlight.setupOnce( - () => {}, - () => mockHub as unknown as Hub, - ); + spotlight.setup?.(mockClient as unknown as Client); - const spotlightBeforeEnvelope = mockHub.getClient().on.mock.calls[0]?.[1] as - | ((envelope: Envelope) => void) - | undefined; + const spotlightBeforeEnvelope = mockClient.on.mock.calls[0]?.[1] as ((envelope: Envelope) => void) | undefined; spotlightBeforeEnvelope?.(createMockEnvelope()); @@ -60,14 +50,12 @@ describe('spotlight', () => { }); }); -function createMockHub() { +function createMockClient() { const client = { on: jest.fn(), }; - return { - getClient: jest.fn().mockReturnValue(client), - }; + return client; } function createMockEnvelope(): Envelope { diff --git a/test/integrations/viewhierarchy.test.ts b/test/integrations/viewhierarchy.test.ts index d136bd8403..68cfc0cc7a 100644 --- a/test/integrations/viewhierarchy.test.ts +++ b/test/integrations/viewhierarchy.test.ts @@ -1,16 +1,14 @@ -import type { Event, EventHint } from '@sentry/types'; +import type { Client, Event, EventHint } from '@sentry/types'; -import { ViewHierarchy } from '../../src/js/integrations/viewhierarchy'; +import { viewHierarchyIntegration } from '../../src/js/integrations/viewhierarchy'; import { NATIVE } from '../../src/js/wrapper'; jest.mock('../../src/js/wrapper'); describe('ViewHierarchy', () => { - let integration: ViewHierarchy; let mockEvent: Event; beforeEach(() => { - integration = new ViewHierarchy(); mockEvent = { exception: { values: [ @@ -27,7 +25,7 @@ describe('ViewHierarchy', () => { throw new Error('Test Error'); }); const mockHint: EventHint = {}; - await executeIntegrationFor(mockEvent, mockHint); + await processEvent(mockEvent, mockHint); expect(mockHint).toEqual({}); }); @@ -35,7 +33,7 @@ describe('ViewHierarchy', () => { (NATIVE.fetchViewHierarchy as jest.Mock).mockImplementation(( (() => Promise.resolve(new Uint8Array([]))) )); - await executeIntegrationFor(mockEvent); + await processEvent(mockEvent); expect(mockEvent).toEqual({ exception: { @@ -53,7 +51,7 @@ describe('ViewHierarchy', () => { (() => Promise.resolve(new Uint8Array([1, 2, 3]))) )); const mockHint: EventHint = {}; - await executeIntegrationFor(mockEvent, mockHint); + await processEvent(mockEvent, mockHint); expect(mockHint).toEqual({ attachments: [ @@ -80,7 +78,7 @@ describe('ViewHierarchy', () => { }, ], }; - await executeIntegrationFor(mockEvent, mockHint); + await processEvent(mockEvent, mockHint); expect(mockHint).toEqual({ attachments: [ @@ -104,21 +102,13 @@ describe('ViewHierarchy', () => { (() => Promise.resolve(null)) )); const mockHint: EventHint = {}; - await executeIntegrationFor(mockEvent, mockHint); + await processEvent(mockEvent, mockHint); expect(mockHint).toEqual({}); }); - function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint = {}): Promise { - return new Promise((resolve, reject) => { - integration.setupOnce(async eventProcessor => { - try { - const processedEvent = await eventProcessor(mockedEvent, mockedHint); - resolve(processedEvent); - } catch (e) { - reject(e); - } - }); - }); + function processEvent(mockedEvent: Event, mockedHint: EventHint = {}): Event | null | PromiseLike { + const integration = viewHierarchyIntegration(); + return integration.processEvent!(mockedEvent, mockedHint, {} as Client); } }); diff --git a/test/touchevents.test.tsx b/test/touchevents.test.tsx index f81a1df8ae..c3a18b246e 100644 --- a/test/touchevents.test.tsx +++ b/test/touchevents.test.tsx @@ -5,32 +5,30 @@ import * as core from '@sentry/core'; import type { SeverityLevel } from '@sentry/types'; import { TouchEventBoundary } from '../src/js/touchevents'; - -jest.mock('@sentry/core'); -jest.mock('../src/js/tracing', () => ({})); +import { getDefaultTestClientOptions,TestClient } from './mocks/client'; describe('TouchEventBoundary._onTouchStart', () => { let addBreadcrumb: jest.SpyInstance; + let addIntegration: jest.SpyInstance; + let client: TestClient; beforeEach(() => { jest.resetAllMocks(); addBreadcrumb = jest.spyOn(core, 'addBreadcrumb'); + + client = new TestClient(getDefaultTestClientOptions()); + core.setCurrentClient(client); + client.init(); }); it('register itself as integration', () => { - const mockAddIntegration = jest.fn(); - (core.getCurrentHub as jest.Mock).mockReturnValue({ - getClient: jest.fn().mockReturnValue({ - addIntegration: mockAddIntegration, - getIntegration: jest.fn(), - }), - }); + addIntegration = jest.spyOn(client, 'addIntegration'); const { defaultProps } = TouchEventBoundary; const boundary = new TouchEventBoundary(defaultProps); boundary.componentDidMount(); - expect(mockAddIntegration).toBeCalledWith(expect.objectContaining({ name: 'TouchEventBoundary' })); + expect(addIntegration).toBeCalledWith(expect.objectContaining({ name: 'TouchEventBoundary' })); }); it('tree without displayName or label is not logged', () => { From fec54f4e536cd9055ed86061ec59131622627fb7 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 13 May 2024 13:14:42 +0200 Subject: [PATCH 2/7] add backwards compatible interface --- src/js/index.ts | 9 +- src/js/integrations/debugsymbolicator.ts | 27 +++- src/js/integrations/default.ts | 50 +++---- src/js/integrations/devicecontext.ts | 20 ++- src/js/integrations/eventorigin.ts | 20 ++- src/js/integrations/expocontext.ts | 27 +++- src/js/integrations/exports.ts | 27 ++++ src/js/integrations/index.ts | 26 ++-- src/js/integrations/modulesloader.ts | 20 ++- src/js/integrations/nativelinkederrors.ts | 22 ++- .../integrations/reactnativeerrorhandlers.ts | 23 +++- src/js/integrations/reactnativeinfo.ts | 20 ++- src/js/integrations/release.ts | 29 +++- src/js/integrations/screenshot.ts | 20 ++- src/js/integrations/sdkinfo.ts | 27 +++- src/js/integrations/spotlight.ts | 22 ++- src/js/integrations/viewhierarchy.ts | 20 ++- src/js/profiling/integration.ts | 126 ++++++++++-------- test/profiling/integration.test.ts | 25 +--- 19 files changed, 403 insertions(+), 157 deletions(-) create mode 100644 src/js/integrations/exports.ts diff --git a/src/js/index.ts b/src/js/index.ts index d1d66f0e1d..a273f433dd 100644 --- a/src/js/index.ts +++ b/src/js/index.ts @@ -63,8 +63,12 @@ export { export { lastEventId } from '@sentry/browser'; -import * as Integrations from './integrations'; -import { SDK_NAME, SDK_VERSION } from './version'; +/** @deprecated Import the integration function directly, e.g. `screenshotIntegration()` instead of `new Integrations.Screenshot(). */ +export * as Integrations from './integrations'; + +export * from './integrations/exports'; + +export { SDK_NAME, SDK_VERSION } from './version'; export type { ReactNativeOptions } from './options'; export { ReactNativeClient } from './client'; @@ -100,4 +104,3 @@ export { } from './tracing'; export type { ReactNavigationTransactionContext, TimeToDisplayProps } from './tracing'; -export { Integrations, SDK_NAME, SDK_VERSION }; diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 78100cdd6f..de7a18c294 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -1,10 +1,20 @@ -import type { Event, EventHint, IntegrationFn, StackFrame as SentryStackFrame } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { + Event, + EventHint, + Integration, + IntegrationClass, + IntegrationFnResult, + StackFrame as SentryStackFrame, +} from '@sentry/types'; import { addContextToFrame, logger } from '@sentry/utils'; import { getFramesToPop, isErrorLike } from '../utils/error'; import type * as ReactNative from '../vendor/react-native'; import { fetchSourceContext, getDevServer, parseErrorStack, symbolicateStackTrace } from './debugsymbolicatorutils'; +const INTEGRATION_NAME = 'DebugSymbolicator'; + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|')); @@ -19,9 +29,9 @@ export type ReactNativeError = Error & { }; /** Tries to symbolicate the JS stack trace on the device. */ -export const debugSymbolicatorIntegration: IntegrationFn = () => { +export const debugSymbolicatorIntegration = (): IntegrationFnResult => { return { - name: 'DebugSymbolicator', + name: INTEGRATION_NAME, setupOnce: () => { /* noop */ }, @@ -29,6 +39,17 @@ export const debugSymbolicatorIntegration: IntegrationFn = () => { }; }; +/** + * Tries to symbolicate the JS stack trace on the device. + * + * @deprecated Use `debugSymbolicatorIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const DebugSymbolicator = convertIntegrationFnToClass( + INTEGRATION_NAME, + debugSymbolicatorIntegration, +) as IntegrationClass; + async function processEvent(event: Event, hint: EventHint): Promise { if (event.exception && isErrorLike(hint.originalException)) { // originalException is ErrorLike object diff --git a/src/js/integrations/default.ts b/src/js/integrations/default.ts index 877c14168b..37abe6ba57 100644 --- a/src/js/integrations/default.ts +++ b/src/js/integrations/default.ts @@ -1,34 +1,34 @@ -import { httpClientIntegration } from '@sentry/integrations'; +import type { Integration } from '@sentry/types'; + +import type { ReactNativeClientOptions } from '../options'; +import { ReactNativeTracing } from '../tracing'; +import { isExpoGo, notWeb } from '../utils/environment'; import { breadcrumbsIntegration, browserApiErrorsIntegration, + browserGlobalHandlersIntegration, + browserLinkedErrorsIntegration, + debugSymbolicatorIntegration, dedupeIntegration, + deviceContextIntegration, + eventOriginIntegration, + expoContextIntegration, functionToStringIntegration, - globalHandlersIntegration as browserGlobalHandlersIntegration, + hermesProfilingIntegration, + httpClientIntegration, httpContextIntegration, inboundFiltersIntegration, - linkedErrorsIntegration as browserLinkedErrorsIntegration, -} from '@sentry/react'; -import type { Integration } from '@sentry/types'; - -import type { ReactNativeClientOptions } from '../options'; -import { HermesProfiling } from '../profiling/integration'; -import { ReactNativeTracing } from '../tracing'; -import { isExpoGo, notWeb } from '../utils/environment'; -import { debugSymbolicatorIntegration } from './debugsymbolicator'; -import { deviceContextIntegration } from './devicecontext'; -import { eventOriginIntegration } from './eventorigin'; -import { expoContextIntegration } from './expocontext'; -import { modulesLoaderIntegration } from './modulesloader'; -import { nativeLinkedErrorsIntegration } from './nativelinkederrors'; -import { reactNativeErrorHandlersIntegration } from './reactnativeerrorhandlers'; -import { reactNativeInfoIntegration } from './reactnativeinfo'; -import { nativeReleaseIntegration } from './release'; + modulesLoaderIntegration, + nativeLinkedErrorsIntegration, + nativeReleaseIntegration, + reactNativeErrorHandlersIntegration, + reactNativeInfoIntegration, + screenshotIntegration, + sdkInfoIntegration, + spotlightIntegration, + viewHierarchyIntegration, +} from './exports'; import { createReactNativeRewriteFrames } from './rewriteframes'; -import { screenshotIntegration } from './screenshot'; -import { sdkInfoIntegration } from './sdkinfo'; -import { Spotlight } from './spotlight'; -import { viewHierarchyIntegration } from './viewhierarchy'; /** * Returns the default ReactNative integrations based on the current environment. @@ -82,7 +82,7 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ integrations.push(viewHierarchyIntegration()); } if (options._experiments && typeof options._experiments.profilesSampleRate === 'number') { - integrations.push(new HermesProfiling()); + integrations.push(hermesProfilingIntegration()); } } @@ -106,7 +106,7 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ if (options.enableSpotlight) { integrations.push( - Spotlight({ + spotlightIntegration({ sidecarUrl: options.spotlightSidecarUrl, }), ); diff --git a/src/js/integrations/devicecontext.ts b/src/js/integrations/devicecontext.ts index 801e4d1c5d..4c7a5a8f5b 100644 --- a/src/js/integrations/devicecontext.ts +++ b/src/js/integrations/devicecontext.ts @@ -1,5 +1,6 @@ /* eslint-disable complexity */ -import type { Event, IntegrationFn } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, Integration, IntegrationClass, IntegrationFnResult } from '@sentry/types'; import { logger, severityLevelFromString } from '@sentry/utils'; import { AppState } from 'react-native'; @@ -7,10 +8,12 @@ import { breadcrumbFromObject } from '../breadcrumb'; import type { NativeDeviceContextsResponse } from '../NativeRNSentry'; import { NATIVE } from '../wrapper'; +const INTEGRATION_NAME = 'DeviceContext'; + /** Load device context from native. */ -export const deviceContextIntegration: IntegrationFn = () => { +export const deviceContextIntegration = (): IntegrationFnResult => { return { - name: 'DeviceContext', + name: INTEGRATION_NAME, setupOnce: () => { /* noop */ }, @@ -18,6 +21,17 @@ export const deviceContextIntegration: IntegrationFn = () => { }; }; +/** + * Load device context from native. + * + * @deprecated Use `deviceContextIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const DeviceContext = convertIntegrationFnToClass( + INTEGRATION_NAME, + deviceContextIntegration, +) as IntegrationClass; + async function processEvent(event: Event): Promise { let native: NativeDeviceContextsResponse | null = null; try { diff --git a/src/js/integrations/eventorigin.ts b/src/js/integrations/eventorigin.ts index e066432036..ec9d666d49 100644 --- a/src/js/integrations/eventorigin.ts +++ b/src/js/integrations/eventorigin.ts @@ -1,9 +1,12 @@ -import type { Event, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, Integration, IntegrationClass, IntegrationFnResult } from '@sentry/types'; + +const INTEGRATION_NAME = 'EventOrigin'; /** Default EventOrigin instrumentation */ -export const eventOriginIntegration = (): Integration => { +export const eventOriginIntegration = (): IntegrationFnResult => { return { - name: 'EventOrigin', + name: INTEGRATION_NAME, setupOnce: () => { // noop }, @@ -17,3 +20,14 @@ export const eventOriginIntegration = (): Integration => { }, }; }; + +/** + * Default EventOrigin instrumentation + * + * @deprecated Use `eventOriginIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const EventOrigin = convertIntegrationFnToClass( + INTEGRATION_NAME, + eventOriginIntegration, +) as IntegrationClass; diff --git a/src/js/integrations/expocontext.ts b/src/js/integrations/expocontext.ts index b28b8a5e9b..adf0c1e89e 100644 --- a/src/js/integrations/expocontext.ts +++ b/src/js/integrations/expocontext.ts @@ -1,11 +1,21 @@ -import type { DeviceContext, Event, Integration, OsContext } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { + DeviceContext, + Event, + Integration, + IntegrationClass, + IntegrationFnResult, + OsContext, +} from '@sentry/types'; import { getExpoDevice } from '../utils/expomodules'; +const INTEGRATION_NAME = 'ExpoContext'; + /** Load device context from expo modules. */ -export const expoContextIntegration = (): Integration => { +export const expoContextIntegration = (): IntegrationFnResult => { return { - name: 'ExpoContext', + name: INTEGRATION_NAME, setupOnce: () => { // noop }, @@ -13,6 +23,17 @@ export const expoContextIntegration = (): Integration => { }; }; +/** + * Load device context from expo modules. + * + * @deprecated Use `expoContextIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const ExpoContext = convertIntegrationFnToClass( + INTEGRATION_NAME, + expoContextIntegration, +) as IntegrationClass; + function processEvent(event: Event): Event { const expoDeviceContext = getExpoDeviceContext(); if (expoDeviceContext) { diff --git a/src/js/integrations/exports.ts b/src/js/integrations/exports.ts new file mode 100644 index 0000000000..b229c3cf50 --- /dev/null +++ b/src/js/integrations/exports.ts @@ -0,0 +1,27 @@ +export { debugSymbolicatorIntegration } from './debugsymbolicator'; +export { deviceContextIntegration } from './devicecontext'; +export { reactNativeErrorHandlersIntegration } from './reactnativeerrorhandlers'; +export { nativeLinkedErrorsIntegration } from './nativelinkederrors'; +export { nativeReleaseIntegration } from './release'; +export { eventOriginIntegration } from './eventorigin'; +export { sdkInfoIntegration } from './sdkinfo'; +export { reactNativeInfoIntegration } from './reactnativeinfo'; +export { modulesLoaderIntegration } from './modulesloader'; +export { hermesProfilingIntegration } from '../profiling/integration'; +export { screenshotIntegration } from './screenshot'; +export { viewHierarchyIntegration } from './viewhierarchy'; +export { expoContextIntegration } from './expocontext'; +export { spotlightIntegration } from './spotlight'; + +export { + breadcrumbsIntegration, + browserApiErrorsIntegration, + dedupeIntegration, + functionToStringIntegration, + globalHandlersIntegration as browserGlobalHandlersIntegration, + httpClientIntegration, + httpContextIntegration, + inboundFiltersIntegration, + linkedErrorsIntegration as browserLinkedErrorsIntegration, + rewriteFramesIntegration, +} from '@sentry/react'; diff --git a/src/js/integrations/index.ts b/src/js/integrations/index.ts index 37772e6927..5b9a32f3da 100644 --- a/src/js/integrations/index.ts +++ b/src/js/integrations/index.ts @@ -1,14 +1,16 @@ -export { debugSymbolicatorIntegration } from './debugsymbolicator'; -export { deviceContextIntegration } from './devicecontext'; -export { reactNativeErrorHandlersIntegration } from './reactnativeerrorhandlers'; -export { nativeLinkedErrorsIntegration } from './nativelinkederrors'; -export { nativeReleaseIntegration } from './release'; -export { eventOriginIntegration } from './eventorigin'; -export { sdkInfoIntegration } from './sdkinfo'; -export { reactNativeInfoIntegration } from './reactnativeinfo'; -export { modulesLoaderIntegration } from './modulesloader'; +// THESE EXPORTS WILL BE REMOVED IN THE NEXT MAJOR RELEASE + +export { DebugSymbolicator } from './debugsymbolicator'; +export { DeviceContext } from './devicecontext'; +export { ReactNativeErrorHandlers } from './reactnativeerrorhandlers'; +export { NativeLinkedErrors } from './nativelinkederrors'; +export { Release } from './release'; +export { EventOrigin } from './eventorigin'; +export { SdkInfo } from './sdkinfo'; +export { ReactNativeInfo } from './reactnativeinfo'; +export { ModulesLoader } from './modulesloader'; export { HermesProfiling } from '../profiling/integration'; -export { screenshotIntegration } from './screenshot'; -export { viewHierarchyIntegration } from './viewhierarchy'; -export { expoContextIntegration } from './expocontext'; +export { Screenshot } from './screenshot'; +export { ViewHierarchy } from './viewhierarchy'; +export { ExpoContext } from './expocontext'; export { Spotlight } from './spotlight'; diff --git a/src/js/integrations/modulesloader.ts b/src/js/integrations/modulesloader.ts index 6bc25a33f5..b49fe164f8 100644 --- a/src/js/integrations/modulesloader.ts +++ b/src/js/integrations/modulesloader.ts @@ -1,12 +1,15 @@ -import type { Event, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, Integration, IntegrationClass, IntegrationFnResult } from '@sentry/types'; import { logger } from '@sentry/utils'; import { NATIVE } from '../wrapper'; +const INTEGRATION_NAME = 'ModulesLoader'; + /** Loads runtime JS modules from prepared file. */ -export const modulesLoaderIntegration = (): Integration => { +export const modulesLoaderIntegration = (): IntegrationFnResult => { return { - name: 'ModulesLoader', + name: INTEGRATION_NAME, setupOnce: () => { // noop }, @@ -14,6 +17,17 @@ export const modulesLoaderIntegration = (): Integration => { }; }; +/** + * Loads runtime JS modules from prepared file. + * + * @deprecated Use `modulesLoaderIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const ModulesLoader = convertIntegrationFnToClass( + INTEGRATION_NAME, + modulesLoaderIntegration, +) as IntegrationClass; + function createProcessEvent(): (event: Event) => Promise { let isSetup = false; let modules: Record | null = null; diff --git a/src/js/integrations/nativelinkederrors.ts b/src/js/integrations/nativelinkederrors.ts index 41d62f07dd..f35d339f63 100644 --- a/src/js/integrations/nativelinkederrors.ts +++ b/src/js/integrations/nativelinkederrors.ts @@ -1,4 +1,5 @@ import { exceptionFromError } from '@sentry/browser'; +import { convertIntegrationFnToClass } from '@sentry/core'; import type { Client, DebugImage, @@ -7,6 +8,8 @@ import type { Exception, ExtendedError, Integration, + IntegrationClass, + IntegrationFnResult, StackFrame, StackParser, } from '@sentry/types'; @@ -15,6 +18,8 @@ import { isInstanceOf, isPlainObject } from '@sentry/utils'; import type { NativeStackFrames } from '../NativeRNSentry'; import { NATIVE } from '../wrapper'; +const INTEGRATION_NAME = 'NativeLinkedErrors'; + const DEFAULT_KEY = 'cause'; const DEFAULT_LIMIT = 5; @@ -26,12 +31,12 @@ interface LinkedErrorsOptions { /** * Processes JS and RN native linked errors. */ -export const nativeLinkedErrorsIntegration = (options: Partial = {}): Integration => { +export const nativeLinkedErrorsIntegration = (options: Partial = {}): IntegrationFnResult => { const key = options.key || DEFAULT_KEY; const limit = options.limit || DEFAULT_LIMIT; return { - name: 'NativeLinkedErrors', + name: INTEGRATION_NAME, setupOnce: (): void => { // noop }, @@ -40,6 +45,19 @@ export const nativeLinkedErrorsIntegration = (options: Partial & { + new (options?: Partial): Integration; +}; + function preprocessEvent(event: Event, hint: EventHint | undefined, client: Client, limit: number, key: string): void { if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { return; diff --git a/src/js/integrations/reactnativeerrorhandlers.ts b/src/js/integrations/reactnativeerrorhandlers.ts index 3b95729b54..380920fde4 100644 --- a/src/js/integrations/reactnativeerrorhandlers.ts +++ b/src/js/integrations/reactnativeerrorhandlers.ts @@ -1,11 +1,13 @@ -import { captureException, getClient, getCurrentScope } from '@sentry/core'; -import type { EventHint, Integration, SeverityLevel } from '@sentry/types'; +import { captureException, convertIntegrationFnToClass, getClient, getCurrentScope } from '@sentry/core'; +import type { EventHint, Integration, IntegrationClass, IntegrationFnResult, SeverityLevel } from '@sentry/types'; import { addExceptionMechanism, logger } from '@sentry/utils'; import { createSyntheticError, isErrorLike } from '../utils/error'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; import { checkPromiseAndWarn, polyfillPromise, requireRejectionTracking } from './reactnativeerrorhandlersutils'; +const INTEGRATION_NAME = 'ReactNativeErrorHandlers'; + /** ReactNativeErrorHandlers Options */ interface ReactNativeErrorHandlersOptions { onerror: boolean; @@ -21,9 +23,9 @@ interface PromiseRejectionTrackingOptions { /** ReactNativeErrorHandlers Integration */ export const reactNativeErrorHandlersIntegration = ( options: Partial = {}, -): Integration => { +): IntegrationFnResult => { return { - name: 'ReactNativeErrorHandlers', + name: INTEGRATION_NAME, setupOnce: () => setup({ onerror: options.onerror || true, @@ -33,6 +35,19 @@ export const reactNativeErrorHandlersIntegration = ( }; }; +/** + * ReactNativeErrorHandlers Integration + * + * @deprecated Use `reactNativeErrorHandlersIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const ReactNativeErrorHandlers = convertIntegrationFnToClass( + INTEGRATION_NAME, + reactNativeErrorHandlersIntegration, +) as IntegrationClass & { + new (options?: Partial): Integration; +}; + function setup(options: ReactNativeErrorHandlersOptions): void { options.onunhandledrejection && setupUnhandledRejectionsTracking(options.patchGlobalPromise); options.onerror && setupErrorUtilsGlobalHandler(); diff --git a/src/js/integrations/reactnativeinfo.ts b/src/js/integrations/reactnativeinfo.ts index 73e05c488e..a139004b7b 100644 --- a/src/js/integrations/reactnativeinfo.ts +++ b/src/js/integrations/reactnativeinfo.ts @@ -1,4 +1,5 @@ -import type { Context, Event, EventHint, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Context, Event, EventHint, Integration, IntegrationClass, IntegrationFnResult } from '@sentry/types'; import { getExpoGoVersion, @@ -12,6 +13,8 @@ import { } from '../utils/environment'; import type { ReactNativeError } from './debugsymbolicator'; +const INTEGRATION_NAME = 'ReactNativeInfo'; + export interface ReactNativeContext extends Context { js_engine?: string; turbo_module: boolean; @@ -26,9 +29,9 @@ export interface ReactNativeContext extends Context { } /** Loads React Native context at runtime */ -export const reactNativeInfoIntegration = (): Integration => { +export const reactNativeInfoIntegration = (): IntegrationFnResult => { return { - name: 'ReactNativeInfo', + name: INTEGRATION_NAME, setupOnce: () => { // noop }, @@ -36,6 +39,17 @@ export const reactNativeInfoIntegration = (): Integration => { }; }; +/** + * Loads React Native context at runtime + * + * @deprecated Use `reactNativeInfoIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const ReactNativeInfo = convertIntegrationFnToClass( + INTEGRATION_NAME, + reactNativeInfoIntegration, +) as IntegrationClass; + function processEvent(event: Event, hint: EventHint): Event { const reactNativeError = hint?.originalException ? (hint?.originalException as ReactNativeError) : undefined; diff --git a/src/js/integrations/release.ts b/src/js/integrations/release.ts index 88b57e1b82..56c8c6c7b6 100644 --- a/src/js/integrations/release.ts +++ b/src/js/integrations/release.ts @@ -1,11 +1,23 @@ -import type { BaseTransportOptions, Client, ClientOptions, Event, EventHint, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { + BaseTransportOptions, + Client, + ClientOptions, + Event, + EventHint, + Integration, + IntegrationClass, + IntegrationFnResult, +} from '@sentry/types'; import { NATIVE } from '../wrapper'; +const INTEGRATION_NAME = 'Release'; + /** Release integration responsible to load release from file. */ -export const nativeReleaseIntegration = (): Integration => { +export const nativeReleaseIntegration = (): IntegrationFnResult => { return { - name: 'Release', + name: INTEGRATION_NAME, setupOnce: () => { // noop }, @@ -13,6 +25,17 @@ export const nativeReleaseIntegration = (): Integration => { }; }; +/** + * Release integration responsible to load release from file. + * + * @deprecated Use `nativeReleaseIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const Release = convertIntegrationFnToClass( + INTEGRATION_NAME, + nativeReleaseIntegration, +) as IntegrationClass; + async function processEvent( event: Event, _: EventHint, diff --git a/src/js/integrations/screenshot.ts b/src/js/integrations/screenshot.ts index b3544ccb00..6c59a195ac 100644 --- a/src/js/integrations/screenshot.ts +++ b/src/js/integrations/screenshot.ts @@ -1,13 +1,16 @@ -import type { Event, EventHint, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, EventHint, Integration, IntegrationClass, IntegrationFnResult } from '@sentry/types'; import type { ReactNativeClient } from '../client'; import type { Screenshot as ScreenshotAttachment } from '../wrapper'; import { NATIVE } from '../wrapper'; +const INTEGRATION_NAME = 'Screenshot'; + /** Adds screenshots to error events */ -export const screenshotIntegration = (): Integration => { +export const screenshotIntegration = (): IntegrationFnResult => { return { - name: 'Screenshot', + name: INTEGRATION_NAME, setupOnce: () => { // noop }, @@ -15,6 +18,17 @@ export const screenshotIntegration = (): Integration => { }; }; +/** + * Adds screenshots to error events + * + * @deprecated Use `screenshotIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const Screenshot = convertIntegrationFnToClass( + INTEGRATION_NAME, + screenshotIntegration, +) as IntegrationClass; + async function processEvent(event: Event, hint: EventHint, client: ReactNativeClient): Promise { const options = client.getOptions(); diff --git a/src/js/integrations/sdkinfo.ts b/src/js/integrations/sdkinfo.ts index 65f5656856..77f7c499be 100644 --- a/src/js/integrations/sdkinfo.ts +++ b/src/js/integrations/sdkinfo.ts @@ -1,10 +1,20 @@ -import type { Event, Integration, Package, SdkInfo as SdkInfoType } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { + Event, + Integration, + IntegrationClass, + IntegrationFnResult, + Package, + SdkInfo as SdkInfoType, +} from '@sentry/types'; import { logger } from '@sentry/utils'; import { isExpoGo, notWeb } from '../utils/environment'; import { SDK_NAME, SDK_PACKAGE_NAME, SDK_VERSION } from '../version'; import { NATIVE } from '../wrapper'; +const INTEGRATION_NAME = 'SdkInfo'; + type DefaultSdkInfo = Pick, 'name' | 'packages' | 'version'>; export const defaultSdkInfo: DefaultSdkInfo = { @@ -19,11 +29,11 @@ export const defaultSdkInfo: DefaultSdkInfo = { }; /** Default SdkInfo instrumentation */ -export const sdkInfoIntegration = (): Integration => { +export const sdkInfoIntegration = (): IntegrationFnResult => { const fetchNativeSdkInfo = createCachedFetchNativeSdkInfo(); return { - name: 'SdkInfo', + name: INTEGRATION_NAME, setupOnce: () => { // noop }, @@ -31,6 +41,17 @@ export const sdkInfoIntegration = (): Integration => { }; }; +/** + * Default SdkInfo instrumentation + * + * @deprecated Use `sdkInfoIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const SdkInfo = convertIntegrationFnToClass( + INTEGRATION_NAME, + sdkInfoIntegration, +) as IntegrationClass; + async function processEvent(event: Event, fetchNativeSdkInfo: () => Promise): Promise { const nativeSdkPackage = await fetchNativeSdkInfo(); diff --git a/src/js/integrations/spotlight.ts b/src/js/integrations/spotlight.ts index ea1f51214a..8a07806e6f 100644 --- a/src/js/integrations/spotlight.ts +++ b/src/js/integrations/spotlight.ts @@ -1,4 +1,11 @@ -import type { BaseTransportOptions, Client, ClientOptions, Envelope, Integration } from '@sentry/types'; +import type { + BaseTransportOptions, + Client, + ClientOptions, + Envelope, + Integration, + IntegrationFnResult, +} from '@sentry/types'; import { logger, serializeEnvelope } from '@sentry/utils'; import { makeUtf8TextEncoder } from '../transports/TextEncoder'; @@ -20,9 +27,9 @@ type SpotlightReactNativeIntegrationOptions = { * * Learn more about spotlight at https://spotlightjs.com */ -export function Spotlight({ +export function spotlightIntegration({ sidecarUrl = getDefaultSidecarUrl(), -}: SpotlightReactNativeIntegrationOptions = {}): Integration { +}: SpotlightReactNativeIntegrationOptions = {}): IntegrationFnResult { logger.info('[Spotlight] Using Sidecar URL', sidecarUrl); return { @@ -38,6 +45,15 @@ export function Spotlight({ }; } +/** + * Use this integration to send errors and transactions to Spotlight. + * + * Learn more about spotlight at https://spotlightjs.com + * + * @deprecated Use `spotlightIntegration()` instead. + */ +export const Spotlight = spotlightIntegration as (...args: Parameters) => Integration; + function setup(client: Client, sidecarUrl: string): void { sendEnvelopesToSidecar(client, sidecarUrl); } diff --git a/src/js/integrations/viewhierarchy.ts b/src/js/integrations/viewhierarchy.ts index 490ed29598..9804ea8fff 100644 --- a/src/js/integrations/viewhierarchy.ts +++ b/src/js/integrations/viewhierarchy.ts @@ -1,4 +1,5 @@ -import type { Attachment, Event, EventHint, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Attachment, Event, EventHint, Integration, IntegrationClass, IntegrationFnResult } from '@sentry/types'; import { logger } from '@sentry/utils'; import { NATIVE } from '../wrapper'; @@ -7,10 +8,12 @@ const filename: string = 'view-hierarchy.json'; const contentType: string = 'application/json'; const attachmentType = 'event.view_hierarchy' as Attachment['attachmentType']; +const INTEGRATION_NAME = 'ViewHierarchy'; + /** Adds ViewHierarchy to error events */ -export const viewHierarchyIntegration = (): Integration => { +export const viewHierarchyIntegration = (): IntegrationFnResult => { return { - name: 'ViewHierarchy', + name: INTEGRATION_NAME, setupOnce: () => { // noop }, @@ -18,6 +21,17 @@ export const viewHierarchyIntegration = (): Integration => { }; }; +/** + * Adds ViewHierarchy to error events + * + * @deprecated Use `viewHierarchyIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const ViewHierarchy = convertIntegrationFnToClass( + INTEGRATION_NAME, + viewHierarchyIntegration, +) as IntegrationClass; + async function processEvent(event: Event, hint: EventHint): Promise { const hasException = event.exception && event.exception.values && event.exception.values.length > 0; if (!hasException) { diff --git a/src/js/profiling/integration.ts b/src/js/profiling/integration.ts index b82ecfad58..8fb6b7bcef 100644 --- a/src/js/profiling/integration.ts +++ b/src/js/profiling/integration.ts @@ -1,7 +1,14 @@ /* eslint-disable complexity */ -import type { Hub } from '@sentry/core'; -import { getActiveTransaction } from '@sentry/core'; -import type { Envelope, Event, EventProcessor, Integration, ThreadCpuProfile, Transaction } from '@sentry/types'; +import { convertIntegrationFnToClass, getActiveTransaction, getClient, getCurrentHub } from '@sentry/core'; +import type { + Envelope, + Event, + Integration, + IntegrationClass, + IntegrationFn, + ThreadCpuProfile, + Transaction, +} from '@sentry/types'; import { logger, uuid4 } from '@sentry/utils'; import { Platform } from 'react-native'; @@ -19,6 +26,8 @@ import { findProfiledTransactionsFromEnvelope, } from './utils'; +const INTEGRATION_NAME = 'HermesProfiling'; + const MS_TO_NS: number = 1e6; /** @@ -26,48 +35,31 @@ const MS_TO_NS: number = 1e6; * * @experimental */ -export class HermesProfiling implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'HermesProfiling'; - - /** - * @inheritDoc - */ - public name: string = HermesProfiling.id; - - private _getCurrentHub?: () => Hub; - - private _currentProfile: +export const hermesProfilingIntegration: IntegrationFn = () => { + let _currentProfile: | { profile_id: string; startTimestampNs: number; } | undefined; + let _currentProfileTimeout: number | undefined; - private _currentProfileTimeout: number | undefined; - - /** - * @inheritDoc - */ - public setupOnce(_: (e: EventProcessor) => void, getCurrentHub: () => Hub): void { + const setupOnce = (): void => { if (!isHermesEnabled()) { logger.log('[Profiling] Hermes is not enabled, not adding profiling integration.'); return; } - this._getCurrentHub = getCurrentHub; - const client = getCurrentHub().getClient(); + const client = getClient(); if (!client || typeof client.on !== 'function') { return; } - this._startCurrentProfileForActiveTransaction(); - client.on('startTransaction', this._startCurrentProfile); + _startCurrentProfileForActiveTransaction(); + client.on('startTransaction', _startCurrentProfile); - client.on('finishTransaction', this._finishCurrentProfile); + client.on('finishTransaction', _finishCurrentProfile); client.on('beforeEnvelope', (envelope: Envelope) => { if (!PROFILE_QUEUE.size()) { @@ -82,42 +74,42 @@ export class HermesProfiling implements Integration { const profilesToAddToEnvelope: ProfileEvent[] = []; for (const profiledTransaction of profiledTransactions) { - const profile = this._createProfileEventFor(profiledTransaction); + const profile = _createProfileEventFor(profiledTransaction); if (profile) { profilesToAddToEnvelope.push(profile); } } addProfilesToEnvelope(envelope, profilesToAddToEnvelope); }); - } + }; - private _startCurrentProfileForActiveTransaction = (): void => { - if (this._currentProfile) { + const _startCurrentProfileForActiveTransaction = (): void => { + if (_currentProfile) { return; } - const transaction = this._getCurrentHub && getActiveTransaction(this._getCurrentHub()); - transaction && this._startCurrentProfile(transaction); + const transaction = getActiveTransaction(getCurrentHub()); + transaction && _startCurrentProfile(transaction); }; - private _startCurrentProfile = (transaction: Transaction): void => { - this._finishCurrentProfile(); + const _startCurrentProfile = (transaction: Transaction): void => { + _finishCurrentProfile(); - const shouldStartProfiling = this._shouldStartProfiling(transaction); + const shouldStartProfiling = _shouldStartProfiling(transaction); if (!shouldStartProfiling) { return; } - this._currentProfileTimeout = setTimeout(this._finishCurrentProfile, MAX_PROFILE_DURATION_MS); - this._startNewProfile(transaction); + _currentProfileTimeout = setTimeout(_finishCurrentProfile, MAX_PROFILE_DURATION_MS); + _startNewProfile(transaction); }; - private _shouldStartProfiling = (transaction: Transaction): boolean => { + const _shouldStartProfiling = (transaction: Transaction): boolean => { if (!transaction.sampled) { logger.log('[Profiling] Transaction is not sampled, skipping profiling'); return false; } - const client = this._getCurrentHub && this._getCurrentHub().getClient(); + const client = getClient(); const options = client && client.getOptions(); const profilesSampleRate = @@ -141,45 +133,45 @@ export class HermesProfiling implements Integration { /** * Starts a new profile and links it to the transaction. */ - private _startNewProfile = (transaction: Transaction): void => { + const _startNewProfile = (transaction: Transaction): void => { const profileStartTimestampNs = startProfiling(); if (!profileStartTimestampNs) { return; } - this._currentProfile = { + _currentProfile = { profile_id: uuid4(), startTimestampNs: profileStartTimestampNs, }; - transaction.setContext('profile', { profile_id: this._currentProfile.profile_id }); + transaction.setContext('profile', { profile_id: _currentProfile.profile_id }); // @ts-expect-error profile_id is not part of the metadata type - transaction.setMetadata({ profile_id: this._currentProfile.profile_id }); - logger.log('[Profiling] started profiling: ', this._currentProfile.profile_id); + transaction.setMetadata({ profile_id: _currentProfile.profile_id }); + logger.log('[Profiling] started profiling: ', _currentProfile.profile_id); }; /** * Stops profiling and adds the profile to the queue to be processed on beforeEnvelope. */ - private _finishCurrentProfile = (): void => { - this._clearCurrentProfileTimeout(); - if (this._currentProfile === undefined) { + const _finishCurrentProfile = (): void => { + _clearCurrentProfileTimeout(); + if (_currentProfile === undefined) { return; } - const profile = stopProfiling(this._currentProfile.startTimestampNs); + const profile = stopProfiling(_currentProfile.startTimestampNs); if (!profile) { logger.warn('[Profiling] Stop failed. Cleaning up...'); - this._currentProfile = undefined; + _currentProfile = undefined; return; } - PROFILE_QUEUE.add(this._currentProfile.profile_id, profile); + PROFILE_QUEUE.add(_currentProfile.profile_id, profile); - logger.log('[Profiling] finished profiling: ', this._currentProfile.profile_id); - this._currentProfile = undefined; + logger.log('[Profiling] finished profiling: ', _currentProfile.profile_id); + _currentProfile = undefined; }; - private _createProfileEventFor = (profiledTransaction: Event): ProfileEvent | null => { + const _createProfileEventFor = (profiledTransaction: Event): ProfileEvent | null => { const profile_id = profiledTransaction?.contexts?.['profile']?.['profile_id']; if (typeof profile_id !== 'string') { @@ -206,11 +198,27 @@ export class HermesProfiling implements Integration { return profileWithEvent; }; - private _clearCurrentProfileTimeout = (): void => { - this._currentProfileTimeout !== undefined && clearTimeout(this._currentProfileTimeout); - this._currentProfileTimeout = undefined; + const _clearCurrentProfileTimeout = (): void => { + _currentProfileTimeout !== undefined && clearTimeout(_currentProfileTimeout); + _currentProfileTimeout = undefined; }; -} + + return { + name: INTEGRATION_NAME, + setupOnce, + }; +}; + +/** + * Profiling integration creates a profile for each transaction and adds it to the event envelope. + * + * @deprecated Use `hermesProfilingIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const HermesProfiling = convertIntegrationFnToClass( + INTEGRATION_NAME, + hermesProfilingIntegration, +) as IntegrationClass; /** * Starts Profilers and returns the timestamp when profiling started in nanoseconds. diff --git a/test/profiling/integration.test.ts b/test/profiling/integration.test.ts index ec4b1ce7c9..b9acc58c3d 100644 --- a/test/profiling/integration.test.ts +++ b/test/profiling/integration.test.ts @@ -7,9 +7,9 @@ import { getCurrentHub } from '@sentry/core'; import type { Envelope, Event, Profile, ThreadCpuProfile, Transaction, Transport } from '@sentry/types'; import * as Sentry from '../../src/js'; -import { HermesProfiling } from '../../src/js/integrations'; import type { NativeDeviceContextsResponse } from '../../src/js/NativeRNSentry'; import { getDebugMetadata } from '../../src/js/profiling/debugid'; +import { hermesProfilingIntegration } from '../../src/js/profiling/integration'; import type { AndroidProfileEvent } from '../../src/js/profiling/types'; import { getDefaultEnvironment, isHermesEnabled, notWeb } from '../../src/js/utils/environment'; import { RN_GLOBAL_OBJ } from '../../src/js/utils/worldwide'; @@ -62,7 +62,7 @@ describe('profiling integration', () => { }); getCurrentHub().getScope()?.setSpan(transaction); - getCurrentHub().getClient()?.addIntegration?.(new HermesProfiling()); + getCurrentHub().getClient()?.addIntegration?.(hermesProfilingIntegration()); transaction.finish(); jest.runAllTimers(); @@ -336,36 +336,23 @@ describe('profiling integration', () => { }); test('profile timeout is reset when transaction is finished', () => { - const integration = getCurrentHermesProfilingIntegration(); + const setTimeoutSpy = jest.spyOn(global, 'setTimeout'); + const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); const transaction: Transaction = Sentry.startTransaction({ name: 'test-name', }); - const timeoutAfterProfileStarted = integration._currentProfileTimeout; + const timeoutAfterProfileStarted = setTimeoutSpy.mock.results[0].value; jest.advanceTimersByTime(40 * 1e6); transaction.finish(); - const timeoutAfterProfileFinished = integration._currentProfileTimeout; + expect(clearTimeoutSpy).toBeCalledWith(timeoutAfterProfileStarted); jest.runAllTimers(); - - expect(timeoutAfterProfileStarted).toBeDefined(); - expect(timeoutAfterProfileFinished).toBeUndefined(); }); }); }); -type TestHermesIntegration = Omit & { - _currentProfileTimeout: number | undefined; -}; -function getCurrentHermesProfilingIntegration(): TestHermesIntegration { - const integration = Sentry.getCurrentHub().getClient()?.getIntegration(HermesProfiling); - if (!integration) { - throw new Error('HermesProfiling integration is not installed'); - } - return integration as unknown as TestHermesIntegration; -} - function initTestClient( testOptions: { withProfiling?: boolean; From 27828e409252491c50f30cab7ed59d879f3280b2 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 13 May 2024 13:29:58 +0200 Subject: [PATCH 3/7] add changelog --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b83b106f8a..177f2c5c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ ## Unreleased +### Features + +- Functional integrations ([#3814](https://github.com/getsentry/sentry-react-native/pull/3814)) + + Instead of installing `@sentry/integrations` and creating integrations using the `new` keyword, you can use direct imports of the functional integrations. + + ```js + // Before + import * as Sentry from '@sentry/react-native'; + import { HttpClient } from '@sentry/integrations'; + + Sentry.init({ + integrations: [ + new Sentry.BrowserIntegrations.Dedupe(), + new Sentry.Integration.Screenshot(), + new HttpClient(), + ], + }); + + // After + import * as Sentry from '@sentry/react-native'; + + Sentry.init({ + integrations: [ + Sentry.dedupeIntegration(), + Sentry.screenshotIntegration(), + Sentry.httpClientIntegration(), + ], + }); + ``` + + Note that the `Sentry.BrowserIntegrations`, `Sentry.Integration` and the Class style integrations will be removed in the next major version of the SDK. + ### Fixes - Remove unused `rnpm` config ([#3811](https://github.com/getsentry/sentry-react-native/pull/3811)) From 3966b885a7a5f0526dc50630b8f6b882a656ecbd Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 13 May 2024 15:06:45 +0200 Subject: [PATCH 4/7] fix tests --- samples/react-native/src/App.tsx | 2 ++ src/js/index.ts | 8 ++++++-- test/integrations/devicecontext.test.ts | 2 +- test/integrations/eventorigin.test.ts | 2 +- test/integrations/modulesloader.test.ts | 2 +- test/integrations/sdkinfo.test.ts | 2 +- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index ad7edc5a20..23b426a674 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -28,6 +28,8 @@ import Ionicons from 'react-native-vector-icons/Ionicons'; const isMobileOs = Platform.OS === 'android' || Platform.OS === 'ios'; +Sentry.Integrations.ReactNativeErrorHandlers.enableNative = false; + const reactNavigationInstrumentation = new Sentry.ReactNavigationInstrumentation({ routeChangeTimeoutMs: 500, // How long it will wait for the route change to complete. Default is 1000ms diff --git a/src/js/index.ts b/src/js/index.ts index a273f433dd..872bd51da3 100644 --- a/src/js/index.ts +++ b/src/js/index.ts @@ -63,8 +63,7 @@ export { export { lastEventId } from '@sentry/browser'; -/** @deprecated Import the integration function directly, e.g. `screenshotIntegration()` instead of `new Integrations.Screenshot(). */ -export * as Integrations from './integrations'; +import * as Integrations from './integrations'; export * from './integrations/exports'; @@ -104,3 +103,8 @@ export { } from './tracing'; export type { ReactNavigationTransactionContext, TimeToDisplayProps } from './tracing'; + +export { + /** @deprecated Import the integration function directly, e.g. `screenshotIntegration()` instead of `new Integrations.Screenshot(). */ + Integrations, +} diff --git a/test/integrations/devicecontext.test.ts b/test/integrations/devicecontext.test.ts index 3bc120c7fa..ff46e5f3c1 100644 --- a/test/integrations/devicecontext.test.ts +++ b/test/integrations/devicecontext.test.ts @@ -1,6 +1,6 @@ import type { Client, Event, EventHint, SeverityLevel } from '@sentry/types'; -import { deviceContextIntegration } from '../../src/js/integrations'; +import { deviceContextIntegration } from '../../src/js/integrations/devicecontext'; import type { NativeDeviceContextsResponse } from '../../src/js/NativeRNSentry'; import { NATIVE } from '../../src/js/wrapper'; diff --git a/test/integrations/eventorigin.test.ts b/test/integrations/eventorigin.test.ts index b359929fdb..017d75129c 100644 --- a/test/integrations/eventorigin.test.ts +++ b/test/integrations/eventorigin.test.ts @@ -1,6 +1,6 @@ import type { Client } from '@sentry/types'; -import { eventOriginIntegration } from '../../src/js/integrations'; +import { eventOriginIntegration } from '../../src/js/integrations/eventorigin'; describe('Event Origin', () => { it('Adds event.origin and event.environment javascript tags to events', async () => { diff --git a/test/integrations/modulesloader.test.ts b/test/integrations/modulesloader.test.ts index 61edd558a8..73f2b08451 100644 --- a/test/integrations/modulesloader.test.ts +++ b/test/integrations/modulesloader.test.ts @@ -1,6 +1,6 @@ import type { Client, Event, EventHint } from '@sentry/types'; -import { modulesLoaderIntegration } from '../../src/js/integrations'; +import { modulesLoaderIntegration } from '../../src/js/integrations/modulesloader'; import { NATIVE } from '../../src/js/wrapper'; jest.mock('../../src/js/wrapper'); diff --git a/test/integrations/sdkinfo.test.ts b/test/integrations/sdkinfo.test.ts index 38ee3f8097..71d19d0ec7 100644 --- a/test/integrations/sdkinfo.test.ts +++ b/test/integrations/sdkinfo.test.ts @@ -1,7 +1,7 @@ import type { Event, EventHint, Package } from '@sentry/types'; import { SDK_NAME, SDK_VERSION } from '../../src/js'; -import { sdkInfoIntegration } from '../../src/js/integrations'; +import { sdkInfoIntegration } from '../../src/js/integrations/sdkinfo'; import { NATIVE } from '../../src/js/wrapper'; let mockedFetchNativeSdkInfo: jest.Mock, []>; From 512c6f24f3c2dd6a679b6ebaa18923f5cd608c80 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 13 May 2024 15:09:56 +0200 Subject: [PATCH 5/7] fix --- src/js/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/index.ts b/src/js/index.ts index 872bd51da3..2864d71e3a 100644 --- a/src/js/index.ts +++ b/src/js/index.ts @@ -107,4 +107,4 @@ export type { ReactNavigationTransactionContext, TimeToDisplayProps } from './tr export { /** @deprecated Import the integration function directly, e.g. `screenshotIntegration()` instead of `new Integrations.Screenshot(). */ Integrations, -} +}; From 1be9a63a64d96a010ce6b5a9459647369fc31837 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 13 May 2024 15:12:55 +0200 Subject: [PATCH 6/7] fix sample --- samples/react-native/src/App.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 23b426a674..ad7edc5a20 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -28,8 +28,6 @@ import Ionicons from 'react-native-vector-icons/Ionicons'; const isMobileOs = Platform.OS === 'android' || Platform.OS === 'ios'; -Sentry.Integrations.ReactNativeErrorHandlers.enableNative = false; - const reactNavigationInstrumentation = new Sentry.ReactNavigationInstrumentation({ routeChangeTimeoutMs: 500, // How long it will wait for the route change to complete. Default is 1000ms From 94b227c7653bbc9dc3b3ba72e2f2e0048a98682e Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 13 May 2024 16:04:56 +0200 Subject: [PATCH 7/7] more fixes --- src/js/integrations/reactnativeerrorhandlers.ts | 7 ++++--- src/js/integrations/sdkinfo.ts | 5 +++-- .../reactnativeerrorhandlers.test.ts | 17 ++++------------- test/integrations/release.test.ts | 7 +++++-- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/js/integrations/reactnativeerrorhandlers.ts b/src/js/integrations/reactnativeerrorhandlers.ts index 380920fde4..274c80232d 100644 --- a/src/js/integrations/reactnativeerrorhandlers.ts +++ b/src/js/integrations/reactnativeerrorhandlers.ts @@ -28,9 +28,10 @@ export const reactNativeErrorHandlersIntegration = ( name: INTEGRATION_NAME, setupOnce: () => setup({ - onerror: options.onerror || true, - onunhandledrejection: options.onunhandledrejection || true, - patchGlobalPromise: options.patchGlobalPromise || true, + onerror: true, + onunhandledrejection: true, + patchGlobalPromise: true, + ...options, }), }; }; diff --git a/src/js/integrations/sdkinfo.ts b/src/js/integrations/sdkinfo.ts index 77f7c499be..62ad0a3b0e 100644 --- a/src/js/integrations/sdkinfo.ts +++ b/src/js/integrations/sdkinfo.ts @@ -75,9 +75,10 @@ function createCachedFetchNativeSdkInfo(): () => Promise { }; } + let isCached: boolean = false; + let nativeSdkPackageCache: Package | null = null; + return async () => { - let isCached: boolean = false; - let nativeSdkPackageCache: Package | null = null; if (isCached) { return nativeSdkPackageCache; } diff --git a/test/integrations/reactnativeerrorhandlers.test.ts b/test/integrations/reactnativeerrorhandlers.test.ts index 66249b3966..69932a9318 100644 --- a/test/integrations/reactnativeerrorhandlers.test.ts +++ b/test/integrations/reactnativeerrorhandlers.test.ts @@ -1,6 +1,6 @@ jest.mock('../../src/js/integrations/reactnativeerrorhandlersutils'); -import { type Hub, setCurrentClient } from '@sentry/core'; +import { setCurrentClient } from '@sentry/core'; import type { ExtendedError, SeverityLevel } from '@sentry/types'; import { reactNativeErrorHandlersIntegration } from '../../src/js/integrations/reactnativeerrorhandlers'; @@ -42,10 +42,7 @@ describe('ReactNativeErrorHandlers', () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce!( - () => {}, - () => ({} as Hub), - ); + integration.setupOnce(); expect(ErrorUtils.setGlobalHandler).toHaveBeenCalledWith(errorHandlerCallback); }); @@ -85,10 +82,7 @@ describe('ReactNativeErrorHandlers', () => { describe('onUnhandledRejection', () => { test('unhandled rejected promise is captured with synthetical error', async () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce!( - () => {}, - () => ({} as Hub), - ); + integration.setupOnce(); const [actualTrackingOptions] = mockEnable.mock.calls[0] || []; actualTrackingOptions?.onUnhandled?.(1, 'Test Error'); @@ -110,10 +104,7 @@ describe('ReactNativeErrorHandlers', () => { test('error like unhandled rejected promise is captured without synthetical error', async () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce!( - () => {}, - () => ({} as Hub), - ); + integration.setupOnce(); const [actualTrackingOptions] = mockEnable.mock.calls[0] || []; actualTrackingOptions?.onUnhandled?.(1, new Error('Test Error')); diff --git a/test/integrations/release.test.ts b/test/integrations/release.test.ts index 8a5a3e4240..71ba72888f 100644 --- a/test/integrations/release.test.ts +++ b/test/integrations/release.test.ts @@ -4,7 +4,7 @@ import { nativeReleaseIntegration } from '../../src/js/integrations/release'; jest.mock('../../src/js/wrapper', () => ({ NATIVE: { - fetchNativeRelease: () => ({ + fetchNativeRelease: async () => ({ build: 'native_build', id: 'native_id', version: 'native_version', @@ -67,7 +67,10 @@ describe('Tests the Release integration', () => { }, {}, { - getOptions: () => ({ dist: 'options_dist' }), + getOptions: () => ({ + dist: 'options_dist', + release: 'options_release', + }), } as Client, );