Skip to content

Commit ef2f0ff

Browse files
authored
feat: Use beforeNavigate in routing instrumentation to match behavior on JS (#1313)
* feat: Use beforeNavigate in routing instrumentatin * feat: Add convenience type for react navigation context * feat: Sample app beforeNavigate example * feat: Safeguard for beforeNavigate returning false * fix: Update hasBeenSeen callback and add a log for debug * meta: Changelog * fix: Fix V4 previous route * test: Updated V4 tests
1 parent 8fb91e1 commit ef2f0ff

File tree

11 files changed

+319
-229
lines changed

11 files changed

+319
-229
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Bump: sentry-android to v4.0.0 #1309
66
- build(ios): Bump sentry-cocoa to 6.1.4 #1308
77
- fix: Handle auto session tracking start on iOS #1308
8+
- feat: Use beforeNavigate in routing instrumentation to match behavior on JS #1313
89

910
## 2.2.0-beta.0
1011

sample/src/App.tsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,7 @@ import {store} from './reduxApp';
2020
import {version as packageVersion} from '../../package.json';
2121
import {SENTRY_INTERNAL_DSN} from './dsn';
2222

23-
const reactNavigationV5Instrumentation = new Sentry.ReactNavigationV5Instrumentation(
24-
{
25-
shouldSendTransaction: (route, previousRoute) => {
26-
if (route.name === 'ManualTracker') {
27-
return false;
28-
}
29-
30-
return true;
31-
},
32-
},
33-
);
23+
const reactNavigationV5Instrumentation = new Sentry.ReactNavigationV5Instrumentation();
3424

3525
Sentry.init({
3626
// Replace the example DSN below with your own DSN:
@@ -46,6 +36,14 @@ Sentry.init({
4636
idleTimeout: 5000,
4737
routingInstrumentation: reactNavigationV5Instrumentation,
4838
tracingOrigins: ['localhost', /^\//, /^https:\/\//],
39+
beforeNavigate: (context: Sentry.ReactNavigationTransactionContext) => {
40+
// Example of not sending a transaction for the screen with the name "Manual Tracker"
41+
if (context.data.route.name === 'ManualTracker') {
42+
context.sampled = false;
43+
}
44+
45+
return context;
46+
},
4947
}),
5048
],
5149
enableAutoSessionTracking: true,

src/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export {
6262
ReactNavigationV4Instrumentation,
6363
ReactNavigationV5Instrumentation,
6464
RoutingInstrumentation,
65+
ReactNavigationTransactionContext,
6566
} from "./tracing";
6667

6768
/**

src/js/tracing/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
export { ReactNativeTracing } from "./reactnativetracing";
2-
export { ReactNavigationV5Instrumentation } from "./reactnavigationv5";
3-
export { ReactNavigationV4Instrumentation } from "./reactnavigationv4";
2+
43
export {
54
RoutingInstrumentation,
65
RoutingInstrumentationInstance,
76
} from "./routingInstrumentation";
7+
8+
export { ReactNavigationV5Instrumentation } from "./reactnavigationv5";
9+
export { ReactNavigationV4Instrumentation } from "./reactnavigationv4";
10+
export {
11+
ReactNavigationCurrentRoute,
12+
ReactNavigationRoute,
13+
ReactNavigationTransactionContext,
14+
} from "./types";

src/js/tracing/reactnativetracing.ts

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import { logger } from "@sentry/utils";
1616
import { RoutingInstrumentationInstance } from "../tracing/routingInstrumentation";
1717
import { adjustTransactionDuration } from "./utils";
1818

19+
export type BeforeNavigate = (
20+
context: TransactionContext
21+
) => TransactionContext;
22+
1923
export interface ReactNativeTracingOptions
2024
extends RequestInstrumentationOptions {
2125
/**
@@ -48,7 +52,7 @@ export interface ReactNativeTracingOptions
4852
*
4953
* Default: true
5054
*/
51-
ignoreEmptyBackNavigationTransactions?: boolean;
55+
ignoreEmptyBackNavigationTransactions: boolean;
5256

5357
/**
5458
* beforeNavigate is called before a navigation transaction is created and allows users to modify transaction
@@ -58,14 +62,15 @@ export interface ReactNativeTracingOptions
5862
*
5963
* @returns A (potentially) modified context object, with `sampled = false` if the transaction should be dropped.
6064
*/
61-
beforeNavigate?(context: TransactionContext): TransactionContext;
65+
beforeNavigate: BeforeNavigate;
6266
}
6367

6468
const defaultReactNativeTracingOptions: ReactNativeTracingOptions = {
6569
...defaultRequestInstrumentationOptions,
6670
idleTimeout: 1000,
6771
maxTransactionDuration: 600,
6872
ignoreEmptyBackNavigationTransactions: true,
73+
beforeNavigate: (context) => context,
6974
};
7075

7176
/**
@@ -114,7 +119,8 @@ export class ReactNativeTracing implements Integration {
114119
this._getCurrentHub = getCurrentHub;
115120

116121
routingInstrumentation?.registerRoutingInstrumentation(
117-
this._onRouteWillChange.bind(this)
122+
this._onRouteWillChange.bind(this),
123+
this.options.beforeNavigate
118124
);
119125

120126
if (!routingInstrumentation) {
@@ -129,14 +135,6 @@ export class ReactNativeTracing implements Integration {
129135
tracingOrigins,
130136
shouldCreateSpanForRequest,
131137
});
132-
133-
addGlobalEventProcessor((event) => {
134-
// eslint-disable-next-line no-empty
135-
if (event.type === "transaction") {
136-
}
137-
138-
return event;
139-
});
140138
}
141139

142140
/** To be called when the route changes, but BEFORE the components of the new route mount. */
@@ -159,37 +157,22 @@ export class ReactNativeTracing implements Integration {
159157
}
160158

161159
// eslint-disable-next-line @typescript-eslint/unbound-method
162-
const {
163-
beforeNavigate,
164-
idleTimeout,
165-
maxTransactionDuration,
166-
} = this.options;
160+
const { idleTimeout, maxTransactionDuration } = this.options;
167161

168162
const expandedContext = {
169163
...context,
170164
trimEnd: true,
171165
};
172166

173-
const modifiedContext =
174-
typeof beforeNavigate === "function"
175-
? beforeNavigate(expandedContext)
176-
: expandedContext;
177-
178-
if (modifiedContext.sampled === false) {
179-
logger.log(
180-
`[ReactNativeTracing] Will not send ${context.op} transaction.`
181-
);
182-
}
183-
184167
const hub = this._getCurrentHub();
185168
const idleTransaction = startIdleTransaction(
186169
hub as any,
187-
context,
170+
expandedContext,
188171
idleTimeout,
189172
true
190173
);
191174
logger.log(
192-
`[ReactNativeTracing] Starting ${context.op} transaction on scope`
175+
`[ReactNativeTracing] Starting ${context.op} transaction "${context.name}" on scope`
193176
);
194177
idleTransaction.registerBeforeFinishCallback(
195178
(transaction, endTimestamp) => {
@@ -204,12 +187,16 @@ export class ReactNativeTracing implements Integration {
204187
if (this.options.ignoreEmptyBackNavigationTransactions) {
205188
idleTransaction.registerBeforeFinishCallback((transaction) => {
206189
if (
207-
transaction.data["routing.route.hasBeenSeen"] &&
190+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
191+
transaction.data?.route?.hasBeenSeen &&
208192
(!transaction.spanRecorder ||
209193
transaction.spanRecorder.spans.filter(
210194
(span) => span.spanId !== transaction.spanId
211195
).length === 0)
212196
) {
197+
logger.log(
198+
`[ReactNativeTracing] Not sampling transaction as route has been seen before. Pass ignoreEmptyBackNavigationTransactions = false to disable this feature.`
199+
);
213200
// Route has been seen before and has no child spans.
214201
transaction.sampled = false;
215202
}

src/js/tracing/reactnavigationv4.ts

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { TransactionContext } from "@sentry/types";
21
import { logger } from "@sentry/utils";
32

43
import { RoutingInstrumentation } from "./routingInstrumentation";
4+
import { ReactNavigationTransactionContext } from "./types";
55

66
export interface NavigationRouteV4 {
77
routeName: string;
88
key: string;
9-
params?: Record<any, any>;
9+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10+
params?: Record<string, any>;
1011
}
1112

1213
export interface NavigationStateV4 {
@@ -22,6 +23,7 @@ export interface AppContainerInstance {
2223
state: NavigationStateV4;
2324
router: {
2425
getStateForAction: (
26+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2527
action: any,
2628
state: NavigationStateV4
2729
) => NavigationStateV4;
@@ -33,22 +35,6 @@ interface AppContainerRef {
3335
current?: AppContainerInstance | null;
3436
}
3537

36-
type ReactNavigationV4ShouldAttachTransaction = (
37-
route: NavigationRouteV4,
38-
previousRoute: NavigationRouteV4 | null
39-
) => boolean;
40-
41-
interface ReactNavigationV4InstrumentationOptions {
42-
shouldSendTransaction: ReactNavigationV4ShouldAttachTransaction;
43-
}
44-
45-
const defaultShouldAttachTransaction: ReactNavigationV4ShouldAttachTransaction = () =>
46-
true;
47-
48-
const DEFAULT_OPTIONS: ReactNavigationV4InstrumentationOptions = {
49-
shouldSendTransaction: defaultShouldAttachTransaction,
50-
};
51-
5238
/**
5339
* Instrumentation for React-Navigation V4.
5440
* Register the app container with `registerAppContainer` to use, or see docs for more details.
@@ -58,22 +44,11 @@ class ReactNavigationV4Instrumentation extends RoutingInstrumentation {
5844

5945
private _appContainerRef: AppContainerRef = { current: null };
6046

61-
private _options: ReactNavigationV4InstrumentationOptions;
62-
6347
private readonly _maxRecentRouteLen: number = 200;
6448

65-
private _prevRoute: NavigationRouteV4 | null = null;
49+
private _prevRoute?: NavigationRouteV4;
6650
private _recentRouteKeys: string[] = [];
6751

68-
constructor(options: Partial<ReactNavigationV4InstrumentationOptions> = {}) {
69-
super();
70-
71-
this._options = {
72-
...DEFAULT_OPTIONS,
73-
...options,
74-
};
75-
}
76-
7752
/**
7853
* Pass the ref to the app container to register it to the instrumentation
7954
* @param appContainerRef Ref to an `AppContainer`
@@ -142,16 +117,31 @@ class ReactNavigationV4Instrumentation extends RoutingInstrumentation {
142117

143118
// If the route is a different key, this is so we ignore actions that pertain to the same screen.
144119
if (!this._prevRoute || currentRoute.key !== this._prevRoute.key) {
145-
const context = this._getTransactionContext(currentRoute);
120+
const originalContext = this._getTransactionContext(
121+
currentRoute,
122+
this._prevRoute
123+
);
124+
let finalContext = this._beforeNavigate?.(originalContext);
125+
126+
// This block is to catch users not returning a transaction context
127+
if (!finalContext) {
128+
logger.error(
129+
`[ReactNavigationV4Instrumentation] beforeNavigate returned ${finalContext}, return context.sampled = false to not send transaction.`
130+
);
131+
132+
finalContext = {
133+
...originalContext,
134+
sampled: false,
135+
};
136+
}
146137

147-
if (!this._options.shouldSendTransaction(currentRoute, this._prevRoute)) {
148-
context.sampled = false;
138+
if (finalContext.sampled === false) {
149139
logger.log(
150-
`[ReactNavigationV4Instrumentation] Will not send transaction "${context.name}" due to shouldSendTransaction.`
140+
`[ReactNavigationV4Instrumentation] Will not send transaction "${finalContext.name}" due to beforeNavigate.`
151141
);
152142
}
153143

154-
this.onRouteWillChange(context);
144+
this.onRouteWillChange(finalContext);
155145

156146
this._pushRecentRouteKey(currentRoute.key);
157147
this._prevRoute = currentRoute;
@@ -161,7 +151,10 @@ class ReactNavigationV4Instrumentation extends RoutingInstrumentation {
161151
/**
162152
* Gets the transaction context for a `NavigationRouteV4`
163153
*/
164-
private _getTransactionContext(route: NavigationRouteV4): TransactionContext {
154+
private _getTransactionContext(
155+
route: NavigationRouteV4,
156+
previousRoute?: NavigationRouteV4
157+
): ReactNavigationTransactionContext {
165158
return {
166159
name: route.routeName,
167160
op: "navigation",
@@ -171,9 +164,19 @@ class ReactNavigationV4Instrumentation extends RoutingInstrumentation {
171164
"routing.route.name": route.routeName,
172165
},
173166
data: {
174-
"routing.route.key": route.key,
175-
"routing.route.params": route.params,
176-
"routing.route.hasBeenSeen": this._recentRouteKeys.includes(route.key),
167+
route: {
168+
name: route.routeName, // Include name here too for use in `beforeNavigate`
169+
key: route.key,
170+
params: route.params ?? {},
171+
hasBeenSeen: this._recentRouteKeys.includes(route.key),
172+
},
173+
previousRoute: previousRoute
174+
? {
175+
name: previousRoute.routeName,
176+
key: previousRoute.key,
177+
params: previousRoute.params ?? {},
178+
}
179+
: null,
177180
},
178181
};
179182
}

0 commit comments

Comments
 (0)