diff --git a/CHANGELOG.md b/CHANGELOG.md index 4834c2d198..b50dc97854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,15 @@ }); ``` +- New Native Frames Integration ([#3996](https://github.com/getsentry/sentry-react-native/pull/3996)) + + ```js + Sentry.init({ + tracesSampleRate: 1.0, + enableNativeFramesTracking: true, // default true + }); + ``` + ## 5.28.0 ### Fixes diff --git a/src/js/integrations/default.ts b/src/js/integrations/default.ts index edf7011479..c5fe6d81ab 100644 --- a/src/js/integrations/default.ts +++ b/src/js/integrations/default.ts @@ -24,6 +24,7 @@ import { inboundFiltersIntegration, mobileReplayIntegration, modulesLoaderIntegration, + nativeFramesIntegration, nativeLinkedErrorsIntegration, nativeReleaseIntegration, reactNativeErrorHandlersIntegration, @@ -98,6 +99,12 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ options.enableTracing || typeof options.tracesSampleRate === 'number' || typeof options.tracesSampler === 'function'; + if (hasTracingEnabled && options.enableAppStartTracking) { + integrations.push(appStartIntegration()); + } + if (hasTracingEnabled && options.enableNativeFramesTracking) { + integrations.push(nativeFramesIntegration()); + } if (hasTracingEnabled && options.enableAutoPerformanceTracing) { integrations.push(new ReactNativeTracing()); } diff --git a/src/js/integrations/exports.ts b/src/js/integrations/exports.ts index 2a34136673..3335cb7128 100644 --- a/src/js/integrations/exports.ts +++ b/src/js/integrations/exports.ts @@ -14,6 +14,7 @@ export { expoContextIntegration } from './expocontext'; export { spotlightIntegration } from './spotlight'; export { mobileReplayIntegration } from '../replay/mobilereplay'; export { appStartIntegration } from '../tracing/integrations/appStart'; +export { nativeFramesIntegration } from '../tracing/integrations/nativeFrames'; export { breadcrumbsIntegration, diff --git a/src/js/options.ts b/src/js/options.ts index c28f0d1581..d7b34191d3 100644 --- a/src/js/options.ts +++ b/src/js/options.ts @@ -194,10 +194,18 @@ export interface BaseReactNativeOptions { * * Requires performance monitoring to be enabled. * - * Default: true + * @default true */ enableAppStartTracking?: boolean; + /** + * Track the slow and frozen frames in the application. Enabling this options will add + * slow and frozen frames measurements to all created root spans (transactions). + * + * @default true + */ + enableNativeFramesTracking?: boolean; + /** * Options which are in beta, or otherwise not guaranteed to be stable. */ diff --git a/src/js/sdk.tsx b/src/js/sdk.tsx index 246f635eed..72e3ed3261 100644 --- a/src/js/sdk.tsx +++ b/src/js/sdk.tsx @@ -34,6 +34,7 @@ const DEFAULT_OPTIONS: ReactNativeOptions = { enableCaptureFailedRequests: false, enableNdk: true, enableAppStartTracking: true, + enableNativeFramesTracking: true, }; /** diff --git a/src/js/tracing/nativeframes.ts b/src/js/tracing/integrations/nativeFrames.ts similarity index 72% rename from src/js/tracing/nativeframes.ts rename to src/js/tracing/integrations/nativeFrames.ts index acf43f5e93..1c00ffddd1 100644 --- a/src/js/tracing/nativeframes.ts +++ b/src/js/tracing/integrations/nativeFrames.ts @@ -2,9 +2,10 @@ import { spanToJSON } from '@sentry/core'; import type { Client, Event, Integration, Measurements, MeasurementUnit, Span } from '@sentry/types'; import { logger, timestampInSeconds } from '@sentry/utils'; -import type { NativeFramesResponse } from '../NativeRNSentry'; -import { isRootSpan } from '../utils/span'; -import { NATIVE } from '../wrapper'; +import type { NativeFramesResponse } from '../../NativeRNSentry'; +import type { ReactNativeClientOptions } from '../../options'; +import { isRootSpan } from '../../utils/span'; +import { NATIVE } from '../../wrapper'; /** * Timeout from the final native frames fetch to processing the associated transaction. @@ -34,42 +35,64 @@ const MARGIN_OF_ERROR_SECONDS = 0.05; /** * Instrumentation to add native slow/frozen frames measurements onto transactions. */ -export class NativeFramesInstrumentation implements Integration { - public name: string = 'NativeFramesInstrumentation'; +export const nativeFramesIntegration = (): Integration => { + const name: string = 'NativeFrames'; /** The native frames at the finish time of the most recent span. */ - private _lastSpanFinishFrames?: { - timestamp: number; - nativeFrames: NativeFramesResponse; - }; - private _spanToNativeFramesAtStartMap: Map = new Map(); - - public constructor() { - logger.log('[ReactNativeTracing] Native frames instrumentation initialized.'); - } + let _lastSpanFinishFrames: + | { + timestamp: number; + nativeFrames: NativeFramesResponse; + } + | undefined = undefined; + const _spanToNativeFramesAtStartMap: Map = new Map(); /** * Hooks into the client start and end span events. */ - public setup(client: Client): void { - client.on('spanStart', this._onSpanStart); - client.on('spanEnd', this._onSpanFinish); - } + const setup = (client: Client): void => { + const { enableNativeFramesTracking } = client.getOptions() as ReactNativeClientOptions; + + if (enableNativeFramesTracking && !NATIVE.enableNative) { + // Do not enable native frames tracking if native is not available. + logger.warn( + '[ReactNativeTracing] NativeFramesTracking is not available on the Web, Expo Go and other platforms without native modules.', + ); + return; + } + + if (!enableNativeFramesTracking && NATIVE.enableNative) { + // Disable native frames tracking when native available and option is false. + NATIVE.disableNativeFramesTracking(); + return; + } + + if (!enableNativeFramesTracking) { + return; + } + + NATIVE.enableNativeFramesTracking(); + + // TODO: Ensure other integrations like ReactNativeTracing and ReactNavigation create spans after all integration are setup. + client.on('spanStart', _onSpanStart); + client.on('spanEnd', _onSpanFinish); + logger.log('[ReactNativeTracing] Native frames instrumentation initialized.'); + }; /** * Adds frames measurements to an event. Called from a valid event processor. * Awaits for finish frames if needed. */ - public processEvent(event: Event): Promise { - return this._processEvent(event); - } + const processEvent = (event: Event): Promise => { + return _processEvent(event); + }; /** * Fetches the native frames in background if the given span is a root span. * * @param {Span} rootSpan - The span that has started. */ - private _onSpanStart = (rootSpan: Span): void => { + const _onSpanStart = (rootSpan: Span): void => { if (!isRootSpan(rootSpan)) { return; } @@ -87,7 +110,7 @@ export class NativeFramesInstrumentation implements Integration { return; } - this._spanToNativeFramesAtStartMap.set(rootSpan.spanContext().traceId, frames); + _spanToNativeFramesAtStartMap.set(rootSpan.spanContext().traceId, frames); }) .then(undefined, error => { logger.error( @@ -101,9 +124,9 @@ export class NativeFramesInstrumentation implements Integration { * Called on a span finish to fetch native frames to support transactions with trimEnd. * Only to be called when a span does not have an end timestamp. */ - private _onSpanFinish = (span: Span): void => { + const _onSpanFinish = (span: Span): void => { if (isRootSpan(span)) { - return this._onTransactionFinish(span); + return _onTransactionFinish(span); } const timestamp = timestampInSeconds(); @@ -114,7 +137,7 @@ export class NativeFramesInstrumentation implements Integration { return; } - this._lastSpanFinishFrames = { + _lastSpanFinishFrames = { timestamp, nativeFrames: frames, }; @@ -127,26 +150,26 @@ export class NativeFramesInstrumentation implements Integration { /** * To be called when a transaction is finished */ - private _onTransactionFinish(span: Span): void { - this._fetchFramesForTransaction(span).then(undefined, (reason: unknown) => { + const _onTransactionFinish = (span: Span): void => { + _fetchFramesForTransaction(span).then(undefined, (reason: unknown) => { logger.error( `[NativeFrames] Error while fetching frames for root span start (${span.spanContext().spanId})`, reason, ); }); - } + }; /** * Returns the computed frames measurements and awaits for them if they are not ready yet. */ - private async _getFramesMeasurements( + const _getFramesMeasurements = ( traceId: string, finalEndTimestamp: number, startFrames: NativeFramesResponse, - ): Promise { + ): Promise => { if (_finishFrames.has(traceId)) { logger.debug(`[NativeFrames] Native end frames already fetched for trace id (${traceId}).`); - return this._prepareMeasurements(traceId, finalEndTimestamp, startFrames); + return Promise.resolve(_prepareMeasurements(traceId, finalEndTimestamp, startFrames)); } return new Promise(resolve => { @@ -159,22 +182,22 @@ export class NativeFramesInstrumentation implements Integration { _framesListeners.set(traceId, () => { logger.debug(`[NativeFrames] Native end frames listener called for trace id (${traceId}).`); - resolve(this._prepareMeasurements(traceId, finalEndTimestamp, startFrames)); + resolve(_prepareMeasurements(traceId, finalEndTimestamp, startFrames)); clearTimeout(timeout); _framesListeners.delete(traceId); }); }); - } + }; /** * Returns the computed frames measurements given ready data */ - private _prepareMeasurements( + const _prepareMeasurements = ( traceId: string, finalEndTimestamp: number, // The actual transaction finish time. startFrames: NativeFramesResponse, - ): FramesMeasurements | null { + ): FramesMeasurements | null => { let finalFinishFrames: NativeFramesResponse | undefined; const finish = _finishFrames.get(traceId); @@ -187,13 +210,13 @@ export class NativeFramesInstrumentation implements Integration { logger.debug(`[NativeFrames] Using frames from root span end (traceId, ${traceId}).`); finalFinishFrames = finish.nativeFrames; } else if ( - this._lastSpanFinishFrames && - Math.abs(this._lastSpanFinishFrames.timestamp - finalEndTimestamp) < MARGIN_OF_ERROR_SECONDS + _lastSpanFinishFrames && + Math.abs(_lastSpanFinishFrames.timestamp - finalEndTimestamp) < MARGIN_OF_ERROR_SECONDS ) { // Fallback to the last span finish if it is within the margin of error of the actual finish timestamp. // This should be the case for trimEnd. logger.debug(`[NativeFrames] Using native frames from last span end (traceId, ${traceId}).`); - finalFinishFrames = this._lastSpanFinishFrames.nativeFrames; + finalFinishFrames = _lastSpanFinishFrames.nativeFrames; } else { logger.warn( `[NativeFrames] Frames were collected within larger than margin of error delay for traceId (${traceId}). Dropping the inaccurate values.`, @@ -228,18 +251,18 @@ export class NativeFramesInstrumentation implements Integration { } return measurements; - } + }; /** * Fetch finish frames for a transaction at the current time. Calls any awaiting listeners. */ - private async _fetchFramesForTransaction(span: Span): Promise { + const _fetchFramesForTransaction = async (span: Span): Promise => { const traceId = spanToJSON(span).trace_id; if (!traceId) { return; } - const startFrames = this._spanToNativeFramesAtStartMap.get(span.spanContext().traceId); + const startFrames = _spanToNativeFramesAtStartMap.get(span.spanContext().traceId); // This timestamp marks when the finish frames were retrieved. It should be pretty close to the transaction finish. const timestamp = timestampInSeconds(); @@ -255,13 +278,13 @@ export class NativeFramesInstrumentation implements Integration { _framesListeners.get(traceId)?.(); - setTimeout(() => this._cancelEndFrames(span), FINAL_FRAMES_TIMEOUT_MS); - } + setTimeout(() => _cancelEndFrames(span), FINAL_FRAMES_TIMEOUT_MS); + }; /** * On a finish frames failure, we cancel the await. */ - private _cancelEndFrames(span: Span): void { + const _cancelEndFrames = (span: Span): void => { const spanJSON = spanToJSON(span); const traceId = spanJSON.trace_id; if (!traceId) { @@ -275,13 +298,13 @@ export class NativeFramesInstrumentation implements Integration { `[NativeFrames] Native frames timed out for ${spanJSON.op} transaction ${spanJSON.description}. Not adding native frames measurements.`, ); } - } + }; /** * Adds frames measurements to an event. Called from a valid event processor. * Awaits for finish frames if needed. */ - private async _processEvent(event: Event): Promise { + const _processEvent = async (event: Event): Promise => { if ( event.type !== 'transaction' || !event.transaction || @@ -295,8 +318,8 @@ export class NativeFramesInstrumentation implements Integration { const traceOp = event.contexts.trace.op; const traceId = event.contexts.trace.trace_id; - const startFrames = this._spanToNativeFramesAtStartMap.get(traceId); - this._spanToNativeFramesAtStartMap.delete(traceId); + const startFrames = _spanToNativeFramesAtStartMap.get(traceId); + _spanToNativeFramesAtStartMap.delete(traceId); if (!startFrames) { logger.warn( `[NativeFrames] Start frames of transaction ${event.transaction} (eventId, ${event.event_id}) are missing, but it already ended.`, @@ -304,7 +327,7 @@ export class NativeFramesInstrumentation implements Integration { return event; } - const measurements = await this._getFramesMeasurements(traceId, event.timestamp, startFrames); + const measurements = await _getFramesMeasurements(traceId, event.timestamp, startFrames); if (!measurements) { logger.log( @@ -329,5 +352,11 @@ export class NativeFramesInstrumentation implements Integration { _finishFrames.delete(traceId); return event; - } -} + }; + + return { + name, + setup, + processEvent, + }; +}; diff --git a/src/js/tracing/reactnativetracing.ts b/src/js/tracing/reactnativetracing.ts index 0d146190ae..8c2e5ef1ac 100644 --- a/src/js/tracing/reactnativetracing.ts +++ b/src/js/tracing/reactnativetracing.ts @@ -14,8 +14,6 @@ import type { Client, Event, Integration, PropagationContext, Scope, Span, Start import { logger, uuid4 } from '@sentry/utils'; import type { RoutingInstrumentationInstance } from '../tracing/routingInstrumentation'; -import { NATIVE } from '../wrapper'; -import { NativeFramesInstrumentation } from './nativeframes'; import { adjustTransactionDuration, cancelInBackground, @@ -91,11 +89,6 @@ export interface ReactNativeTracingOptions extends RequestInstrumentationOptions */ beforeNavigate: BeforeNavigate; - /** - * Track slow/frozen frames from the native layer and adds them as measurements to all transactions. - */ - enableNativeFramesTracking: boolean; - /** * Track when and how long the JS event loop stalls for. Adds stalls as measurements to all transactions. */ @@ -117,7 +110,6 @@ const defaultReactNativeTracingOptions: ReactNativeTracingOptions = { finalTimeoutMs: 600000, ignoreEmptyBackNavigationTransactions: true, beforeNavigate: context => context, - enableNativeFramesTracking: true, enableStallTracking: true, enableUserInteractionTracing: false, }; @@ -139,7 +131,6 @@ export class ReactNativeTracing implements Integration { /** ReactNativeTracing options */ public options: ReactNativeTracingOptions; - public nativeFramesInstrumentation?: NativeFramesInstrumentation; public stallTrackingInstrumentation?: StallTrackingInstrumentation; public useAppStartWithProfiler: boolean = false; @@ -201,8 +192,6 @@ export class ReactNativeTracing implements Integration { (this._hasSetTracePropagationTargets && thisOptionsTracePropagationTargets) || DEFAULT_TRACE_PROPAGATION_TARGETS; - this._enableNativeFramesTracking(client); - if (enableStallTracking) { this.stallTrackingInstrumentation = new StallTrackingInstrumentation(); this.stallTrackingInstrumentation.setup(client); @@ -233,9 +222,7 @@ export class ReactNativeTracing implements Integration { */ public processEvent(event: Event): Promise | Event { const eventWithView = this._getCurrentViewEventProcessor(event); - return this.nativeFramesInstrumentation - ? this.nativeFramesInstrumentation.processEvent(eventWithView) - : eventWithView; + return eventWithView; } /** @@ -318,33 +305,6 @@ export class ReactNativeTracing implements Integration { return this._inflightInteractionTransaction; } - /** - * Enables or disables native frames tracking based on the `enableNativeFramesTracking` option. - */ - private _enableNativeFramesTracking(client: Client): void { - if (this.options.enableNativeFramesTracking && !NATIVE.enableNative) { - // Do not enable native frames tracking if native is not available. - logger.warn( - '[ReactNativeTracing] NativeFramesTracking is not available on the Web, Expo Go and other platforms without native modules.', - ); - return; - } - - if (!this.options.enableNativeFramesTracking && NATIVE.enableNative) { - // Disable native frames tracking when native available and option is false. - NATIVE.disableNativeFramesTracking(); - return; - } - - if (!this.options.enableNativeFramesTracking) { - return; - } - - NATIVE.enableNativeFramesTracking(); - this.nativeFramesInstrumentation = new NativeFramesInstrumentation(); - this.nativeFramesInstrumentation.setup(client); - } - /** * Sets the current view name into the app context. * @param event Le event. diff --git a/test/sdk.test.ts b/test/sdk.test.ts index 672dc1959a..0882b32184 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -315,10 +315,7 @@ describe('Tests the SDK functionality', () => { it('no http client integration by default', () => { init({}); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'HttpClient' })])); + expectNotIntegration('HttpClient'); }); it('adds http client integration', () => { @@ -326,10 +323,7 @@ describe('Tests the SDK functionality', () => { enableCaptureFailedRequests: true, }); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'HttpClient' })])); + expectIntegration('HttpClient'); }); it('user defined http client integration overwrites default', () => { @@ -361,10 +355,7 @@ describe('Tests the SDK functionality', () => { it('no screenshot integration by default', () => { init({}); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Screenshot' })])); + expectNotIntegration('Screenshot'); }); it('adds screenshot integration', () => { @@ -372,21 +363,13 @@ describe('Tests the SDK functionality', () => { attachScreenshot: true, }); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'Screenshot' })])); + expectIntegration('Screenshot'); }); it('no view hierarchy integration by default', () => { init({}); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual( - expect.not.arrayContaining([expect.objectContaining({ name: 'ViewHierarchy' })]), - ); + expectNotIntegration('ViewHierarchy'); }); it('adds view hierarchy integration', () => { @@ -394,20 +377,13 @@ describe('Tests the SDK functionality', () => { attachViewHierarchy: true, }); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'ViewHierarchy' })])); + expectIntegration('ViewHierarchy'); }); it('no profiling integration by default', () => { init({}); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - expect(actualIntegrations).toEqual( - expect.not.arrayContaining([expect.objectContaining({ name: 'HermesProfiling' })]), - ); + expectNotIntegration('HermesProfiling'); }); it('adds profiling integration', () => { @@ -417,19 +393,13 @@ describe('Tests the SDK functionality', () => { }, }); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - expect(actualIntegrations).toEqual( - expect.arrayContaining([expect.objectContaining({ name: 'HermesProfiling' })]), - ); + expectIntegration('HermesProfiling'); }); it('no spotlight integration by default', () => { init({}); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Spotlight' })])); + expectNotIntegration('Spotlight'); }); it('adds spotlight integration', () => { @@ -437,9 +407,53 @@ describe('Tests the SDK functionality', () => { enableSpotlight: true, }); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'Spotlight' })])); + expectIntegration('Spotlight'); + }); + + it('no app start integration by default', () => { + init({}); + + expectNotIntegration('AppStart'); + }); + + it('when tracing enabled app start integration added by default', () => { + init({ + tracesSampleRate: 0.5, + }); + + expectIntegration('AppStart'); + }); + + it('when tracing enabled and app start disabled the integration is not added', () => { + init({ + tracesSampleRate: 0.5, + enableAppStartTracking: false, + }); + + expectNotIntegration('AppStart'); + }); + + it('no native frames integration by default', () => { + init({}); + + expectNotIntegration('NativeFrames'); + }); + + it('when tracing enabled native frames integration added by default', () => { + init({ + tracesSampleRate: 0.5, + }); + + expectIntegration('NativeFrames'); + }); + + it('when tracing enabled and native frames disabled the integration is not added', () => { + init({ + tracesSampleRate: 0.5, + enableNativeFramesTracking: false, + }); + + expectNotIntegration('NativeFrames'); }); it('no app start integration by default', () => { @@ -561,50 +575,29 @@ describe('Tests the SDK functionality', () => { it('adds react default integrations', () => { init({}); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'InboundFilters' }), - expect.objectContaining({ name: 'FunctionToString' }), - expect.objectContaining({ name: 'Breadcrumbs' }), - expect.objectContaining({ name: 'Dedupe' }), - expect.objectContaining({ name: 'HttpContext' }), - ]), - ); + expectIntegration('InboundFilters'); + expectIntegration('FunctionToString'); + expectIntegration('Breadcrumbs'); + expectIntegration('Dedupe'); + expectIntegration('HttpContext'); }); it('adds all platform default integrations', () => { init({}); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'Release' }), - expect.objectContaining({ name: 'EventOrigin' }), - expect.objectContaining({ name: 'SdkInfo' }), - expect.objectContaining({ name: 'ReactNativeInfo' }), - ]), - ); + expectIntegration('Release'); + expectIntegration('EventOrigin'); + expectIntegration('SdkInfo'); + expectIntegration('ReactNativeInfo'); }); it('adds web platform specific default integrations', () => { (notWeb as jest.Mock).mockImplementation(() => false); init({}); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'BrowserApiErrors' }), - expect.objectContaining({ name: 'GlobalHandlers' }), - expect.objectContaining({ name: 'LinkedErrors' }), - ]), - ); + expectIntegration('BrowserApiErrors'); + expectIntegration('GlobalHandlers'); + expectIntegration('LinkedErrors'); }); it('does not add native integrations if native disabled', () => { @@ -617,22 +610,11 @@ describe('Tests the SDK functionality', () => { }, }); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual( - expect.not.arrayContaining([expect.objectContaining({ name: 'DeviceContext' })]), - ); - expect(actualIntegrations).toEqual( - expect.not.arrayContaining([expect.objectContaining({ name: 'ModulesLoader' })]), - ); - expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Screenshot' })])); - expect(actualIntegrations).toEqual( - expect.not.arrayContaining([expect.objectContaining({ name: 'ViewHierarchy' })]), - ); - expect(actualIntegrations).toEqual( - expect.not.arrayContaining([expect.objectContaining({ name: 'HermesProfiling' })]), - ); + expectNotIntegration('DeviceContext'); + expectNotIntegration('ModulesLoader'); + expectNotIntegration('Screenshot'); + expectNotIntegration('ViewHierarchy'); + expectNotIntegration('HermesProfiling'); }); }); @@ -640,13 +622,22 @@ describe('Tests the SDK functionality', () => { (isExpoGo as jest.Mock).mockImplementation(() => true); init({}); - const actualOptions = usedOptions(); - const actualIntegrations = actualOptions?.integrations; - - expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'ExpoContext' })])); + expectIntegration('ExpoContext'); }); }); +function expectIntegration(name: string): void { + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; + expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name })])); +} + +function expectNotIntegration(name: string): void { + const actualOptions = usedOptions(); + const actualIntegrations = actualOptions?.integrations; + expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name })])); +} + function createMockedIntegration({ name }: { name?: string } = {}): Integration { return { name: name ?? 'MockedIntegration', diff --git a/test/tracing/nativeframes.test.ts b/test/tracing/integrations/nativeframes.test.ts similarity index 77% rename from test/tracing/nativeframes.test.ts rename to test/tracing/integrations/nativeframes.test.ts index 1ca2f70f8f..bcab5bcb2e 100644 --- a/test/tracing/nativeframes.test.ts +++ b/test/tracing/integrations/nativeframes.test.ts @@ -1,12 +1,12 @@ import { getCurrentScope, getGlobalScope, getIsolationScope, setCurrentClient, startSpan } from '@sentry/core'; import type { Event, Measurements } from '@sentry/types'; -import { ReactNativeTracing } from '../../src/js'; -import { NATIVE } from '../../src/js/wrapper'; -import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; -import { mockFunction } from '../testutils'; +import { nativeFramesIntegration } from '../../../src/js'; +import { NATIVE } from '../../../src/js/wrapper'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; +import { mockFunction } from '../../testutils'; -jest.mock('../../src/js/wrapper', () => { +jest.mock('../../../src/js/wrapper', () => { return { NATIVE: { fetchNativeFrames: jest.fn(), @@ -29,11 +29,8 @@ describe('NativeFramesInstrumentation', () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0, - integrations: [ - new ReactNativeTracing({ - enableNativeFramesTracking: true, - }), - ], + enableNativeFramesTracking: true, + integrations: [nativeFramesIntegration()], }); client = new TestClient(options); setCurrentClient(client); @@ -144,18 +141,18 @@ describe('NativeFramesInstrumentation', () => { await jest.runOnlyPendingTimersAsync(); await client.flush(); - expect(client.event!).toEqual( + expect(client.event!).toBeOneOf([ + expect.not.objectContaining>({ + measurements: expect.anything(), + }), expect.objectContaining>({ - measurements: expect.toBeOneOf([ - expect.not.objectContaining({ - frames_total: expect.any(Object), - frames_slow: expect.any(Object), - frames_frozen: expect.any(Object), - }), - undefined, - ]), + measurements: expect.not.objectContaining({ + frames_total: expect.any(Object), + frames_slow: expect.any(Object), + frames_frozen: expect.any(Object), + }), }), - ); + ]); }); it('does not set measurements on transactions without startFrames', async () => { @@ -174,15 +171,18 @@ describe('NativeFramesInstrumentation', () => { await jest.runOnlyPendingTimersAsync(); await client.flush(); - expect(client.event!).toEqual( + expect(client.event!).toBeOneOf([ + expect.not.objectContaining>({ + measurements: expect.anything(), + }), expect.objectContaining>({ - measurements: expect.not.objectContaining({ - frames_total: {}, - frames_slow: {}, - frames_frozen: {}, + measurements: expect.not.objectContaining({ + frames_total: expect.any(Object), + frames_slow: expect.any(Object), + frames_frozen: expect.any(Object), }), }), - ); + ]); }); it('does not set measurements on transactions without finishFrames', async () => { @@ -201,15 +201,18 @@ describe('NativeFramesInstrumentation', () => { await jest.runOnlyPendingTimersAsync(); await client.flush(); - expect(client.event!).toEqual( + expect(client.event!).toBeOneOf([ + expect.not.objectContaining>({ + measurements: expect.anything(), + }), expect.objectContaining>({ - measurements: expect.not.objectContaining({ - frames_total: {}, - frames_slow: {}, - frames_frozen: {}, + measurements: expect.not.objectContaining({ + frames_total: expect.any(Object), + frames_slow: expect.any(Object), + frames_frozen: expect.any(Object), }), }), - ); + ]); }); it('does not set measurements on a transaction event for which finishFrames times out.', async () => { @@ -233,14 +236,17 @@ describe('NativeFramesInstrumentation', () => { await jest.advanceTimersByTimeAsync(2100); // hardcoded final frames timeout 2000ms await client.flush(); - expect(client.event!).toEqual( + expect(client.event!).toBeOneOf([ + expect.not.objectContaining>({ + measurements: expect.anything(), + }), expect.objectContaining>({ - measurements: expect.not.objectContaining({ - frames_total: {}, - frames_slow: {}, - frames_frozen: {}, + measurements: expect.not.objectContaining({ + frames_total: expect.any(Object), + frames_slow: expect.any(Object), + frames_frozen: expect.any(Object), }), }), - ); + ]); }); }); diff --git a/test/tracing/reactnativetracing.test.ts b/test/tracing/reactnativetracing.test.ts index f01d049b60..1d840e0f31 100644 --- a/test/tracing/reactnativetracing.test.ts +++ b/test/tracing/reactnativetracing.test.ts @@ -212,39 +212,6 @@ describe('ReactNativeTracing', () => { }); }); - describe('Native Frames', () => { - let client: TestClient; - - beforeEach(() => { - client = setupTestClient(); - }); - - it('Initialize native frames instrumentation if flag is true', async () => { - const integration = new ReactNativeTracing({ - enableNativeFramesTracking: true, - }); - integration.setup(client); - - await jest.advanceTimersByTimeAsync(500); - - expect(integration.nativeFramesInstrumentation).toBeDefined(); - expect(NATIVE.enableNativeFramesTracking).toBeCalledTimes(1); - }); - it('Does not initialize native frames instrumentation if flag is false', async () => { - const integration = new ReactNativeTracing({ - enableNativeFramesTracking: false, - }); - - integration.setup(client); - - await jest.advanceTimersByTimeAsync(500); - - expect(integration.nativeFramesInstrumentation).toBeUndefined(); - expect(NATIVE.disableNativeFramesTracking).toBeCalledTimes(1); - expect(NATIVE.fetchNativeFrames).not.toBeCalled(); - }); - }); - describe('Routing Instrumentation', () => { let client: TestClient;