Skip to content

Commit

Permalink
Add SentryNavigatorObserver current route to `event.app.contexts.vi…
Browse files Browse the repository at this point in the history
…ewNames` (#1545)
  • Loading branch information
denrase authored Oct 31, 2023
1 parent 30c1193 commit 3de8b9b
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 26 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
### Features

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

## 7.11.0

Expand Down
40 changes: 26 additions & 14 deletions dart/lib/src/protocol/sentry_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class SentryApp {
this.deviceAppHash,
this.appMemory,
this.inForeground,
this.viewNames,
});

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

/// The names of the currently visible views.
final List<String>? viewNames;

/// Deserializes a [SentryApp] from JSON [Map].
factory SentryApp.fromJson(Map<String, dynamic> data) => SentryApp(
name: data['app_name'],
version: data['app_version'],
identifier: data['app_identifier'],
build: data['app_build'],
buildType: data['build_type'],
startTime: data['app_start_time'] != null
? DateTime.tryParse(data['app_start_time'])
: null,
deviceAppHash: data['device_app_hash'],
appMemory: data['app_memory'],
inForeground: data['in_foreground'],
);
factory SentryApp.fromJson(Map<String, dynamic> data) {
final viewNamesJson = data['view_names'] as List<dynamic>?;
return SentryApp(
name: data['app_name'],
version: data['app_version'],
identifier: data['app_identifier'],
build: data['app_build'],
buildType: data['build_type'],
startTime: data['app_start_time'] != null
? DateTime.tryParse(data['app_start_time'])
: null,
deviceAppHash: data['device_app_hash'],
appMemory: data['app_memory'],
inForeground: data['in_foreground'],
viewNames: viewNamesJson?.map((e) => e as String).toList(),
);
}

/// Produces a [Map] that can be serialized to JSON.
Map<String, dynamic> toJson() {
Expand All @@ -71,10 +79,11 @@ class SentryApp {
if (identifier != null) 'app_identifier': identifier!,
if (build != null) 'app_build': build!,
if (buildType != null) 'build_type': buildType!,
if (startTime != null) 'app_start_time': startTime!.toIso8601String(),
if (deviceAppHash != null) 'device_app_hash': deviceAppHash!,
if (appMemory != null) 'app_memory': appMemory!,
if (startTime != null) 'app_start_time': startTime!.toIso8601String(),
if (inForeground != null) 'in_foreground': inForeground!,
if (viewNames != null && viewNames!.isNotEmpty) 'view_names': viewNames!,
};
}

Expand All @@ -88,6 +97,7 @@ class SentryApp {
deviceAppHash: deviceAppHash,
appMemory: appMemory,
inForeground: inForeground,
viewNames: viewNames,
);

SentryApp copyWith({
Expand All @@ -100,6 +110,7 @@ class SentryApp {
String? deviceAppHash,
int? appMemory,
bool? inForeground,
List<String>? viewNames,
}) =>
SentryApp(
name: name ?? this.name,
Expand All @@ -111,5 +122,6 @@ class SentryApp {
deviceAppHash: deviceAppHash ?? this.deviceAppHash,
appMemory: appMemory ?? this.appMemory,
inForeground: inForeground ?? this.inForeground,
viewNames: viewNames ?? this.viewNames,
);
}
30 changes: 22 additions & 8 deletions dart/test/protocol/sentry_app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ void main() {
startTime: testStartTime,
deviceAppHash: 'fixture-deviceAppHash',
inForeground: true,
viewNames: ['fixture-viewName', 'fixture-viewName2'],
);

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

group('json', () {
test('toJson', () {
final json = sentryApp.toJson();

expect(
MapEquality().equals(sentryAppJson, json),
true,
);
expect(json['app_name'], 'fixture-name');
expect(json['app_version'], 'fixture-version');
expect(json['app_identifier'], 'fixture-identifier');
expect(json['app_build'], 'fixture-build');
expect(json['build_type'], 'fixture-buildType');
expect(json['app_start_time'], testStartTime.toIso8601String());
expect(json['device_app_hash'], 'fixture-deviceAppHash');
expect(json['in_foreground'], true);
expect(json['view_names'], ['fixture-viewName', 'fixture-viewName2']);
});
test('fromJson', () {
final sentryApp = SentryApp.fromJson(sentryAppJson);
final json = sentryApp.toJson();

expect(
MapEquality().equals(sentryAppJson, json),
true,
);
expect(json['app_name'], 'fixture-name');
expect(json['app_version'], 'fixture-version');
expect(json['app_identifier'], 'fixture-identifier');
expect(json['app_build'], 'fixture-build');
expect(json['build_type'], 'fixture-buildType');
expect(json['app_start_time'], testStartTime.toIso8601String());
expect(json['device_app_hash'], 'fixture-deviceAppHash');
expect(json['in_foreground'], true);
expect(json['view_names'], ['fixture-viewName', 'fixture-viewName2']);
});
});

Expand Down Expand Up @@ -73,6 +85,7 @@ void main() {
startTime: startTime,
deviceAppHash: 'hash1',
inForeground: true,
viewNames: ['screen1'],
);

expect('name1', copy.name);
Expand All @@ -83,6 +96,7 @@ void main() {
expect(startTime, copy.startTime);
expect('hash1', copy.deviceAppHash);
expect(true, copy.inForeground);
expect(['screen1'], copy.viewNames);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sentry/sentry.dart';

import '../navigation/sentry_navigator_observer.dart';
import '../sentry_flutter_options.dart';

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

final app = contexts.app;
if (app != null) {
contexts.app = _appWithCurrentRouteViewName(app);
}

// Flutter has a lot of Accessibility Settings available and exposes them
contexts['accessibility'] = _getAccessibilityContext();

Expand Down Expand Up @@ -237,4 +243,15 @@ class FlutterEnricherEventProcessor implements EventProcessor {
inForeground: inForeground,
);
}

SentryApp _appWithCurrentRouteViewName(SentryApp app) {
final currentRouteName = SentryNavigatorObserver.currentRouteName;
if (currentRouteName != null) {
final viewNames = app.viewNames ?? [];
viewNames.add(currentRouteName);
return app.copyWith(viewNames: viewNames);
} else {
return app;
}
}
}
27 changes: 23 additions & 4 deletions flutter/lib/src/navigation/sentry_navigator_observer.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
import '../event_processor/flutter_enricher_event_processor.dart';
import '../native/sentry_native.dart';

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

