Skip to content

Commit

Permalink
feat(tracing): Tabs auto instrumentation for React Native Navigation (#…
Browse files Browse the repository at this point in the history
…2932)

Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com>
  • Loading branch information
krystofwoldrich and marandaneto authored Mar 30, 2023
1 parent a115c54 commit 6d44dea
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 177 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
### Features

- Add `enableTracing` option ([#2933](https://github.com/getsentry/sentry-react-native/pull/2933))
- Add Tabs auto instrumentation for React Native Navigation ([#2932](https://github.com/getsentry/sentry-react-native/pull/2932))
- This is enabled by default, if you want to disable tabs instrumentation see the example below.

```js
const routingInstrumentation = new Sentry.ReactNativeNavigationInstrumentation(Navigation, { enableTabsInstrumentation: false })
```

### Fixes

Expand Down
135 changes: 79 additions & 56 deletions src/js/tracing/reactnativenavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@ interface ReactNativeNavigationOptions {
* Default: 1000
*/
routeChangeTimeoutMs: number;
/**
* Instrumentation will create a transaction on tab change.
* By default only navigation commands create transactions.
*
* Default: true
*/
enableTabsInstrumentation: boolean;
}

const defaultOptions: ReactNativeNavigationOptions = {
routeChangeTimeoutMs: 1000,
enableTabsInstrumentation: true,
};

interface ComponentEvent {
Expand All @@ -46,13 +54,20 @@ export interface EventSubscription {
remove(): void;
}

export interface BottomTabPressedEvent {
tabIndex: number;
}

export interface EventsRegistry {
registerComponentWillAppearListener(
callback: (event: ComponentWillAppearEvent) => void
): EmitterSubscription;
registerCommandListener(
callback: (name: string, params: unknown) => void
): EventSubscription;
registerBottomTabPressedListener(
callback: (event: BottomTabPressedEvent) => void
): EmitterSubscription;
}

export interface NavigationDelegate {
Expand Down Expand Up @@ -112,7 +127,13 @@ export class ReactNativeNavigationInstrumentation extends InternalRoutingInstrum

this._navigation
.events()
.registerCommandListener(this._onCommand.bind(this));
.registerCommandListener(this._onNavigation.bind(this));

if (this._options.enableTabsInstrumentation) {
this._navigation
.events()
.registerBottomTabPressedListener(this._onNavigation.bind(this));
}

this._navigation
.events()
Expand All @@ -122,9 +143,9 @@ export class ReactNativeNavigationInstrumentation extends InternalRoutingInstrum
}

/**
* To be called when a navigation command is dispatched
* To be called when a navigation is initiated. (Command, BottomTabSelected, etc.)
*/
private _onCommand(): void {
private _onNavigation(): void {
if (this._latestTransaction) {
this._discardLatestTransaction();
}
Expand All @@ -143,61 +164,63 @@ export class ReactNativeNavigationInstrumentation extends InternalRoutingInstrum
* To be called AFTER the state has been changed to populate the transaction with the current route.
*/
private _onComponentWillAppear(event: ComponentWillAppearEvent): void {
// If the route is a different key, this is so we ignore actions that pertain to the same screen.
if (this._latestTransaction) {
if (
!this._prevComponentEvent ||
event.componentId != this._prevComponentEvent.componentId
) {
this._clearStateChangeTimeout();

const originalContext = this._latestTransaction.toContext();
const routeHasBeenSeen = this._recentComponentIds.includes(
event.componentId
);

const data: RouteChangeContextData = {
...originalContext.data,
route: {
...event,
name: event.componentName,
hasBeenSeen: routeHasBeenSeen,
},
previousRoute: this._prevComponentEvent
? {
...this._prevComponentEvent,
name: this._prevComponentEvent?.componentName,
}
: null,
};

const updatedContext = {
...originalContext,
name: event.componentName,
tags: {
...originalContext.tags,
'routing.route.name': event.componentName,
},
data,
};

const finalContext = this._prepareFinalContext(updatedContext);
this._latestTransaction.updateWithContext(finalContext);

const isCustomName = updatedContext.name !== finalContext.name;
this._latestTransaction.setName(
finalContext.name,
isCustomName ? customTransactionSource : defaultTransactionSource,
);

this._onConfirmRoute?.(finalContext);
this._prevComponentEvent = event;
} else {
this._discardLatestTransaction();
}
if (!this._latestTransaction) {
return;
}

this._latestTransaction = undefined;
// We ignore actions that pertain to the same screen.
const isSameComponent = this._prevComponentEvent
&& event.componentId === this._prevComponentEvent.componentId;
if (isSameComponent) {
this._discardLatestTransaction();
return;
}

this._clearStateChangeTimeout();

const originalContext = this._latestTransaction.toContext();
const routeHasBeenSeen = this._recentComponentIds.includes(
event.componentId
);

const data: RouteChangeContextData = {
...originalContext.data,
route: {
...event,
name: event.componentName,
hasBeenSeen: routeHasBeenSeen,
},
previousRoute: this._prevComponentEvent
? {
...this._prevComponentEvent,
name: this._prevComponentEvent?.componentName,
}
: null,
};

const updatedContext = {
...originalContext,
name: event.componentName,
tags: {
...originalContext.tags,
'routing.route.name': event.componentName,
},
data,
};

const finalContext = this._prepareFinalContext(updatedContext);
this._latestTransaction.updateWithContext(finalContext);

const isCustomName = updatedContext.name !== finalContext.name;
this._latestTransaction.setName(
finalContext.name,
isCustomName ? customTransactionSource : defaultTransactionSource,
);

this._onConfirmRoute?.(finalContext);
this._prevComponentEvent = event;

this._latestTransaction = undefined;
}

/** Creates final transaction context before confirmation */
Expand Down
Loading

0 comments on commit 6d44dea

Please sign in to comment.