diff --git a/CHANGELOG.md b/CHANGELOG.md index 677e978999..027c4be048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index a794cfffab..717434c8f9 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -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'; diff --git a/dart/lib/src/noop_sentry_span.dart b/dart/lib/src/noop_sentry_span.dart index 79b8d258de..55cbe1261b 100644 --- a/dart/lib/src/noop_sentry_span.dart +++ b/dart/lib/src/noop_sentry_span.dart @@ -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; diff --git a/dart/lib/src/protocol/sentry_span.dart b/dart/lib/src/protocol/sentry_span.dart index 6aeae4565a..bad87759b9 100644 --- a/dart/lib/src/protocol/sentry_span.dart +++ b/dart/lib/src/protocol/sentry_span.dart @@ -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(); diff --git a/dart/lib/src/protocol/sentry_transaction.dart b/dart/lib/src/protocol/sentry_transaction.dart index 1b7c6947cb..74f30ea67a 100644 --- a/dart/lib/src/protocol/sentry_transaction.dart +++ b/dart/lib/src/protocol/sentry_transaction.dart @@ -11,7 +11,7 @@ class SentryTransaction extends SentryEvent { static const String _type = 'transaction'; late final List spans; final SentryTracer _tracer; - late final List measurements; + late final Map measurements; late final SentryTransactionInfo? transactionInfo; SentryTransaction( @@ -33,7 +33,7 @@ class SentryTransaction extends SentryEvent { SdkVersion? sdk, SentryRequest? request, String? type, - List? measurements, + Map? measurements, SentryTransactionInfo? transactionInfo, }) : super( eventId: eventId, @@ -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, @@ -81,8 +81,8 @@ class SentryTransaction extends SentryEvent { if (measurements.isNotEmpty) { final map = {}; - 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; } @@ -127,7 +127,7 @@ class SentryTransaction extends SentryEvent { List? exceptions, List? threads, String? type, - List? measurements, + Map? measurements, SentryTransactionInfo? transactionInfo, }) => SentryTransaction( @@ -150,9 +150,7 @@ class SentryTransaction extends SentryEvent { sdk: sdk ?? this.sdk, request: request ?? this.request, type: type ?? this.type, - measurements: (measurements != null - ? List.from(measurements) - : null) ?? + measurements: (measurements != null ? Map.from(measurements) : null) ?? this.measurements, transactionInfo: transactionInfo ?? this.transactionInfo, ); diff --git a/dart/lib/src/sentry_measurement.dart b/dart/lib/src/sentry_measurement.dart index 43a2ac18bd..d7c568ee29 100644 --- a/dart/lib/src/sentry_measurement.dart +++ b/dart/lib/src/sentry_measurement.dart @@ -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 toJson() { - return { + return { 'value': value, + if (unit != null) 'unit': unit?.toStringValue(), }; } } diff --git a/dart/lib/src/sentry_measurement_unit.dart b/dart/lib/src/sentry_measurement_unit.dart new file mode 100644 index 0000000000..e53432b827 --- /dev/null +++ b/dart/lib/src/sentry_measurement_unit.dart @@ -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'; + } + } +} diff --git a/dart/lib/src/sentry_span_interface.dart b/dart/lib/src/sentry_span_interface.dart index 3b94613991..74d55570d2 100644 --- a/dart/lib/src/sentry_span_interface.dart +++ b/dart/lib/src/sentry_span_interface.dart @@ -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(); diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index 3982865ffb..f81d14c1fb 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -16,7 +16,7 @@ class SentryTracer extends ISentrySpan { late final SentrySpan _rootSpan; final List _children = []; final Map _extra = {}; - final List _measurements = []; + final Map _measurements = {}; Timer? _autoFinishAfterTimer; Function(SentryTracer)? _onFinish; @@ -264,12 +264,9 @@ class SentryTracer extends ISentrySpan { @override SentryTraceHeader toSentryTrace() => _rootSpan.toSentryTrace(); - void addMeasurements(List measurements) { - _measurements.addAll(measurements); - } - @visibleForTesting - List get measurements => _measurements; + Map get measurements => + Map.unmodifiable(_measurements); bool _haveAllChildrenFinished() { for (final child in children) { @@ -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(); diff --git a/dart/lib/src/tracing.dart b/dart/lib/src/tracing.dart index 0b7649d2d3..bc13c0a768 100644 --- a/dart/lib/src/tracing.dart +++ b/dart/lib/src/tracing.dart @@ -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'; diff --git a/dart/test/sentry_measurement_test.dart b/dart/test/sentry_measurement_test.dart new file mode 100644 index 0000000000..144a8f9b7f --- /dev/null +++ b/dart/test/sentry_measurement_test.dart @@ -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 = { + 'value': 10, + 'unit': 'millisecond', + }; + + expect(MapEquality().equals(measurement.toJson(), map), true); + }); + }); +} diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index 4affebb40e..298e8f3808 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -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; } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index b4bdb63cc3..b32d18e1ae 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -186,7 +186,15 @@ class SentryNavigatorObserver extends RouteObserver> { 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, + ); + } } } }, @@ -278,11 +286,14 @@ class RouteObserverBreadcrumb extends Breadcrumb { } extension NativeFramesMeasurement on NativeFrames { - List toMeasurements() { - return [ - SentryMeasurement.totalFrames(totalFrames), - SentryMeasurement.slowFrames(slowFrames), - SentryMeasurement.frozenFrames(frozenFrames), - ]; + Map 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, + }; } } diff --git a/flutter/test/integrations/native_app_start_integration_test.dart b/flutter/test/integrations/native_app_start_integration_test.dart index 4ca72808e8..ef851af6ac 100644 --- a/flutter/test/integrations/native_app_start_integration_test.dart +++ b/flutter/test/integrations/native_app_start_integration_test.dart @@ -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', @@ -69,7 +69,7 @@ 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; @@ -77,7 +77,7 @@ void main() { 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 { diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 83c9448f5a..9bce2502a0 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -28,6 +28,7 @@ import 'mocks.dart' as _i12; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeSentrySpanContext_0 extends _i1.SmartFake implements _i2.SentrySpanContext { @@ -144,7 +145,6 @@ class MockTransport extends _i1.Mock implements _i2.Transport { /// A class which mocks [SentryTracer]. /// /// See the documentation for Mockito's code generation for more information. -// ignore: invalid_use_of_internal_member class MockSentryTracer extends _i1.Mock implements _i8.SentryTracer { MockSentryTracer() { _i1.throwOnMissingStub(this); @@ -232,10 +232,10 @@ class MockSentryTracer extends _i1.Mock implements _i8.SentryTracer { returnValue: {}, ) as Map); @override - List<_i2.SentryMeasurement> get measurements => (super.noSuchMethod( + Map get measurements => (super.noSuchMethod( Invocation.getter(#measurements), - returnValue: <_i2.SentryMeasurement>[], - ) as List<_i2.SentryMeasurement>); + returnValue: {}, + ) as Map); @override _i6.Future finish({ _i3.SpanStatus? status, @@ -375,11 +375,19 @@ class MockSentryTracer extends _i1.Mock implements _i8.SentryTracer { ), ) as _i3.SentryTraceHeader); @override - void addMeasurements(List<_i2.SentryMeasurement>? measurements) => + void setMeasurement( + String? name, + num? value, { + _i2.SentryMeasurementUnit? unit, + }) => super.noSuchMethod( Invocation.method( - #addMeasurements, - [measurements], + #setMeasurement, + [ + name, + value, + ], + {#unit: unit}, ), returnValueForMissingStub: null, ); diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index e7ffa83d28..205ddb7ac0 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -96,7 +96,7 @@ void main() { expect(mockNativeChannel.numberOfEndNativeFramesCalls, 1); - final measurements = actualTransaction?.measurements ?? []; + final measurements = actualTransaction?.measurements ?? {}; expect(measurements.length, 3); @@ -104,7 +104,8 @@ void main() { final expectedSlow = SentryMeasurement.slowFrames(2); final expectedFrozen = SentryMeasurement.frozenFrames(1); - for (final measurement in measurements) { + for (final item in measurements.entries) { + final measurement = item.value; if (measurement.name == expectedTotal.name) { expect(measurement.value, expectedTotal.value); } else if (measurement.name == expectedSlow.name) {