Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- Instrument Expo Router `push`, `replace`, `navigate`, `back`, and `dismiss` (in addition to `prefetch`) with breadcrumbs and spans, and tag the resulting idle navigation span with the initiating `navigation.method` ([#6221](https://github.com/getsentry/sentry-react-native/pull/6221))
- Note: Expo Router span/breadcrumb attributes that may contain user identifiers (`route.href`, `route.params`, and concrete pathnames derived from string hrefs such as `/users/42`) are now gated behind `sendDefaultPii`. When `sendDefaultPii` is off (the default), prefetch spans for string hrefs use `route.name: 'unknown'` and omit `route.href`. Templated object hrefs (e.g. `{ pathname: '/users/[id]' }`) are unaffected.
- Warn when Gradle resolves `sentry-android` to a version incompatible with the SDK ([#6238](https://github.com/getsentry/sentry-react-native/pull/6238))
- Attach the active TurboModule method to native crash reports as `contexts.turbo_module` + `turbo_module.name` / `turbo_module.method` tags ([#6227](https://github.com/getsentry/sentry-react-native/pull/6227))

### Fixes

Expand Down
58 changes: 51 additions & 7 deletions packages/core/etc/sentry-react-native.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
//
// @public
export const appStartIntegration: (input?: {
standalone?: boolean;
standalone?: boolean | undefined;
}) => AppStartIntegration;

export { Breadcrumb }
Expand Down Expand Up @@ -335,7 +335,7 @@
export const feedbackIntegration: (initOptions?: Partial<FeedbackFormProps> & {
buttonOptions?: FeedbackButtonProps;
screenshotButtonOptions?: ScreenshotButtonProps;
colorScheme?: "system" | "light" | "dark";
colorScheme?: 'system' | 'light' | 'dark';
themeLight?: Partial<FeedbackFormTheme>;
themeDark?: Partial<FeedbackFormTheme>;
enableShakeToReport?: boolean;
Expand All @@ -354,6 +354,9 @@

export { getActiveSpan }

// @public
export function getActiveTurboModuleCall(): TurboModuleCall | undefined;

Check warning on line 359 in packages/core/etc/sentry-react-native.api.md

View check run for this annotation

@sentry/warden / warden: code-review

[AEP-X5X] popTurboModuleCall clears scope that still has active calls when scopes interleave (additional location)

In `popTurboModuleCall` (`turboModuleTracker.ts`), after removing a frame, the code looks only at the new stack top: if its scope differs from the popped frame's scope it calls `clearScope(popped.scope)`. It never checks whether deeper frames still hold that same scope. Whenever scopes interleave on the stack (e.g. `[A@scope1, B@scope2]` LIFO-popping `B`, or `[A@scope1, B@scope1, C@scope2]` out-of-order-popping `B`), an active lower call on `popped.scope` silently loses its `turbo_module` context/tags until that call itself finishes and re-syncs. This degrades the crash-time context the integration is designed to provide.
export { getClient }

// Warning: (ae-forgotten-export) The symbol "ReactNativeTracingIntegration" needs to be exported by the entry point index.d.ts
Expand All @@ -378,6 +381,9 @@

export { getRootSpan }

// @public
export function getTurboModuleCallStack(): TurboModuleCall[];

// Warning: (ae-forgotten-export) The symbol "GlobalErrorBoundaryState" needs to be exported by the entry point index.d.ts
//
// @public
Expand Down Expand Up @@ -530,11 +536,22 @@
// @public
export function pauseAppHangTracking(): void;

// @public
export function popTurboModuleCall(callId: number): void;

// @public
export const primitiveTagIntegration: () => Integration;

export { Profiler }

// @public
export function pushTurboModuleCall(args: {
name: string;
method: string;
kind: 'sync' | 'async';
Comment thread
sentry-warden[bot] marked this conversation as resolved.
scope?: Scope;
}): number;

// Warning: (ae-forgotten-export) The symbol "ReactNativeClientOptions" needs to be exported by the entry point index.d.ts
//
// @public
Expand Down Expand Up @@ -729,14 +746,15 @@

// @public
export const stallTrackingIntegration: (input?: {
minimumStallThresholdMs?: number;
minimumStallThresholdMs?: number | undefined;
}) => Integration;

// Warning: (ae-forgotten-export) The symbol "defaultIdleOptions" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export const startIdleNavigationSpan: (startSpanOption: StartSpanOptions, input?: Partial<typeof defaultIdleOptions> & {
isAppRestart?: boolean;
export const startIdleNavigationSpan: (startSpanOption: StartSpanOptions, input?: Partial<{
idleTimeout: number;
finalTimeout: number;
}> & {
isAppRestart?: boolean | undefined;
}) => Span | undefined;

// @public
Expand Down Expand Up @@ -808,6 +826,27 @@

export { TransactionEvent }

// @public
export interface TurboModuleCall {
callId: number;
kind: 'sync' | 'async';
method: string;
name: string;
startedAtMs: number;
}

// @public
export const turboModuleContextIntegration: (options?: TurboModuleContextOptions) => Integration;

// @public (undocumented)
export interface TurboModuleContextOptions {
modules?: Array<{
name: string;
module: object | null | undefined;
skipMethods?: ReadonlyArray<string>;
}>;
}

// @public (undocumented)
export const Unmask: HostComponent<ViewProps> | React_2.ComponentType<ViewProps>;

Expand Down Expand Up @@ -850,8 +889,13 @@
export function wrapExpoImage<T extends ExpoImage>(imageClass: T): T;

// @public
export function wrapExpoRouter<T extends ExpoRouter>(router: T): T;

Check warning on line 892 in packages/core/etc/sentry-react-native.api.md

View check run for this annotation

@sentry/warden / warden: code-review

[AEP-X5X] popTurboModuleCall clears scope that still has active calls when scopes interleave (additional location)

In `popTurboModuleCall` (`turboModuleTracker.ts`), after removing a frame, the code looks only at the new stack top: if its scope differs from the popped frame's scope it calls `clearScope(popped.scope)`. It never checks whether deeper frames still hold that same scope. Whenever scopes interleave on the stack (e.g. `[A@scope1, B@scope2]` LIFO-popping `B`, or `[A@scope1, B@scope1, C@scope2]` out-of-order-popping `B`), an active lower call on `popped.scope` silently loses its `turbo_module` context/tags until that call itself finishes and re-syncs. This degrades the crash-time context the integration is designed to provide.

// @public
export function wrapTurboModule<T extends object>(name: string, module: T | null | undefined, options?: {
skip?: ReadonlyArray<string>;
}): T | null | undefined;

// Warnings were encountered during analysis:
//
// src/js/feedback/integration.ts:21:5 - (ae-forgotten-export) The symbol "ScreenshotButtonProps" needs to be exported by the entry point index.d.ts
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,12 @@ export { FeedbackForm as FeedbackWidget } from './feedback/FeedbackForm';
export { showFeedbackForm as showFeedbackWidget } from './feedback/FeedbackFormManager';

export { getDataFromUri } from './wrapper';

export {
getActiveTurboModuleCall,
getTurboModuleCallStack,
popTurboModuleCall,
pushTurboModuleCall,
wrapTurboModule,
} from './turbomodule';
export type { TurboModuleCall } from './turbomodule';
6 changes: 6 additions & 0 deletions packages/core/src/js/integrations/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
spotlightIntegration,
stallTrackingIntegration,
timeToDisplayIntegration,
turboModuleContextIntegration,

Check warning on line 44 in packages/core/src/js/integrations/default.ts

View check run for this annotation

@sentry/warden / warden: code-review

popTurboModuleCall clears scope that still has active calls when scopes interleave

In `popTurboModuleCall` (`turboModuleTracker.ts`), after removing a frame, the code looks only at the new stack top: if its scope differs from the popped frame's scope it calls `clearScope(popped.scope)`. It never checks whether deeper frames still hold that same scope. Whenever scopes interleave on the stack (e.g. `[A@scope1, B@scope2]` LIFO-popping `B`, or `[A@scope1, B@scope1, C@scope2]` out-of-order-popping `B`), an active lower call on `popped.scope` silently loses its `turbo_module` context/tags until that call itself finishes and re-syncs. This degrades the crash-time context the integration is designed to provide.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

popTurboModuleCall clears scope that still has active calls when scopes interleave

In popTurboModuleCall (turboModuleTracker.ts), after removing a frame, the code looks only at the new stack top: if its scope differs from the popped frame's scope it calls clearScope(popped.scope). It never checks whether deeper frames still hold that same scope. Whenever scopes interleave on the stack (e.g. [A@scope1, B@scope2] LIFO-popping B, or [A@scope1, B@scope1, C@scope2] out-of-order-popping B), an active lower call on popped.scope silently loses its turbo_module context/tags until that call itself finishes and re-syncs. This degrades the crash-time context the integration is designed to provide.

Evidence
  • popTurboModuleCall in packages/core/src/js/turbomodule/turboModuleTracker.ts lines ~140-160 checks only stack[stack.length - 1] against popped.scope.
  • If the new top is on a different scope, it calls clearScope(popped.scope) unconditionally via setContext(CONTEXT_KEY, null) and resets the tag pair — even if a deeper frame in stack still holds that scope.
  • pushTurboModuleCall pins scope = args.scope ?? getCurrentScope(), so interleaving scopes on the stack is an explicitly supported case (the comment on InternalCall.scope calls this out).
  • popTurboModuleCall never scans stack for other frames with popped.scope, and tests in turboModuleTracker.test.ts do not cover multi-scope interleaving — only single-scope nesting.
Also found at 3 additional locations
  • packages/core/src/js/turbomodule/turboModuleTracker.ts:152-160
  • packages/core/etc/sentry-react-native.api.md:357-359
  • packages/core/etc/sentry-react-native.api.md:892

Identified by Warden code-review · AEP-X5X

userInteractionIntegration,
viewHierarchyIntegration,
} from './exports';
Expand Down Expand Up @@ -174,5 +175,10 @@

integrations.push(primitiveTagIntegration());

if (options.enableNative) {
// Attribute native crashes to the active TurboModule method (see #6163).
integrations.push(turboModuleContextIntegration());
}

return integrations;
}
2 changes: 2 additions & 0 deletions packages/core/src/js/integrations/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export { appRegistryIntegration } from './appRegistry';
export { timeToDisplayIntegration } from '../tracing/integrations/timeToDisplayIntegration';
export { breadcrumbsIntegration } from './breadcrumbs';
export { primitiveTagIntegration } from './primitiveTagIntegration';
export { turboModuleContextIntegration } from './turboModuleContext';
export type { TurboModuleContextOptions } from './turboModuleContext';
export { logEnricherIntegration } from './logEnricherIntegration';
export { graphqlIntegration } from './graphql';
export { supabaseIntegration } from './supabase';
Expand Down
50 changes: 50 additions & 0 deletions packages/core/src/js/integrations/turboModuleContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Integration } from '@sentry/core';

import { wrapTurboModule } from '../turbomodule';
import { getRNSentryModule } from '../wrapper';

export const INTEGRATION_NAME = 'TurboModuleContext';

export interface TurboModuleContextOptions {
/**
* Additional TurboModules to track. Each entry's methods will be wrapped so
* that any native crash happening inside a method call gets `contexts.turbo_module`
* + `turbo_module.name` / `turbo_module.method` attached to the crash report.
*
* The built-in `RNSentry` TurboModule is always tracked.
*/
modules?: Array<{ name: string; module: object | null | undefined; skipMethods?: ReadonlyArray<string> }>;
}

// `addListener` / `removeListeners` are RN event-emitter stubs that fire on
// every subscriber registration — tracking them would just churn the scope.
const RNSENTRY_SKIP = ['addListener', 'removeListeners'] as const;

/**
* Attaches the currently-executing TurboModule method to the Sentry scope so
* that native crashes can be attributed to the high-level RN module + method
* (e.g. `RNSentry.captureEnvelope`) on top of the native stack trace.
*
* The active call is mirrored as `contexts.turbo_module` and the
* `turbo_module.name` / `turbo_module.method` tags, both of which are already
* synced to the native SDKs by the existing scope-sync hooks and therefore end
* up in crash reports captured by sentry-cocoa / sentry-java.
*
* See https://github.com/getsentry/sentry-react-native/issues/6163.
*/
export const turboModuleContextIntegration = (options: TurboModuleContextOptions = {}): Integration => {
return {
name: INTEGRATION_NAME,
setupOnce() {
// Wrap the live RNSentry TurboModule. Other integrations import the same
// instance by reference, so wrapping here transparently tracks every call
// made from JS — including the SDK's own internal envelope/scope sync
// calls, which are the most likely entry points for native crashes.
wrapTurboModule('RNSentry', getRNSentryModule(), { skip: RNSENTRY_SKIP });

for (const entry of options.modules ?? []) {
wrapTurboModule(entry.name, entry.module, { skip: entry.skipMethods });
}
},
};
};
8 changes: 8 additions & 0 deletions packages/core/src/js/turbomodule/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {
getActiveTurboModuleCall,
getTurboModuleCallStack,
popTurboModuleCall,
pushTurboModuleCall,
} from './turboModuleTracker';
export type { TurboModuleCall } from './turboModuleTracker';
export { wrapTurboModule } from './wrapTurboModule';
Loading
Loading