Skip to content
Merged
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
43 changes: 43 additions & 0 deletions packages/core/etc/sentry-react-native.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,9 @@ export { functionToStringIntegration }

export { getActiveSpan }

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

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 function getReactNativeTracingIntegration(client: Client): ReactNativeTra

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 @@ export { OpenAiOptions }
// @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;
Comment thread
sentry-warden[bot] marked this conversation as resolved.

// Warning: (ae-forgotten-export) The symbol "ReactNativeClientOptions" needs to be exported by the entry point index.d.ts
//
// @public
Expand Down Expand Up @@ -808,6 +825,27 @@ export class TouchEventBoundary extends React_2.Component<TouchEventBoundaryProp

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 @@ -852,6 +890,11 @@ export function wrapExpoImage<T extends ExpoImage>(imageClass: T): T;
// @public
export function wrapExpoRouter<T extends ExpoRouter>(router: T): T;

// @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 @@ import {
spotlightIntegration,
stallTrackingIntegration,
timeToDisplayIntegration,
turboModuleContextIntegration,
Comment thread
alwx marked this conversation as resolved.
userInteractionIntegration,
viewHierarchyIntegration,
} from './exports';
Expand Down Expand Up @@ -174,5 +175,10 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ

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
71 changes: 71 additions & 0 deletions packages/core/src/js/integrations/turboModuleContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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> }>;
}

// Methods on RNSentry that must NOT be tracked:
//
// - `addListener` / `removeListeners` are RN event-emitter stubs that fire on
// every subscriber registration โ€” tracking them would just churn the scope.
//
// - The scope-sync methods (`setContext`, `setTag`, `setExtra`, `setUser`,
// `addBreadcrumb`, `clearBreadcrumbs`, `setAttribute`, `setAttributes`,
// `removeAttribute`) are called by our own `enableSyncToNative` hook every
// time anything writes to a JS Scope. Tracking them would cause infinite
// recursion: `pushTurboModuleCall` -> `scope.setContext` -> `NATIVE.setContext`
// -> `RNSentry.setContext` (wrapped) -> `pushTurboModuleCall` -> ... .
const RNSENTRY_SKIP = [
'addListener',
'removeListeners',
'setContext',
'setTag',
'setExtra',
'setUser',
'addBreadcrumb',
'clearBreadcrumbs',
'setAttribute',
'setAttributes',
'removeAttribute',
] as const;

/**
Comment thread
alwx marked this conversation as resolved.
* 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 });
Comment thread
sentry[bot] marked this conversation as resolved.

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