Skip to content

Commit a510d1d

Browse files
authored
Auto performance monitoring for widgets (#1137)
1 parent 873fb42 commit a510d1d

17 files changed

+1009
-120
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- User Interaction transactions and breadcrumbs ([#1137](https://github.com/getsentry/sentry-dart/pull/1137))
8+
39
## 6.16.1
410

511
### Fixes

dart/lib/src/noop_sentry_span.dart

+3
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,7 @@ class NoOpSentrySpan extends ISentrySpan {
8686

8787
@override
8888
SentryTracesSamplingDecision? get samplingDecision => null;
89+
90+
@override
91+
void scheduleFinish() {}
8992
}

dart/lib/src/protocol/breadcrumb.dart

+27
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,33 @@ class Breadcrumb {
8181
);
8282
}
8383

84+
factory Breadcrumb.userInteraction({
85+
String? message,
86+
SentryLevel? level,
87+
DateTime? timestamp,
88+
Map<String, dynamic>? data,
89+
required String subCategory,
90+
String? viewId,
91+
String? viewClass,
92+
}) {
93+
final newData = data ?? {};
94+
if (viewId != null) {
95+
newData['view.id'] = viewId;
96+
}
97+
if (viewClass != null) {
98+
newData['view.class'] = viewClass;
99+
}
100+
101+
return Breadcrumb(
102+
message: message,
103+
level: level,
104+
category: 'ui.$subCategory',
105+
type: 'user',
106+
timestamp: timestamp,
107+
data: newData,
108+
);
109+
}
110+
84111
/// Describes the breadcrumb.
85112
///
86113
/// This field is optional and may be set to null.

dart/lib/src/protocol/sentry_span.dart

+3
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,7 @@ class SentrySpan extends ISentrySpan {
197197

198198
@override
199199
SentryTraceContextHeader? traceContext() => _tracer.traceContext();
200+
201+
@override
202+
void scheduleFinish() => _tracer.scheduleFinish();
200203
}

dart/lib/src/sentry_options.dart

+9
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,15 @@ class SentryOptions {
299299
/// array, and only attach tracing headers if a match was found.
300300
final List<String> tracePropagationTargets = ['.*'];
301301

302+
/// The idle time to wait until the transaction will be finished.
303+
/// The transaction will use the end timestamp of the last finished span as
304+
/// the endtime for the transaction.
305+
///
306+
/// When set to null the transaction must be finished manually.
307+
///
308+
/// The default is 3 seconds.
309+
Duration? idleTimeout = Duration(seconds: 3);
310+
302311
SentryOptions({this.dsn, PlatformChecker? checker}) {
303312
if (checker != null) {
304313
platformChecker = checker;

dart/lib/src/sentry_span_interface.dart

+3
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,7 @@ abstract class ISentrySpan {
7171
/// Returns the trace context.
7272
@experimental
7373
SentryTraceContextHeader? traceContext();
74+
75+
@internal
76+
void scheduleFinish();
7477
}

dart/lib/src/sentry_tracer.dart

+36-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ class SentryTracer extends ISentrySpan {
1818
final Map<String, SentryMeasurement> _measurements = {};
1919

2020
Timer? _autoFinishAfterTimer;
21+
Duration? _autoFinishAfter;
22+
23+
@visibleForTesting
24+
Timer? get autoFinishAfterTimer => _autoFinishAfterTimer;
25+
2126
Function(SentryTracer)? _onFinish;
2227
var _finishStatus = SentryTracerFinishStatus.notFinishing();
2328
late final bool _trimEnd;
@@ -56,11 +61,9 @@ class SentryTracer extends ISentrySpan {
5661
startTimestamp: startTimestamp,
5762
);
5863
_waitForChildren = waitForChildren;
59-
if (autoFinishAfter != null) {
60-
_autoFinishAfterTimer = Timer(autoFinishAfter, () async {
61-
await finish(status: status ?? SpanStatus.ok());
62-
});
63-
}
64+
_autoFinishAfter = autoFinishAfter;
65+
66+
_scheduleTimer();
6467
name = transactionContext.name;
6568
// always default to custom if not provided
6669
transactionNameSource = transactionContext.transactionNameSource ??
@@ -117,6 +120,11 @@ class SentryTracer extends ISentrySpan {
117120
}
118121
});
119122

123+
// if it's an idle transaction which has no children, we drop it to save user's quota
124+
if (children.isEmpty && _autoFinishAfter != null) {
125+
return;
126+
}
127+
120128
final transaction = SentryTransaction(this);
121129
transaction.measurements.addAll(_measurements);
122130
await _hub.captureTransaction(
@@ -197,6 +205,9 @@ class SentryTracer extends ISentrySpan {
197205
return NoOpSentrySpan();
198206
}
199207

208+
// reset the timer if a new child is added
209+
_scheduleTimer();
210+
200211
if (children.length >= _hub.options.maxSpans) {
201212
_hub.options.logger(
202213
SentryLevel.warning,
@@ -346,4 +357,24 @@ class SentryTracer extends ISentrySpan {
346357
@override
347358
SentryTracesSamplingDecision? get samplingDecision =>
348359
_rootSpan.samplingDecision;
360+
361+
@override
362+
void scheduleFinish() {
363+
if (finished) {
364+
return;
365+
}
366+
if (_autoFinishAfterTimer != null) {
367+
_scheduleTimer();
368+
}
369+
}
370+
371+
void _scheduleTimer() {
372+
final autoFinishAfter = _autoFinishAfter;
373+
if (autoFinishAfter != null) {
374+
_autoFinishAfterTimer?.cancel();
375+
_autoFinishAfterTimer = Timer(autoFinishAfter, () async {
376+
await finish(status: status ?? SpanStatus.ok());
377+
});
378+
}
379+
}
349380
}

dart/test/protocol/breadcrumb_test.dart

+107-74
Original file line numberDiff line numberDiff line change
@@ -79,86 +79,119 @@ void main() {
7979
});
8080
});
8181

82-
test('Breadcrumb http ctor', () {
83-
final breadcrumb = Breadcrumb.http(
84-
url: Uri.parse('https://example.org'),
85-
method: 'GET',
86-
level: SentryLevel.fatal,
87-
reason: 'OK',
88-
statusCode: 200,
89-
requestDuration: Duration.zero,
90-
timestamp: DateTime.now(),
91-
requestBodySize: 2,
92-
responseBodySize: 3,
93-
);
94-
final json = breadcrumb.toJson();
95-
96-
expect(json, {
97-
'timestamp': formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
98-
'category': 'http',
99-
'data': {
100-
'url': 'https://example.org',
101-
'method': 'GET',
102-
'status_code': 200,
103-
'reason': 'OK',
104-
'duration': '0:00:00.000000',
105-
'request_body_size': 2,
106-
'response_body_size': 3,
107-
},
108-
'level': 'fatal',
109-
'type': 'http',
82+
group('ctor', () {
83+
test('Breadcrumb http', () {
84+
final breadcrumb = Breadcrumb.http(
85+
url: Uri.parse('https://example.org'),
86+
method: 'GET',
87+
level: SentryLevel.fatal,
88+
reason: 'OK',
89+
statusCode: 200,
90+
requestDuration: Duration.zero,
91+
timestamp: DateTime.now(),
92+
requestBodySize: 2,
93+
responseBodySize: 3,
94+
);
95+
final json = breadcrumb.toJson();
96+
97+
expect(json, {
98+
'timestamp':
99+
formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
100+
'category': 'http',
101+
'data': {
102+
'url': 'https://example.org',
103+
'method': 'GET',
104+
'status_code': 200,
105+
'reason': 'OK',
106+
'duration': '0:00:00.000000',
107+
'request_body_size': 2,
108+
'response_body_size': 3,
109+
},
110+
'level': 'fatal',
111+
'type': 'http',
112+
});
110113
});
111-
});
112114

113-
test('Minimal Breadcrumb http ctor', () {
114-
final breadcrumb = Breadcrumb.http(
115-
url: Uri.parse('https://example.org'),
116-
method: 'GET',
117-
);
118-
final json = breadcrumb.toJson();
119-
120-
expect(json, {
121-
'timestamp': formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
122-
'category': 'http',
123-
'data': {
124-
'url': 'https://example.org',
125-
'method': 'GET',
126-
},
127-
'level': 'info',
128-
'type': 'http',
115+
test('Minimal Breadcrumb http', () {
116+
final breadcrumb = Breadcrumb.http(
117+
url: Uri.parse('https://example.org'),
118+
method: 'GET',
119+
);
120+
final json = breadcrumb.toJson();
121+
122+
expect(json, {
123+
'timestamp':
124+
formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
125+
'category': 'http',
126+
'data': {
127+
'url': 'https://example.org',
128+
'method': 'GET',
129+
},
130+
'level': 'info',
131+
'type': 'http',
132+
});
129133
});
130-
});
131134

132-
test('Breadcrumb console ctor', () {
133-
final breadcrumb = Breadcrumb.console(
134-
message: 'Foo Bar',
135-
);
136-
final json = breadcrumb.toJson();
137-
138-
expect(json, {
139-
'message': 'Foo Bar',
140-
'timestamp': formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
141-
'category': 'console',
142-
'type': 'debug',
143-
'level': 'info',
135+
test('Breadcrumb console', () {
136+
final breadcrumb = Breadcrumb.console(
137+
message: 'Foo Bar',
138+
);
139+
final json = breadcrumb.toJson();
140+
141+
expect(json, {
142+
'message': 'Foo Bar',
143+
'timestamp':
144+
formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
145+
'category': 'console',
146+
'type': 'debug',
147+
'level': 'info',
148+
});
144149
});
145-
});
146150

147-
test('extensive Breadcrumb console ctor', () {
148-
final breadcrumb = Breadcrumb.console(
149-
message: 'Foo Bar',
150-
level: SentryLevel.error,
151-
data: {'foo': 'bar'},
152-
);
153-
final json = breadcrumb.toJson();
154-
155-
expect(json, {
156-
'message': 'Foo Bar',
157-
'timestamp': formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
158-
'category': 'console',
159-
'type': 'debug',
160-
'level': 'error',
161-
'data': {'foo': 'bar'},
151+
test('extensive Breadcrumb console', () {
152+
final breadcrumb = Breadcrumb.console(
153+
message: 'Foo Bar',
154+
level: SentryLevel.error,
155+
data: {'foo': 'bar'},
156+
);
157+
final json = breadcrumb.toJson();
158+
159+
expect(json, {
160+
'message': 'Foo Bar',
161+
'timestamp':
162+
formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
163+
'category': 'console',
164+
'type': 'debug',
165+
'level': 'error',
166+
'data': {'foo': 'bar'},
167+
});
168+
});
169+
170+
test('extensive Breadcrumb user interaction', () {
171+
final time = DateTime.now().toUtc();
172+
final breadcrumb = Breadcrumb.userInteraction(
173+
message: 'Foo Bar',
174+
level: SentryLevel.error,
175+
timestamp: time,
176+
data: {'foo': 'bar'},
177+
subCategory: 'click',
178+
viewId: 'foo',
179+
viewClass: 'bar',
180+
);
181+
final json = breadcrumb.toJson();
182+
183+
expect(json, {
184+
'message': 'Foo Bar',
185+
'timestamp': formatDateAsIso8601WithMillisPrecision(time),
186+
'category': 'ui.click',
187+
'type': 'user',
188+
'level': 'error',
189+
'data': {
190+
'foo': 'bar',
191+
'view.id': 'foo',
192+
'view.class': 'bar',
193+
},
194+
});
162195
});
163196
});
164197
}

dart/test/sentry_options_test.dart

+6
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,10 @@ void main() {
9595

9696
expect(options.tracePropagationTargets, ['.*']);
9797
});
98+
99+
test('SentryOptions has default idleTimeout', () {
100+
final options = SentryOptions.empty();
101+
102+
expect(options.idleTimeout?.inSeconds, Duration(seconds: 3).inSeconds);
103+
});
98104
}

0 commit comments

Comments
 (0)