Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set custom measurements on transactions #1011

Merged
merged 14 commits into from
Sep 23, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### Features

- Dynamic sampling ([#1004](https://github.com/getsentry/sentry-dart/pull/1004))
- Set custom measurements on transactions ([#1011](https://github.com/getsentry/sentry-dart/pull/1011))

## 6.10.0

Expand Down
1 change: 0 additions & 1 deletion dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,3 @@ export 'src/sentry_user_feedback.dart';
export 'src/utils/tracing_utils.dart';
// tracing
export 'src/tracing.dart';
export 'src/sentry_measurement.dart';
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 @@ -75,6 +75,9 @@ class NoOpSentrySpan extends ISentrySpan {
@override
SentryTraceHeader toSentryTrace() => _header;

@override
void setMeasurement(String name, num value, {SentryMeasurementUnit? unit}) {}

@override
SentryBaggageHeader? toBaggageHeader() => null;

Expand Down
9 changes: 9 additions & 0 deletions dart/lib/src/protocol/sentry_span.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,15 @@ class SentrySpan extends ISentrySpan {
sampled: samplingDecision?.sampled,
);

@override
void setMeasurement(
String name,
num value, {
SentryMeasurementUnit? unit,
}) {
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
_tracer.setMeasurement(name, value, unit: unit);
}

@override
SentryBaggageHeader? toBaggageHeader() => _tracer.toBaggageHeader();

Expand Down
16 changes: 7 additions & 9 deletions dart/lib/src/protocol/sentry_transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class SentryTransaction extends SentryEvent {
static const String _type = 'transaction';
late final List<SentrySpan> spans;
final SentryTracer _tracer;
late final List<SentryMeasurement> measurements;
late final Map<String, SentryMeasurement> measurements;
late final SentryTransactionInfo? transactionInfo;

SentryTransaction(
Expand All @@ -33,7 +33,7 @@ class SentryTransaction extends SentryEvent {
SdkVersion? sdk,
SentryRequest? request,
String? type,
List<SentryMeasurement>? measurements,
Map<String, SentryMeasurement>? measurements,
SentryTransactionInfo? transactionInfo,
}) : super(
eventId: eventId,
Expand All @@ -58,7 +58,7 @@ class SentryTransaction extends SentryEvent {

final spanContext = _tracer.context;
spans = _tracer.children;
this.measurements = measurements ?? [];
this.measurements = measurements ?? {};

this.contexts.trace = spanContext.toTraceContext(
sampled: _tracer.samplingDecision?.sampled,
Expand All @@ -81,8 +81,8 @@ class SentryTransaction extends SentryEvent {

if (measurements.isNotEmpty) {
final map = <String, dynamic>{};
for (final measurement in measurements) {
map[measurement.name] = measurement.toJson();
for (final item in measurements.entries) {
map[item.key] = item.value.toJson();
}
json['measurements'] = map;
}
Expand Down Expand Up @@ -127,7 +127,7 @@ class SentryTransaction extends SentryEvent {
List<SentryException>? exceptions,
List<SentryThread>? threads,
String? type,
List<SentryMeasurement>? measurements,
Map<String, SentryMeasurement>? measurements,
SentryTransactionInfo? transactionInfo,
}) =>
SentryTransaction(
Expand All @@ -150,9 +150,7 @@ class SentryTransaction extends SentryEvent {
sdk: sdk ?? this.sdk,
request: request ?? this.request,
type: type ?? this.type,
measurements: (measurements != null
? List<SentryMeasurement>.from(measurements)
: null) ??
measurements: (measurements != null ? Map.from(measurements) : null) ??
this.measurements,
transactionInfo: transactionInfo ?? this.transactionInfo,
);
Expand Down
32 changes: 25 additions & 7 deletions dart/lib/src/sentry_measurement.dart
Original file line number Diff line number Diff line change
@@ -1,34 +1,52 @@
import 'sentry_measurement_unit.dart';

class SentryMeasurement {
SentryMeasurement(this.name, this.value);
SentryMeasurement(
this.name,
this.value, {
this.unit,
});

/// Amount of frames drawn during a transaction
SentryMeasurement.totalFrames(this.value) : name = 'frames_total';
SentryMeasurement.totalFrames(this.value)
: name = 'frames_total',
unit = SentryMeasurementUnit.none;
marandaneto marked this conversation as resolved.
Show resolved Hide resolved

/// Amount of slow frames drawn during a transaction.
/// A slow frame is any frame longer than 1s / refreshrate.
/// So for example any frame slower than 16ms for a refresh rate of 60hz.
SentryMeasurement.slowFrames(this.value) : name = 'frames_slow';
SentryMeasurement.slowFrames(this.value)
: name = 'frames_slow',
unit = SentryMeasurementUnit.none;

/// Amount of frozen frames drawn during a transaction.
/// Typically defined as frames slower than 500ms.
SentryMeasurement.frozenFrames(this.value) : name = 'frames_frozen';
SentryMeasurement.frozenFrames(this.value)
: name = 'frames_frozen',
unit = SentryMeasurementUnit.none;

/// Duration of the Cold App start in milliseconds
SentryMeasurement.coldAppStart(Duration duration)
: assert(!duration.isNegative),
name = 'app_start_cold',
value = duration.inMilliseconds;
value = duration.inMilliseconds,
unit = SentryMeasurementUnit.milliSecond;

/// Duration of the Warm App start in milliseconds
SentryMeasurement.warmAppStart(Duration duration)
: assert(!duration.isNegative),
name = 'app_start_warm',
value = duration.inMilliseconds;
value = duration.inMilliseconds,
unit = SentryMeasurementUnit.milliSecond;

final String name;
final num value;
final SentryMeasurementUnit? unit;

Map<String, dynamic> toJson() {
return <String, num>{
return <String, dynamic>{
'value': value,
if (unit != null) 'unit': unit?.toStringValue(),
};
}
}
53 changes: 53 additions & 0 deletions dart/lib/src/sentry_measurement_unit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
enum SentryMeasurementUnit {
/// Nanosecond (`"nanosecond"`), 10^-9 seconds.
nanoSecond,

/// Microsecond (`"microsecond"`), 10^-6 seconds.
microSecond,

/// Millisecond (`"millisecond"`), 10^-3 seconds.
milliSecond,

/// Full second (`"second"`).
second,

/// Minute (`"minute"`), 60 seconds.
minute,

/// Hour (`"hour"`), 3600 seconds.
hour,

/// Day (`"day"`), 86,400 seconds.
day,

/// Week (`"week"`), 604,800 seconds.
week,
marandaneto marked this conversation as resolved.
Show resolved Hide resolved

/// Untyped value without a unit.
none,
}

extension SentryMeasurementUnitExtension on SentryMeasurementUnit {
String toStringValue() {
switch (this) {
case SentryMeasurementUnit.nanoSecond:
return 'nanosecond';
case SentryMeasurementUnit.microSecond:
return 'microsecond';
case SentryMeasurementUnit.milliSecond:
return 'millisecond';
case SentryMeasurementUnit.second:
return 'second';
case SentryMeasurementUnit.minute:
return 'minute';
case SentryMeasurementUnit.hour:
return 'hour';
case SentryMeasurementUnit.day:
return 'day';
case SentryMeasurementUnit.week:
return 'week';
case SentryMeasurementUnit.none:
return 'none';
}
}
}
7 changes: 7 additions & 0 deletions dart/lib/src/sentry_span_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ abstract class ISentrySpan {
/// Returns the trace information that could be sent as a sentry-trace header.
SentryTraceHeader toSentryTrace();

/// Set observed measurement for this transaction.
void setMeasurement(
String name,
num value, {
SentryMeasurementUnit? unit,
});

/// Returns the baggage that can be sent as "baggage" header.
@experimental
SentryBaggageHeader? toBaggageHeader();
Expand Down
15 changes: 9 additions & 6 deletions dart/lib/src/sentry_tracer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class SentryTracer extends ISentrySpan {
late final SentrySpan _rootSpan;
final List<SentrySpan> _children = [];
final Map<String, dynamic> _extra = {};
final List<SentryMeasurement> _measurements = [];
final Map<String, SentryMeasurement> _measurements = {};

Timer? _autoFinishAfterTimer;
Function(SentryTracer)? _onFinish;
Expand Down Expand Up @@ -264,12 +264,9 @@ class SentryTracer extends ISentrySpan {
@override
SentryTraceHeader toSentryTrace() => _rootSpan.toSentryTrace();

void addMeasurements(List<SentryMeasurement> measurements) {
_measurements.addAll(measurements);
}

@visibleForTesting
List<SentryMeasurement> get measurements => _measurements;
Map<String, SentryMeasurement> get measurements =>
Map.unmodifiable(_measurements);

bool _haveAllChildrenFinished() {
for (final child in children) {
Expand All @@ -285,6 +282,12 @@ class SentryTracer extends ISentrySpan {
!span.startTimestamp
.isAfter((span.endTimestamp ?? endTimestampCandidate));

@override
void setMeasurement(String name, num value, {SentryMeasurementUnit? unit}) {
final measurement = SentryMeasurement(name, value, unit: unit);
_measurements[name] = measurement;
}

@override
SentryBaggageHeader? toBaggageHeader() {
final context = traceContext();
Expand Down
2 changes: 2 additions & 0 deletions dart/lib/src/tracing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ export 'sentry_span_context.dart';
export 'sentry_span_interface.dart';
export 'noop_sentry_span.dart';
export 'invalid_sentry_trace_header_exception.dart';
export 'sentry_measurement.dart';
export 'sentry_measurement_unit.dart';
export 'sentry_trace_context_header.dart';
export 'sentry_traces_sampling_decision.dart';
42 changes: 42 additions & 0 deletions dart/test/sentry_measurement_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:collection/collection.dart';
import 'package:sentry/sentry.dart';
import 'package:test/test.dart';

void main() {
group('$SentryMeasurement', () {
test('total frames has none unit', () {
expect(
SentryMeasurement.totalFrames(10).unit, SentryMeasurementUnit.none);
});

test('slow frames has none unit', () {
expect(SentryMeasurement.slowFrames(10).unit, SentryMeasurementUnit.none);
});

test('frozen frames has none unit', () {
expect(
SentryMeasurement.frozenFrames(10).unit, SentryMeasurementUnit.none);
});

test('warm start has milliseconds unit', () {
expect(SentryMeasurement.warmAppStart(Duration(seconds: 1)).unit,
SentryMeasurementUnit.milliSecond);
});

test('cold start has milliseconds unit', () {
expect(SentryMeasurement.coldAppStart(Duration(seconds: 1)).unit,
SentryMeasurementUnit.milliSecond);
});

test('toJson sets unit if given', () {
final measurement = SentryMeasurement('name', 10,
unit: SentryMeasurementUnit.milliSecond);
final map = <String, dynamic>{
'value': 10,
'unit': 'millisecond',
};

expect(MapEquality().equals(measurement.toJson(), map), true);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class NativeAppStartEventProcessor extends EventProcessor {
if (measurement.value >= _maxAppStartMillis) {
return event;
}
event.measurements.add(measurement);
event.measurements[measurement.name] = measurement;
}
return event;
}
Expand Down
25 changes: 18 additions & 7 deletions flutter/lib/src/navigation/sentry_navigator_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,15 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
final nativeFrames = await _native
.endNativeFramesCollection(transaction.context.traceId);
if (nativeFrames != null) {
transaction.addMeasurements(nativeFrames.toMeasurements());
final measurements = nativeFrames.toMeasurements();
for (final item in measurements.entries) {
final measurement = item.value;
transaction.setMeasurement(
item.key,
measurement.value,
unit: measurement.unit,
);
}
}
}
},
Expand Down Expand Up @@ -278,11 +286,14 @@ class RouteObserverBreadcrumb extends Breadcrumb {
}

extension NativeFramesMeasurement on NativeFrames {
List<SentryMeasurement> toMeasurements() {
return [
SentryMeasurement.totalFrames(totalFrames),
SentryMeasurement.slowFrames(slowFrames),
SentryMeasurement.frozenFrames(frozenFrames),
];
Map<String, SentryMeasurement> toMeasurements() {
final total = SentryMeasurement.totalFrames(totalFrames);
final slow = SentryMeasurement.slowFrames(slowFrames);
final frozen = SentryMeasurement.frozenFrames(frozenFrames);
return {
total.name: total,
slow.name: slow,
frozen.name: frozen,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ void main() {
final processor = fixture.options.eventProcessors.first;
final enriched = await processor.apply(transaction) as SentryTransaction;

final expected = SentryMeasurement('app_start_cold', 10);
expect(enriched.measurements[0].name, expected.name);
expect(enriched.measurements[0].value, expected.value);
final measurement = enriched.measurements['app_start_cold']!;
expect(measurement.value, 10);
expect(measurement.unit, SentryMeasurementUnit.milliSecond);
});

test('native app start measurement not added to following transactions',
Expand Down Expand Up @@ -69,15 +69,15 @@ void main() {

final tracer = fixture.createTracer();
final transaction = SentryTransaction(tracer).copyWith();
transaction.measurements.add(measurement);
transaction.measurements[measurement.name] = measurement;

final processor = fixture.options.eventProcessors.first;

var enriched = await processor.apply(transaction) as SentryTransaction;
var secondEnriched = await processor.apply(enriched) as SentryTransaction;

expect(secondEnriched.measurements.length, 2);
expect(secondEnriched.measurements.contains(measurement), true);
expect(secondEnriched.measurements.containsKey(measurement.name), true);
});

test('native app start measurement not added if more than 60s', () async {
Expand Down
Loading