diff --git a/CHANGELOG.md b/CHANGELOG.md index e46db13f1..644e10846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Features - Emit `transaction.data` inside `contexts.trace.data` ([#2284](https://github.com/getsentry/sentry-dart/pull/2284)) -- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291) +- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291)) ### Enhancements @@ -20,6 +20,12 @@ - Only start frame tracking if we receive valid display refresh data ([#2307](https://github.com/getsentry/sentry-dart/pull/2307)) - Rounding error used on frames.total and reject frame measurements if frames.total is less than frames.slow or frames.frozen ([#2308](https://github.com/getsentry/sentry-dart/pull/2308)) - iOS replay integration when only `onErrorSampleRate` is specified ([#2306](https://github.com/getsentry/sentry-dart/pull/2306)) +- Fix TTID timing issue ([#2326](https://github.com/getsentry/sentry-dart/pull/2326)) + +### Deprecate + +- Metrics API ([#2312](https://github.com/getsentry/sentry-dart/pull/2312)) + - Learn more: https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Metrics-Beta-Coming-to-an-End ## 8.9.0 diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 2626670a3..cbbc30dd3 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -44,6 +44,8 @@ class Hub { late final MetricsApi _metricsApi; @internal + @Deprecated( + 'Metrics will be deprecated and removed in the next major release. Sentry will reject all metrics sent after October 7, 2024. Learn more: https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Upcoming-API-Changes-to-Metrics') MetricsApi get metricsApi => _metricsApi; @internal diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 6b2ece3c5..137e028f9 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -28,6 +28,7 @@ class HubAdapter implements Hub { @override @internal + // ignore: deprecated_member_use_from_same_package MetricsApi get metricsApi => Sentry.currentHub.metricsApi; factory HubAdapter() { diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 29217abe4..48a88bd46 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -316,6 +316,8 @@ class Sentry { static ISentrySpan? getSpan() => _hub.getSpan(); /// Gets access to the metrics API for the current hub. + @Deprecated( + 'Metrics will be deprecated and removed in the next major release. Sentry will reject all metrics sent after October 7, 2024. Learn more: https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Upcoming-API-Changes-to-Metrics') static MetricsApi metrics() => _hub.metricsApi; @internal diff --git a/dart/test/metrics/local_metrics_aggregator_test.dart b/dart/test/metrics/local_metrics_aggregator_test.dart deleted file mode 100644 index 42648fdaf..000000000 --- a/dart/test/metrics/local_metrics_aggregator_test.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:sentry/src/metrics/local_metrics_aggregator.dart'; -import 'package:test/expect.dart'; -import 'package:test/scaffolding.dart'; - -import '../mocks.dart'; - -void main() { - group('add', () { - late LocalMetricsAggregator aggregator; - - setUp(() { - aggregator = LocalMetricsAggregator(); - }); - - test('same metric multiple times aggregates them', () async { - aggregator.add(fakeMetric, 1); - aggregator.add(fakeMetric, 2); - final summaries = aggregator.getSummaries(); - expect(summaries.length, 1); - final summary = summaries.values.first; - expect(summary.length, 1); - }); - - test('same metric different tags aggregates summary bucket', () async { - aggregator.add(fakeMetric, 1); - aggregator.add(fakeMetric..tags.clear(), 2); - final summaries = aggregator.getSummaries(); - expect(summaries.length, 1); - final summary = summaries.values.first; - expect(summary.length, 2); - }); - - test('different metrics does not aggregate them', () async { - aggregator.add(fakeMetric, 1); - aggregator.add(fakeMetric2, 2); - final summaries = aggregator.getSummaries(); - expect(summaries.length, 2); - }); - }); -} diff --git a/dart/test/metrics/metric_test.dart b/dart/test/metrics/metric_test.dart deleted file mode 100644 index f3edc0486..000000000 --- a/dart/test/metrics/metric_test.dart +++ /dev/null @@ -1,315 +0,0 @@ -import 'package:sentry/sentry.dart'; -import 'package:sentry/src/metrics/metric.dart'; -import 'package:test/expect.dart'; -import 'package:test/scaffolding.dart'; - -import 'metrics_aggregator_test.dart'; - -void main() { - group('fromType', () { - test('counter creates a CounterMetric', () async { - final Metric metric = Metric.fromType( - type: MetricType.counter, - key: mockKey, - value: 1, - unit: mockUnit, - tags: mockTags); - expect(metric, isA()); - }); - - test('gauge creates a GaugeMetric', () async { - final Metric metric = Metric.fromType( - type: MetricType.gauge, - key: mockKey, - value: 1, - unit: mockUnit, - tags: mockTags); - expect(metric, isA()); - }); - - test('distribution creates a DistributionMetric', () async { - final Metric metric = Metric.fromType( - type: MetricType.distribution, - key: mockKey, - value: 1, - unit: mockUnit, - tags: mockTags); - expect(metric, isA()); - }); - - test('set creates a SetMetric', () async { - final Metric metric = Metric.fromType( - type: MetricType.set, - key: mockKey, - value: 1, - unit: mockUnit, - tags: mockTags); - expect(metric, isA()); - }); - }); - - group('Encode to statsd', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('encode CounterMetric', () async { - final int bucketKey = 10; - final expectedStatsd = - 'key_metric_@hour:2.1|c|#tag1:tag\\u{2c} value 1,key2:&@"13/-d_s|T10'; - final actualStatsd = fixture.counterMetric.encodeToStatsd(bucketKey); - expect(actualStatsd, expectedStatsd); - }); - - test('sanitize name', () async { - final metric = Metric.fromType( - type: MetricType.counter, - value: 2.1, - key: 'key£ - @# metric!', - unit: DurationSentryMeasurementUnit.day, - tags: {}, - ); - - final expectedStatsd = 'key_-_metric_@day:2.1|c|T10'; - expect(metric.encodeToStatsd(10), expectedStatsd); - }); - - test('sanitize unit', () async { - final metric = Metric.fromType( - type: MetricType.counter, - value: 2.1, - key: 'key', - unit: CustomSentryMeasurementUnit('weird-measurement name!'), - tags: {}, - ); - - final expectedStatsd = 'key@weirdmeasurementname:2.1|c|T10'; - expect(metric.encodeToStatsd(10), expectedStatsd); - }); - - test('sanitize tags', () async { - final metric = Metric.fromType( - type: MetricType.counter, - value: 2.1, - key: 'key', - unit: DurationSentryMeasurementUnit.day, - tags: {'tag1': 'tag, value 1', 'key 2': '&@"13/-d_s'}, - ); - - final expectedStatsd = - 'key@day:2.1|c|#tag1:tag\\u{2c} value 1,key2:&@"13/-d_s|T10'; - expect(metric.encodeToStatsd(10), expectedStatsd); - }); - }); - - group('getCompositeKey', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('escapes commas from tags', () async { - final Iterable tags = fixture.counterMetric.tags.values; - final joinedTags = tags.join(); - final Iterable expectedTags = - tags.map((e) => e.replaceAll(',', '\\,')); - final actualKey = fixture.counterMetric.getCompositeKey(); - - expect(joinedTags.contains(','), true); - expect(joinedTags.contains('\\,'), false); - expect(actualKey.contains('\\,'), true); - for (var tag in expectedTags) { - expect(actualKey.contains(tag), true); - } - }); - - test('CounterMetric', () async { - final expectedKey = - 'c_key metric!_hour_tag1=tag\\, value 1,key 2=&@"13/-d_s'; - final actualKey = fixture.counterMetric.getCompositeKey(); - expect(actualKey, expectedKey); - }); - - test('GaugeMetric', () async { - final expectedKey = - 'g_key metric!_hour_tag1=tag\\, value 1,key 2=&@"13/-d_s'; - final actualKey = fixture.gaugeMetric.getCompositeKey(); - expect(actualKey, expectedKey); - }); - - test('DistributionMetric', () async { - final expectedKey = - 'd_key metric!_hour_tag1=tag\\, value 1,key 2=&@"13/-d_s'; - final actualKey = fixture.distributionMetric.getCompositeKey(); - expect(actualKey, expectedKey); - }); - - test('SetMetric', () async { - final expectedKey = - 's_key metric!_hour_tag1=tag\\, value 1,key 2=&@"13/-d_s'; - final actualKey = fixture.setMetric.getCompositeKey(); - expect(actualKey, expectedKey); - }); - }); - - group('getSpanAggregationKey', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('CounterMetric', () async { - final expectedKey = 'c:key metric!@hour'; - final actualKey = fixture.counterMetric.getSpanAggregationKey(); - expect(actualKey, expectedKey); - }); - - test('GaugeMetric', () async { - final expectedKey = 'g:key metric!@hour'; - final actualKey = fixture.gaugeMetric.getSpanAggregationKey(); - expect(actualKey, expectedKey); - }); - - test('DistributionMetric', () async { - final expectedKey = 'd:key metric!@hour'; - final actualKey = fixture.distributionMetric.getSpanAggregationKey(); - expect(actualKey, expectedKey); - }); - - test('SetMetric', () async { - final expectedKey = 's:key metric!@hour'; - final actualKey = fixture.setMetric.getSpanAggregationKey(); - expect(actualKey, expectedKey); - }); - }); - - group('getWeight', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('counter always returns 1', () async { - final CounterMetric metric = fixture.counterMetric; - expect(metric.getWeight(), 1); - metric.add(5); - metric.add(2); - expect(metric.getWeight(), 1); - }); - - test('gauge always returns 5', () async { - final GaugeMetric metric = fixture.gaugeMetric; - expect(metric.getWeight(), 5); - metric.add(5); - metric.add(2); - expect(metric.getWeight(), 5); - }); - - test('distribution returns number of values', () async { - final DistributionMetric metric = fixture.distributionMetric; - expect(metric.getWeight(), 1); - metric.add(5); - // Repeated values are counted - metric.add(5); - expect(metric.getWeight(), 3); - }); - - test('set returns number of unique values', () async { - final SetMetric metric = fixture.setMetric; - expect(metric.getWeight(), 1); - metric.add(5); - // Repeated values are not counted - metric.add(5); - expect(metric.getWeight(), 2); - }); - }); - - group('add', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('counter increments', () async { - final CounterMetric metric = fixture.counterMetric; - expect(metric.value, 2.1); - metric.add(5); - metric.add(2); - expect(metric.value, 9.1); - }); - - test('gauge stores min, max, last, sum and count', () async { - final GaugeMetric metric = fixture.gaugeMetric; - expect(metric.minimum, 2.1); - expect(metric.maximum, 2.1); - expect(metric.last, 2.1); - expect(metric.sum, 2.1); - expect(metric.count, 1); - metric.add(1.4); - metric.add(5.4); - expect(metric.minimum, 1.4); - expect(metric.maximum, 5.4); - expect(metric.last, 5.4); - expect(metric.sum, 8.9); - expect(metric.count, 3); - }); - - test('distribution stores all values', () async { - final DistributionMetric metric = fixture.distributionMetric; - metric.add(2); - metric.add(4); - metric.add(4); - expect(metric.values, [2.1, 2, 4, 4]); - }); - - test('set stores unique int values', () async { - final SetMetric metric = fixture.setMetric; - metric.add(5); - // Repeated values are not counted - metric.add(5); - expect(metric.values, {2, 5}); - }); - }); -} - -class Fixture { - // We use a fractional number because on some platforms converting '2' to - // string return '2', while others '2.0', and we'd have issues testing. - final CounterMetric counterMetric = Metric.fromType( - type: MetricType.counter, - value: 2.1, - key: 'key metric!', - unit: DurationSentryMeasurementUnit.hour, - tags: {'tag1': 'tag, value 1', 'key 2': '&@"13/-d_s'}, - ) as CounterMetric; - - final GaugeMetric gaugeMetric = Metric.fromType( - type: MetricType.gauge, - value: 2.1, - key: 'key metric!', - unit: DurationSentryMeasurementUnit.hour, - tags: {'tag1': 'tag, value 1', 'key 2': '&@"13/-d_s'}, - ) as GaugeMetric; - - final DistributionMetric distributionMetric = Metric.fromType( - type: MetricType.distribution, - value: 2.1, - key: 'key metric!', - unit: DurationSentryMeasurementUnit.hour, - tags: {'tag1': 'tag, value 1', 'key 2': '&@"13/-d_s'}, - ) as DistributionMetric; - - final SetMetric setMetric = Metric.fromType( - type: MetricType.set, - value: 2.1, - key: 'key metric!', - unit: DurationSentryMeasurementUnit.hour, - tags: {'tag1': 'tag, value 1', 'key 2': '&@"13/-d_s'}, - ) as SetMetric; -} diff --git a/dart/test/metrics/metrics_aggregator_test.dart b/dart/test/metrics/metrics_aggregator_test.dart deleted file mode 100644 index 19c6a9f18..000000000 --- a/dart/test/metrics/metrics_aggregator_test.dart +++ /dev/null @@ -1,494 +0,0 @@ -import 'package:sentry/sentry.dart'; -import 'package:sentry/src/metrics/metric.dart'; -import 'package:sentry/src/metrics/metrics_aggregator.dart'; -import 'package:test/test.dart'; - -import '../mocks/mock_hub.dart'; -import '../test_utils.dart'; - -void main() { - group('emit', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('counter', () async { - final MetricsAggregator sut = fixture.getSut(); - final String key = 'metric key'; - final double value = 5; - final SentryMeasurementUnit unit = DurationSentryMeasurementUnit.minute; - final Map tags = {'tag1': 'val1', 'tag2': 'val2'}; - sut.emit(MetricType.counter, key, value, unit, tags); - - final metricsCaptured = sut.buckets.values.first.values; - expect(metricsCaptured.length, 1); - expect(metricsCaptured.first, isA()); - expect(metricsCaptured.first.type, MetricType.counter); - expect(metricsCaptured.first.key, key); - expect((metricsCaptured.first as CounterMetric).value, value); - expect(metricsCaptured.first.unit, unit); - expect(metricsCaptured.first.tags, tags); - }); - - test('gauge', () async { - final MetricsAggregator sut = fixture.getSut(); - sut.testEmit(type: MetricType.gauge); - - final metricsCaptured = sut.buckets.values.first.values; - expect(metricsCaptured.first, isA()); - expect(metricsCaptured.first.type, MetricType.gauge); - }); - - test('distribution', () async { - final MetricsAggregator sut = fixture.getSut(); - sut.testEmit(type: MetricType.distribution); - - final metricsCaptured = sut.buckets.values.first.values; - expect(metricsCaptured.first, isA()); - expect(metricsCaptured.first.type, MetricType.distribution); - }); - - test('set', () async { - final MetricsAggregator sut = fixture.getSut(); - sut.testEmit(type: MetricType.set); - - final metricsCaptured = sut.buckets.values.first.values; - expect(metricsCaptured.first, isA()); - expect(metricsCaptured.first.type, MetricType.set); - }); - }); - - group('span local aggregation', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('emit calls add', () async { - final MetricsAggregator sut = fixture.getSut(hub: fixture.hub); - final t = fixture.hub.startTransaction('test', 'op', bindToScope: true); - - var spanSummary = t.localMetricsAggregator?.getSummaries().values; - expect(spanSummary, isEmpty); - - sut.testEmit(); - - spanSummary = t.localMetricsAggregator?.getSummaries().values; - expect(spanSummary, isNotEmpty); - }); - - test('emit counter', () async { - final MetricsAggregator sut = fixture.getSut(hub: fixture.hub); - final t = fixture.hub.startTransaction('test', 'op', bindToScope: true); - - sut.testEmit(type: MetricType.counter, value: 1); - sut.testEmit(type: MetricType.counter, value: 4); - - final spanSummary = t.localMetricsAggregator?.getSummaries().values.first; - expect(spanSummary!.length, 1); - expect(spanSummary.first.sum, 5); - expect(spanSummary.first.min, 1); - expect(spanSummary.first.max, 4); - expect(spanSummary.first.count, 2); - expect(spanSummary.first.tags, mockTags); - }); - - test('emit distribution', () async { - final MetricsAggregator sut = fixture.getSut(hub: fixture.hub); - final t = fixture.hub.startTransaction('test', 'op', bindToScope: true); - - sut.testEmit(type: MetricType.distribution, value: 1); - sut.testEmit(type: MetricType.distribution, value: 4); - - final spanSummary = t.localMetricsAggregator?.getSummaries().values.first; - expect(spanSummary!.length, 1); - expect(spanSummary.first.sum, 5); - expect(spanSummary.first.min, 1); - expect(spanSummary.first.max, 4); - expect(spanSummary.first.count, 2); - expect(spanSummary.first.tags, mockTags); - }); - - test('emit gauge', () async { - final MetricsAggregator sut = fixture.getSut(hub: fixture.hub); - final t = fixture.hub.startTransaction('test', 'op', bindToScope: true); - - sut.testEmit(type: MetricType.gauge, value: 1); - sut.testEmit(type: MetricType.gauge, value: 4); - - final spanSummary = t.localMetricsAggregator?.getSummaries().values.first; - expect(spanSummary!.length, 1); - expect(spanSummary.first.sum, 5); - expect(spanSummary.first.min, 1); - expect(spanSummary.first.max, 4); - expect(spanSummary.first.count, 2); - expect(spanSummary.first.tags, mockTags); - }); - - test('emit set', () async { - final MetricsAggregator sut = fixture.getSut(hub: fixture.hub); - final t = fixture.hub.startTransaction('test', 'op', bindToScope: true); - - sut.testEmit(type: MetricType.set, value: 1); - sut.testEmit(type: MetricType.set, value: 4); - - final spanSummary = t.localMetricsAggregator?.getSummaries().values.first; - expect(spanSummary!.length, 1); - expect(spanSummary.first.sum, 2); - expect(spanSummary.first.min, 1); - expect(spanSummary.first.max, 1); - expect(spanSummary.first.count, 2); - expect(spanSummary.first.tags, mockTags); - }); - }); - - group('emit in same time bucket', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('same metric with different keys emit different metrics', () async { - final MetricsAggregator sut = fixture.getSut(); - sut.testEmit(key: mockKey); - sut.testEmit(key: mockKey2); - - final timeBuckets = sut.buckets; - final bucket = timeBuckets.values.first; - - expect(bucket.length, 2); - expect(bucket.values.firstWhere((e) => e.key == mockKey), isNotNull); - expect(bucket.values.firstWhere((e) => e.key == mockKey2), isNotNull); - }); - - test('same metric with different units emit different metrics', () async { - final MetricsAggregator sut = fixture.getSut(); - sut.testEmit(unit: mockUnit); - sut.testEmit(unit: mockUnit2); - - final timeBuckets = sut.buckets; - final bucket = timeBuckets.values.first; - - expect(bucket.length, 2); - expect(bucket.values.firstWhere((e) => e.unit == mockUnit), isNotNull); - expect(bucket.values.firstWhere((e) => e.unit == mockUnit2), isNotNull); - }); - - test('same metric with different tags emit different metrics', () async { - final MetricsAggregator sut = fixture.getSut(); - sut.testEmit(tags: mockTags); - sut.testEmit(tags: mockTags2); - - final timeBuckets = sut.buckets; - final bucket = timeBuckets.values.first; - - expect(bucket.length, 2); - expect(bucket.values.firstWhere((e) => e.tags == mockTags), isNotNull); - expect(bucket.values.firstWhere((e) => e.tags == mockTags2), isNotNull); - }); - - test('increment same metric emit only one counter', () async { - final MetricsAggregator sut = fixture.getSut(); - sut.testEmit(type: MetricType.counter, value: 1); - sut.testEmit(type: MetricType.counter, value: 2); - - final timeBuckets = sut.buckets; - final bucket = timeBuckets.values.first; - - expect(bucket.length, 1); - expect((bucket.values.first as CounterMetric).value, 3); - }); - }); - - group('time buckets', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('same metric in < 10 seconds interval emit only one metric', () async { - final MetricsAggregator sut = fixture.getSut(); - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(0); - sut.testEmit(); - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(9999); - sut.testEmit(); - - final timeBuckets = sut.buckets; - expect(timeBuckets.length, 1); - }); - - test('same metric in >= 10 seconds interval emit two metrics', () async { - final MetricsAggregator sut = fixture.getSut(); - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(0); - sut.testEmit(); - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(10000); - sut.testEmit(); - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(20000); - sut.testEmit(); - - final timeBuckets = sut.buckets; - expect(timeBuckets.length, 3); - }); - }); - - group('flush metrics', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('emitting a metric schedules flushing', () async { - final MetricsAggregator sut = fixture.getSut(); - - expect(sut.flushCompleter, isNull); - sut.testEmit(); - expect(sut.flushCompleter, isNotNull); - }); - - test('flush calls hub captureMetrics', () async { - final MetricsAggregator sut = fixture.getSut(); - - // emit a metric - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(0); - sut.testEmit(); - expect(fixture.mockHub.captureMetricsCalls, isEmpty); - - // mock clock to allow metric time aggregation - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(10000); - // wait for flush - await sut.flushCompleter!.future; - expect(fixture.mockHub.captureMetricsCalls, isNotEmpty); - - Map> capturedMetrics = - fixture.mockHub.captureMetricsCalls.first.metricsBuckets; - Metric capturedMetric = capturedMetrics.values.first.first; - expect(capturedMetric.key, mockKey); - }); - - test('flush don\'t schedules flushing if no other metrics', () async { - final MetricsAggregator sut = fixture.getSut(); - - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(0); - sut.testEmit(); - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(10000); - expect(sut.flushCompleter, isNotNull); - await sut.flushCompleter!.future; - expect(sut.flushCompleter, isNull); - }); - - test('flush schedules flushing if there are other metrics', () async { - final MetricsAggregator sut = fixture.getSut(); - - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(0); - sut.testEmit(); - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(10000); - sut.testEmit(); - expect(sut.flushCompleter, isNotNull); - await sut.flushCompleter!.future; - // we expect the aggregator flushed metrics and schedules flushing again - expect(fixture.mockHub.captureMetricsCalls, isNotEmpty); - expect(sut.flushCompleter, isNotNull); - }); - - test('flush schedules flushing if no metric was captured', () async { - final MetricsAggregator sut = - fixture.getSut(flushInterval: Duration(milliseconds: 100)); - - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(10000); - sut.testEmit(); - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(10050); - - expect(sut.flushCompleter, isNotNull); - await sut.flushCompleter!.future; - // we expect the aggregator didn't flush anything and schedules flushing - expect(fixture.mockHub.captureMetricsCalls, isEmpty); - expect(sut.flushCompleter, isNotNull); - }); - - test('flush ignores last 10 seconds', () async { - final MetricsAggregator sut = fixture.getSut(); - - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(10000); - sut.testEmit(); - - // The 10 second bucket is not finished, so it shouldn't capture anything - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(19999); - await sut.flushCompleter!.future; - expect(fixture.mockHub.captureMetricsCalls, isEmpty); - - // The 10 second bucket finished, so it should capture metrics - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(20000); - await sut.flushCompleter!.future; - expect(fixture.mockHub.captureMetricsCalls, isNotEmpty); - }); - - test('flush ignores last flushShiftMs milliseconds', () async { - final MetricsAggregator sut = fixture.getSut(flushShiftMs: 4000); - - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(10000); - sut.testEmit(); - - // The 10 second bucket is not finished, so it shouldn't capture anything - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(19999); - await sut.flushCompleter!.future; - expect(fixture.mockHub.captureMetricsCalls, isEmpty); - - // The 10 second bucket finished, but flushShiftMs didn't pass, so it shouldn't capture anything - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(23999); - await sut.flushCompleter!.future; - expect(fixture.mockHub.captureMetricsCalls, isEmpty); - - // The 10 second bucket finished and flushShiftMs passed, so it should capture metrics - fixture.options.clock = () => DateTime.fromMillisecondsSinceEpoch(24000); - await sut.flushCompleter!.future; - expect(fixture.mockHub.captureMetricsCalls, isNotEmpty); - }); - - test('close flushes everything', () async { - final MetricsAggregator sut = fixture.getSut(); - sut.testEmit(); - sut.testEmit(type: MetricType.gauge); - // We have some metrics, but we don't flush them, yet - await sut.flushCompleter!.future; - expect(fixture.mockHub.captureMetricsCalls, isEmpty); - - // Closing the aggregator. Flush everything - sut.close(); - expect(fixture.mockHub.captureMetricsCalls, isNotEmpty); - expect(sut.buckets, isEmpty); - }); - }); - - group('beforeMetric', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('emits if not set', () async { - final MetricsAggregator sut = fixture.getSut(maxWeight: 4); - sut.testEmit(key: 'key1'); - final metricsCaptured = sut.buckets.values.first.values; - expect(metricsCaptured.length, 1); - expect(metricsCaptured.first.key, 'key1'); - }); - - test('drops if it return false', () async { - final MetricsAggregator sut = fixture.getSut(maxWeight: 4); - fixture.options.beforeMetricCallback = (key, {tags}) => key != 'key2'; - sut.testEmit(key: 'key1'); - sut.testEmit(key: 'key2'); - final metricsCaptured = sut.buckets.values.first.values; - expect(metricsCaptured.length, 1); - expect(metricsCaptured.first.key, 'key1'); - }); - - test('emits if it return true', () async { - final MetricsAggregator sut = fixture.getSut(maxWeight: 4); - fixture.options.beforeMetricCallback = (key, {tags}) => true; - sut.testEmit(key: 'key1'); - sut.testEmit(key: 'key2'); - final metricsCaptured = sut.buckets.values.first.values; - expect(metricsCaptured.length, 2); - expect(metricsCaptured.first.key, 'key1'); - expect(metricsCaptured.last.key, 'key2'); - }); - - test('emits if it throws', () async { - fixture.options.automatedTestMode = false; - final MetricsAggregator sut = fixture.getSut(maxWeight: 4); - fixture.options.beforeMetricCallback = (key, {tags}) => throw Exception(); - sut.testEmit(key: 'key1'); - sut.testEmit(key: 'key2'); - final metricsCaptured = sut.buckets.values.first.values; - expect(metricsCaptured.length, 2); - expect(metricsCaptured.first.key, 'key1'); - expect(metricsCaptured.last.key, 'key2'); - }); - }); - - group('overweight', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('flush if exceeds maxWeight', () async { - final MetricsAggregator sut = fixture.getSut(maxWeight: 4); - sut.testEmit(type: MetricType.counter, key: 'key1'); - sut.testEmit(type: MetricType.counter, key: 'key2'); - sut.testEmit(type: MetricType.counter, key: 'key3'); - await sut.flushCompleter!.future; - expect(fixture.mockHub.captureMetricsCalls, isEmpty); - // After the 4th metric is emitted, the aggregator flushes immediately - sut.testEmit(type: MetricType.counter, key: 'key4'); - expect(fixture.mockHub.captureMetricsCalls, isNotEmpty); - }); - - test('does not flush if not exceeds maxWeight', () async { - final MetricsAggregator sut = fixture.getSut(maxWeight: 2); - // We are emitting the same metric, so no weight is added - sut.testEmit(type: MetricType.counter); - sut.testEmit(type: MetricType.counter); - sut.testEmit(type: MetricType.counter); - sut.testEmit(type: MetricType.counter); - await sut.flushCompleter!.future; - expect(fixture.mockHub.captureMetricsCalls, isEmpty); - }); - }); -} - -const String mockKey = 'metric key'; -const String mockKey2 = 'metric key 2'; -const double mockValue = 5; -const SentryMeasurementUnit mockUnit = DurationSentryMeasurementUnit.minute; -const SentryMeasurementUnit mockUnit2 = DurationSentryMeasurementUnit.second; -const Map mockTags = {'tag1': 'val1', 'tag2': 'val2'}; -const Map mockTags2 = {'tag1': 'val1'}; -final DateTime mockTimestamp = DateTime.fromMillisecondsSinceEpoch(1); - -class Fixture { - final options = defaultTestOptions(); - final mockHub = MockHub(); - late final hub = Hub(options); - - Fixture() { - options.tracesSampleRate = 1; - options.enableMetrics = true; - options.enableSpanLocalMetricAggregation = true; - } - - MetricsAggregator getSut({ - Hub? hub, - Duration flushInterval = const Duration(milliseconds: 1), - int flushShiftMs = 0, - int maxWeight = 100000, - }) { - return MetricsAggregator( - hub: hub ?? mockHub, - options: options, - flushInterval: flushInterval, - flushShiftMs: flushShiftMs, - maxWeight: maxWeight); - } -} - -extension _MetricsAggregatorUtils on MetricsAggregator { - testEmit({ - MetricType type = MetricType.counter, - String key = mockKey, - double value = mockValue, - SentryMeasurementUnit unit = mockUnit, - Map tags = mockTags, - }) { - emit(type, key, value, unit, tags); - } -} diff --git a/dart/test/metrics/metrics_api_test.dart b/dart/test/metrics/metrics_api_test.dart deleted file mode 100644 index 8e18d2199..000000000 --- a/dart/test/metrics/metrics_api_test.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'dart:async'; - -import 'package:sentry/sentry.dart'; -import 'package:sentry/src/metrics/metric.dart'; -import 'package:sentry/src/metrics/metrics_api.dart'; -import 'package:sentry/src/sentry_tracer.dart'; -import 'package:test/test.dart'; - -import '../mocks/mock_hub.dart'; -import '../test_utils.dart'; - -void main() { - group('api', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('counter', () async { - MetricsApi api = fixture.getSut(); - api.increment('key'); - api.increment('key', value: 2.4); - - Iterable sentMetrics = - fixture.mockHub.metricsAggregator!.buckets.values.first.values; - expect(sentMetrics.first.type, MetricType.counter); - expect((sentMetrics.first as CounterMetric).value, 3.4); - }); - - test('gauge', () async { - MetricsApi api = fixture.getSut(); - api.gauge('key', value: 1.5); - api.gauge('key', value: 2.4); - - Iterable sentMetrics = - fixture.mockHub.metricsAggregator!.buckets.values.first.values; - expect(sentMetrics.first.type, MetricType.gauge); - expect((sentMetrics.first as GaugeMetric).minimum, 1.5); - expect((sentMetrics.first as GaugeMetric).maximum, 2.4); - expect((sentMetrics.first as GaugeMetric).last, 2.4); - expect((sentMetrics.first as GaugeMetric).sum, 3.9); - expect((sentMetrics.first as GaugeMetric).count, 2); - }); - - test('distribution', () async { - MetricsApi api = fixture.getSut(); - api.distribution('key', value: 1.5); - api.distribution('key', value: 2.4); - - Iterable sentMetrics = - fixture.mockHub.metricsAggregator!.buckets.values.first.values; - expect(sentMetrics.first.type, MetricType.distribution); - expect((sentMetrics.first as DistributionMetric).values, [1.5, 2.4]); - }); - - test('set', () async { - MetricsApi api = fixture.getSut(); - api.set('key', value: 1); - api.set('key', value: 2); - // This is ignored as it's a repeated value - api.set('key', value: 2); - // This adds both an int and a crc32 of the string to the metric - api.set('key', value: 4, stringValue: 'value'); - // No value provided. This does nothing - api.set('key'); - // Empty String provided. This does nothing - api.set('key', stringValue: ''); - - Iterable sentMetrics = - fixture.mockHub.metricsAggregator!.buckets.values.first.values; - expect(sentMetrics.first.type, MetricType.set); - expect((sentMetrics.first as SetMetric).values, {1, 2, 4, 494360628}); - }); - - test('timing emits distribution', () async { - final delay = Duration(milliseconds: 100); - final completer = Completer(); - MetricsApi api = fixture.getSut(); - - // The timing API tries to start a child span - expect(fixture.mockHub.getSpanCalls, 0); - api.timing('key', - function: () => Future.delayed(delay, () => completer.complete())); - expect(fixture.mockHub.getSpanCalls, 1); - - await completer.future; - Iterable sentMetrics = - fixture.mockHub.metricsAggregator!.buckets.values.first.values; - - // The timing API emits a distribution metric - expect(sentMetrics.first.type, MetricType.distribution); - // The default unit is second - expect(sentMetrics.first.unit, DurationSentryMeasurementUnit.second); - // It awaits for the function completion, which means 100 milliseconds in - // this case. Since the unit is second, its value (duration) is >= 0.1 - expect( - (sentMetrics.first as DistributionMetric).values.first >= 0.1, true); - }); - - test('timing starts a span', () async { - final delay = Duration(milliseconds: 100); - final completer = Completer(); - fixture._options.tracesSampleRate = 1; - fixture._options.enableMetrics = true; - MetricsApi api = fixture.getSut(hub: fixture.hub); - - // Start a transaction so that timing api can start a child span - final transaction = fixture.hub.startTransaction( - 'name', - 'operation', - bindToScope: true, - ) as SentryTracer; - expect(transaction.children, isEmpty); - - // Timing starts a span - api.timing('my key', - unit: DurationSentryMeasurementUnit.milliSecond, - function: () => Future.delayed(delay, () => completer.complete())); - final span = transaction.children.first; - expect(span.finished, false); - expect(span.context.operation, 'metric.timing'); - expect(span.context.description, 'my key'); - - // Timing finishes the span when the function is finished, which takes 100 milliseconds - await completer.future; - expect(span.finished, true); - final spanDuration = span.endTimestamp!.difference(span.startTimestamp); - expect(spanDuration.inMilliseconds >= 100, true); - await Future.delayed(Duration()); - - Iterable sentMetrics = - fixture.hub.metricsAggregator!.buckets.values.first.values; - - // The emitted metric value should be aggregated in the span - expect(span.localMetricsAggregator?.getSummaries(), isNotEmpty); - // The emitted metric value should match the span duration - expect(sentMetrics.first.unit, DurationSentryMeasurementUnit.milliSecond); - // Duration.inMilliseconds returns an int, so we have to assert it - expect((sentMetrics.first as DistributionMetric).values.first.toInt(), - spanDuration.inMilliseconds); - }); - }); -} - -class Fixture { - final _options = defaultTestOptions(); - final mockHub = MockHub(); - late final hub = Hub(_options); - - MetricsApi getSut({Hub? hub}) => MetricsApi(hub: hub ?? mockHub); -} diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 201062ed5..69b89eae3 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -131,10 +131,6 @@ void main() { expect(Sentry.getSpan(), isNull); }); - - test('should provide metrics API', () async { - expect(Sentry.metrics(), Sentry.currentHub.metricsApi); - }); }); group('Sentry is enabled or disabled', () { diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index ec25b32f8..40afa2f4d 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -145,13 +145,21 @@ Future testCaptureException( // the localhost port can change final absPathUri = Uri.parse(topFrame['abs_path'] as String); expect(absPathUri.host, 'localhost'); - expect(absPathUri.path, '/sentry_browser_test.dart.browser_test.dart.js'); + expect( + absPathUri.path, + anyOf([ + '/sentry_browser_test.dart.browser_test.dart.js', + '/sentry_browser_test.dart.browser_test.dart.wasm' + ])); expect( - topFrame['filename'], - 'sentry_browser_test.dart.browser_test.dart.js', - ); - expect(topFrame['function'], 'Object.wrapException'); + topFrame['filename'], + anyOf([ + 'sentry_browser_test.dart.browser_test.dart.js', + 'sentry_browser_test.dart.browser_test.dart.wasm' + ])); + expect(topFrame['function'], + anyOf(['Object.wrapException', 'testCaptureException'])); expect(data['event_id'], sentryId.toString()); expect(data['timestamp'], '2017-01-02T00:00:00.000Z'); diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index dd870eb58..8de265083 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -539,17 +539,23 @@ class MainScaffold extends StatelessWidget { Sentry.startTransaction( 'testMetrics', 'span summary example', bindToScope: true); + // ignore: deprecated_member_use Sentry.metrics().increment('increment key', unit: DurationSentryMeasurementUnit.day); + // ignore: deprecated_member_use Sentry.metrics().distribution('distribution key', value: Random().nextDouble() * 10); + // ignore: deprecated_member_use Sentry.metrics().set('set int key', value: Random().nextInt(100), tags: {'myTag': 'myValue', 'myTag2': 'myValue2'}); + // ignore: deprecated_member_use Sentry.metrics().set('set string key', stringValue: 'Random n ${Random().nextInt(100)}'); + // ignore: deprecated_member_use Sentry.metrics() .gauge('gauge key', value: Random().nextDouble() * 10); + // ignore: deprecated_member_use Sentry.metrics().timing( 'timing key', function: () async => await Future.delayed( diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 0554c0ad2..1444a7e92 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -159,8 +159,10 @@ class SentryNavigatorObserver extends RouteObserver> { // Clearing the display tracker here is safe since didPush happens before the Widget is built _timeToDisplayTracker?.clear(); - _finishTimeToDisplayTracking(); - _startTimeToDisplayTracking(route); + + DateTime timestamp = _hub.options.clock(); + _finishTimeToDisplayTracking(endTimestamp: timestamp); + _startTimeToDisplayTracking(route, timestamp); } @override @@ -200,7 +202,8 @@ class SentryNavigatorObserver extends RouteObserver> { to: previousRoute?.settings, ); - _finishTimeToDisplayTracking(clearAfter: true); + final timestamp = _hub.options.clock(); + _finishTimeToDisplayTracking(endTimestamp: timestamp, clearAfter: true); } void _addBreadcrumb({ @@ -295,7 +298,8 @@ class SentryNavigatorObserver extends RouteObserver> { await _native?.beginNativeFrames(); } - Future _finishTimeToDisplayTracking({bool clearAfter = false}) async { + Future _finishTimeToDisplayTracking( + {required DateTime endTimestamp, bool clearAfter = false}) async { final transaction = _transaction; _transaction = null; try { @@ -317,7 +321,10 @@ class SentryNavigatorObserver extends RouteObserver> { final isTTFDSpan = child.context.operation == SentrySpanOperations.uiTimeToFullDisplay; if (!child.finished && (isTTIDSpan || isTTFDSpan)) { - await child.finish(status: SpanStatus.deadlineExceeded()); + await child.finish( + endTimestamp: endTimestamp, + status: SpanStatus.deadlineExceeded(), + ); } } } catch (exception, stacktrace) { @@ -331,14 +338,15 @@ class SentryNavigatorObserver extends RouteObserver> { rethrow; } } finally { - await transaction?.finish(); + await transaction?.finish(endTimestamp: endTimestamp); if (clearAfter) { _clear(); } } } - Future _startTimeToDisplayTracking(Route? route) async { + Future _startTimeToDisplayTracking( + Route? route, DateTime startTimestamp) async { try { final routeName = _getRouteName(route) ?? _currentRouteName; if (!_enableAutoTransactions || routeName == null) { @@ -346,8 +354,6 @@ class SentryNavigatorObserver extends RouteObserver> { } bool isAppStart = routeName == '/'; - DateTime startTimestamp = _hub.options.clock(); - await _startTransaction(route, startTimestamp); final transaction = _transaction; diff --git a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart index 051d0602b..7895ec4fa 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -29,6 +29,8 @@ class TimeToInitialDisplayTracker { bool _isManual = false; Completer? _trackingCompleter; DateTime? _endTimestamp; + DateTime? _completeTrackingTimeStamp; + final Duration _determineEndtimeTimeout = Duration(seconds: 5); /// This endTimestamp is needed in the [TimeToFullDisplayTracker] class @@ -87,6 +89,13 @@ class TimeToInitialDisplayTracker { // If we already know it's manual we can return the future immediately if (_isManual) { + final completeTrackingTimeStamp = _completeTrackingTimeStamp; + if (completeTrackingTimeStamp != null) { + // If complete was called before we could call start, complete it here. + _endTimestamp = completeTrackingTimeStamp; + _trackingCompleter?.complete(completeTrackingTimeStamp); + _completeTrackingTimeStamp = null; + } return future; } @@ -106,10 +115,13 @@ class TimeToInitialDisplayTracker { } void completeTracking() { + final timestamp = DateTime.now(); + if (_trackingCompleter != null && !_trackingCompleter!.isCompleted) { - final endTimestamp = DateTime.now(); - _endTimestamp = endTimestamp; - _trackingCompleter?.complete(endTimestamp); + _endTimestamp = timestamp; + _trackingCompleter?.complete(timestamp); + } else { + _completeTrackingTimeStamp = timestamp; } } diff --git a/flutter/test/navigation/time_to_initial_display_tracker_test.dart b/flutter/test/navigation/time_to_initial_display_tracker_test.dart index 526343a8b..b73e9d4d5 100644 --- a/flutter/test/navigation/time_to_initial_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_initial_display_tracker_test.dart @@ -84,6 +84,37 @@ void main() { .difference(ttidSpan.startTimestamp) .inMilliseconds); }); + + test('starting after completing still finished correctly', () async { + await Future.delayed(fixture.finishFrameDuration, () { + sut.markAsManual(); + sut.completeTracking(); + }); + + final transaction = fixture.getTransaction() as SentryTracer; + await sut.trackRegularRoute(transaction, fixture.startTimestamp); + + final children = transaction.children; + expect(children, hasLength(1)); + + final ttidSpan = children.first; + expect(ttidSpan.context.operation, + SentrySpanOperations.uiTimeToInitialDisplay); + expect(ttidSpan.finished, isTrue); + expect(ttidSpan.context.description, 'Regular route initial display'); + expect(ttidSpan.origin, SentryTraceOrigins.manualUiTimeToDisplay); + final ttidMeasurement = + transaction.measurements['time_to_initial_display']; + expect(ttidMeasurement, isNotNull); + expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); + expect(ttidMeasurement?.value, + greaterThanOrEqualTo(fixture.finishFrameDuration.inMilliseconds)); + expect( + ttidMeasurement?.value, + ttidSpan.endTimestamp! + .difference(ttidSpan.startTimestamp) + .inMilliseconds); + }); }); group('determineEndtime', () { diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index 6d487a3b4..b71abd847 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -317,7 +317,8 @@ void main() { expect(scope.span, null); }); - verify(span.finish()).called(2); + verify(span.finish(endTimestamp: captureAnyNamed('endTimestamp'))) + .called(2); }); test('didPop finishes transaction', () async { @@ -344,7 +345,8 @@ void main() { expect(scope.span, null); }); - verify(span.finish()).called(1); + verify(span.finish(endTimestamp: captureAnyNamed('endTimestamp'))) + .called(1); }); test('multiple didPop only finish transaction once', () async { @@ -373,7 +375,8 @@ void main() { expect(scope.span, null); }); - verify(span.finish()).called(1); + verify(span.finish(endTimestamp: captureAnyNamed('endTimestamp'))) + .called(1); }); test( @@ -413,9 +416,13 @@ void main() { await Future.delayed(const Duration(milliseconds: 100)); - verify(mockChildA.finish(status: SpanStatus.deadlineExceeded())) + verify(mockChildA.finish( + endTimestamp: captureAnyNamed('endTimestamp'), + status: SpanStatus.deadlineExceeded())) .called(1); - verify(mockChildB.finish(status: SpanStatus.deadlineExceeded())) + verify(mockChildB.finish( + endTimestamp: captureAnyNamed('endTimestamp'), + status: SpanStatus.deadlineExceeded())) .called(1); }); @@ -456,9 +463,13 @@ void main() { await Future.delayed(const Duration(milliseconds: 100)); - verify(mockChildA.finish(status: SpanStatus.deadlineExceeded())) + verify(mockChildA.finish( + endTimestamp: captureAnyNamed('endTimestamp'), + status: SpanStatus.deadlineExceeded())) .called(1); - verify(mockChildB.finish(status: SpanStatus.deadlineExceeded())) + verify(mockChildB.finish( + endTimestamp: captureAnyNamed('endTimestamp'), + status: SpanStatus.deadlineExceeded())) .called(1); });