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
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