Skip to content

Commit 3de8b9b

Browse files
authored
Add SentryNavigatorObserver current route to event.app.contexts.viewNames (#1545)
1 parent 30c1193 commit 3de8b9b

File tree

7 files changed

+180
-26
lines changed

7 files changed

+180
-26
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
### Features
1111

1212
- Initial (alpha) support for profiling on iOS and macOS ([#1611](https://github.com/getsentry/sentry-dart/pull/1611))
13+
- Add `SentryNavigatorObserver` current route to `event.app.contexts.viewNames` ([#1545](https://github.com/getsentry/sentry-dart/pull/1545))
14+
- Requires relay version [23.9.0](https://github.com/getsentry/relay/blob/master/CHANGELOG.md#2390) for self-hosted instances
1315

1416
## 7.11.0
1517

dart/lib/src/protocol/sentry_app.dart

+26-14
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class SentryApp {
1818
this.deviceAppHash,
1919
this.appMemory,
2020
this.inForeground,
21+
this.viewNames,
2122
});
2223

2324
/// Human readable application name, as it appears on the platform.
@@ -48,20 +49,27 @@ class SentryApp {
4849
/// An app is in foreground when it's visible to the user.
4950
final bool? inForeground;
5051

52+
/// The names of the currently visible views.
53+
final List<String>? viewNames;
54+
5155
/// Deserializes a [SentryApp] from JSON [Map].
52-
factory SentryApp.fromJson(Map<String, dynamic> data) => SentryApp(
53-
name: data['app_name'],
54-
version: data['app_version'],
55-
identifier: data['app_identifier'],
56-
build: data['app_build'],
57-
buildType: data['build_type'],
58-
startTime: data['app_start_time'] != null
59-
? DateTime.tryParse(data['app_start_time'])
60-
: null,
61-
deviceAppHash: data['device_app_hash'],
62-
appMemory: data['app_memory'],
63-
inForeground: data['in_foreground'],
64-
);
56+
factory SentryApp.fromJson(Map<String, dynamic> data) {
57+
final viewNamesJson = data['view_names'] as List<dynamic>?;
58+
return SentryApp(
59+
name: data['app_name'],
60+
version: data['app_version'],
61+
identifier: data['app_identifier'],
62+
build: data['app_build'],
63+
buildType: data['build_type'],
64+
startTime: data['app_start_time'] != null
65+
? DateTime.tryParse(data['app_start_time'])
66+
: null,
67+
deviceAppHash: data['device_app_hash'],
68+
appMemory: data['app_memory'],
69+
inForeground: data['in_foreground'],
70+
viewNames: viewNamesJson?.map((e) => e as String).toList(),
71+
);
72+
}
6573

6674
/// Produces a [Map] that can be serialized to JSON.
6775
Map<String, dynamic> toJson() {
@@ -71,10 +79,11 @@ class SentryApp {
7179
if (identifier != null) 'app_identifier': identifier!,
7280
if (build != null) 'app_build': build!,
7381
if (buildType != null) 'build_type': buildType!,
82+
if (startTime != null) 'app_start_time': startTime!.toIso8601String(),
7483
if (deviceAppHash != null) 'device_app_hash': deviceAppHash!,
7584
if (appMemory != null) 'app_memory': appMemory!,
76-
if (startTime != null) 'app_start_time': startTime!.toIso8601String(),
7785
if (inForeground != null) 'in_foreground': inForeground!,
86+
if (viewNames != null && viewNames!.isNotEmpty) 'view_names': viewNames!,
7887
};
7988
}
8089

@@ -88,6 +97,7 @@ class SentryApp {
8897
deviceAppHash: deviceAppHash,
8998
appMemory: appMemory,
9099
inForeground: inForeground,
100+
viewNames: viewNames,
91101
);
92102

93103
SentryApp copyWith({
@@ -100,6 +110,7 @@ class SentryApp {
100110
String? deviceAppHash,
101111
int? appMemory,
102112
bool? inForeground,
113+
List<String>? viewNames,
103114
}) =>
104115
SentryApp(
105116
name: name ?? this.name,
@@ -111,5 +122,6 @@ class SentryApp {
111122
deviceAppHash: deviceAppHash ?? this.deviceAppHash,
112123
appMemory: appMemory ?? this.appMemory,
113124
inForeground: inForeground ?? this.inForeground,
125+
viewNames: viewNames ?? this.viewNames,
114126
);
115127
}

dart/test/protocol/sentry_app_test.dart

+22-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ void main() {
1414
startTime: testStartTime,
1515
deviceAppHash: 'fixture-deviceAppHash',
1616
inForeground: true,
17+
viewNames: ['fixture-viewName', 'fixture-viewName2'],
1718
);
1819

1920
final sentryAppJson = <String, dynamic>{
@@ -25,25 +26,36 @@ void main() {
2526
'app_start_time': testStartTime.toIso8601String(),
2627
'device_app_hash': 'fixture-deviceAppHash',
2728
'in_foreground': true,
29+
'view_names': ['fixture-viewName', 'fixture-viewName2'],
2830
};
2931

3032
group('json', () {
3133
test('toJson', () {
3234
final json = sentryApp.toJson();
3335

34-
expect(
35-
MapEquality().equals(sentryAppJson, json),
36-
true,
37-
);
36+
expect(json['app_name'], 'fixture-name');
37+
expect(json['app_version'], 'fixture-version');
38+
expect(json['app_identifier'], 'fixture-identifier');
39+
expect(json['app_build'], 'fixture-build');
40+
expect(json['build_type'], 'fixture-buildType');
41+
expect(json['app_start_time'], testStartTime.toIso8601String());
42+
expect(json['device_app_hash'], 'fixture-deviceAppHash');
43+
expect(json['in_foreground'], true);
44+
expect(json['view_names'], ['fixture-viewName', 'fixture-viewName2']);
3845
});
3946
test('fromJson', () {
4047
final sentryApp = SentryApp.fromJson(sentryAppJson);
4148
final json = sentryApp.toJson();
4249

43-
expect(
44-
MapEquality().equals(sentryAppJson, json),
45-
true,
46-
);
50+
expect(json['app_name'], 'fixture-name');
51+
expect(json['app_version'], 'fixture-version');
52+
expect(json['app_identifier'], 'fixture-identifier');
53+
expect(json['app_build'], 'fixture-build');
54+
expect(json['build_type'], 'fixture-buildType');
55+
expect(json['app_start_time'], testStartTime.toIso8601String());
56+
expect(json['device_app_hash'], 'fixture-deviceAppHash');
57+
expect(json['in_foreground'], true);
58+
expect(json['view_names'], ['fixture-viewName', 'fixture-viewName2']);
4759
});
4860
});
4961

@@ -73,6 +85,7 @@ void main() {
7385
startTime: startTime,
7486
deviceAppHash: 'hash1',
7587
inForeground: true,
88+
viewNames: ['screen1'],
7689
);
7790

7891
expect('name1', copy.name);
@@ -83,6 +96,7 @@ void main() {
8396
expect(startTime, copy.startTime);
8497
expect('hash1', copy.deviceAppHash);
8598
expect(true, copy.inForeground);
99+
expect(['screen1'], copy.viewNames);
86100
});
87101
});
88102
}

flutter/lib/src/event_processor/flutter_enricher_event_processor.dart

+17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
55
import 'package:flutter/material.dart';
66
import 'package:sentry/sentry.dart';
77

8+
import '../navigation/sentry_navigator_observer.dart';
89
import '../sentry_flutter_options.dart';
910

1011
typedef WidgetBindingGetter = WidgetsBinding? Function();
@@ -47,6 +48,11 @@ class FlutterEnricherEventProcessor implements EventProcessor {
4748
app: _getApp(event.contexts.app),
4849
);
4950

51+
final app = contexts.app;
52+
if (app != null) {
53+
contexts.app = _appWithCurrentRouteViewName(app);
54+
}
55+
5056
// Flutter has a lot of Accessibility Settings available and exposes them
5157
contexts['accessibility'] = _getAccessibilityContext();
5258

@@ -237,4 +243,15 @@ class FlutterEnricherEventProcessor implements EventProcessor {
237243
inForeground: inForeground,
238244
);
239245
}
246+
247+
SentryApp _appWithCurrentRouteViewName(SentryApp app) {
248+
final currentRouteName = SentryNavigatorObserver.currentRouteName;
249+
if (currentRouteName != null) {
250+
final viewNames = app.viewNames ?? [];
251+
viewNames.add(currentRouteName);
252+
return app.copyWith(viewNames: viewNames);
253+
} else {
254+
return app;
255+
}
256+
}
240257
}

flutter/lib/src/navigation/sentry_navigator_observer.dart

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:flutter/widgets.dart';
2+
import 'package:meta/meta.dart';
23

34
import '../../sentry_flutter.dart';
5+
import '../event_processor/flutter_enricher_event_processor.dart';
46
import '../native/sentry_native.dart';
57

68
/// This key must be used so that the web interface displays the events nicely
@@ -22,6 +24,9 @@ typedef AdditionalInfoExtractor = Map<String, dynamic>? Function(
2224
/// The [RouteSettings] is null if a developer has not specified any
2325
/// RouteSettings.
2426
///
27+
/// The current route name will also be set to [SentryEvent]
28+
/// `contexts.app.view_names` by [FlutterEnricherEventProcessor].
29+
///
2530
/// [SentryNavigatorObserver] must be added to the [navigation observer](https://api.flutter.dev/flutter/material/MaterialApp/navigatorObservers.html) of
2631
/// your used app. This is an example for [MaterialApp](https://api.flutter.dev/flutter/material/MaterialApp/navigatorObservers.html),
2732
/// but the integration for [CupertinoApp](https://api.flutter.dev/flutter/cupertino/CupertinoApp/navigatorObservers.html)
@@ -84,11 +89,17 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
8489

8590
ISentrySpan? _transaction;
8691

92+
static String? _currentRouteName;
93+
94+
@internal
95+
static String? get currentRouteName => _currentRouteName;
96+
8797
@override
8898
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
8999
super.didPush(route, previousRoute);
90100

91-
_setCurrentRoute(route);
101+
_setCurrentRouteName(route);
102+
_setCurrentRouteNameAsTransaction(route);
92103

93104
_addBreadcrumb(
94105
type: 'didPush',
@@ -104,7 +115,9 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
104115
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
105116
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
106117

107-
_setCurrentRoute(newRoute);
118+
_setCurrentRouteName(newRoute);
119+
_setCurrentRouteNameAsTransaction(newRoute);
120+
108121
_addBreadcrumb(
109122
type: 'didReplace',
110123
from: oldRoute?.settings,
@@ -116,7 +129,9 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
116129
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
117130
super.didPop(route, previousRoute);
118131

119-
_setCurrentRoute(previousRoute);
132+
_setCurrentRouteName(previousRoute);
133+
_setCurrentRouteNameAsTransaction(previousRoute);
134+
120135
_addBreadcrumb(
121136
type: 'didPop',
122137
from: route.settings,
@@ -147,7 +162,11 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
147162
?.name;
148163
}
149164

150-
Future<void> _setCurrentRoute(Route<dynamic>? route) async {
165+
Future<void> _setCurrentRouteName(Route<dynamic>? route) async {
166+
_currentRouteName = _getRouteName(route);
167+
}
168+
169+
Future<void> _setCurrentRouteNameAsTransaction(Route<dynamic>? route) async {
151170
final name = _getRouteName(route);
152171
if (name == null) {
153172
return;

flutter/test/event_processor/flutter_enricher_event_processor_test.dart

+23
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,24 @@ void main() {
322322
.length;
323323
expect(ioEnricherCount, 1);
324324
});
325+
326+
testWidgets('adds SentryNavigatorObserver.currentRouteName as app.screen',
327+
(tester) async {
328+
final observer = SentryNavigatorObserver();
329+
final route =
330+
fixture.route(RouteSettings(name: 'fixture-currentRouteName'));
331+
observer.didPush(route, null);
332+
333+
final eventWithContextsApp =
334+
SentryEvent(contexts: Contexts(app: SentryApp()));
335+
336+
final enricher = fixture.getSut(
337+
binding: () => tester.binding,
338+
);
339+
final event = await enricher.apply(eventWithContextsApp);
340+
341+
expect(event?.contexts.app?.viewNames, ['fixture-currentRouteName']);
342+
});
325343
});
326344
}
327345

@@ -342,6 +360,11 @@ class Fixture {
342360
)..reportPackages = reportPackages;
343361
return FlutterEnricherEventProcessor(options);
344362
}
363+
364+
PageRoute<dynamic> route(RouteSettings? settings) => PageRouteBuilder<void>(
365+
pageBuilder: (_, __, ___) => Container(),
366+
settings: settings,
367+
);
345368
}
346369

347370
void loadTestPackage() {

flutter/test/sentry_navigator_observer_test.dart

+67
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,73 @@ void main() {
367367
expect(scope.span, span);
368368
});
369369
});
370+
371+
test('didPush sets current route name', () {
372+
const name = 'Current Route';
373+
final currentRoute = route(RouteSettings(name: name));
374+
375+
const op = 'navigation';
376+
final hub = _MockHub();
377+
final span = getMockSentryTracer(name: name);
378+
when(span.context).thenReturn(SentrySpanContext(operation: op));
379+
_whenAnyStart(hub, span);
380+
381+
final sut = fixture.getSut(
382+
hub: hub,
383+
autoFinishAfter: Duration(seconds: 5),
384+
);
385+
386+
sut.didPush(currentRoute, null);
387+
388+
expect(SentryNavigatorObserver.currentRouteName, 'Current Route');
389+
});
390+
391+
test('didReplace sets new route name', () {
392+
const oldRouteName = 'Old Route';
393+
final oldRoute = route(RouteSettings(name: oldRouteName));
394+
const newRouteName = 'New Route';
395+
final newRoute = route(RouteSettings(name: newRouteName));
396+
397+
const op = 'navigation';
398+
final hub = _MockHub();
399+
final span = getMockSentryTracer(name: oldRouteName);
400+
when(span.context).thenReturn(SentrySpanContext(operation: op));
401+
_whenAnyStart(hub, span);
402+
403+
final sut = fixture.getSut(
404+
hub: hub,
405+
autoFinishAfter: Duration(seconds: 5),
406+
);
407+
408+
sut.didPush(oldRoute, null);
409+
sut.didReplace(newRoute: newRoute, oldRoute: oldRoute);
410+
411+
expect(SentryNavigatorObserver.currentRouteName, 'New Route');
412+
});
413+
414+
test('popRoute sets previous route name', () {
415+
const oldRouteName = 'Old Route';
416+
final oldRoute = route(RouteSettings(name: oldRouteName));
417+
const newRouteName = 'New Route';
418+
final newRoute = route(RouteSettings(name: newRouteName));
419+
420+
const op = 'navigation';
421+
final hub = _MockHub();
422+
final span = getMockSentryTracer(name: oldRouteName);
423+
when(span.context).thenReturn(SentrySpanContext(operation: op));
424+
when(span.status).thenReturn(null);
425+
_whenAnyStart(hub, span);
426+
427+
final sut = fixture.getSut(
428+
hub: hub,
429+
autoFinishAfter: Duration(seconds: 5),
430+
);
431+
432+
sut.didPush(oldRoute, null);
433+
sut.didPop(newRoute, oldRoute);
434+
435+
expect(SentryNavigatorObserver.currentRouteName, 'Old Route');
436+
});
370437
});
371438

372439
group('RouteObserverBreadcrumb', () {

0 commit comments

Comments
 (0)