Skip to content

Commit

Permalink
Auto performance monitoring for widgets (#1137)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Nov 25, 2022
1 parent 873fb42 commit a510d1d
Show file tree
Hide file tree
Showing 17 changed files with 1,009 additions and 120 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- User Interaction transactions and breadcrumbs ([#1137](https://github.com/getsentry/sentry-dart/pull/1137))

## 6.16.1

### Fixes
Expand Down
3 changes: 3 additions & 0 deletions dart/lib/src/noop_sentry_span.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,7 @@ class NoOpSentrySpan extends ISentrySpan {

@override
SentryTracesSamplingDecision? get samplingDecision => null;

@override
void scheduleFinish() {}
}
27 changes: 27 additions & 0 deletions dart/lib/src/protocol/breadcrumb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,33 @@ class Breadcrumb {
);
}

factory Breadcrumb.userInteraction({
String? message,
SentryLevel? level,
DateTime? timestamp,
Map<String, dynamic>? data,
required String subCategory,
String? viewId,
String? viewClass,
}) {
final newData = data ?? {};
if (viewId != null) {
newData['view.id'] = viewId;
}
if (viewClass != null) {
newData['view.class'] = viewClass;
}

return Breadcrumb(
message: message,
level: level,
category: 'ui.$subCategory',
type: 'user',
timestamp: timestamp,
data: newData,
);
}

/// Describes the breadcrumb.
///
/// This field is optional and may be set to null.
Expand Down
3 changes: 3 additions & 0 deletions dart/lib/src/protocol/sentry_span.dart
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,7 @@ class SentrySpan extends ISentrySpan {

@override
SentryTraceContextHeader? traceContext() => _tracer.traceContext();

@override
void scheduleFinish() => _tracer.scheduleFinish();
}
9 changes: 9 additions & 0 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,15 @@ class SentryOptions {
/// array, and only attach tracing headers if a match was found.
final List<String> tracePropagationTargets = ['.*'];

/// The idle time to wait until the transaction will be finished.
/// The transaction will use the end timestamp of the last finished span as
/// the endtime for the transaction.
///
/// When set to null the transaction must be finished manually.
///
/// The default is 3 seconds.
Duration? idleTimeout = Duration(seconds: 3);

SentryOptions({this.dsn, PlatformChecker? checker}) {
if (checker != null) {
platformChecker = checker;
Expand Down
3 changes: 3 additions & 0 deletions dart/lib/src/sentry_span_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,7 @@ abstract class ISentrySpan {
/// Returns the trace context.
@experimental
SentryTraceContextHeader? traceContext();

@internal
void scheduleFinish();
}
41 changes: 36 additions & 5 deletions dart/lib/src/sentry_tracer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ class SentryTracer extends ISentrySpan {
final Map<String, SentryMeasurement> _measurements = {};

Timer? _autoFinishAfterTimer;
Duration? _autoFinishAfter;

@visibleForTesting
Timer? get autoFinishAfterTimer => _autoFinishAfterTimer;

Function(SentryTracer)? _onFinish;
var _finishStatus = SentryTracerFinishStatus.notFinishing();
late final bool _trimEnd;
Expand Down Expand Up @@ -56,11 +61,9 @@ class SentryTracer extends ISentrySpan {
startTimestamp: startTimestamp,
);
_waitForChildren = waitForChildren;
if (autoFinishAfter != null) {
_autoFinishAfterTimer = Timer(autoFinishAfter, () async {
await finish(status: status ?? SpanStatus.ok());
});
}
_autoFinishAfter = autoFinishAfter;

_scheduleTimer();
name = transactionContext.name;
// always default to custom if not provided
transactionNameSource = transactionContext.transactionNameSource ??
Expand Down Expand Up @@ -117,6 +120,11 @@ class SentryTracer extends ISentrySpan {
}
});

// if it's an idle transaction which has no children, we drop it to save user's quota
if (children.isEmpty && _autoFinishAfter != null) {
return;
}

final transaction = SentryTransaction(this);
transaction.measurements.addAll(_measurements);
await _hub.captureTransaction(
Expand Down Expand Up @@ -197,6 +205,9 @@ class SentryTracer extends ISentrySpan {
return NoOpSentrySpan();
}

// reset the timer if a new child is added
_scheduleTimer();

if (children.length >= _hub.options.maxSpans) {
_hub.options.logger(
SentryLevel.warning,
Expand Down Expand Up @@ -346,4 +357,24 @@ class SentryTracer extends ISentrySpan {
@override
SentryTracesSamplingDecision? get samplingDecision =>
_rootSpan.samplingDecision;

@override
void scheduleFinish() {
if (finished) {
return;
}
if (_autoFinishAfterTimer != null) {
_scheduleTimer();
}
}

void _scheduleTimer() {
final autoFinishAfter = _autoFinishAfter;
if (autoFinishAfter != null) {
_autoFinishAfterTimer?.cancel();
_autoFinishAfterTimer = Timer(autoFinishAfter, () async {
await finish(status: status ?? SpanStatus.ok());
});
}
}
}
181 changes: 107 additions & 74 deletions dart/test/protocol/breadcrumb_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,86 +79,119 @@ void main() {
});
});

test('Breadcrumb http ctor', () {
final breadcrumb = Breadcrumb.http(
url: Uri.parse('https://example.org'),
method: 'GET',
level: SentryLevel.fatal,
reason: 'OK',
statusCode: 200,
requestDuration: Duration.zero,
timestamp: DateTime.now(),
requestBodySize: 2,
responseBodySize: 3,
);
final json = breadcrumb.toJson();

expect(json, {
'timestamp': formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
'category': 'http',
'data': {
'url': 'https://example.org',
'method': 'GET',
'status_code': 200,
'reason': 'OK',
'duration': '0:00:00.000000',
'request_body_size': 2,
'response_body_size': 3,
},
'level': 'fatal',
'type': 'http',
group('ctor', () {
test('Breadcrumb http', () {
final breadcrumb = Breadcrumb.http(
url: Uri.parse('https://example.org'),
method: 'GET',
level: SentryLevel.fatal,
reason: 'OK',
statusCode: 200,
requestDuration: Duration.zero,
timestamp: DateTime.now(),
requestBodySize: 2,
responseBodySize: 3,
);
final json = breadcrumb.toJson();

expect(json, {
'timestamp':
formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
'category': 'http',
'data': {
'url': 'https://example.org',
'method': 'GET',
'status_code': 200,
'reason': 'OK',
'duration': '0:00:00.000000',
'request_body_size': 2,
'response_body_size': 3,
},
'level': 'fatal',
'type': 'http',
});
});
});

test('Minimal Breadcrumb http ctor', () {
final breadcrumb = Breadcrumb.http(
url: Uri.parse('https://example.org'),
method: 'GET',
);
final json = breadcrumb.toJson();

expect(json, {
'timestamp': formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
'category': 'http',
'data': {
'url': 'https://example.org',
'method': 'GET',
},
'level': 'info',
'type': 'http',
test('Minimal Breadcrumb http', () {
final breadcrumb = Breadcrumb.http(
url: Uri.parse('https://example.org'),
method: 'GET',
);
final json = breadcrumb.toJson();

expect(json, {
'timestamp':
formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
'category': 'http',
'data': {
'url': 'https://example.org',
'method': 'GET',
},
'level': 'info',
'type': 'http',
});
});
});

test('Breadcrumb console ctor', () {
final breadcrumb = Breadcrumb.console(
message: 'Foo Bar',
);
final json = breadcrumb.toJson();

expect(json, {
'message': 'Foo Bar',
'timestamp': formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
'category': 'console',
'type': 'debug',
'level': 'info',
test('Breadcrumb console', () {
final breadcrumb = Breadcrumb.console(
message: 'Foo Bar',
);
final json = breadcrumb.toJson();

expect(json, {
'message': 'Foo Bar',
'timestamp':
formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
'category': 'console',
'type': 'debug',
'level': 'info',
});
});
});

test('extensive Breadcrumb console ctor', () {
final breadcrumb = Breadcrumb.console(
message: 'Foo Bar',
level: SentryLevel.error,
data: {'foo': 'bar'},
);
final json = breadcrumb.toJson();

expect(json, {
'message': 'Foo Bar',
'timestamp': formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
'category': 'console',
'type': 'debug',
'level': 'error',
'data': {'foo': 'bar'},
test('extensive Breadcrumb console', () {
final breadcrumb = Breadcrumb.console(
message: 'Foo Bar',
level: SentryLevel.error,
data: {'foo': 'bar'},
);
final json = breadcrumb.toJson();

expect(json, {
'message': 'Foo Bar',
'timestamp':
formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
'category': 'console',
'type': 'debug',
'level': 'error',
'data': {'foo': 'bar'},
});
});

test('extensive Breadcrumb user interaction', () {
final time = DateTime.now().toUtc();
final breadcrumb = Breadcrumb.userInteraction(
message: 'Foo Bar',
level: SentryLevel.error,
timestamp: time,
data: {'foo': 'bar'},
subCategory: 'click',
viewId: 'foo',
viewClass: 'bar',
);
final json = breadcrumb.toJson();

expect(json, {
'message': 'Foo Bar',
'timestamp': formatDateAsIso8601WithMillisPrecision(time),
'category': 'ui.click',
'type': 'user',
'level': 'error',
'data': {
'foo': 'bar',
'view.id': 'foo',
'view.class': 'bar',
},
});
});
});
}
6 changes: 6 additions & 0 deletions dart/test/sentry_options_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,10 @@ void main() {

expect(options.tracePropagationTargets, ['.*']);
});

test('SentryOptions has default idleTimeout', () {
final options = SentryOptions.empty();

expect(options.idleTimeout?.inSeconds, Duration(seconds: 3).inSeconds);
});
}
Loading

0 comments on commit a510d1d

Please sign in to comment.