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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
- Search for feature flags that are prefixed with `flutter:*`
- This works on Flutter builds that include [this PR](https://github.com/flutter/flutter/pull/171545)
- Add `LoggingIntegration` support for `SentryLog` ([#3050](https://github.com/getsentry/sentry-dart/pull/3050))
- Add `enableNewTraceOnNavigation` flag to `SentryNavigatorObserver` ([#3096](https://github.com/getsentry/sentry-dart/pull/3096))
- **Default:** `true`
- **Disable** by passing `false`, e.g.:
```dart
SentryNavigatorObserver(enableNewTraceOnNavigation: false)
```
- _Note: traces differ from transactions/spans — see tracing concepts [here](https://docs.sentry.io/concepts/key-terms/tracing/)_

### Fixes

Expand Down
18 changes: 15 additions & 3 deletions flutter/lib/src/navigation/sentry_navigator_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ typedef AdditionalInfoExtractor = Map<String, dynamic>? Function(
/// if those happen to take longer. The transaction will be set to [Scope.span]
/// if the latter is empty.
///
/// If [enableNewTraceOnNavigation] is true (default), a
/// fresh trace is generated before each push, pop, or replace event.
///
/// Enabling the [setRouteNameAsTransaction] option overrides the current
/// [Scope.transaction] which will also override the name of the current
/// [Scope.span]. So be careful when this is used together with performance
Expand All @@ -74,13 +77,15 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
SentryNavigatorObserver({
Hub? hub,
bool enableAutoTransactions = true,
bool enableNewTraceOnNavigation = true,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would enableAutoTransactions somehow have an effect on enableNewTraceOnNavigation?

Copy link
Copy Markdown
Contributor Author

@buenaflor buenaflor Jul 21, 2025

Choose a reason for hiding this comment

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

no, the trace does not collide with transactions at all. it's all a bit confusing since trace and transactions can be confused with one another.

the trace here refers to the concept of a trace (e.g traceId) that all transactions and errors are part of.

that being said I plan on deprecating the enableAutoTransactions at some point anyway

Duration autoFinishAfter = const Duration(seconds: 3),
bool setRouteNameAsTransaction = false,
RouteNameExtractor? routeNameExtractor,
AdditionalInfoExtractor? additionalInfoProvider,
List<String>? ignoreRoutes,
}) : _hub = hub ?? HubAdapter(),
_enableAutoTransactions = enableAutoTransactions,
_enableNewTraceOnNavigation = enableNewTraceOnNavigation,
_autoFinishAfter = autoFinishAfter,
_setRouteNameAsTransaction = setRouteNameAsTransaction,
_routeNameExtractor = routeNameExtractor,
Expand Down Expand Up @@ -110,6 +115,7 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {

final Hub _hub;
final bool _enableAutoTransactions;
final bool _enableNewTraceOnNavigation;
final Duration _autoFinishAfter;
final bool _setRouteNameAsTransaction;
final RouteNameExtractor? _routeNameExtractor;
Expand Down Expand Up @@ -143,7 +149,7 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
return;
}

_hub.generateNewTrace();
_startNewTraceIfEnabled();
_setCurrentRouteName(route);
_setCurrentRouteNameAsTransaction(route);

Expand Down Expand Up @@ -174,7 +180,7 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
return;
}

_hub.generateNewTrace();
_startNewTraceIfEnabled();
_setCurrentRouteName(newRoute);
_setCurrentRouteNameAsTransaction(newRoute);

Expand All @@ -196,7 +202,7 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
return;
}

_hub.generateNewTrace();
_startNewTraceIfEnabled();
_setCurrentRouteName(previousRoute);
_setCurrentRouteNameAsTransaction(previousRoute);

Expand All @@ -212,6 +218,12 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
_finishTransaction(endTimestamp: timestamp);
}

void _startNewTraceIfEnabled() {
if (_enableNewTraceOnNavigation) {
_hub.generateNewTrace();
}
}

void _addWebSessions({Route<dynamic>? from, Route<dynamic>? to}) async {
final fromName = from != null ? _getRouteName(from) : null;
final toName = to != null ? _getRouteName(to) : null;
Expand Down
182 changes: 113 additions & 69 deletions flutter/test/navigation/sentry_navigator_observer_traces_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,82 +15,120 @@ void main() {
fixture = Fixture();
});

test('didPush generates a new trace', () {
final fromRoute = _route(RouteSettings(name: 'From Route'));
final toRoute = _route(RouteSettings(name: 'To Route'));
final oldTraceId = fixture.hub.scope.propagationContext.traceId;
group('SentryNavigatorObserver', () {
group('when starting traces on navigation is enabled (default)', () {
test('didPush should start a new trace', () {
final from = _route(RouteSettings(name: 'From Route'));
final to = _route(RouteSettings(name: 'To Route'));
final before = fixture.hub.scope.propagationContext.traceId;

fixture.getSut().didPush(to, from);

final after = fixture.hub.scope.propagationContext.traceId;
expect(after, isNot(before));
});

test('didPop should start a new trace', () {
final from = _route(RouteSettings(name: 'From Route'));
final to = _route(RouteSettings(name: 'To Route'));
final before = fixture.hub.scope.propagationContext.traceId;

fixture.getSut().didPop(to, from);

final after = fixture.hub.scope.propagationContext.traceId;
expect(after, isNot(before));
});

test('didReplace should start a new trace', () {
final from = _route(RouteSettings(name: 'From Route'));
final to = _route(RouteSettings(name: 'To Route'));
final before = fixture.hub.scope.propagationContext.traceId;

fixture.getSut().didReplace(newRoute: to, oldRoute: from);

final after = fixture.hub.scope.propagationContext.traceId;
expect(after, isNot(before));
});

group('execution order', () {
void _stubHub() {
when(fixture.mockHub.generateNewTrace()).thenReturn(null);
when(fixture.mockHub.configureScope(any))
.thenAnswer((_) => Future.value());
when(fixture.mockHub.startTransactionWithContext(
any,
bindToScope: anyNamed('bindToScope'),
waitForChildren: anyNamed('waitForChildren'),
autoFinishAfter: anyNamed('autoFinishAfter'),
trimEnd: anyNamed('trimEnd'),
onFinish: anyNamed('onFinish'),
customSamplingContext: anyNamed('customSamplingContext'),
startTimestamp: anyNamed('startTimestamp'),
)).thenReturn(NoOpSentrySpan());
}

test(
'didPush should call generateNewTrace before starting the transaction',
() {
final from = _route(RouteSettings(name: 'From Route'));
final to = _route(RouteSettings(name: 'To Route'));

_stubHub();
final sut = fixture.getSut(hub: fixture.mockHub);
sut.didPush(to, from);

verifyInOrder([
fixture.mockHub.generateNewTrace(),
fixture.mockHub.startTransactionWithContext(
any,
bindToScope: anyNamed('bindToScope'),
waitForChildren: anyNamed('waitForChildren'),
autoFinishAfter: anyNamed('autoFinishAfter'),
trimEnd: anyNamed('trimEnd'),
onFinish: anyNamed('onFinish'),
customSamplingContext: anyNamed('customSamplingContext'),
startTimestamp: anyNamed('startTimestamp'),
),
]);
});
});
});

final sut = fixture.getSut();
sut.didPush(toRoute, fromRoute);
group('when starting traces on navigation is disabled', () {
test('didPush should not start a new trace', () {
final from = _route(RouteSettings(name: 'From Route'));
final to = _route(RouteSettings(name: 'To Route'));
final before = fixture.hub.scope.propagationContext.traceId;

final newTraceId = fixture.hub.scope.propagationContext.traceId;
expect(oldTraceId, isNot(newTraceId));
});
fixture.getSut(enableNewTraceOnNavigation: false).didPush(to, from);

test('didPop generates a new trace', () {
final fromRoute = _route(RouteSettings(name: 'From Route'));
final toRoute = _route(RouteSettings(name: 'To Route'));
final oldTraceId = fixture.hub.scope.propagationContext.traceId;
final after = fixture.hub.scope.propagationContext.traceId;
expect(after, equals(before));
});

final sut = fixture.getSut();
sut.didPop(toRoute, fromRoute);
test('didPop should not start a new trace', () {
final from = _route(RouteSettings(name: 'From Route'));
final to = _route(RouteSettings(name: 'To Route'));
final before = fixture.hub.scope.propagationContext.traceId;

final newTraceId = fixture.hub.scope.propagationContext.traceId;
expect(oldTraceId, isNot(newTraceId));
});
fixture.getSut(enableNewTraceOnNavigation: false).didPop(to, from);

test('didReplace generates a new trace', () {
final fromRoute = _route(RouteSettings(name: 'From Route'));
final toRoute = _route(RouteSettings(name: 'To Route'));
final oldTraceId = fixture.hub.scope.propagationContext.traceId;
final after = fixture.hub.scope.propagationContext.traceId;
expect(after, equals(before));
});

final sut = fixture.getSut();
sut.didReplace(newRoute: toRoute, oldRoute: fromRoute);
test('didReplace should not start a new trace', () {
final from = _route(RouteSettings(name: 'From Route'));
final to = _route(RouteSettings(name: 'To Route'));
final before = fixture.hub.scope.propagationContext.traceId;

final newTraceId = fixture.hub.scope.propagationContext.traceId;
expect(oldTraceId, isNot(newTraceId));
});
fixture
.getSut(enableNewTraceOnNavigation: false)
.didReplace(newRoute: to, oldRoute: from);

group('execution order', () {
/// Prepares mocks, we don't care about what they exactly do.
/// We only test the order of execution in this group.
void _prepareMocks() {
when(fixture.mockHub.generateNewTrace()).thenAnswer((_) => {});
when(fixture.mockHub.configureScope(any))
.thenAnswer((_) => Future.value());
when(fixture.mockHub.startTransactionWithContext(
any,
bindToScope: anyNamed('bindToScope'),
waitForChildren: anyNamed('waitForChildren'),
autoFinishAfter: anyNamed('autoFinishAfter'),
trimEnd: anyNamed('trimEnd'),
onFinish: anyNamed('onFinish'),
customSamplingContext: anyNamed('customSamplingContext'),
startTimestamp: anyNamed('startTimestamp'),
)).thenReturn(NoOpSentrySpan());
}

test('didPush generates a new trace before creating transaction spans', () {
final fromRoute = _route(RouteSettings(name: 'From Route'));
final toRoute = _route(RouteSettings(name: 'To Route'));

_prepareMocks();

final sut = fixture.getSut(hub: fixture.mockHub);
sut.didPush(toRoute, fromRoute);
verifyInOrder([
fixture.mockHub.generateNewTrace(),
fixture.mockHub.startTransactionWithContext(
any,
bindToScope: anyNamed('bindToScope'),
waitForChildren: anyNamed('waitForChildren'),
autoFinishAfter: anyNamed('autoFinishAfter'),
trimEnd: anyNamed('trimEnd'),
onFinish: anyNamed('onFinish'),
customSamplingContext: anyNamed('customSamplingContext'),
startTimestamp: anyNamed('startTimestamp'),
),
]);
final after = fixture.hub.scope.propagationContext.traceId;
expect(after, equals(before));
});
});
});
}
Expand All @@ -105,11 +143,17 @@ class Fixture {
late final mockHub = MockHub();
late final hub = Hub(options);

SentryNavigatorObserver getSut({Hub? hub}) {
SentryNavigatorObserver getSut({
Hub? hub,
bool enableNewTraceOnNavigation = true,
}) {
hub ??= this.hub;
if (hub == mockHub) {
when(mockHub.options).thenReturn(options);
}
return SentryNavigatorObserver(hub: hub);
return SentryNavigatorObserver(
hub: hub,
enableNewTraceOnNavigation: enableNewTraceOnNavigation,
);
}
}
Loading