Skip to content

Commit

Permalink
Feat View hierarchy (#1189)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Jan 10, 2023
1 parent 20def04 commit 1275c1e
Show file tree
Hide file tree
Showing 27 changed files with 922 additions and 30 deletions.
8 changes: 8 additions & 0 deletions dart/lib/src/hint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class Hint {

SentryAttachment? screenshot;

SentryAttachment? viewHierarchy;

Hint();

factory Hint.withMap(Map<String, Object> map) {
Expand All @@ -39,6 +41,12 @@ class Hint {
return hint;
}

factory Hint.withViewHierarchy(SentryAttachment viewHierarchy) {
final hint = Hint();
hint.viewHierarchy = viewHierarchy;
return hint;
}

// Objects

void addAll(Map<String, Object> keysAndValues) {
Expand Down
3 changes: 3 additions & 0 deletions dart/lib/src/protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ export 'protocol/sentry_trace_header.dart';
export 'protocol/sentry_transaction_name_source.dart';
export 'protocol/sentry_baggage_header.dart';
export 'protocol/sentry_transaction_info.dart';
// view hierarchy
export 'protocol/sentry_view_hierarchy.dart';
export 'protocol/sentry_view_hierarchy_element.dart';
20 changes: 20 additions & 0 deletions dart/lib/src/protocol/sentry_view_hierarchy.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:meta/meta.dart';

import 'sentry_view_hierarchy_element.dart';

@immutable
class SentryViewHierarchy {
SentryViewHierarchy(this.renderingSystem);

final String renderingSystem;
final List<SentryViewHierarchyElement> windows = [];

/// Header encoded as JSON
Map<String, dynamic> toJson() {
return {
'rendering_system': renderingSystem,
if (windows.isNotEmpty)
'windows': windows.map((e) => e.toJson()).toList(growable: false),
};
}
}
55 changes: 55 additions & 0 deletions dart/lib/src/protocol/sentry_view_hierarchy_element.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:meta/meta.dart';

@immutable
class SentryViewHierarchyElement {
SentryViewHierarchyElement(
this.type, {
this.depth,
this.identifier,
this.width,
this.height,
this.x,
this.y,
this.z,
this.visible,
this.alpha,
this.extra,
});

final String type;
final int? depth;
final String? identifier;
final List<SentryViewHierarchyElement> children = [];
final double? width;
final double? height;
final double? x;
final double? y;
final double? z;
final bool? visible;
final double? alpha;
final Map<String, dynamic>? extra;

/// Header encoded as JSON
Map<String, dynamic> toJson() {
final jsonMap = {
'type': type,
if (depth != null) 'depth': depth,
if (identifier != null) 'identifier': identifier,
if (width != null) 'width': width,
if (height != null) 'height': height,
if (x != null) 'x': x,
if (y != null) 'y': y,
if (z != null) 'z': z,
if (visible != null) 'visible': visible,
if (alpha != null) 'alpha': alpha,
if (children.isNotEmpty)
'children': children.map((e) => e.toJson()).toList(growable: false),
};

if (extra?.isNotEmpty ?? false) {
jsonMap.addAll(extra!);
}

return jsonMap;
}
}
14 changes: 14 additions & 0 deletions dart/lib/src/sentry_attachment/sentry_attachment.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'dart:async';
import 'dart:typed_data';

import '../protocol/sentry_view_hierarchy.dart';
import '../utils.dart';

// https://develop.sentry.dev/sdk/features/#attachments
// https://develop.sentry.dev/sdk/envelopes/#attachment

Expand Down Expand Up @@ -28,6 +31,8 @@ class SentryAttachment {
/// breadcrumbs.
static const String typeUnrealLogs = 'unreal.logs';

static const String typeViewHierarchy = 'event.view_hierarchy';

SentryAttachment.fromLoader({
required ContentLoader loader,
required this.filename,
Expand Down Expand Up @@ -88,6 +93,15 @@ class SentryAttachment {
contentType: 'image/png',
attachmentType: SentryAttachment.typeAttachmentDefault);

SentryAttachment.fromViewHierarchy(SentryViewHierarchy sentryViewHierarchy)
: this.fromLoader(
loader: () => Uint8List.fromList(
utf8JsonEncoder.convert(sentryViewHierarchy.toJson())),
filename: 'view-hierarchy.json',
contentType: 'application/json',
attachmentType: SentryAttachment.typeViewHierarchy,
);

/// Attachment type.
/// Should be one of types given in [AttachmentType].
final String attachmentType;
Expand Down
5 changes: 5 additions & 0 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ class SentryClient {
attachments.add(screenshot);
}

var viewHierarchy = hint.viewHierarchy;
if (viewHierarchy != null) {
attachments.add(viewHierarchy);
}

final envelope = SentryEnvelope.fromEvent(
preparedEvent,
_options.sdk,
Expand Down
65 changes: 65 additions & 0 deletions dart/test/protocol/sentry_view_hierarchy_element_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'package:sentry/sentry.dart';
import 'package:test/test.dart';

void main() {
group('json', () {
test('toJson with children', () {
final element = SentryViewHierarchyElement(
'RenderObjectToWidgetAdapter<RenderBox>',
depth: 1,
identifier: 'RenderView#a2216',
width: 100,
height: 200,
x: 100,
y: 50,
z: 30,
visible: true,
alpha: 90,
extra: {'key': 'value'},
);
final element2 = SentryViewHierarchyElement(
'SentryScreenshotWidget',
depth: 2,
);
element.children.add(element2);

final map = element.toJson();

expect(map, {
'type': 'RenderObjectToWidgetAdapter<RenderBox>',
'depth': 1,
'identifier': 'RenderView#a2216',
'children': [
{
'type': 'SentryScreenshotWidget',
'depth': 2,
},
],
'width': 100,
'height': 200,
'x': 100,
'y': 50,
'z': 30,
'visible': true,
'alpha': 90,
'key': 'value',
});
});

test('toJson no children', () {
final element = SentryViewHierarchyElement(
'RenderObjectToWidgetAdapter<RenderBox>',
depth: 1,
identifier: 'RenderView#a2216',
);

final map = element.toJson();

expect(map, {
'type': 'RenderObjectToWidgetAdapter<RenderBox>',
'depth': 1,
'identifier': 'RenderView#a2216',
});
});
});
}
52 changes: 52 additions & 0 deletions dart/test/protocol/sentry_view_hierarchy_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:sentry/sentry.dart';
import 'package:test/test.dart';

void main() {
group('json', () {
test('toJson with children', () {
final element = SentryViewHierarchyElement(
'RenderObjectToWidgetAdapter<RenderBox>',
depth: 1,
identifier: 'RenderView#a2216',
);

final element2 = SentryViewHierarchyElement(
'SentryScreenshotWidget',
depth: 2,
);
element.children.add(element2);

final viewHierrchy = SentryViewHierarchy('flutter');
viewHierrchy.windows.add(element);

final map = viewHierrchy.toJson();

expect(map, {
'rendering_system': 'flutter',
'windows': [
{
'type': 'RenderObjectToWidgetAdapter<RenderBox>',
'depth': 1,
'identifier': 'RenderView#a2216',
'children': [
{
'type': 'SentryScreenshotWidget',
'depth': 2,
},
]
},
],
});
});

test('toJson no children', () {
final viewHierrchy = SentryViewHierarchy('flutter');

final map = viewHierrchy.toJson();

expect(map, {
'rendering_system': 'flutter',
});
});
});
}
10 changes: 10 additions & 0 deletions dart/test/sentry_attachment_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ void main() {
expect(attachment.filename, 'screenshot.png');
expect(attachment.addToTransactions, false);
});

test('fromViewHierarchy', () async {
final view = SentryViewHierarchy('flutter');
final attachment = SentryAttachment.fromViewHierarchy(view);

expect(attachment.attachmentType, SentryAttachment.typeViewHierarchy);
expect(attachment.contentType, 'application/json');
expect(attachment.filename, 'view-hierarchy.json');
expect(attachment.addToTransactions, false);
});
});
}

Expand Down
18 changes: 17 additions & 1 deletion dart/test/sentry_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1143,7 +1143,23 @@ void main() {
final capturedEnvelope = (fixture.transport).envelopes.first;
final attachmentItem = capturedEnvelope.items.firstWhereOrNull(
(element) => element.header.type == SentryItemType.attachment);
expect(attachmentItem != null, true);
expect(attachmentItem?.header.fileName, 'screenshot.png');
});

test('captureEvent adds viewHierarchy from hint', () async {
final client = fixture.getSut();
final view = SentryViewHierarchy('flutter');
final attachment = SentryAttachment.fromViewHierarchy(view);
final hint = Hint.withViewHierarchy(attachment);

await client.captureEvent(fakeEvent, hint: hint);

final capturedEnvelope = (fixture.transport).envelopes.first;
final attachmentItem = capturedEnvelope.items.firstWhereOrNull(
(element) => element.header.type == SentryItemType.attachment);

expect(attachmentItem?.header.attachmentType,
SentryAttachment.typeViewHierarchy);
});

test('captureTransaction adds trace context', () async {
Expand Down
3 changes: 2 additions & 1 deletion flutter/example/integration_test/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

// Using fake DSN for testing purposes.
Future<void> setupSentryAndApp(WidgetTester tester) async {
await setupSentry(() async {
await tester.pumpWidget(SentryScreenshotWidget(
Expand All @@ -16,7 +17,7 @@ void main() {
child: const MyApp(),
)));
await tester.pumpAndSettle();
});
}, 'https://[email protected]/1234567');
}

// Tests
Expand Down
33 changes: 23 additions & 10 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,21 @@ const String _exampleDsn =
const _channel = MethodChannel('example.flutter.sentry.io');

Future<void> main() async {
await setupSentry(() => runApp(
SentryScreenshotWidget(
child: SentryUserInteractionWidget(
child: DefaultAssetBundle(
bundle: SentryAssetBundle(enableStructuredDataTracing: true),
child: const MyApp(),
await setupSentry(
() => runApp(
SentryScreenshotWidget(
child: SentryUserInteractionWidget(
child: DefaultAssetBundle(
bundle: SentryAssetBundle(enableStructuredDataTracing: true),
child: const MyApp(),
),
),
),
),
),
));
_exampleDsn);
}

Future<void> setupSentry(AppRunner appRunner) async {
Future<void> setupSentry(AppRunner appRunner, String dsn) async {
await SentryFlutter.init((options) {
options.dsn = _exampleDsn;
options.tracesSampleRate = 1.0;
Expand All @@ -50,6 +52,7 @@ Future<void> setupSentry(AppRunner appRunner) async {
options.enableNdkScopeSync = true;
options.enableUserInteractionTracing = true;
options.attachScreenshot = true;
options.attachViewHierarchy = true;
// We can enable Sentry debug logging during development. This is likely
// going to log too much for your app, but can be useful when figuring out
// configuration issues, e.g. finding out why your events are not uploaded.
Expand All @@ -58,7 +61,6 @@ Future<void> setupSentry(AppRunner appRunner) async {
options.captureFailedHttpRequests = true;
options.maxRequestBodySize = MaxRequestBodySize.always;
options.maxResponseBodySize = MaxResponseBodySize.always;
options.captureFailedHttpRequests = true;
},
// Init your App.
appRunner: appRunner);
Expand Down Expand Up @@ -222,6 +224,17 @@ class MainScaffold extends StatelessWidget {
},
child: const Text('Capture from PlatformDispatcher.onError'),
),
ElevatedButton(
key: const Key('view hierarchy'),
onPressed: () => {},
child: const Visibility(
visible: true,
child: Opacity(
opacity: 0.5,
child: Text('view hierarchy'),
),
),
),
ElevatedButton(
onPressed: () => makeWebRequest(context),
child: const Text('Dart: Web request'),
Expand Down
Loading

0 comments on commit 1275c1e

Please sign in to comment.