ISentrySpan? _transaction;

static String? _currentRouteName;

@internal
static String? get currentRouteName => _currentRouteName;

@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);

_setCurrentRoute(route);
_setCurrentRouteName(route);
_setCurrentRouteNameAsTransaction(route);

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

_setCurrentRoute(newRoute);
_setCurrentRouteName(newRoute);
_setCurrentRouteNameAsTransaction(newRoute);

_addBreadcrumb(
type: 'didReplace',
from: oldRoute?.settings,
Expand All @@ -116,7 +129,9 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPop(route, previousRoute);

_setCurrentRoute(previousRoute);
_setCurrentRouteName(previousRoute);
_setCurrentRouteNameAsTransaction(previousRoute);

_addBreadcrumb(
type: 'didPop',
from: route.settings,
Expand Down Expand Up @@ -147,7 +162,11 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
?.name;
}

Future<void> _setCurrentRoute(Route<dynamic>? route) async {
Future<void> _setCurrentRouteName(Route<dynamic>? route) async {
_currentRouteName = _getRouteName(route);
}

Future<void> _setCurrentRouteNameAsTransaction(Route<dynamic>? route) async {
final name = _getRouteName(route);
if (name == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,24 @@ void main() {
.length;
expect(ioEnricherCount, 1);
});

testWidgets('adds SentryNavigatorObserver.currentRouteName as app.screen',
(tester) async {
final observer = SentryNavigatorObserver();
final route =
fixture.route(RouteSettings(name: 'fixture-currentRouteName'));
observer.didPush(route, null);

final eventWithContextsApp =
SentryEvent(contexts: Contexts(app: SentryApp()));

final enricher = fixture.getSut(
binding: () => tester.binding,
);
final event = await enricher.apply(eventWithContextsApp);

expect(event?.contexts.app?.viewNames, ['fixture-currentRouteName']);
});
});
}

Expand All @@ -342,6 +360,11 @@ class Fixture {
)..reportPackages = reportPackages;
return FlutterEnricherEventProcessor(options);
}

PageRoute<dynamic> route(RouteSettings? settings) => PageRouteBuilder<void>(
pageBuilder: (_, __, ___) => Container(),
settings: settings,
);
}

void loadTestPackage() {
Expand Down
67 changes: 67 additions & 0 deletions flutter/test/sentry_navigator_observer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,73 @@ void main() {
expect(scope.span, span);
});
});

test('didPush sets current route name', () {
const name = 'Current Route';
final currentRoute = route(RouteSettings(name: name));

const op = 'navigation';
final hub = _MockHub();
final span = getMockSentryTracer(name: name);
when(span.context).thenReturn(SentrySpanContext(operation: op));
_whenAnyStart(hub, span);

final sut = fixture.getSut(
hub: hub,
autoFinishAfter: Duration(seconds: 5),
);

sut.didPush(currentRoute, null);

expect(SentryNavigatorObserver.currentRouteName, 'Current Route');
});

test('didReplace sets new route name', () {
const oldRouteName = 'Old Route';
final oldRoute = route(RouteSettings(name: oldRouteName));
const newRouteName = 'New Route';
final newRoute = route(RouteSettings(name: newRouteName));

const op = 'navigation';
final hub = _MockHub();
final span = getMockSentryTracer(name: oldRouteName);
when(span.context).thenReturn(SentrySpanContext(operation: op));
_whenAnyStart(hub, span);

final sut = fixture.getSut(
hub: hub,
autoFinishAfter: Duration(seconds: 5),
);

sut.didPush(oldRoute, null);
sut.didReplace(newRoute: newRoute, oldRoute: oldRoute);

expect(SentryNavigatorObserver.currentRouteName, 'New Route');
});

test('popRoute sets previous route name', () {
const oldRouteName = 'Old Route';
final oldRoute = route(RouteSettings(name: oldRouteName));
const newRouteName = 'New Route';
final newRoute = route(RouteSettings(name: newRouteName));

const op = 'navigation';
final hub = _MockHub();
final span = getMockSentryTracer(name: oldRouteName);
when(span.context).thenReturn(SentrySpanContext(operation: op));
when(span.status).thenReturn(null);
_whenAnyStart(hub, span);

final sut = fixture.getSut(
hub: hub,
autoFinishAfter: Duration(seconds: 5),
);

sut.didPush(oldRoute, null);
sut.didPop(newRoute, oldRoute);

expect(SentryNavigatorObserver.currentRouteName, 'Old Route');
});
});

group('RouteObserverBreadcrumb', () {
Expand Down

0 comments on commit 3de8b9b

Please sign in to comment.