Skip to content
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,
}) {
_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;

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

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