From 1a4b78b03b537e646ded1b661ebf014da075fc93 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 17:43:43 +0100 Subject: [PATCH 01/79] Add TelemetryProcessor for span and log buffering Co-Authored-By: Claude Sonnet 4.5 --- packages/dart/lib/sentry.dart | 1 + packages/dart/lib/src/sentry_client.dart | 28 +- packages/dart/lib/src/sentry_options.dart | 20 +- .../lib/src/telemetry/processing/buffer.dart | 13 + .../telemetry/processing/buffer_config.dart | 15 + .../processing/in_memory_buffer.dart | 180 ++++++ .../src/telemetry/processing/processor.dart | 100 ++++ .../processing/processor_integration.dart | 61 +++ .../test/mocks/mock_telemetry_buffer.dart | 23 + .../test/mocks/mock_telemetry_processor.dart | 25 + .../test/sentry_client_lifecycle_test.dart | 13 +- .../sentry_client_sdk_lifecycle_test.dart | 13 +- packages/dart/test/sentry_client_test.dart | 171 +++--- .../telemetry/processing/buffer_test.dart | 310 +++++++++++ .../processor_integration_test.dart | 161 ++++++ .../telemetry/processing/processor_test.dart | 173 ++++++ .../dart/test/telemetry/span/span_test.dart | 512 ++++++++++++++++++ .../lib/src/widgets_binding_observer.dart | 2 +- packages/flutter/test/mocks.dart | 24 + .../test/widgets_binding_observer_test.dart | 23 +- 20 files changed, 1757 insertions(+), 111 deletions(-) create mode 100644 packages/dart/lib/src/telemetry/processing/buffer.dart create mode 100644 packages/dart/lib/src/telemetry/processing/buffer_config.dart create mode 100644 packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart create mode 100644 packages/dart/lib/src/telemetry/processing/processor.dart create mode 100644 packages/dart/lib/src/telemetry/processing/processor_integration.dart create mode 100644 packages/dart/test/mocks/mock_telemetry_buffer.dart create mode 100644 packages/dart/test/mocks/mock_telemetry_processor.dart create mode 100644 packages/dart/test/telemetry/processing/buffer_test.dart create mode 100644 packages/dart/test/telemetry/processing/processor_integration_test.dart create mode 100644 packages/dart/test/telemetry/processing/processor_test.dart create mode 100644 packages/dart/test/telemetry/span/span_test.dart diff --git a/packages/dart/lib/sentry.dart b/packages/dart/lib/sentry.dart index 8d01a3181c..ccfd5b5e8f 100644 --- a/packages/dart/lib/sentry.dart +++ b/packages/dart/lib/sentry.dart @@ -41,6 +41,7 @@ export 'src/sdk_lifecycle_hooks.dart'; export 'src/sentry_envelope.dart'; export 'src/sentry_envelope_item.dart'; export 'src/sentry_options.dart'; +export 'src/telemetry/sentry_trace_lifecycle.dart'; // ignore: invalid_export_of_internal_element export 'src/sentry_trace_origins.dart'; export 'src/span_data_convention.dart'; diff --git a/packages/dart/lib/src/sentry_client.dart b/packages/dart/lib/src/sentry_client.dart index b53eb998ac..17b97fc0e1 100644 --- a/packages/dart/lib/src/sentry_client.dart +++ b/packages/dart/lib/src/sentry_client.dart @@ -18,6 +18,7 @@ import 'sentry_exception_factory.dart'; import 'sentry_options.dart'; import 'sentry_stack_trace_factory.dart'; import 'sentry_trace_context_header.dart'; +import 'telemetry/span/sentry_span_v2.dart'; import 'transport/client_report_transport.dart'; import 'transport/data_category.dart'; import 'transport/http_transport.dart'; @@ -28,7 +29,6 @@ import 'type_check_hint.dart'; import 'utils/isolate_utils.dart'; import 'utils/regex_utils.dart'; import 'utils/stacktrace_utils.dart'; -import 'sentry_log_batcher.dart'; import 'version.dart'; /// Default value for [SentryUser.ipAddress]. It gets set when an event does not have @@ -75,9 +75,6 @@ class SentryClient { if (enableFlutterSpotlight) { options.transport = SpotlightHttpTransport(options, options.transport); } - if (options.enableLogs) { - options.logBatcher = SentryLogBatcher(options); - } return SentryClient._(options); } @@ -493,6 +490,25 @@ class SentryClient { ); } + void captureSpan( + SentrySpanV2 span, { + Scope? scope, + }) { + switch (span) { + case UnsetSentrySpanV2(): + _options.log( + SentryLevel.warning, + "captureSpan: span is in an invalid state $UnsetSentrySpanV2.", + ); + case NoOpSentrySpanV2(): + return; + case RecordingSentrySpanV2 span: + // TODO(next-pr): add common attributes, merge scope attributes + + _options.telemetryProcessor.addSpan(span); + } + } + @internal FutureOr captureLog( SentryLog log, { @@ -578,7 +594,7 @@ class SentryClient { if (processedLog != null) { await _options.lifecycleRegistry .dispatchCallback(OnBeforeCaptureLog(processedLog)); - _options.logBatcher.addLog(processedLog); + _options.telemetryProcessor.addLog(processedLog); } else { _options.recorder.recordLostEvent( DiscardReason.beforeSend, @@ -588,7 +604,7 @@ class SentryClient { } FutureOr close() { - final flush = _options.logBatcher.flush(); + final flush = _options.telemetryProcessor.flush(); if (flush is Future) { return flush.then((_) => _options.httpClient.close()); } diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 9cb8b881fa..ab188f8e3e 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -12,10 +12,9 @@ import 'noop_client.dart'; import 'platform/platform.dart'; import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.dart'; +import 'telemetry/processing/processor.dart'; import 'transport/noop_transport.dart'; import 'version.dart'; -import 'sentry_log_batcher.dart'; -import 'noop_log_batcher.dart'; import 'dart:developer' as developer; // TODO: shutdownTimeout, flushTimeoutMillis @@ -233,6 +232,21 @@ class SentryOptions { /// sent. Events are picked randomly. Default is null (disabled) double? sampleRate; + /// Chooses between two tracing systems. You can only use one at a time. + /// + /// [SentryTraceLifecycle.streaming] sends each span to Sentry as it finishes. + /// Use [Sentry.startSpan] to create spans. The older transaction APIs + /// ([Sentry.startTransaction], [ISentrySpan.startChild]) will do nothing. + /// + /// [SentryTraceLifecycle.static] collects all spans and sends them together + /// when the transaction ends. Use [Sentry.startTransaction] to create traces. + /// The newer span APIs ([Sentry.startSpan]) will do nothing. + /// + /// Integrations automatically switch to the correct API based on this setting. + /// + /// Defaults to [SentryTraceLifecycle.static]. + SentryTraceLifecycle traceLifecycle = SentryTraceLifecycle.static; + /// The ignoreErrors tells the SDK which errors should be not sent to the sentry server. /// If an null or an empty list is used, the SDK will send all transactions. /// To use regex add the `^` and the `$` to the string. @@ -554,7 +568,7 @@ class SentryOptions { late final SentryLogger logger = SentryLogger(clock); @internal - SentryLogBatcher logBatcher = NoopLogBatcher(); + TelemetryProcessor telemetryProcessor = NoOpTelemetryProcessor(); SentryOptions({String? dsn, Platform? platform, RuntimeChecker? checker}) { this.dsn = dsn; diff --git a/packages/dart/lib/src/telemetry/processing/buffer.dart b/packages/dart/lib/src/telemetry/processing/buffer.dart new file mode 100644 index 0000000000..518c9808ad --- /dev/null +++ b/packages/dart/lib/src/telemetry/processing/buffer.dart @@ -0,0 +1,13 @@ +import 'dart:async'; + +/// A buffer that batches telemetry items for efficient transmission to Sentry. +/// +/// Collects items of type [T] and sends them in batches rather than +/// individually, reducing network overhead. +abstract class TelemetryBuffer { + /// Adds an item to the buffer. + void add(T item); + + /// When executed immediately sends all buffered items to Sentry and clears the buffer. + FutureOr flush(); +} diff --git a/packages/dart/lib/src/telemetry/processing/buffer_config.dart b/packages/dart/lib/src/telemetry/processing/buffer_config.dart new file mode 100644 index 0000000000..7ef8214aa1 --- /dev/null +++ b/packages/dart/lib/src/telemetry/processing/buffer_config.dart @@ -0,0 +1,15 @@ +final class TelemetryBufferConfig { + final Duration flushTimeout; + final int maxBufferSizeBytes; + final int maxItemCount; + + const TelemetryBufferConfig({ + this.flushTimeout = defaultFlushTimeout, + this.maxBufferSizeBytes = defaultMaxBufferSizeBytes, + this.maxItemCount = defaultMaxItemCount, + }); + + static const Duration defaultFlushTimeout = Duration(seconds: 5); + static const int defaultMaxBufferSizeBytes = 1024 * 1024; + static const int defaultMaxItemCount = 100; +} diff --git a/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart b/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart new file mode 100644 index 0000000000..9b62344e52 --- /dev/null +++ b/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart @@ -0,0 +1,180 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../../utils/internal_logger.dart'; +import 'buffer.dart'; +import 'buffer_config.dart'; + +/// Callback invoked when the buffer is flushed with the accumulated data. +typedef OnFlushCallback = FutureOr Function(T data); + +/// Encodes an item of type [T] into bytes. +typedef ItemEncoder = List Function(T item); + +/// Extracts a grouping key from items of type [T]. +typedef GroupKeyExtractor = String Function(T item); + +/// Base class for in-memory telemetry buffers. +/// +/// Buffers telemetry items in memory and flushes them when either the +/// configured size limit, item count limit, or flush timeout is reached. +abstract base class _BaseInMemoryTelemetryBuffer + implements TelemetryBuffer { + final TelemetryBufferConfig _config; + final ItemEncoder _encoder; + final OnFlushCallback _onFlush; + + S _storage; + int _bufferSize = 0; + int _itemCount = 0; + Timer? _flushTimer; + + _BaseInMemoryTelemetryBuffer({ + required ItemEncoder encoder, + required OnFlushCallback onFlush, + required S initialStorage, + TelemetryBufferConfig config = const TelemetryBufferConfig(), + }) : _encoder = encoder, + _onFlush = onFlush, + _storage = initialStorage, + _config = config; + + S _createEmptyStorage(); + void _store(List encoded, T item); + bool get _isEmpty; + + bool get _isBufferFull => + _bufferSize >= _config.maxBufferSizeBytes || + _itemCount >= _config.maxItemCount; + + @override + void add(T item) { + final List encoded; + try { + encoded = _encoder(item); + } catch (exception, stackTrace) { + internalLogger.error( + '$runtimeType: Failed to encode item, dropping', + error: exception, + stackTrace: stackTrace, + ); + return; + } + + if (encoded.length > _config.maxBufferSizeBytes) { + internalLogger.warning( + '$runtimeType: Item size ${encoded.length} exceeds buffer limit ${_config.maxBufferSizeBytes}, dropping', + ); + return; + } + + _store(encoded, item); + _bufferSize += encoded.length; + _itemCount++; + + if (_isBufferFull) { + internalLogger.debug( + '$runtimeType: Buffer full, flushing $_itemCount items', + ); + flush(); + } else { + _flushTimer ??= Timer(_config.flushTimeout, flush); + } + } + + @override + FutureOr flush() { + _flushTimer?.cancel(); + _flushTimer = null; + + if (_isEmpty) return null; + + final toFlush = _storage; + final flushedCount = _itemCount; + final flushedSize = _bufferSize; + _storage = _createEmptyStorage(); + _bufferSize = 0; + _itemCount = 0; + + final successMessage = + '$runtimeType: Flushed $flushedCount items ($flushedSize bytes)'; + final errorMessage = + '$runtimeType: Flush failed for $flushedCount items ($flushedSize bytes)'; + + try { + final result = _onFlush(toFlush); + if (result is Future) { + return result.then( + (_) => internalLogger.debug(successMessage), + onError: (exception, stackTrace) => internalLogger.warning( + errorMessage, + error: exception, + stackTrace: stackTrace, + ), + ); + } + internalLogger.debug(successMessage); + } catch (exception, stackTrace) { + internalLogger.warning( + errorMessage, + error: exception, + stackTrace: stackTrace, + ); + } + } +} + +/// In-memory buffer that collects telemetry items as a flat list. +/// +/// Items are encoded and stored in insertion order. On flush, the entire +/// list of encoded items is passed to the [OnFlushCallback]. +final class InMemoryTelemetryBuffer + extends _BaseInMemoryTelemetryBuffer>> { + InMemoryTelemetryBuffer({ + required super.encoder, + required super.onFlush, + super.config, + }) : super(initialStorage: []); + + @override + List> _createEmptyStorage() => []; + + @override + void _store(List encoded, T item) => _storage.add(encoded); + + @override + bool get _isEmpty => _storage.isEmpty; +} + +/// In-memory buffer that groups telemetry items by a key. +/// +/// Same idea as [InMemoryTelemetryBuffer], but grouped. +final class GroupedInMemoryTelemetryBuffer + extends _BaseInMemoryTelemetryBuffer>, T)>> { + final GroupKeyExtractor _groupKey; + + @visibleForTesting + GroupKeyExtractor get groupKey => _groupKey; + + GroupedInMemoryTelemetryBuffer({ + required super.encoder, + required super.onFlush, + required GroupKeyExtractor groupKeyExtractor, + super.config, + }) : _groupKey = groupKeyExtractor, + super(initialStorage: {}); + + @override + Map>, T)> _createEmptyStorage() => {}; + + @override + void _store(List encoded, T item) { + final key = _groupKey(item); + final bucket = _storage.putIfAbsent(key, () => ([], item)); + bucket.$1.add(encoded); + } + + @override + bool get _isEmpty => _storage.isEmpty; +} diff --git a/packages/dart/lib/src/telemetry/processing/processor.dart b/packages/dart/lib/src/telemetry/processing/processor.dart new file mode 100644 index 0000000000..c2c3b47b27 --- /dev/null +++ b/packages/dart/lib/src/telemetry/processing/processor.dart @@ -0,0 +1,100 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../../../sentry.dart'; +import '../span/sentry_span_v2.dart'; +import 'buffer.dart'; + +/// Interface for processing and buffering telemetry data before sending. +/// +/// Implementations collect spans and logs, buffering them until flushed. +/// This enables batching of telemetry data for efficient transport. +abstract class TelemetryProcessor { + /// Adds a span to be processed and buffered. + void addSpan(RecordingSentrySpanV2 span); + + /// Adds a log to be processed and buffered. + void addLog(SentryLog log); + + /// Flushes all buffered telemetry data. + /// + /// Returns a [Future] if any buffer performs async flushing, otherwise + /// returns synchronously. + FutureOr flush(); +} + +/// Default telemetry processor that routes items to type-specific buffers. +/// +/// Spans and logs are dispatched to their respective [TelemetryBuffer] +/// instances. If no buffer is registered for a telemetry type, items are +/// dropped with a warning. +class DefaultTelemetryProcessor implements TelemetryProcessor { + final SdkLogCallback _logger; + + /// The buffer for span data, or `null` if span buffering is disabled. + @visibleForTesting + TelemetryBuffer? spanBuffer; + + /// The buffer for log data, or `null` if log buffering is disabled. + @visibleForTesting + TelemetryBuffer? logBuffer; + + DefaultTelemetryProcessor( + this._logger, { + this.spanBuffer, + this.logBuffer, + }); + + @override + void addSpan(RecordingSentrySpanV2 span) => _add(span); + + @override + void addLog(SentryLog log) => _add(log); + + void _add(dynamic item) { + final buffer = switch (item) { + RecordingSentrySpanV2 _ => spanBuffer, + SentryLog _ => logBuffer, + _ => null, + }; + + if (buffer == null) { + _logger( + SentryLevel.warning, + '$runtimeType: No buffer registered for ${item.runtimeType} - item was dropped', + ); + return; + } + + buffer.add(item); + } + + @override + FutureOr flush() { + _logger(SentryLevel.debug, '$runtimeType: Clearing buffers'); + + final results = >[ + spanBuffer?.flush(), + logBuffer?.flush(), + ]; + + final futures = results.whereType().toList(); + if (futures.isEmpty) { + return null; + } + + return Future.wait(futures).then((_) {}); + } +} + +class NoOpTelemetryProcessor implements TelemetryProcessor { + @override + void addSpan(RecordingSentrySpanV2 span) {} + + @override + void addLog(SentryLog log) {} + + @override + FutureOr flush() {} +} diff --git a/packages/dart/lib/src/telemetry/processing/processor_integration.dart b/packages/dart/lib/src/telemetry/processing/processor_integration.dart new file mode 100644 index 0000000000..5ac4c0f802 --- /dev/null +++ b/packages/dart/lib/src/telemetry/processing/processor_integration.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../../../sentry.dart'; +import '../span/sentry_span_v2.dart'; +import 'in_memory_buffer.dart'; +import 'processor.dart'; + +class DefaultTelemetryProcessorIntegration extends Integration { + static const integrationName = 'DefaultTelemetryProcessor'; + + @visibleForTesting + final GroupKeyExtractor spanGroupKeyExtractor = + (RecordingSentrySpanV2 item) => + '${item.traceId}-${item.segmentSpan.spanId}'; + + @override + void call(Hub hub, SentryOptions options) { + if (options.telemetryProcessor is! NoOpTelemetryProcessor) { + options.log( + SentryLevel.debug, + '$integrationName: ${options.telemetryProcessor.runtimeType} already set, skipping', + ); + return; + } + + options.telemetryProcessor = DefaultTelemetryProcessor(options.log, + logBuffer: _createLogBuffer(options), + spanBuffer: _createSpanBuffer(options)); + + options.sdk.addIntegration(integrationName); + } + + InMemoryTelemetryBuffer _createLogBuffer(SentryOptions options) => + InMemoryTelemetryBuffer( + encoder: (SentryLog item) => utf8JsonEncoder.convert(item.toJson()), + onFlush: (items) { + final envelope = SentryEnvelope.fromLogsData( + items.map((item) => item).toList(), options.sdk); + return options.transport.send(envelope).then((_) {}); + }); + + GroupedInMemoryTelemetryBuffer _createSpanBuffer( + SentryOptions options) => + GroupedInMemoryTelemetryBuffer( + encoder: (RecordingSentrySpanV2 item) => + utf8JsonEncoder.convert(item.toJson()), + onFlush: (items) { + final futures = items.values.map((itemData) { + final dsc = itemData.$2.resolveDsc(); + final envelope = SentryEnvelope.fromSpansData( + itemData.$1, options.sdk, + traceContext: dsc); + return options.transport.send(envelope); + }).toList(); + if (futures.isEmpty) return null; + return Future.wait(futures).then((_) {}); + }, + groupKeyExtractor: spanGroupKeyExtractor); +} diff --git a/packages/dart/test/mocks/mock_telemetry_buffer.dart b/packages/dart/test/mocks/mock_telemetry_buffer.dart new file mode 100644 index 0000000000..da257d2751 --- /dev/null +++ b/packages/dart/test/mocks/mock_telemetry_buffer.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +import 'package:sentry/src/telemetry/processing/buffer.dart'; + +class MockTelemetryBuffer extends TelemetryBuffer { + final List addedItems = []; + int flushCallCount = 0; + final bool asyncFlush; + + MockTelemetryBuffer({this.asyncFlush = false}); + + @override + void add(T item) => addedItems.add(item); + + @override + FutureOr flush() { + flushCallCount++; + if (asyncFlush) { + return Future.value(); + } + return null; + } +} diff --git a/packages/dart/test/mocks/mock_telemetry_processor.dart b/packages/dart/test/mocks/mock_telemetry_processor.dart new file mode 100644 index 0000000000..db1b80470d --- /dev/null +++ b/packages/dart/test/mocks/mock_telemetry_processor.dart @@ -0,0 +1,25 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/processing/processor.dart'; +import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; + +class MockTelemetryProcessor implements TelemetryProcessor { + final List addedSpans = []; + final List addedLogs = []; + int flushCalls = 0; + int closeCalls = 0; + + @override + void addSpan(RecordingSentrySpanV2 span) { + addedSpans.add(span); + } + + @override + void addLog(SentryLog log) { + addedLogs.add(log); + } + + @override + void flush() { + flushCalls++; + } +} diff --git a/packages/dart/test/sentry_client_lifecycle_test.dart b/packages/dart/test/sentry_client_lifecycle_test.dart index 18c397a46c..060cb6e261 100644 --- a/packages/dart/test/sentry_client_lifecycle_test.dart +++ b/packages/dart/test/sentry_client_lifecycle_test.dart @@ -4,7 +4,7 @@ import 'package:sentry/src/sentry_tracer.dart'; import 'package:test/test.dart'; import 'mocks/mock_client_report_recorder.dart'; -import 'mocks/mock_log_batcher.dart'; +import 'mocks/mock_telemetry_processor.dart'; import 'mocks/mock_transport.dart'; import 'sentry_client_test.dart'; import 'test_utils.dart'; @@ -41,7 +41,8 @@ void main() { scope.span = span; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; fixture.options.lifecycleRegistry .registerCallback((event) { @@ -50,9 +51,8 @@ void main() { await client.captureLog(log, scope: scope); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final capturedLog = mockProcessor.addedLogs.first; expect(capturedLog.attributes['test']?.value, "test-value"); expect(capturedLog.attributes['test']?.type, 'string'); @@ -72,7 +72,8 @@ void main() { scope.span = span; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; fixture.options.lifecycleRegistry .registerCallback((event) { diff --git a/packages/dart/test/sentry_client_sdk_lifecycle_test.dart b/packages/dart/test/sentry_client_sdk_lifecycle_test.dart index 18c397a46c..060cb6e261 100644 --- a/packages/dart/test/sentry_client_sdk_lifecycle_test.dart +++ b/packages/dart/test/sentry_client_sdk_lifecycle_test.dart @@ -4,7 +4,7 @@ import 'package:sentry/src/sentry_tracer.dart'; import 'package:test/test.dart'; import 'mocks/mock_client_report_recorder.dart'; -import 'mocks/mock_log_batcher.dart'; +import 'mocks/mock_telemetry_processor.dart'; import 'mocks/mock_transport.dart'; import 'sentry_client_test.dart'; import 'test_utils.dart'; @@ -41,7 +41,8 @@ void main() { scope.span = span; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; fixture.options.lifecycleRegistry .registerCallback((event) { @@ -50,9 +51,8 @@ void main() { await client.captureLog(log, scope: scope); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final capturedLog = mockProcessor.addedLogs.first; expect(capturedLog.attributes['test']?.value, "test-value"); expect(capturedLog.attributes['test']?.type, 'string'); @@ -72,7 +72,8 @@ void main() { scope.span = span; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; fixture.options.lifecycleRegistry .registerCallback((event) { diff --git a/packages/dart/test/sentry_client_test.dart b/packages/dart/test/sentry_client_test.dart index 534f665774..32c51484ef 100644 --- a/packages/dart/test/sentry_client_test.dart +++ b/packages/dart/test/sentry_client_test.dart @@ -16,19 +16,18 @@ import 'package:sentry/src/transport/data_category.dart'; import 'package:sentry/src/transport/noop_transport.dart'; import 'package:sentry/src/transport/spotlight_http_transport.dart'; import 'package:sentry/src/utils/iterable_utils.dart'; +import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; import 'package:test/test.dart'; -import 'package:sentry/src/noop_log_batcher.dart'; -import 'package:sentry/src/sentry_log_batcher.dart'; import 'package:mockito/mockito.dart'; import 'package:http/http.dart' as http; import 'mocks.dart'; import 'mocks/mock_client_report_recorder.dart'; import 'mocks/mock_hub.dart'; +import 'mocks/mock_telemetry_processor.dart'; import 'mocks/mock_transport.dart'; import 'test_utils.dart'; import 'utils/url_details_test.dart'; -import 'mocks/mock_log_batcher.dart'; void main() { group('SentryClient captures message', () { @@ -1734,41 +1733,32 @@ void main() { ); } - test('sets log batcher on options when logs are enabled', () async { - expect(fixture.options.logBatcher is NoopLogBatcher, true); - - fixture.options.enableLogs = true; - fixture.getSut(); - - expect(fixture.options.logBatcher is NoopLogBatcher, false); - }); - test('disabled by default', () async { final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; final log = givenLog(); await client.captureLog(log); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls, isEmpty); + expect(mockProcessor.addedLogs, isEmpty); }); test('should capture logs as envelope', () async { fixture.options.enableLogs = true; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; final log = givenLog(); await client.captureLog(log); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); + expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + final capturedLog = mockProcessor.addedLogs.first; expect(capturedLog.traceId, log.traceId); expect(capturedLog.level, log.level); @@ -1789,13 +1779,13 @@ void main() { scope.span = span; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; await client.captureLog(log, scope: scope); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final capturedLog = mockProcessor.addedLogs.first; expect( capturedLog.attributes['sentry.sdk.name']?.value, @@ -1843,7 +1833,8 @@ void main() { fixture.options.enableLogs = true; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; final log = givenLog(); final scope = Scope(fixture.options); @@ -1851,9 +1842,8 @@ void main() { await client.captureLog(log, scope: scope); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final capturedLog = mockProcessor.addedLogs.first; expect(capturedLog.attributes['from_scope']?.value, 12); }); @@ -1861,7 +1851,8 @@ void main() { fixture.options.enableLogs = true; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; final log = givenLog(); final scope = Scope(fixture.options); @@ -1875,9 +1866,8 @@ void main() { await client.captureLog(log, scope: scope); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final captured = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final captured = mockProcessor.addedLogs.first; expect(captured.attributes['overridden']?.value, 'fromLog'); expect(captured.attributes['kept']?.value, true); @@ -1897,13 +1887,13 @@ void main() { await scope.setUser(user); final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; await client.captureLog(log, scope: scope); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final capturedLog = mockProcessor.addedLogs.first; expect( capturedLog.attributes['user.id']?.value, @@ -1937,16 +1927,16 @@ void main() { fixture.options.enableLogs = true; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; final log = givenLog(); final scope = Scope(fixture.options); await client.captureLog(log, scope: scope); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final capturedLog = mockProcessor.addedLogs.first; expect(capturedLog.traceId, scope.propagationContext.traceId); }); @@ -1958,14 +1948,14 @@ void main() { fixture.options.beforeSendLog = (log) => null; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; final log = givenLog(); await client.captureLog(log); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 0); + expect(mockProcessor.addedLogs.length, 0); expect( fixture.recorder.discardedEvents.first.reason, @@ -1985,15 +1975,15 @@ void main() { }; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; final log = givenLog(); await client.captureLog(log); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final capturedLog = mockProcessor.addedLogs.first; expect(capturedLog.body, 'modified'); }); @@ -2007,15 +1997,15 @@ void main() { }; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; final log = givenLog(); await client.captureLog(log); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final capturedLog = mockProcessor.addedLogs.first; expect(capturedLog.body, 'modified'); }); @@ -2029,14 +2019,14 @@ void main() { }; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; final log = givenLog(); await client.captureLog(log); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final capturedLog = mockProcessor.addedLogs.first; expect(capturedLog.body, 'test'); }); @@ -2053,7 +2043,8 @@ void main() { scope.span = span; final client = fixture.getSut(); - fixture.options.logBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = mockProcessor; fixture.options.lifecycleRegistry .registerCallback((event) { @@ -2062,15 +2053,61 @@ void main() { await client.captureLog(log, scope: scope); - final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; - expect(mockLogBatcher.addLogCalls.length, 1); - final capturedLog = mockLogBatcher.addLogCalls.first; + expect(mockProcessor.addedLogs.length, 1); + final capturedLog = mockProcessor.addedLogs.first; expect(capturedLog.attributes['test']?.value, "test-value"); expect(capturedLog.attributes['test']?.type, 'string'); }); }); + group('SentryClient', () { + group('when capturing span', () { + late Fixture fixture; + late MockTelemetryProcessor processor; + + setUp(() { + fixture = Fixture(); + processor = MockTelemetryProcessor(); + fixture.options.telemetryProcessor = processor; + }); + + test('adds recording span to telemetry processor', () { + final client = fixture.getSut(); + + final span = RecordingSentrySpanV2.root( + name: 'test-span', + traceId: SentryId.newId(), + onSpanEnd: (_) {}, + clock: fixture.options.clock, + dscCreator: (s) => SentryTraceContextHeader(SentryId.newId(), 'key'), + samplingDecision: SentryTracesSamplingDecision(true), + ); + + client.captureSpan(span); + + expect(processor.addedSpans, hasLength(1)); + expect(processor.addedSpans.first, equals(span)); + }); + + test('does nothing for NoOpSentrySpanV2', () { + final client = fixture.getSut(); + + client.captureSpan(const NoOpSentrySpanV2()); + + expect(processor.addedSpans, isEmpty); + }); + + test('does nothing for UnsetSentrySpanV2', () { + final client = fixture.getSut(); + + client.captureSpan(const UnsetSentrySpanV2()); + + expect(processor.addedSpans, isEmpty); + }); + }); + }); + group('SentryClient captures envelope', () { late Fixture fixture; final fakeEnvelope = getFakeEnvelope(); @@ -2678,15 +2715,15 @@ void main() { final flushCompleter = Completer(); bool flushStarted = false; - // Create a mock log batcher with async flush - final mockLogBatcher = MockLogBatcherWithAsyncFlush( + // Create a mock telemetry processor with async flush + final mockProcessor = MockTelemetryProcessorWithAsyncFlush( onFlush: () async { flushStarted = true; // Wait for the completer to complete await flushCompleter.future; }, ); - fixture.options.logBatcher = mockLogBatcher; + fixture.options.telemetryProcessor = mockProcessor; // Start close() in the background final closeFuture = client.close(); @@ -2904,20 +2941,14 @@ class Fixture { class MockHttpClient extends Mock implements http.Client {} -class MockLogBatcherWithAsyncFlush implements SentryLogBatcher { +class MockTelemetryProcessorWithAsyncFlush extends MockTelemetryProcessor { final Future Function() onFlush; - final addLogCalls = []; - MockLogBatcherWithAsyncFlush({required this.onFlush}); - - @override - void addLog(SentryLog log) { - addLogCalls.add(log); - } + MockTelemetryProcessorWithAsyncFlush({required this.onFlush}); @override FutureOr flush() async { - await onFlush(); + return onFlush(); } } diff --git a/packages/dart/test/telemetry/processing/buffer_test.dart b/packages/dart/test/telemetry/processing/buffer_test.dart new file mode 100644 index 0000000000..e74cb81d01 --- /dev/null +++ b/packages/dart/test/telemetry/processing/buffer_test.dart @@ -0,0 +1,310 @@ +import 'dart:convert'; + +import 'package:sentry/src/telemetry/processing/in_memory_buffer.dart'; +import 'package:sentry/src/telemetry/processing/buffer_config.dart'; +import 'package:test/test.dart'; + +void main() { + group('InMemoryTelemetryBuffer', () { + late _SimpleFixture fixture; + + setUp(() { + fixture = _SimpleFixture(); + }); + + test('items are flushed after timeout', () async { + final flushTimeout = Duration(milliseconds: 1); + final buffer = fixture.getSut( + config: TelemetryBufferConfig(flushTimeout: flushTimeout), + ); + + buffer.add(_TestItem('item1')); + buffer.add(_TestItem('item2')); + + expect(fixture.flushedItems, isEmpty); + + await Future.delayed(flushTimeout + Duration(milliseconds: 10)); + + expect(fixture.flushCallCount, 1); + expect(fixture.flushedItems, hasLength(2)); + }); + + test('items exceeding max size are flushed immediately', () async { + // Each item encodes to ~14 bytes ({"id":"item1"}), so 20 bytes triggers flush on 2nd item + final buffer = fixture.getSut( + config: TelemetryBufferConfig(maxBufferSizeBytes: 20), + ); + + buffer.add(_TestItem('item1')); + expect(fixture.flushCallCount, 0); + + buffer.add(_TestItem('item2')); + + // Wait briefly for async flush + await Future.delayed(Duration(milliseconds: 1)); + + expect(fixture.flushCallCount, 1); + expect(fixture.flushedItems, hasLength(2)); + }); + + test('single item exceeding max buffer size is rejected', () async { + // Set max buffer size to 10 bytes, but item encodes to ~14 bytes + final buffer = fixture.getSut( + config: TelemetryBufferConfig(maxBufferSizeBytes: 10), + ); + + buffer.add(_TestItem('item1')); + + // Item should be rejected, not added to buffer + await buffer.flush(); + + expect(fixture.flushedItems, isEmpty); + }); + + test('items exceeding max item count are flushed immediately', () async { + final buffer = fixture.getSut( + config: TelemetryBufferConfig(maxItemCount: 2), + ); + + buffer.add(_TestItem('item1')); + expect(fixture.flushCallCount, 0); + + buffer.add(_TestItem('item2')); + + // Wait briefly for async flush + await Future.delayed(Duration(milliseconds: 1)); + + expect(fixture.flushCallCount, 1); + expect(fixture.flushedItems, hasLength(2)); + }); + + test('calling flush directly sends items', () async { + final buffer = fixture.getSut(); + + buffer.add(_TestItem('item1')); + buffer.add(_TestItem('item2')); + + await buffer.flush(); + + expect(fixture.flushCallCount, 1); + expect(fixture.flushedItems, hasLength(2)); + }); + + test('timer is only started once and not restarted on subsequent additions', + () async { + final flushTimeout = Duration(milliseconds: 100); + final buffer = fixture.getSut( + config: TelemetryBufferConfig(flushTimeout: flushTimeout), + ); + + buffer.add(_TestItem('item1')); + expect(fixture.flushCallCount, 0); + + buffer.add(_TestItem('item2')); + expect(fixture.flushCallCount, 0); + + await Future.delayed(flushTimeout + Duration(milliseconds: 10)); + + expect(fixture.flushCallCount, 1); + expect(fixture.flushedItems, hasLength(2)); + }); + + test('flush with empty buffer returns null', () async { + final buffer = fixture.getSut(); + + final result = buffer.flush(); + + expect(result, isNull); + expect(fixture.flushedItems, isEmpty); + }); + + test('buffer is cleared after flush', () async { + final buffer = fixture.getSut(); + + buffer.add(_TestItem('item1')); + await buffer.flush(); + + expect(fixture.flushCallCount, 1); + expect(fixture.flushedItems, hasLength(1)); + + // Second flush should not send anything + fixture.reset(); + final result = buffer.flush(); + + expect(result, isNull); + expect(fixture.flushCallCount, 0); + expect(fixture.flushedItems, isEmpty); + }); + + test('encoding failure does not crash and item is skipped', () async { + final buffer = fixture.getSut(); + + buffer.add(_ThrowingTestItem()); + buffer.add(_TestItem('valid')); + await buffer.flush(); + + // Only the valid item should be in the buffer + expect(fixture.flushedItems, hasLength(1)); + expect(fixture.flushCallCount, 1); + }); + + test('onFlush receives List> directly', () async { + final buffer = fixture.getSut(); + + buffer.add(_TestItem('item1')); + buffer.add(_TestItem('item2')); + await buffer.flush(); + + // Verify callback received a simple list, not a map + expect(fixture.flushedItems, hasLength(2)); + expect(fixture.flushCallCount, 1); + }); + }); + + group('GroupedInMemoryTelemetryBuffer', () { + late _GroupedFixture fixture; + + setUp(() { + fixture = _GroupedFixture(); + }); + + test('items are grouped by key', () async { + final buffer = fixture.getSut( + groupKeyExtractor: (item) => item.group, + ); + + buffer.add(_TestItem('item1', group: 'group1')); + buffer.add(_TestItem('item2', group: 'group2')); + buffer.add(_TestItem('item3', group: 'group1')); + + await buffer.flush(); + + expect(fixture.flushCallCount, 1); + expect(fixture.flushedGroups.keys, containsAll(['group1', 'group2'])); + expect( + fixture.flushedGroups['group1']?.$1, hasLength(2)); // item1 and item3 + expect(fixture.flushedGroups['group2']?.$1, hasLength(1)); // item2 + }); + + test('items are flushed after timeout', () async { + final flushTimeout = Duration(milliseconds: 1); + final buffer = fixture.getSut( + config: TelemetryBufferConfig(flushTimeout: flushTimeout), + groupKeyExtractor: (item) => item.group, + ); + + buffer.add(_TestItem('item1', group: 'a')); + buffer.add(_TestItem('item2', group: 'b')); + + expect(fixture.flushedGroups, isEmpty); + + await Future.delayed(flushTimeout + Duration(milliseconds: 10)); + + expect(fixture.flushCallCount, 1); + expect(fixture.flushedGroups.keys, hasLength(2)); + }); + + test('flush with empty buffer returns null', () async { + final buffer = fixture.getSut( + groupKeyExtractor: (item) => item.group, + ); + + final result = buffer.flush(); + + expect(result, isNull); + expect(fixture.flushedGroups, isEmpty); + }); + + test('buffer is cleared after flush', () async { + final buffer = fixture.getSut( + groupKeyExtractor: (item) => item.group, + ); + + buffer.add(_TestItem('item1', group: 'a')); + await buffer.flush(); + + expect(fixture.flushCallCount, 1); + + fixture.reset(); + final result = buffer.flush(); + + expect(result, isNull); + expect(fixture.flushCallCount, 0); + }); + + test('onFlush receives Map>>', () async { + final buffer = fixture.getSut( + groupKeyExtractor: (item) => item.group, + ); + + buffer.add(_TestItem('item1', group: 'myGroup')); + await buffer.flush(); + + expect(fixture.flushedGroups.containsKey('myGroup'), isTrue); + }); + }); +} + +class _TestItem { + final String id; + final String group; + + _TestItem(this.id, {this.group = 'default'}); + + Map toJson() => {'id': id}; +} + +class _ThrowingTestItem extends _TestItem { + _ThrowingTestItem() : super('throwing'); + + @override + Map toJson() => throw Exception('Encoding failed'); +} + +class _SimpleFixture { + List> flushedItems = []; + int flushCallCount = 0; + + InMemoryTelemetryBuffer<_TestItem> getSut({ + TelemetryBufferConfig config = const TelemetryBufferConfig(), + }) { + return InMemoryTelemetryBuffer<_TestItem>( + encoder: (item) => utf8.encode(jsonEncode(item.toJson())), + onFlush: (items) { + flushCallCount++; + flushedItems = items; + }, + config: config, + ); + } + + void reset() { + flushedItems = []; + flushCallCount = 0; + } +} + +class _GroupedFixture { + Map>, _TestItem)> flushedGroups = {}; + int flushCallCount = 0; + + GroupedInMemoryTelemetryBuffer<_TestItem> getSut({ + required GroupKeyExtractor<_TestItem> groupKeyExtractor, + TelemetryBufferConfig config = const TelemetryBufferConfig(), + }) { + return GroupedInMemoryTelemetryBuffer<_TestItem>( + encoder: (item) => utf8.encode(jsonEncode(item.toJson())), + onFlush: (groups) { + flushCallCount++; + flushedGroups = groups; + }, + groupKeyExtractor: groupKeyExtractor, + config: config, + ); + } + + void reset() { + flushedGroups = {}; + flushCallCount = 0; + } +} diff --git a/packages/dart/test/telemetry/processing/processor_integration_test.dart b/packages/dart/test/telemetry/processing/processor_integration_test.dart new file mode 100644 index 0000000000..2fedd77d1e --- /dev/null +++ b/packages/dart/test/telemetry/processing/processor_integration_test.dart @@ -0,0 +1,161 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/processing/in_memory_buffer.dart'; +import 'package:sentry/src/telemetry/processing/processor.dart'; +import 'package:sentry/src/telemetry/processing/processor_integration.dart'; +import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_hub.dart'; +import '../../mocks/mock_transport.dart'; +import '../../test_utils.dart'; + +void main() { + group('DefaultTelemetryProcessorIntegration', () { + late _Fixture fixture; + + setUp(() { + fixture = _Fixture(); + }); + + test( + 'sets up DefaultTelemetryProcessor when NoOpTelemetryProcessor is active', + () { + final options = fixture.options; + expect(options.telemetryProcessor, isA()); + + fixture.getSut().call(fixture.hub, options); + + expect(options.telemetryProcessor, isA()); + }); + + test('does not override existing telemetry processor', () { + final options = fixture.options; + final existingProcessor = DefaultTelemetryProcessor(options.log); + options.telemetryProcessor = existingProcessor; + + fixture.getSut().call(fixture.hub, options); + + expect(identical(options.telemetryProcessor, existingProcessor), isTrue); + }); + + test('adds integration name to SDK', () { + final options = fixture.options; + + fixture.getSut().call(fixture.hub, options); + + expect( + options.sdk.integrations, + contains(DefaultTelemetryProcessorIntegration.integrationName), + ); + }); + + test('configures log buffer as InMemoryTelemetryBuffer', () { + final options = fixture.options; + + fixture.getSut().call(fixture.hub, options); + + final processor = options.telemetryProcessor as DefaultTelemetryProcessor; + expect(processor.logBuffer, isA>()); + }); + + test('configures span buffer as GroupedInMemoryTelemetryBuffer', () { + final options = fixture.options; + + fixture.getSut().call(fixture.hub, options); + + final processor = options.telemetryProcessor as DefaultTelemetryProcessor; + expect(processor.spanBuffer, + isA>()); + }); + + test('configures span buffer with group key extractor', () { + final options = fixture.options; + + final integration = fixture.getSut(); + integration.call(fixture.hub, options); + + final processor = options.telemetryProcessor as DefaultTelemetryProcessor; + + expect( + (processor.spanBuffer + as GroupedInMemoryTelemetryBuffer) + .groupKey, + integration.spanGroupKeyExtractor); + }); + + test('spanGroupKeyExtractor uses traceId-spanId format', () { + final options = fixture.options; + + final integration = fixture.getSut(); + integration.call(fixture.hub, options); + + final span = fixture.createSpan(); + final key = integration.spanGroupKeyExtractor(span); + + expect(key, '${span.traceId}-${span.spanId}'); + }); + + group('flush', () { + test('log reaches transport as envelope', () async { + final options = fixture.options; + fixture.getSut().call(fixture.hub, options); + + final processor = + options.telemetryProcessor as DefaultTelemetryProcessor; + processor.addLog(fixture.createLog()); + await processor.flush(); + + expect(fixture.transport.envelopes, hasLength(1)); + }); + + test('span reaches transport as envelope', () async { + final options = fixture.options; + fixture.getSut().call(fixture.hub, options); + + final processor = + options.telemetryProcessor as DefaultTelemetryProcessor; + final span = fixture.createSpan(); + span.end(); + processor.addSpan(span); + await processor.flush(); + + expect(fixture.transport.envelopes, hasLength(1)); + }); + }); + }); +} + +class _Fixture { + final hub = MockHub(); + final transport = MockTransport(); + late SentryOptions options; + + _Fixture() { + options = defaultTestOptions()..transport = transport; + } + + DefaultTelemetryProcessorIntegration getSut() { + return DefaultTelemetryProcessorIntegration(); + } + + SentryLog createLog() { + return SentryLog( + timestamp: DateTime.now().toUtc(), + level: SentryLogLevel.info, + body: 'test log', + attributes: {}, + ); + } + + RecordingSentrySpanV2 createSpan() { + return RecordingSentrySpanV2.root( + name: 'test-span', + traceId: SentryId.newId(), + onSpanEnd: (_) {}, + clock: options.clock, + dscCreator: (_) => + SentryTraceContextHeader(SentryId.newId(), 'publicKey'), + samplingDecision: SentryTracesSamplingDecision(true), + ); + } +} diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart new file mode 100644 index 0000000000..6e20939b09 --- /dev/null +++ b/packages/dart/test/telemetry/processing/processor_test.dart @@ -0,0 +1,173 @@ +import 'dart:async'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/processing/processor.dart'; +import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_telemetry_buffer.dart'; +import '../../test_utils.dart'; + +void main() { + group('DefaultTelemetryProcessor', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('addSpan', () { + test('routes span to span buffer', () { + final mockSpanBuffer = MockTelemetryBuffer(); + final processor = fixture.getSut(spanBuffer: mockSpanBuffer); + + final span = fixture.createSpan(); + span.end(); + processor.addSpan(span); + + expect(mockSpanBuffer.addedItems.length, 1); + expect(mockSpanBuffer.addedItems.first, span); + }); + + test('does not throw when no span buffer registered', () { + final processor = fixture.getSut(); + processor.spanBuffer = null; + + final span = fixture.createSpan(); + span.end(); + processor.addSpan(span); + + // Nothing to assert - just verifying no exception thrown + }); + }); + + group('addLog', () { + test('routes log to log buffer', () { + final mockLogBuffer = MockTelemetryBuffer(); + final processor = + fixture.getSut(enableLogs: true, logBuffer: mockLogBuffer); + + final log = fixture.createLog(); + processor.addLog(log); + + expect(mockLogBuffer.addedItems.length, 1); + expect(mockLogBuffer.addedItems.first, log); + }); + + test('does not throw when no log buffer registered', () { + final processor = fixture.getSut(); + processor.logBuffer = null; + + final log = fixture.createLog(); + processor.addLog(log); + }); + }); + + group('flush', () { + test('flushes all registered buffers', () async { + final mockSpanBuffer = MockTelemetryBuffer(); + final mockLogBuffer = MockTelemetryBuffer(); + final processor = fixture.getSut( + enableLogs: true, + spanBuffer: mockSpanBuffer, + logBuffer: mockLogBuffer, + ); + + await processor.flush(); + + expect(mockSpanBuffer.flushCallCount, 1); + expect(mockLogBuffer.flushCallCount, 1); + }); + + test('flushes only span buffer when log buffer is null', () async { + final mockSpanBuffer = MockTelemetryBuffer(); + final processor = fixture.getSut(spanBuffer: mockSpanBuffer); + processor.logBuffer = null; + + await processor.flush(); + + expect(mockSpanBuffer.flushCallCount, 1); + }); + + test('returns sync (null) when all buffers flush synchronously', () { + final mockSpanBuffer = + MockTelemetryBuffer(asyncFlush: false); + final processor = fixture.getSut(spanBuffer: mockSpanBuffer); + processor.logBuffer = null; + + final result = processor.flush(); + + expect(result, isNull); + }); + + test('returns Future when at least one buffer flushes asynchronously', + () async { + final mockSpanBuffer = + MockTelemetryBuffer(asyncFlush: true); + final processor = fixture.getSut(spanBuffer: mockSpanBuffer); + processor.logBuffer = null; + + final result = processor.flush(); + + expect(result, isA()); + await result; + }); + }); + }); +} + +class Fixture { + late SentryOptions options; + + Fixture() { + options = defaultTestOptions(); + } + + DefaultTelemetryProcessor getSut({ + bool enableLogs = false, + MockTelemetryBuffer? spanBuffer, + MockTelemetryBuffer? logBuffer, + }) { + options.enableLogs = enableLogs; + return DefaultTelemetryProcessor( + options.log, + spanBuffer: spanBuffer, + logBuffer: logBuffer, + ); + } + + RecordingSentrySpanV2 createSpan({String name = 'test-span'}) { + return RecordingSentrySpanV2.root( + name: name, + traceId: SentryId.newId(), + onSpanEnd: (_) {}, + clock: options.clock, + dscCreator: (_) => + SentryTraceContextHeader(SentryId.newId(), 'publicKey'), + samplingDecision: SentryTracesSamplingDecision(true), + ); + } + + RecordingSentrySpanV2 createChildSpan({ + required RecordingSentrySpanV2 parent, + String name = 'child-span', + }) { + return RecordingSentrySpanV2.child( + parent: parent, + name: name, + onSpanEnd: (_) {}, + clock: options.clock, + dscCreator: (_) => + SentryTraceContextHeader(SentryId.newId(), 'publicKey'), + ); + } + + SentryLog createLog({String body = 'test log'}) { + return SentryLog( + timestamp: DateTime.now().toUtc(), + level: SentryLogLevel.info, + body: body, + attributes: {}, + ); + } +} diff --git a/packages/dart/test/telemetry/span/span_test.dart b/packages/dart/test/telemetry/span/span_test.dart new file mode 100644 index 0000000000..3bd8a372c3 --- /dev/null +++ b/packages/dart/test/telemetry/span/span_test.dart @@ -0,0 +1,512 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/span/sentry_span_status_v2.dart'; +import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; +import 'package:test/test.dart'; + +import '../../test_utils.dart'; + +void main() { + group('RecordingSentrySpanV2', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('end finishes the span', () { + final span = fixture.createSpan(name: 'test-span'); + + span.end(); + + expect(span.endTimestamp, isNotNull); + expect(span.isEnded, isTrue); + }); + + test('end sets current time by default', () { + final span = fixture.createSpan(name: 'test-span'); + + final before = DateTime.now().toUtc(); + span.end(); + final after = DateTime.now().toUtc(); + + expect(span.endTimestamp, isNotNull); + expect(span.endTimestamp!.isAfter(before) || span.endTimestamp == before, + isTrue, + reason: 'endTimestamp should be >= time before end() was called'); + expect(span.endTimestamp!.isBefore(after) || span.endTimestamp == after, + isTrue, + reason: 'endTimestamp should be <= time after end() was called'); + }); + + test('end with custom timestamp sets end time', () { + final span = fixture.createSpan(name: 'test-span'); + final endTime = DateTime.now().add(Duration(seconds: 5)).toUtc(); + + span.end(endTimestamp: endTime); + + expect(span.endTimestamp, equals(endTime)); + }); + + test('end sets endTimestamp as UTC', () { + final span1 = fixture.createSpan(name: 'test-span'); + span1.end(); + expect(span1.endTimestamp!.isUtc, isTrue); + + final span2 = fixture.createSpan(name: 'test-span'); + // Should transform non-utc time to utc + span2.end(endTimestamp: DateTime.now()); + expect(span2.endTimestamp!.isUtc, isTrue); + }); + + test('end calls onSpanEnded callback', () { + RecordingSentrySpanV2? capturedSpan; + final span = fixture.createSpan( + name: 'test-span', + onSpanEnded: (s) => capturedSpan = s, + ); + + span.end(); + + expect(capturedSpan, same(span)); + }); + + test('end is idempotent once finished', () { + var callCount = 0; + final span = fixture.createSpan( + name: 'test-span', + onSpanEnded: (_) => callCount++, + ); + final firstEndTimestamp = DateTime.utc(2024, 1, 1); + final secondEndTimestamp = DateTime.utc(2024, 1, 2); + + span.end(endTimestamp: firstEndTimestamp); + span.end(endTimestamp: secondEndTimestamp); + + expect(span.endTimestamp, equals(firstEndTimestamp)); + expect(span.isEnded, isTrue); + expect(callCount, 1); + }); + + test('setAttribute sets single attribute', () { + final span = fixture.createSpan(name: 'test-span'); + + final attributeValue = SentryAttribute.string('value'); + span.setAttribute('key', attributeValue); + + expect(span.attributes, equals({'key': attributeValue})); + }); + + test('setAttributes sets multiple attributes', () { + final span = fixture.createSpan(name: 'test-span'); + + final attributes = { + 'key1': SentryAttribute.string('value1'), + 'key2': SentryAttribute.int(42), + }; + span.setAttributes(attributes); + + expect(span.attributes, equals(attributes)); + }); + + test('setName sets span name', () { + final span = fixture.createSpan(name: 'initial-name'); + + span.name = 'updated-name'; + expect(span.name, equals('updated-name')); + }); + + test('setStatus sets span status', () { + final span = fixture.createSpan(name: 'test-span'); + + span.status = SentrySpanStatusV2.ok; + expect(span.status, equals(SentrySpanStatusV2.ok)); + + span.status = SentrySpanStatusV2.error; + expect(span.status, equals(SentrySpanStatusV2.error)); + }); + + test('parentSpan returns the parent span', () { + final parent = fixture.createSpan(name: 'parent'); + final child = fixture.createSpan(name: 'child', parentSpan: parent); + + expect(child.parentSpan, equals(parent)); + }); + + test('parentSpan returns null for root span', () { + final span = fixture.createSpan(name: 'root'); + + expect(span.parentSpan, isNull); + }); + + test('name returns the span name', () { + final span = fixture.createSpan(name: 'my-span-name'); + + expect(span.name, equals('my-span-name')); + }); + + test('spanId is created when span is created', () { + final span = fixture.createSpan(name: 'test-span'); + + expect(span.spanId.toString(), isNot(SpanId.empty().toString())); + }); + + group('segmentSpan', () { + test('returns null when parentSpan is null', () { + final span = fixture.createSpan(name: 'root-span'); + + expect(span.segmentSpan, same(span)); + }); + + test('returns parent segmentSpan when parentSpan is set', () { + final root = fixture.createSpan(name: 'root'); + final child = fixture.createSpan(name: 'child', parentSpan: root); + + expect(child.segmentSpan, same(root)); + }); + + test('returns root segmentSpan for deeply nested spans', () { + final root = fixture.createSpan(name: 'root'); + final child = fixture.createSpan(name: 'child', parentSpan: root); + final grandchild = + fixture.createSpan(name: 'grandchild', parentSpan: child); + final greatGrandchild = fixture.createSpan( + name: 'great-grandchild', parentSpan: grandchild); + + expect(grandchild.segmentSpan, same(root)); + expect(greatGrandchild.segmentSpan, same(root)); + }); + }); + + group('traceId', () { + test('uses defaultTraceId when no parent', () { + final traceId = SentryId.newId(); + final span = fixture.createSpan(name: 'test-span', traceId: traceId); + + expect(span.traceId, equals(traceId)); + }); + + test('child span inherits traceId from parent', () { + final parent = fixture.createSpan(name: 'parent'); + final child = fixture.createSpan(name: 'child', parentSpan: parent); + + expect(child.traceId, equals(parent.traceId)); + }); + + test('child span ignores defaultTraceId when parent exists', () { + final parentTraceId = SentryId.newId(); + final differentTraceId = SentryId.newId(); + + final parent = + fixture.createSpan(name: 'parent', traceId: parentTraceId); + final child = fixture.createSpan( + name: 'child', + parentSpan: parent, + traceId: differentTraceId, + ); + + expect(child.traceId, equals(parentTraceId)); + expect(child.traceId, isNot(equals(differentTraceId))); + }); + }); + + group('when resolving DSC', () { + test('creates DSC on first access for root span', () { + final span = fixture.createSpan(name: 'root-span'); + + final dsc = span.resolveDsc(); + + expect(dsc, isNotNull); + expect(dsc.publicKey, equals('publicKey')); + }); + + test('returns same DSC on subsequent access', () { + final span = fixture.createSpan(name: 'root-span'); + + final dsc1 = span.resolveDsc(); + final dsc2 = span.resolveDsc(); + + expect(identical(dsc1, dsc2), isTrue); + }); + + test('returns DSC from segment span for child span', () { + final root = fixture.createSpan(name: 'root'); + final child = fixture.createSpan(name: 'child', parentSpan: root); + + final rootDsc = root.resolveDsc(); + final childDsc = child.resolveDsc(); + + expect(identical(rootDsc, childDsc), isTrue); + }); + + test('returns same DSC for deeply nested spans', () { + final root = fixture.createSpan(name: 'root'); + final child = fixture.createSpan(name: 'child', parentSpan: root); + final grandchild = + fixture.createSpan(name: 'grandchild', parentSpan: child); + + final rootDsc = root.resolveDsc(); + final childDsc = child.resolveDsc(); + final grandchildDsc = grandchild.resolveDsc(); + + expect(identical(rootDsc, childDsc), isTrue); + expect(identical(rootDsc, grandchildDsc), isTrue); + }); + + test('freezes DSC after first access', () { + var callCount = 0; + final span = fixture.createSpan( + name: 'root-span', + dscCreator: (s) { + callCount++; + return SentryTraceContextHeader(SentryId.newId(), 'publicKey'); + }, + ); + + span.resolveDsc(); + span.resolveDsc(); + span.resolveDsc(); + + expect(callCount, equals(1), + reason: 'DSC creator should only be called once'); + }); + }); + + group('when accessing samplingDecision', () { + test('returns stored decision for root span', () { + final decision = SentryTracesSamplingDecision( + true, + sampleRate: 0.5, + sampleRand: 0.25, + ); + final span = fixture.createSpan( + name: 'root-span', + samplingDecision: decision, + ); + + expect(span.samplingDecision.sampled, isTrue); + expect(span.samplingDecision.sampleRate, equals(0.5)); + expect(span.samplingDecision.sampleRand, equals(0.25)); + }); + + test('returns inherited decision for child span', () { + final decision = SentryTracesSamplingDecision( + true, + sampleRate: 0.75, + sampleRand: 0.1, + ); + final root = fixture.createSpan( + name: 'root', + samplingDecision: decision, + ); + final child = fixture.createSpan(name: 'child', parentSpan: root); + + expect(child.samplingDecision.sampled, + equals(root.samplingDecision.sampled)); + expect(child.samplingDecision.sampleRate, + equals(root.samplingDecision.sampleRate)); + expect(child.samplingDecision.sampleRand, + equals(root.samplingDecision.sampleRand)); + }); + + test('returns root decision for deeply nested span', () { + final decision = SentryTracesSamplingDecision( + true, + sampleRate: 0.3, + sampleRand: 0.99, + ); + final root = fixture.createSpan( + name: 'root', + samplingDecision: decision, + ); + final child = fixture.createSpan(name: 'child', parentSpan: root); + final grandchild = + fixture.createSpan(name: 'grandchild', parentSpan: child); + + expect(grandchild.samplingDecision.sampled, equals(decision.sampled)); + expect(grandchild.samplingDecision.sampleRate, + equals(decision.sampleRate)); + expect(grandchild.samplingDecision.sampleRand, + equals(decision.sampleRand)); + }); + }); + + group('toJson', () { + test('serializes basic span without parent', () { + final span = fixture.createSpan(name: 'test-span'); + span.end(); + + final json = span.toJson(); + + expect(json['trace_id'], equals(span.traceId.toString())); + expect(json['span_id'], equals(span.spanId.toString())); + expect(json['name'], equals('test-span')); + expect(json['is_segment'], isTrue); + expect(json['status'], equals('ok')); + expect(json['start_timestamp'], isA()); + expect(json['end_timestamp'], isA()); + expect(json.containsKey('parent_span_id'), isFalse); + }); + + test('serializes span with parent', () { + final parent = fixture.createSpan(name: 'parent'); + final child = fixture.createSpan(name: 'child', parentSpan: parent); + child.end(); + + final json = child.toJson(); + + expect(json['parent_span_id'], equals(parent.spanId.toString())); + expect(json['is_segment'], isFalse); + }); + + test('serializes span with error status', () { + final span = fixture.createSpan(name: 'test-span'); + span.status = SentrySpanStatusV2.error; + span.end(); + + final json = span.toJson(); + + expect(json['status'], equals('error')); + }); + + test('serializes span with attributes', () { + final span = fixture.createSpan(name: 'test-span'); + span.setAttribute('string_attr', SentryAttribute.string('value')); + span.setAttribute('int_attr', SentryAttribute.int(42)); + span.setAttribute('bool_attr', SentryAttribute.bool(true)); + span.setAttribute('double_attr', SentryAttribute.double(3.14)); + span.end(); + + final json = span.toJson(); + + expect(json.containsKey('attributes'), isTrue); + final attributes = Map.from(json['attributes']); + + expect(attributes['string_attr'], {'value': 'value', 'type': 'string'}); + expect(attributes['int_attr'], {'value': 42, 'type': 'integer'}); + expect(attributes['bool_attr'], {'value': true, 'type': 'boolean'}); + expect(attributes['double_attr'], {'value': 3.14, 'type': 'double'}); + }); + + test('end_timestamp is null when span is not finished', () { + final span = fixture.createSpan(name: 'test-span'); + + final json = span.toJson(); + + expect(json['end_timestamp'], isNull); + }); + + test( + 'timestamps are serialized as unix seconds with microsecond precision', + () { + final span = fixture.createSpan(name: 'test-span'); + final customEndTime = DateTime.utc(2024, 6, 15, 12, 30, 45, 123, 456); + span.end(endTimestamp: customEndTime); + + final json = span.toJson(); + + final endTimestamp = json['end_timestamp'] as double; + final expectedMicros = customEndTime.microsecondsSinceEpoch; + final expectedSeconds = expectedMicros / 1000000; + + expect(endTimestamp, closeTo(expectedSeconds, 0.000001)); + }); + + test('serializes updated name', () { + final span = fixture.createSpan(name: 'original-name'); + span.name = 'updated-name'; + span.end(); + + final json = span.toJson(); + + expect(json['name'], equals('updated-name')); + }); + }); + }); + + group('NoOpSentrySpanV2', () { + test('operations do not throw', () { + const span = NoOpSentrySpanV2(); + + // All operations should be no-ops and not throw + span.end(); + span.end(endTimestamp: DateTime.now()); + span.setAttribute('key', SentryAttribute.string('value')); + span.setAttributes({'key': SentryAttribute.string('value')}); + span.removeAttribute('key'); + span.name = 'name'; + span.status = SentrySpanStatusV2.ok; + span.status = SentrySpanStatusV2.error; + }); + + test('returns default values', () { + const span = NoOpSentrySpanV2(); + + expect(span.spanId.toString(), SpanId.empty().toString()); + expect(span.traceId.toString(), SentryId.empty().toString()); + expect(span.name, 'NoOpSpan'); + expect(span.status, SentrySpanStatusV2.ok); + expect(span.parentSpan, isNull); + expect(span.endTimestamp, isNull); + expect(span.attributes, isEmpty); + }); + }); + + group('UnsetSentrySpanV2', () { + test('all APIs throw to prevent accidental use', () { + const span = UnsetSentrySpanV2(); + + expect(() => span.spanId, throwsA(isA())); + expect(() => span.traceId, throwsA(isA())); + expect(() => span.name, throwsA(isA())); + expect(() => span.status, throwsA(isA())); + expect(() => span.parentSpan, throwsA(isA())); + expect(() => span.endTimestamp, throwsA(isA())); + expect(() => span.attributes, throwsA(isA())); + + expect(() => span.name = 'foo', throwsA(isA())); + expect(() => span.status = SentrySpanStatusV2.ok, + throwsA(isA())); + expect(() => span.setAttribute('k', SentryAttribute.string('v')), + throwsA(isA())); + expect(() => span.setAttributes({'k': SentryAttribute.string('v')}), + throwsA(isA())); + expect( + () => span.removeAttribute('k'), throwsA(isA())); + expect(() => span.end(), throwsA(isA())); + }); + }); +} + +class Fixture { + final options = defaultTestOptions(); + + RecordingSentrySpanV2 createSpan({ + required String name, + RecordingSentrySpanV2? parentSpan, + SentryId? traceId, + OnSpanEndCallback? onSpanEnded, + DscCreatorCallback? dscCreator, + SentryTracesSamplingDecision? samplingDecision, + }) { + final defaultDscCreator = (RecordingSentrySpanV2 span) => + SentryTraceContextHeader(SentryId.newId(), 'publicKey'); + + if (parentSpan != null) { + return RecordingSentrySpanV2.child( + parent: parentSpan, + name: name, + onSpanEnd: onSpanEnded ?? (_) {}, + clock: options.clock, + dscCreator: dscCreator ?? defaultDscCreator, + ); + } + return RecordingSentrySpanV2.root( + name: name, + traceId: traceId ?? SentryId.newId(), + onSpanEnd: onSpanEnded ?? (_) {}, + clock: options.clock, + dscCreator: dscCreator ?? defaultDscCreator, + samplingDecision: samplingDecision ?? SentryTracesSamplingDecision(true), + ); + } +} diff --git a/packages/flutter/lib/src/widgets_binding_observer.dart b/packages/flutter/lib/src/widgets_binding_observer.dart index f26a080bcd..12811068e1 100644 --- a/packages/flutter/lib/src/widgets_binding_observer.dart +++ b/packages/flutter/lib/src/widgets_binding_observer.dart @@ -94,7 +94,7 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver { if (!_isNavigatorObserverCreated() && !_options.platform.isWeb) { if (state == AppLifecycleState.inactive) { _appInBackgroundStopwatch.start(); - _options.logBatcher.flush(); + _options.telemetryProcessor.flush(); } else if (_appInBackgroundStopwatch.isRunning && state == AppLifecycleState.resumed) { _appInBackgroundStopwatch.stop(); diff --git a/packages/flutter/test/mocks.dart b/packages/flutter/test/mocks.dart index dec5362a20..34ddbf8a9d 100644 --- a/packages/flutter/test/mocks.dart +++ b/packages/flutter/test/mocks.dart @@ -8,6 +8,8 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry/src/platform/platform.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry/src/telemetry/processing/processor.dart'; +import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/frames_tracking/sentry_delayed_frames_tracker.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; @@ -236,3 +238,25 @@ class MockLogItem { const MockLogItem(this.level, this.message, {this.logger, this.exception, this.stackTrace}); } + +class MockTelemetryProcessor implements TelemetryProcessor { + final List addedSpans = []; + final List addedLogs = []; + int flushCalls = 0; + int closeCalls = 0; + + @override + void addSpan(RecordingSentrySpanV2 span) { + addedSpans.add(span); + } + + @override + void addLog(SentryLog log) { + addedLogs.add(log); + } + + @override + void flush() { + flushCalls++; + } +} diff --git a/packages/flutter/test/widgets_binding_observer_test.dart b/packages/flutter/test/widgets_binding_observer_test.dart index d4ed7e8a52..8470de76d7 100644 --- a/packages/flutter/test/widgets_binding_observer_test.dart +++ b/packages/flutter/test/widgets_binding_observer_test.dart @@ -1,7 +1,6 @@ // ignore_for_file: invalid_use_of_internal_member import 'dart:ui'; -import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -11,8 +10,6 @@ import 'package:sentry/src/platform/mock_platform.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/widgets_binding_observer.dart'; -import 'package:sentry/src/sentry_log_batcher.dart'; - import 'mocks.dart'; import 'mocks.mocks.dart'; @@ -567,17 +564,17 @@ void main() { }); testWidgets( - 'calls flush on logs batcher when transitioning to inactive state', + 'calls flush on telemetry processor when transitioning to inactive state', (WidgetTester tester) async { final hub = MockHub(); - final mockLogBatcher = MockLogBatcher(); + final mockProcessor = MockTelemetryProcessor(); final options = defaultTestOptions(); options.platform = MockPlatform(isWeb: false); options.bindingUtils = TestBindingWrapper(); - options.logBatcher = mockLogBatcher; + options.telemetryProcessor = mockProcessor; options.enableLogs = true; final observer = SentryWidgetsBindingObserver( @@ -590,21 +587,9 @@ void main() { await sendLifecycle('inactive'); - expect(mockLogBatcher.flushCalled, true); + expect(mockProcessor.flushCalls, 1); instance.removeObserver(observer); }); }); } - -class MockLogBatcher implements SentryLogBatcher { - var flushCalled = false; - - @override - void addLog(SentryLog log) {} - - @override - FutureOr flush() async { - flushCalled = true; - } -} From 3897122a12d02268f9e870151e337c1c1195ed93 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 17:52:18 +0100 Subject: [PATCH 02/79] Remove SpanV2 and TraceLifecycle dependencies - Remove addSpan method from TelemetryProcessor interface - Remove span buffer from DefaultTelemetryProcessor - Remove captureSpan method from SentryClient - Remove traceLifecycle property from SentryOptions - Remove span imports and exports - Update mocks to remove span-related code Co-Authored-By: Claude Sonnet 4.5 --- packages/dart/lib/sentry.dart | 1 - packages/dart/lib/src/sentry_client.dart | 20 - packages/dart/lib/src/sentry_options.dart | 15 - .../src/telemetry/processing/processor.dart | 47 +- .../processing/processor_integration.dart | 29 +- .../test/mocks/mock_telemetry_processor.dart | 7 - .../dart/test/telemetry/span/span_test.dart | 512 ------------------ packages/flutter/test/mocks.dart | 6 - 8 files changed, 11 insertions(+), 626 deletions(-) delete mode 100644 packages/dart/test/telemetry/span/span_test.dart diff --git a/packages/dart/lib/sentry.dart b/packages/dart/lib/sentry.dart index ccfd5b5e8f..8d01a3181c 100644 --- a/packages/dart/lib/sentry.dart +++ b/packages/dart/lib/sentry.dart @@ -41,7 +41,6 @@ export 'src/sdk_lifecycle_hooks.dart'; export 'src/sentry_envelope.dart'; export 'src/sentry_envelope_item.dart'; export 'src/sentry_options.dart'; -export 'src/telemetry/sentry_trace_lifecycle.dart'; // ignore: invalid_export_of_internal_element export 'src/sentry_trace_origins.dart'; export 'src/span_data_convention.dart'; diff --git a/packages/dart/lib/src/sentry_client.dart b/packages/dart/lib/src/sentry_client.dart index 17b97fc0e1..02e6841e1d 100644 --- a/packages/dart/lib/src/sentry_client.dart +++ b/packages/dart/lib/src/sentry_client.dart @@ -18,7 +18,6 @@ import 'sentry_exception_factory.dart'; import 'sentry_options.dart'; import 'sentry_stack_trace_factory.dart'; import 'sentry_trace_context_header.dart'; -import 'telemetry/span/sentry_span_v2.dart'; import 'transport/client_report_transport.dart'; import 'transport/data_category.dart'; import 'transport/http_transport.dart'; @@ -490,25 +489,6 @@ class SentryClient { ); } - void captureSpan( - SentrySpanV2 span, { - Scope? scope, - }) { - switch (span) { - case UnsetSentrySpanV2(): - _options.log( - SentryLevel.warning, - "captureSpan: span is in an invalid state $UnsetSentrySpanV2.", - ); - case NoOpSentrySpanV2(): - return; - case RecordingSentrySpanV2 span: - // TODO(next-pr): add common attributes, merge scope attributes - - _options.telemetryProcessor.addSpan(span); - } - } - @internal FutureOr captureLog( SentryLog log, { diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index ab188f8e3e..f0b7472f3e 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -232,21 +232,6 @@ class SentryOptions { /// sent. Events are picked randomly. Default is null (disabled) double? sampleRate; - /// Chooses between two tracing systems. You can only use one at a time. - /// - /// [SentryTraceLifecycle.streaming] sends each span to Sentry as it finishes. - /// Use [Sentry.startSpan] to create spans. The older transaction APIs - /// ([Sentry.startTransaction], [ISentrySpan.startChild]) will do nothing. - /// - /// [SentryTraceLifecycle.static] collects all spans and sends them together - /// when the transaction ends. Use [Sentry.startTransaction] to create traces. - /// The newer span APIs ([Sentry.startSpan]) will do nothing. - /// - /// Integrations automatically switch to the correct API based on this setting. - /// - /// Defaults to [SentryTraceLifecycle.static]. - SentryTraceLifecycle traceLifecycle = SentryTraceLifecycle.static; - /// The ignoreErrors tells the SDK which errors should be not sent to the sentry server. /// If an null or an empty list is used, the SDK will send all transactions. /// To use regex add the `^` and the `$` to the string. diff --git a/packages/dart/lib/src/telemetry/processing/processor.dart b/packages/dart/lib/src/telemetry/processing/processor.dart index c2c3b47b27..2e4268bdb2 100644 --- a/packages/dart/lib/src/telemetry/processing/processor.dart +++ b/packages/dart/lib/src/telemetry/processing/processor.dart @@ -3,17 +3,13 @@ import 'dart:async'; import 'package:meta/meta.dart'; import '../../../sentry.dart'; -import '../span/sentry_span_v2.dart'; import 'buffer.dart'; /// Interface for processing and buffering telemetry data before sending. /// -/// Implementations collect spans and logs, buffering them until flushed. +/// Implementations collect logs, buffering them until flushed. /// This enables batching of telemetry data for efficient transport. abstract class TelemetryProcessor { - /// Adds a span to be processed and buffered. - void addSpan(RecordingSentrySpanV2 span); - /// Adds a log to be processed and buffered. void addLog(SentryLog log); @@ -26,72 +22,49 @@ abstract class TelemetryProcessor { /// Default telemetry processor that routes items to type-specific buffers. /// -/// Spans and logs are dispatched to their respective [TelemetryBuffer] +/// Logs are dispatched to their respective [TelemetryBuffer] /// instances. If no buffer is registered for a telemetry type, items are /// dropped with a warning. class DefaultTelemetryProcessor implements TelemetryProcessor { final SdkLogCallback _logger; - /// The buffer for span data, or `null` if span buffering is disabled. - @visibleForTesting - TelemetryBuffer? spanBuffer; - /// The buffer for log data, or `null` if log buffering is disabled. @visibleForTesting TelemetryBuffer? logBuffer; DefaultTelemetryProcessor( this._logger, { - this.spanBuffer, this.logBuffer, }); @override - void addSpan(RecordingSentrySpanV2 span) => _add(span); - - @override - void addLog(SentryLog log) => _add(log); - - void _add(dynamic item) { - final buffer = switch (item) { - RecordingSentrySpanV2 _ => spanBuffer, - SentryLog _ => logBuffer, - _ => null, - }; - - if (buffer == null) { + void addLog(SentryLog log) { + if (logBuffer == null) { _logger( SentryLevel.warning, - '$runtimeType: No buffer registered for ${item.runtimeType} - item was dropped', + '$runtimeType: No buffer registered for ${log.runtimeType} - item was dropped', ); return; } - buffer.add(item); + logBuffer!.add(log); } @override FutureOr flush() { _logger(SentryLevel.debug, '$runtimeType: Clearing buffers'); - final results = >[ - spanBuffer?.flush(), - logBuffer?.flush(), - ]; + final result = logBuffer?.flush(); - final futures = results.whereType().toList(); - if (futures.isEmpty) { - return null; + if (result is Future) { + return result; } - return Future.wait(futures).then((_) {}); + return null; } } class NoOpTelemetryProcessor implements TelemetryProcessor { - @override - void addSpan(RecordingSentrySpanV2 span) {} - @override void addLog(SentryLog log) {} diff --git a/packages/dart/lib/src/telemetry/processing/processor_integration.dart b/packages/dart/lib/src/telemetry/processing/processor_integration.dart index 5ac4c0f802..a2796882e5 100644 --- a/packages/dart/lib/src/telemetry/processing/processor_integration.dart +++ b/packages/dart/lib/src/telemetry/processing/processor_integration.dart @@ -1,20 +1,12 @@ import 'dart:async'; -import 'package:meta/meta.dart'; - import '../../../sentry.dart'; -import '../span/sentry_span_v2.dart'; import 'in_memory_buffer.dart'; import 'processor.dart'; class DefaultTelemetryProcessorIntegration extends Integration { static const integrationName = 'DefaultTelemetryProcessor'; - @visibleForTesting - final GroupKeyExtractor spanGroupKeyExtractor = - (RecordingSentrySpanV2 item) => - '${item.traceId}-${item.segmentSpan.spanId}'; - @override void call(Hub hub, SentryOptions options) { if (options.telemetryProcessor is! NoOpTelemetryProcessor) { @@ -26,8 +18,7 @@ class DefaultTelemetryProcessorIntegration extends Integration { } options.telemetryProcessor = DefaultTelemetryProcessor(options.log, - logBuffer: _createLogBuffer(options), - spanBuffer: _createSpanBuffer(options)); + logBuffer: _createLogBuffer(options)); options.sdk.addIntegration(integrationName); } @@ -40,22 +31,4 @@ class DefaultTelemetryProcessorIntegration extends Integration { items.map((item) => item).toList(), options.sdk); return options.transport.send(envelope).then((_) {}); }); - - GroupedInMemoryTelemetryBuffer _createSpanBuffer( - SentryOptions options) => - GroupedInMemoryTelemetryBuffer( - encoder: (RecordingSentrySpanV2 item) => - utf8JsonEncoder.convert(item.toJson()), - onFlush: (items) { - final futures = items.values.map((itemData) { - final dsc = itemData.$2.resolveDsc(); - final envelope = SentryEnvelope.fromSpansData( - itemData.$1, options.sdk, - traceContext: dsc); - return options.transport.send(envelope); - }).toList(); - if (futures.isEmpty) return null; - return Future.wait(futures).then((_) {}); - }, - groupKeyExtractor: spanGroupKeyExtractor); } diff --git a/packages/dart/test/mocks/mock_telemetry_processor.dart b/packages/dart/test/mocks/mock_telemetry_processor.dart index db1b80470d..a52fd97a2f 100644 --- a/packages/dart/test/mocks/mock_telemetry_processor.dart +++ b/packages/dart/test/mocks/mock_telemetry_processor.dart @@ -1,18 +1,11 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/telemetry/processing/processor.dart'; -import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; class MockTelemetryProcessor implements TelemetryProcessor { - final List addedSpans = []; final List addedLogs = []; int flushCalls = 0; int closeCalls = 0; - @override - void addSpan(RecordingSentrySpanV2 span) { - addedSpans.add(span); - } - @override void addLog(SentryLog log) { addedLogs.add(log); diff --git a/packages/dart/test/telemetry/span/span_test.dart b/packages/dart/test/telemetry/span/span_test.dart deleted file mode 100644 index 3bd8a372c3..0000000000 --- a/packages/dart/test/telemetry/span/span_test.dart +++ /dev/null @@ -1,512 +0,0 @@ -import 'package:sentry/sentry.dart'; -import 'package:sentry/src/telemetry/span/sentry_span_status_v2.dart'; -import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; -import 'package:test/test.dart'; - -import '../../test_utils.dart'; - -void main() { - group('RecordingSentrySpanV2', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('end finishes the span', () { - final span = fixture.createSpan(name: 'test-span'); - - span.end(); - - expect(span.endTimestamp, isNotNull); - expect(span.isEnded, isTrue); - }); - - test('end sets current time by default', () { - final span = fixture.createSpan(name: 'test-span'); - - final before = DateTime.now().toUtc(); - span.end(); - final after = DateTime.now().toUtc(); - - expect(span.endTimestamp, isNotNull); - expect(span.endTimestamp!.isAfter(before) || span.endTimestamp == before, - isTrue, - reason: 'endTimestamp should be >= time before end() was called'); - expect(span.endTimestamp!.isBefore(after) || span.endTimestamp == after, - isTrue, - reason: 'endTimestamp should be <= time after end() was called'); - }); - - test('end with custom timestamp sets end time', () { - final span = fixture.createSpan(name: 'test-span'); - final endTime = DateTime.now().add(Duration(seconds: 5)).toUtc(); - - span.end(endTimestamp: endTime); - - expect(span.endTimestamp, equals(endTime)); - }); - - test('end sets endTimestamp as UTC', () { - final span1 = fixture.createSpan(name: 'test-span'); - span1.end(); - expect(span1.endTimestamp!.isUtc, isTrue); - - final span2 = fixture.createSpan(name: 'test-span'); - // Should transform non-utc time to utc - span2.end(endTimestamp: DateTime.now()); - expect(span2.endTimestamp!.isUtc, isTrue); - }); - - test('end calls onSpanEnded callback', () { - RecordingSentrySpanV2? capturedSpan; - final span = fixture.createSpan( - name: 'test-span', - onSpanEnded: (s) => capturedSpan = s, - ); - - span.end(); - - expect(capturedSpan, same(span)); - }); - - test('end is idempotent once finished', () { - var callCount = 0; - final span = fixture.createSpan( - name: 'test-span', - onSpanEnded: (_) => callCount++, - ); - final firstEndTimestamp = DateTime.utc(2024, 1, 1); - final secondEndTimestamp = DateTime.utc(2024, 1, 2); - - span.end(endTimestamp: firstEndTimestamp); - span.end(endTimestamp: secondEndTimestamp); - - expect(span.endTimestamp, equals(firstEndTimestamp)); - expect(span.isEnded, isTrue); - expect(callCount, 1); - }); - - test('setAttribute sets single attribute', () { - final span = fixture.createSpan(name: 'test-span'); - - final attributeValue = SentryAttribute.string('value'); - span.setAttribute('key', attributeValue); - - expect(span.attributes, equals({'key': attributeValue})); - }); - - test('setAttributes sets multiple attributes', () { - final span = fixture.createSpan(name: 'test-span'); - - final attributes = { - 'key1': SentryAttribute.string('value1'), - 'key2': SentryAttribute.int(42), - }; - span.setAttributes(attributes); - - expect(span.attributes, equals(attributes)); - }); - - test('setName sets span name', () { - final span = fixture.createSpan(name: 'initial-name'); - - span.name = 'updated-name'; - expect(span.name, equals('updated-name')); - }); - - test('setStatus sets span status', () { - final span = fixture.createSpan(name: 'test-span'); - - span.status = SentrySpanStatusV2.ok; - expect(span.status, equals(SentrySpanStatusV2.ok)); - - span.status = SentrySpanStatusV2.error; - expect(span.status, equals(SentrySpanStatusV2.error)); - }); - - test('parentSpan returns the parent span', () { - final parent = fixture.createSpan(name: 'parent'); - final child = fixture.createSpan(name: 'child', parentSpan: parent); - - expect(child.parentSpan, equals(parent)); - }); - - test('parentSpan returns null for root span', () { - final span = fixture.createSpan(name: 'root'); - - expect(span.parentSpan, isNull); - }); - - test('name returns the span name', () { - final span = fixture.createSpan(name: 'my-span-name'); - - expect(span.name, equals('my-span-name')); - }); - - test('spanId is created when span is created', () { - final span = fixture.createSpan(name: 'test-span'); - - expect(span.spanId.toString(), isNot(SpanId.empty().toString())); - }); - - group('segmentSpan', () { - test('returns null when parentSpan is null', () { - final span = fixture.createSpan(name: 'root-span'); - - expect(span.segmentSpan, same(span)); - }); - - test('returns parent segmentSpan when parentSpan is set', () { - final root = fixture.createSpan(name: 'root'); - final child = fixture.createSpan(name: 'child', parentSpan: root); - - expect(child.segmentSpan, same(root)); - }); - - test('returns root segmentSpan for deeply nested spans', () { - final root = fixture.createSpan(name: 'root'); - final child = fixture.createSpan(name: 'child', parentSpan: root); - final grandchild = - fixture.createSpan(name: 'grandchild', parentSpan: child); - final greatGrandchild = fixture.createSpan( - name: 'great-grandchild', parentSpan: grandchild); - - expect(grandchild.segmentSpan, same(root)); - expect(greatGrandchild.segmentSpan, same(root)); - }); - }); - - group('traceId', () { - test('uses defaultTraceId when no parent', () { - final traceId = SentryId.newId(); - final span = fixture.createSpan(name: 'test-span', traceId: traceId); - - expect(span.traceId, equals(traceId)); - }); - - test('child span inherits traceId from parent', () { - final parent = fixture.createSpan(name: 'parent'); - final child = fixture.createSpan(name: 'child', parentSpan: parent); - - expect(child.traceId, equals(parent.traceId)); - }); - - test('child span ignores defaultTraceId when parent exists', () { - final parentTraceId = SentryId.newId(); - final differentTraceId = SentryId.newId(); - - final parent = - fixture.createSpan(name: 'parent', traceId: parentTraceId); - final child = fixture.createSpan( - name: 'child', - parentSpan: parent, - traceId: differentTraceId, - ); - - expect(child.traceId, equals(parentTraceId)); - expect(child.traceId, isNot(equals(differentTraceId))); - }); - }); - - group('when resolving DSC', () { - test('creates DSC on first access for root span', () { - final span = fixture.createSpan(name: 'root-span'); - - final dsc = span.resolveDsc(); - - expect(dsc, isNotNull); - expect(dsc.publicKey, equals('publicKey')); - }); - - test('returns same DSC on subsequent access', () { - final span = fixture.createSpan(name: 'root-span'); - - final dsc1 = span.resolveDsc(); - final dsc2 = span.resolveDsc(); - - expect(identical(dsc1, dsc2), isTrue); - }); - - test('returns DSC from segment span for child span', () { - final root = fixture.createSpan(name: 'root'); - final child = fixture.createSpan(name: 'child', parentSpan: root); - - final rootDsc = root.resolveDsc(); - final childDsc = child.resolveDsc(); - - expect(identical(rootDsc, childDsc), isTrue); - }); - - test('returns same DSC for deeply nested spans', () { - final root = fixture.createSpan(name: 'root'); - final child = fixture.createSpan(name: 'child', parentSpan: root); - final grandchild = - fixture.createSpan(name: 'grandchild', parentSpan: child); - - final rootDsc = root.resolveDsc(); - final childDsc = child.resolveDsc(); - final grandchildDsc = grandchild.resolveDsc(); - - expect(identical(rootDsc, childDsc), isTrue); - expect(identical(rootDsc, grandchildDsc), isTrue); - }); - - test('freezes DSC after first access', () { - var callCount = 0; - final span = fixture.createSpan( - name: 'root-span', - dscCreator: (s) { - callCount++; - return SentryTraceContextHeader(SentryId.newId(), 'publicKey'); - }, - ); - - span.resolveDsc(); - span.resolveDsc(); - span.resolveDsc(); - - expect(callCount, equals(1), - reason: 'DSC creator should only be called once'); - }); - }); - - group('when accessing samplingDecision', () { - test('returns stored decision for root span', () { - final decision = SentryTracesSamplingDecision( - true, - sampleRate: 0.5, - sampleRand: 0.25, - ); - final span = fixture.createSpan( - name: 'root-span', - samplingDecision: decision, - ); - - expect(span.samplingDecision.sampled, isTrue); - expect(span.samplingDecision.sampleRate, equals(0.5)); - expect(span.samplingDecision.sampleRand, equals(0.25)); - }); - - test('returns inherited decision for child span', () { - final decision = SentryTracesSamplingDecision( - true, - sampleRate: 0.75, - sampleRand: 0.1, - ); - final root = fixture.createSpan( - name: 'root', - samplingDecision: decision, - ); - final child = fixture.createSpan(name: 'child', parentSpan: root); - - expect(child.samplingDecision.sampled, - equals(root.samplingDecision.sampled)); - expect(child.samplingDecision.sampleRate, - equals(root.samplingDecision.sampleRate)); - expect(child.samplingDecision.sampleRand, - equals(root.samplingDecision.sampleRand)); - }); - - test('returns root decision for deeply nested span', () { - final decision = SentryTracesSamplingDecision( - true, - sampleRate: 0.3, - sampleRand: 0.99, - ); - final root = fixture.createSpan( - name: 'root', - samplingDecision: decision, - ); - final child = fixture.createSpan(name: 'child', parentSpan: root); - final grandchild = - fixture.createSpan(name: 'grandchild', parentSpan: child); - - expect(grandchild.samplingDecision.sampled, equals(decision.sampled)); - expect(grandchild.samplingDecision.sampleRate, - equals(decision.sampleRate)); - expect(grandchild.samplingDecision.sampleRand, - equals(decision.sampleRand)); - }); - }); - - group('toJson', () { - test('serializes basic span without parent', () { - final span = fixture.createSpan(name: 'test-span'); - span.end(); - - final json = span.toJson(); - - expect(json['trace_id'], equals(span.traceId.toString())); - expect(json['span_id'], equals(span.spanId.toString())); - expect(json['name'], equals('test-span')); - expect(json['is_segment'], isTrue); - expect(json['status'], equals('ok')); - expect(json['start_timestamp'], isA()); - expect(json['end_timestamp'], isA()); - expect(json.containsKey('parent_span_id'), isFalse); - }); - - test('serializes span with parent', () { - final parent = fixture.createSpan(name: 'parent'); - final child = fixture.createSpan(name: 'child', parentSpan: parent); - child.end(); - - final json = child.toJson(); - - expect(json['parent_span_id'], equals(parent.spanId.toString())); - expect(json['is_segment'], isFalse); - }); - - test('serializes span with error status', () { - final span = fixture.createSpan(name: 'test-span'); - span.status = SentrySpanStatusV2.error; - span.end(); - - final json = span.toJson(); - - expect(json['status'], equals('error')); - }); - - test('serializes span with attributes', () { - final span = fixture.createSpan(name: 'test-span'); - span.setAttribute('string_attr', SentryAttribute.string('value')); - span.setAttribute('int_attr', SentryAttribute.int(42)); - span.setAttribute('bool_attr', SentryAttribute.bool(true)); - span.setAttribute('double_attr', SentryAttribute.double(3.14)); - span.end(); - - final json = span.toJson(); - - expect(json.containsKey('attributes'), isTrue); - final attributes = Map.from(json['attributes']); - - expect(attributes['string_attr'], {'value': 'value', 'type': 'string'}); - expect(attributes['int_attr'], {'value': 42, 'type': 'integer'}); - expect(attributes['bool_attr'], {'value': true, 'type': 'boolean'}); - expect(attributes['double_attr'], {'value': 3.14, 'type': 'double'}); - }); - - test('end_timestamp is null when span is not finished', () { - final span = fixture.createSpan(name: 'test-span'); - - final json = span.toJson(); - - expect(json['end_timestamp'], isNull); - }); - - test( - 'timestamps are serialized as unix seconds with microsecond precision', - () { - final span = fixture.createSpan(name: 'test-span'); - final customEndTime = DateTime.utc(2024, 6, 15, 12, 30, 45, 123, 456); - span.end(endTimestamp: customEndTime); - - final json = span.toJson(); - - final endTimestamp = json['end_timestamp'] as double; - final expectedMicros = customEndTime.microsecondsSinceEpoch; - final expectedSeconds = expectedMicros / 1000000; - - expect(endTimestamp, closeTo(expectedSeconds, 0.000001)); - }); - - test('serializes updated name', () { - final span = fixture.createSpan(name: 'original-name'); - span.name = 'updated-name'; - span.end(); - - final json = span.toJson(); - - expect(json['name'], equals('updated-name')); - }); - }); - }); - - group('NoOpSentrySpanV2', () { - test('operations do not throw', () { - const span = NoOpSentrySpanV2(); - - // All operations should be no-ops and not throw - span.end(); - span.end(endTimestamp: DateTime.now()); - span.setAttribute('key', SentryAttribute.string('value')); - span.setAttributes({'key': SentryAttribute.string('value')}); - span.removeAttribute('key'); - span.name = 'name'; - span.status = SentrySpanStatusV2.ok; - span.status = SentrySpanStatusV2.error; - }); - - test('returns default values', () { - const span = NoOpSentrySpanV2(); - - expect(span.spanId.toString(), SpanId.empty().toString()); - expect(span.traceId.toString(), SentryId.empty().toString()); - expect(span.name, 'NoOpSpan'); - expect(span.status, SentrySpanStatusV2.ok); - expect(span.parentSpan, isNull); - expect(span.endTimestamp, isNull); - expect(span.attributes, isEmpty); - }); - }); - - group('UnsetSentrySpanV2', () { - test('all APIs throw to prevent accidental use', () { - const span = UnsetSentrySpanV2(); - - expect(() => span.spanId, throwsA(isA())); - expect(() => span.traceId, throwsA(isA())); - expect(() => span.name, throwsA(isA())); - expect(() => span.status, throwsA(isA())); - expect(() => span.parentSpan, throwsA(isA())); - expect(() => span.endTimestamp, throwsA(isA())); - expect(() => span.attributes, throwsA(isA())); - - expect(() => span.name = 'foo', throwsA(isA())); - expect(() => span.status = SentrySpanStatusV2.ok, - throwsA(isA())); - expect(() => span.setAttribute('k', SentryAttribute.string('v')), - throwsA(isA())); - expect(() => span.setAttributes({'k': SentryAttribute.string('v')}), - throwsA(isA())); - expect( - () => span.removeAttribute('k'), throwsA(isA())); - expect(() => span.end(), throwsA(isA())); - }); - }); -} - -class Fixture { - final options = defaultTestOptions(); - - RecordingSentrySpanV2 createSpan({ - required String name, - RecordingSentrySpanV2? parentSpan, - SentryId? traceId, - OnSpanEndCallback? onSpanEnded, - DscCreatorCallback? dscCreator, - SentryTracesSamplingDecision? samplingDecision, - }) { - final defaultDscCreator = (RecordingSentrySpanV2 span) => - SentryTraceContextHeader(SentryId.newId(), 'publicKey'); - - if (parentSpan != null) { - return RecordingSentrySpanV2.child( - parent: parentSpan, - name: name, - onSpanEnd: onSpanEnded ?? (_) {}, - clock: options.clock, - dscCreator: dscCreator ?? defaultDscCreator, - ); - } - return RecordingSentrySpanV2.root( - name: name, - traceId: traceId ?? SentryId.newId(), - onSpanEnd: onSpanEnded ?? (_) {}, - clock: options.clock, - dscCreator: dscCreator ?? defaultDscCreator, - samplingDecision: samplingDecision ?? SentryTracesSamplingDecision(true), - ); - } -} diff --git a/packages/flutter/test/mocks.dart b/packages/flutter/test/mocks.dart index 34ddbf8a9d..10dc8e259d 100644 --- a/packages/flutter/test/mocks.dart +++ b/packages/flutter/test/mocks.dart @@ -240,16 +240,10 @@ class MockLogItem { } class MockTelemetryProcessor implements TelemetryProcessor { - final List addedSpans = []; final List addedLogs = []; int flushCalls = 0; int closeCalls = 0; - @override - void addSpan(RecordingSentrySpanV2 span) { - addedSpans.add(span); - } - @override void addLog(SentryLog log) { addedLogs.add(log); From 2c617bb0a7aabccdf20cd2fc4e8d7e613442d881 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 17:54:28 +0100 Subject: [PATCH 03/79] Remove span-related tests from sentry_client_test Co-Authored-By: Claude Sonnet 4.5 --- packages/dart/test/sentry_client_test.dart | 48 ---------------------- 1 file changed, 48 deletions(-) diff --git a/packages/dart/test/sentry_client_test.dart b/packages/dart/test/sentry_client_test.dart index 32c51484ef..e7c46ab55e 100644 --- a/packages/dart/test/sentry_client_test.dart +++ b/packages/dart/test/sentry_client_test.dart @@ -16,7 +16,6 @@ import 'package:sentry/src/transport/data_category.dart'; import 'package:sentry/src/transport/noop_transport.dart'; import 'package:sentry/src/transport/spotlight_http_transport.dart'; import 'package:sentry/src/utils/iterable_utils.dart'; -import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; import 'package:test/test.dart'; import 'package:mockito/mockito.dart'; import 'package:http/http.dart' as http; @@ -2061,53 +2060,6 @@ void main() { }); }); - group('SentryClient', () { - group('when capturing span', () { - late Fixture fixture; - late MockTelemetryProcessor processor; - - setUp(() { - fixture = Fixture(); - processor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = processor; - }); - - test('adds recording span to telemetry processor', () { - final client = fixture.getSut(); - - final span = RecordingSentrySpanV2.root( - name: 'test-span', - traceId: SentryId.newId(), - onSpanEnd: (_) {}, - clock: fixture.options.clock, - dscCreator: (s) => SentryTraceContextHeader(SentryId.newId(), 'key'), - samplingDecision: SentryTracesSamplingDecision(true), - ); - - client.captureSpan(span); - - expect(processor.addedSpans, hasLength(1)); - expect(processor.addedSpans.first, equals(span)); - }); - - test('does nothing for NoOpSentrySpanV2', () { - final client = fixture.getSut(); - - client.captureSpan(const NoOpSentrySpanV2()); - - expect(processor.addedSpans, isEmpty); - }); - - test('does nothing for UnsetSentrySpanV2', () { - final client = fixture.getSut(); - - client.captureSpan(const UnsetSentrySpanV2()); - - expect(processor.addedSpans, isEmpty); - }); - }); - }); - group('SentryClient captures envelope', () { late Fixture fixture; final fakeEnvelope = getFakeEnvelope(); From 6ef8c3c8aa2951ee73bc142b5ad6e2d22e824574 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 17:54:54 +0100 Subject: [PATCH 04/79] Remove span-related processor tests Co-Authored-By: Claude Sonnet 4.5 --- .../processor_integration_test.dart | 161 ---------------- .../telemetry/processing/processor_test.dart | 173 ------------------ 2 files changed, 334 deletions(-) delete mode 100644 packages/dart/test/telemetry/processing/processor_integration_test.dart delete mode 100644 packages/dart/test/telemetry/processing/processor_test.dart diff --git a/packages/dart/test/telemetry/processing/processor_integration_test.dart b/packages/dart/test/telemetry/processing/processor_integration_test.dart deleted file mode 100644 index 2fedd77d1e..0000000000 --- a/packages/dart/test/telemetry/processing/processor_integration_test.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:sentry/sentry.dart'; -import 'package:sentry/src/telemetry/processing/in_memory_buffer.dart'; -import 'package:sentry/src/telemetry/processing/processor.dart'; -import 'package:sentry/src/telemetry/processing/processor_integration.dart'; -import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; -import 'package:test/test.dart'; - -import '../../mocks/mock_hub.dart'; -import '../../mocks/mock_transport.dart'; -import '../../test_utils.dart'; - -void main() { - group('DefaultTelemetryProcessorIntegration', () { - late _Fixture fixture; - - setUp(() { - fixture = _Fixture(); - }); - - test( - 'sets up DefaultTelemetryProcessor when NoOpTelemetryProcessor is active', - () { - final options = fixture.options; - expect(options.telemetryProcessor, isA()); - - fixture.getSut().call(fixture.hub, options); - - expect(options.telemetryProcessor, isA()); - }); - - test('does not override existing telemetry processor', () { - final options = fixture.options; - final existingProcessor = DefaultTelemetryProcessor(options.log); - options.telemetryProcessor = existingProcessor; - - fixture.getSut().call(fixture.hub, options); - - expect(identical(options.telemetryProcessor, existingProcessor), isTrue); - }); - - test('adds integration name to SDK', () { - final options = fixture.options; - - fixture.getSut().call(fixture.hub, options); - - expect( - options.sdk.integrations, - contains(DefaultTelemetryProcessorIntegration.integrationName), - ); - }); - - test('configures log buffer as InMemoryTelemetryBuffer', () { - final options = fixture.options; - - fixture.getSut().call(fixture.hub, options); - - final processor = options.telemetryProcessor as DefaultTelemetryProcessor; - expect(processor.logBuffer, isA>()); - }); - - test('configures span buffer as GroupedInMemoryTelemetryBuffer', () { - final options = fixture.options; - - fixture.getSut().call(fixture.hub, options); - - final processor = options.telemetryProcessor as DefaultTelemetryProcessor; - expect(processor.spanBuffer, - isA>()); - }); - - test('configures span buffer with group key extractor', () { - final options = fixture.options; - - final integration = fixture.getSut(); - integration.call(fixture.hub, options); - - final processor = options.telemetryProcessor as DefaultTelemetryProcessor; - - expect( - (processor.spanBuffer - as GroupedInMemoryTelemetryBuffer) - .groupKey, - integration.spanGroupKeyExtractor); - }); - - test('spanGroupKeyExtractor uses traceId-spanId format', () { - final options = fixture.options; - - final integration = fixture.getSut(); - integration.call(fixture.hub, options); - - final span = fixture.createSpan(); - final key = integration.spanGroupKeyExtractor(span); - - expect(key, '${span.traceId}-${span.spanId}'); - }); - - group('flush', () { - test('log reaches transport as envelope', () async { - final options = fixture.options; - fixture.getSut().call(fixture.hub, options); - - final processor = - options.telemetryProcessor as DefaultTelemetryProcessor; - processor.addLog(fixture.createLog()); - await processor.flush(); - - expect(fixture.transport.envelopes, hasLength(1)); - }); - - test('span reaches transport as envelope', () async { - final options = fixture.options; - fixture.getSut().call(fixture.hub, options); - - final processor = - options.telemetryProcessor as DefaultTelemetryProcessor; - final span = fixture.createSpan(); - span.end(); - processor.addSpan(span); - await processor.flush(); - - expect(fixture.transport.envelopes, hasLength(1)); - }); - }); - }); -} - -class _Fixture { - final hub = MockHub(); - final transport = MockTransport(); - late SentryOptions options; - - _Fixture() { - options = defaultTestOptions()..transport = transport; - } - - DefaultTelemetryProcessorIntegration getSut() { - return DefaultTelemetryProcessorIntegration(); - } - - SentryLog createLog() { - return SentryLog( - timestamp: DateTime.now().toUtc(), - level: SentryLogLevel.info, - body: 'test log', - attributes: {}, - ); - } - - RecordingSentrySpanV2 createSpan() { - return RecordingSentrySpanV2.root( - name: 'test-span', - traceId: SentryId.newId(), - onSpanEnd: (_) {}, - clock: options.clock, - dscCreator: (_) => - SentryTraceContextHeader(SentryId.newId(), 'publicKey'), - samplingDecision: SentryTracesSamplingDecision(true), - ); - } -} diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart deleted file mode 100644 index 6e20939b09..0000000000 --- a/packages/dart/test/telemetry/processing/processor_test.dart +++ /dev/null @@ -1,173 +0,0 @@ -import 'dart:async'; - -import 'package:sentry/sentry.dart'; -import 'package:sentry/src/telemetry/processing/processor.dart'; -import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; -import 'package:test/test.dart'; - -import '../../mocks/mock_telemetry_buffer.dart'; -import '../../test_utils.dart'; - -void main() { - group('DefaultTelemetryProcessor', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - group('addSpan', () { - test('routes span to span buffer', () { - final mockSpanBuffer = MockTelemetryBuffer(); - final processor = fixture.getSut(spanBuffer: mockSpanBuffer); - - final span = fixture.createSpan(); - span.end(); - processor.addSpan(span); - - expect(mockSpanBuffer.addedItems.length, 1); - expect(mockSpanBuffer.addedItems.first, span); - }); - - test('does not throw when no span buffer registered', () { - final processor = fixture.getSut(); - processor.spanBuffer = null; - - final span = fixture.createSpan(); - span.end(); - processor.addSpan(span); - - // Nothing to assert - just verifying no exception thrown - }); - }); - - group('addLog', () { - test('routes log to log buffer', () { - final mockLogBuffer = MockTelemetryBuffer(); - final processor = - fixture.getSut(enableLogs: true, logBuffer: mockLogBuffer); - - final log = fixture.createLog(); - processor.addLog(log); - - expect(mockLogBuffer.addedItems.length, 1); - expect(mockLogBuffer.addedItems.first, log); - }); - - test('does not throw when no log buffer registered', () { - final processor = fixture.getSut(); - processor.logBuffer = null; - - final log = fixture.createLog(); - processor.addLog(log); - }); - }); - - group('flush', () { - test('flushes all registered buffers', () async { - final mockSpanBuffer = MockTelemetryBuffer(); - final mockLogBuffer = MockTelemetryBuffer(); - final processor = fixture.getSut( - enableLogs: true, - spanBuffer: mockSpanBuffer, - logBuffer: mockLogBuffer, - ); - - await processor.flush(); - - expect(mockSpanBuffer.flushCallCount, 1); - expect(mockLogBuffer.flushCallCount, 1); - }); - - test('flushes only span buffer when log buffer is null', () async { - final mockSpanBuffer = MockTelemetryBuffer(); - final processor = fixture.getSut(spanBuffer: mockSpanBuffer); - processor.logBuffer = null; - - await processor.flush(); - - expect(mockSpanBuffer.flushCallCount, 1); - }); - - test('returns sync (null) when all buffers flush synchronously', () { - final mockSpanBuffer = - MockTelemetryBuffer(asyncFlush: false); - final processor = fixture.getSut(spanBuffer: mockSpanBuffer); - processor.logBuffer = null; - - final result = processor.flush(); - - expect(result, isNull); - }); - - test('returns Future when at least one buffer flushes asynchronously', - () async { - final mockSpanBuffer = - MockTelemetryBuffer(asyncFlush: true); - final processor = fixture.getSut(spanBuffer: mockSpanBuffer); - processor.logBuffer = null; - - final result = processor.flush(); - - expect(result, isA()); - await result; - }); - }); - }); -} - -class Fixture { - late SentryOptions options; - - Fixture() { - options = defaultTestOptions(); - } - - DefaultTelemetryProcessor getSut({ - bool enableLogs = false, - MockTelemetryBuffer? spanBuffer, - MockTelemetryBuffer? logBuffer, - }) { - options.enableLogs = enableLogs; - return DefaultTelemetryProcessor( - options.log, - spanBuffer: spanBuffer, - logBuffer: logBuffer, - ); - } - - RecordingSentrySpanV2 createSpan({String name = 'test-span'}) { - return RecordingSentrySpanV2.root( - name: name, - traceId: SentryId.newId(), - onSpanEnd: (_) {}, - clock: options.clock, - dscCreator: (_) => - SentryTraceContextHeader(SentryId.newId(), 'publicKey'), - samplingDecision: SentryTracesSamplingDecision(true), - ); - } - - RecordingSentrySpanV2 createChildSpan({ - required RecordingSentrySpanV2 parent, - String name = 'child-span', - }) { - return RecordingSentrySpanV2.child( - parent: parent, - name: name, - onSpanEnd: (_) {}, - clock: options.clock, - dscCreator: (_) => - SentryTraceContextHeader(SentryId.newId(), 'publicKey'), - ); - } - - SentryLog createLog({String body = 'test log'}) { - return SentryLog( - timestamp: DateTime.now().toUtc(), - level: SentryLogLevel.info, - body: body, - attributes: {}, - ); - } -} From 3ca4c08a9cb2c79bd7eacfc152745f31d6e5ce5b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 17:57:26 +0100 Subject: [PATCH 05/79] Remove span import from Flutter mocks Co-Authored-By: Claude Sonnet 4.5 --- packages/flutter/test/mocks.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flutter/test/mocks.dart b/packages/flutter/test/mocks.dart index 10dc8e259d..af190bf78e 100644 --- a/packages/flutter/test/mocks.dart +++ b/packages/flutter/test/mocks.dart @@ -9,7 +9,6 @@ import 'package:mockito/mockito.dart'; import 'package:sentry/src/platform/platform.dart'; import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry/src/telemetry/processing/processor.dart'; -import 'package:sentry/src/telemetry/span/sentry_span_v2.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/frames_tracking/sentry_delayed_frames_tracker.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; From 9b34042ffaf502d153876bbc518cd9fd8a6f3236 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 18:52:04 +0100 Subject: [PATCH 06/79] Fix wiring up --- packages/dart/lib/src/sentry.dart | 2 ++ packages/dart/test/sentry_test.dart | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/dart/lib/src/sentry.dart b/packages/dart/lib/src/sentry.dart index f65bc721b1..06821ccedf 100644 --- a/packages/dart/lib/src/sentry.dart +++ b/packages/dart/lib/src/sentry.dart @@ -23,6 +23,7 @@ import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; import 'sentry_run_zoned_guarded.dart'; +import 'telemetry/processing/processor_integration.dart'; import 'tracing.dart'; import 'transport/data_category.dart'; import 'transport/task_queue.dart'; @@ -111,6 +112,7 @@ class Sentry { options.addIntegration(FeatureFlagsIntegration()); options.addIntegration(LogsEnricherIntegration()); + options.addIntegration(DefaultTelemetryProcessorIntegration()); options.addEventProcessor(EnricherEventProcessor(options)); options.addEventProcessor(ExceptionEventProcessor(options)); diff --git a/packages/dart/test/sentry_test.dart b/packages/dart/test/sentry_test.dart index 9230d71396..1c03c84b5e 100644 --- a/packages/dart/test/sentry_test.dart +++ b/packages/dart/test/sentry_test.dart @@ -8,6 +8,7 @@ import 'package:sentry/src/dart_exception_type_identifier.dart'; import 'package:sentry/src/event_processor/deduplication_event_processor.dart'; import 'package:sentry/src/logs_enricher_integration.dart'; import 'package:sentry/src/feature_flags_integration.dart'; +import 'package:sentry/src/telemetry/processing/processor_integration.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -319,6 +320,27 @@ void main() { ); }); + test('should add DefaultTelemetryProcessorIntegration', () async { + late SentryOptions optionsReference; + final options = defaultTestOptions(); + + await Sentry.init( + options: options, + (options) { + options.dsn = fakeDsn; + optionsReference = options; + }, + appRunner: appRunner, + ); + + expect( + optionsReference.integrations + .whereType() + .length, + 1, + ); + }); + test('should add only web compatible default integrations', () async { final options = defaultTestOptions(); await Sentry.init( From e0b564cd7a6420f6aae08e3a22c860f2ed9231b9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 19:32:19 +0100 Subject: [PATCH 07/79] Update --- .../processing/in_memory_buffer.dart | 35 ------ .../telemetry/processing/buffer_test.dart | 111 +----------------- .../processor_integration_test.dart | 97 +++++++++++++++ .../telemetry/processing/processor_test.dart | 104 ++++++++++++++++ 4 files changed, 202 insertions(+), 145 deletions(-) create mode 100644 packages/dart/test/telemetry/processing/processor_integration_test.dart create mode 100644 packages/dart/test/telemetry/processing/processor_test.dart diff --git a/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart b/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart index 9b62344e52..21809f9740 100644 --- a/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart +++ b/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart @@ -12,9 +12,6 @@ typedef OnFlushCallback = FutureOr Function(T data); /// Encodes an item of type [T] into bytes. typedef ItemEncoder = List Function(T item); -/// Extracts a grouping key from items of type [T]. -typedef GroupKeyExtractor = String Function(T item); - /// Base class for in-memory telemetry buffers. /// /// Buffers telemetry items in memory and flushes them when either the @@ -146,35 +143,3 @@ final class InMemoryTelemetryBuffer @override bool get _isEmpty => _storage.isEmpty; } - -/// In-memory buffer that groups telemetry items by a key. -/// -/// Same idea as [InMemoryTelemetryBuffer], but grouped. -final class GroupedInMemoryTelemetryBuffer - extends _BaseInMemoryTelemetryBuffer>, T)>> { - final GroupKeyExtractor _groupKey; - - @visibleForTesting - GroupKeyExtractor get groupKey => _groupKey; - - GroupedInMemoryTelemetryBuffer({ - required super.encoder, - required super.onFlush, - required GroupKeyExtractor groupKeyExtractor, - super.config, - }) : _groupKey = groupKeyExtractor, - super(initialStorage: {}); - - @override - Map>, T)> _createEmptyStorage() => {}; - - @override - void _store(List encoded, T item) { - final key = _groupKey(item); - final bucket = _storage.putIfAbsent(key, () => ([], item)); - bucket.$1.add(encoded); - } - - @override - bool get _isEmpty => _storage.isEmpty; -} diff --git a/packages/dart/test/telemetry/processing/buffer_test.dart b/packages/dart/test/telemetry/processing/buffer_test.dart index e74cb81d01..fe5335f743 100644 --- a/packages/dart/test/telemetry/processing/buffer_test.dart +++ b/packages/dart/test/telemetry/processing/buffer_test.dart @@ -160,96 +160,12 @@ void main() { expect(fixture.flushCallCount, 1); }); }); - - group('GroupedInMemoryTelemetryBuffer', () { - late _GroupedFixture fixture; - - setUp(() { - fixture = _GroupedFixture(); - }); - - test('items are grouped by key', () async { - final buffer = fixture.getSut( - groupKeyExtractor: (item) => item.group, - ); - - buffer.add(_TestItem('item1', group: 'group1')); - buffer.add(_TestItem('item2', group: 'group2')); - buffer.add(_TestItem('item3', group: 'group1')); - - await buffer.flush(); - - expect(fixture.flushCallCount, 1); - expect(fixture.flushedGroups.keys, containsAll(['group1', 'group2'])); - expect( - fixture.flushedGroups['group1']?.$1, hasLength(2)); // item1 and item3 - expect(fixture.flushedGroups['group2']?.$1, hasLength(1)); // item2 - }); - - test('items are flushed after timeout', () async { - final flushTimeout = Duration(milliseconds: 1); - final buffer = fixture.getSut( - config: TelemetryBufferConfig(flushTimeout: flushTimeout), - groupKeyExtractor: (item) => item.group, - ); - - buffer.add(_TestItem('item1', group: 'a')); - buffer.add(_TestItem('item2', group: 'b')); - - expect(fixture.flushedGroups, isEmpty); - - await Future.delayed(flushTimeout + Duration(milliseconds: 10)); - - expect(fixture.flushCallCount, 1); - expect(fixture.flushedGroups.keys, hasLength(2)); - }); - - test('flush with empty buffer returns null', () async { - final buffer = fixture.getSut( - groupKeyExtractor: (item) => item.group, - ); - - final result = buffer.flush(); - - expect(result, isNull); - expect(fixture.flushedGroups, isEmpty); - }); - - test('buffer is cleared after flush', () async { - final buffer = fixture.getSut( - groupKeyExtractor: (item) => item.group, - ); - - buffer.add(_TestItem('item1', group: 'a')); - await buffer.flush(); - - expect(fixture.flushCallCount, 1); - - fixture.reset(); - final result = buffer.flush(); - - expect(result, isNull); - expect(fixture.flushCallCount, 0); - }); - - test('onFlush receives Map>>', () async { - final buffer = fixture.getSut( - groupKeyExtractor: (item) => item.group, - ); - - buffer.add(_TestItem('item1', group: 'myGroup')); - await buffer.flush(); - - expect(fixture.flushedGroups.containsKey('myGroup'), isTrue); - }); - }); } class _TestItem { final String id; - final String group; - _TestItem(this.id, {this.group = 'default'}); + _TestItem(this.id); Map toJson() => {'id': id}; } @@ -283,28 +199,3 @@ class _SimpleFixture { flushCallCount = 0; } } - -class _GroupedFixture { - Map>, _TestItem)> flushedGroups = {}; - int flushCallCount = 0; - - GroupedInMemoryTelemetryBuffer<_TestItem> getSut({ - required GroupKeyExtractor<_TestItem> groupKeyExtractor, - TelemetryBufferConfig config = const TelemetryBufferConfig(), - }) { - return GroupedInMemoryTelemetryBuffer<_TestItem>( - encoder: (item) => utf8.encode(jsonEncode(item.toJson())), - onFlush: (groups) { - flushCallCount++; - flushedGroups = groups; - }, - groupKeyExtractor: groupKeyExtractor, - config: config, - ); - } - - void reset() { - flushedGroups = {}; - flushCallCount = 0; - } -} diff --git a/packages/dart/test/telemetry/processing/processor_integration_test.dart b/packages/dart/test/telemetry/processing/processor_integration_test.dart new file mode 100644 index 0000000000..009a6c3613 --- /dev/null +++ b/packages/dart/test/telemetry/processing/processor_integration_test.dart @@ -0,0 +1,97 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/processing/in_memory_buffer.dart'; +import 'package:sentry/src/telemetry/processing/processor.dart'; +import 'package:sentry/src/telemetry/processing/processor_integration.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_hub.dart'; +import '../../mocks/mock_transport.dart'; +import '../../test_utils.dart'; + +void main() { + group('DefaultTelemetryProcessorIntegration', () { + late _Fixture fixture; + + setUp(() { + fixture = _Fixture(); + }); + + test( + 'sets up DefaultTelemetryProcessor when NoOpTelemetryProcessor is active', + () { + final options = fixture.options; + expect(options.telemetryProcessor, isA()); + + fixture.getSut().call(fixture.hub, options); + + expect(options.telemetryProcessor, isA()); + }); + + test('does not override existing telemetry processor', () { + final options = fixture.options; + final existingProcessor = DefaultTelemetryProcessor(options.log); + options.telemetryProcessor = existingProcessor; + + fixture.getSut().call(fixture.hub, options); + + expect(identical(options.telemetryProcessor, existingProcessor), isTrue); + }); + + test('adds integration name to SDK', () { + final options = fixture.options; + + fixture.getSut().call(fixture.hub, options); + + expect( + options.sdk.integrations, + contains(DefaultTelemetryProcessorIntegration.integrationName), + ); + }); + + test('configures log buffer as InMemoryTelemetryBuffer', () { + final options = fixture.options; + + fixture.getSut().call(fixture.hub, options); + + final processor = options.telemetryProcessor as DefaultTelemetryProcessor; + expect(processor.logBuffer, isA>()); + }); + + group('flush', () { + test('log reaches transport as envelope', () async { + final options = fixture.options; + fixture.getSut().call(fixture.hub, options); + + final processor = + options.telemetryProcessor as DefaultTelemetryProcessor; + processor.addLog(fixture.createLog()); + await processor.flush(); + + expect(fixture.transport.envelopes, hasLength(1)); + }); + }); + }); +} + +class _Fixture { + final hub = MockHub(); + final transport = MockTransport(); + late SentryOptions options; + + _Fixture() { + options = defaultTestOptions()..transport = transport; + } + + DefaultTelemetryProcessorIntegration getSut() { + return DefaultTelemetryProcessorIntegration(); + } + + SentryLog createLog() { + return SentryLog( + timestamp: DateTime.now().toUtc(), + level: SentryLogLevel.info, + body: 'test log', + attributes: {}, + ); + } +} diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart new file mode 100644 index 0000000000..3dbf1b2887 --- /dev/null +++ b/packages/dart/test/telemetry/processing/processor_test.dart @@ -0,0 +1,104 @@ +import 'dart:async'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/processing/processor.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_telemetry_buffer.dart'; +import '../../test_utils.dart'; + +void main() { + group('DefaultTelemetryProcessor', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('addLog', () { + test('routes log to log buffer', () { + final mockLogBuffer = MockTelemetryBuffer(); + final processor = + fixture.getSut(enableLogs: true, logBuffer: mockLogBuffer); + + final log = fixture.createLog(); + processor.addLog(log); + + expect(mockLogBuffer.addedItems.length, 1); + expect(mockLogBuffer.addedItems.first, log); + }); + + test('does not throw when no log buffer registered', () { + final processor = fixture.getSut(); + processor.logBuffer = null; + + final log = fixture.createLog(); + processor.addLog(log); + }); + }); + + group('flush', () { + test('flushes all registered buffers', () async { + final mockLogBuffer = MockTelemetryBuffer(); + final processor = fixture.getSut( + enableLogs: true, + logBuffer: mockLogBuffer, + ); + + await processor.flush(); + + expect(mockLogBuffer.flushCallCount, 1); + }); + + test('returns sync (null) when all buffers flush synchronously', () { + final mockLogBuffer = MockTelemetryBuffer(asyncFlush: false); + final processor = fixture.getSut(logBuffer: mockLogBuffer); + processor.logBuffer = null; + + final result = processor.flush(); + + expect(result, isNull); + }); + + test('returns Future when at least one buffer flushes asynchronously', + () async { + final mockLogBuffer = MockTelemetryBuffer(asyncFlush: true); + final processor = fixture.getSut(logBuffer: mockLogBuffer); + processor.logBuffer = null; + + final result = processor.flush(); + + expect(result, isA()); + await result; + }); + }); + }); +} + +class Fixture { + late SentryOptions options; + + Fixture() { + options = defaultTestOptions(); + } + + DefaultTelemetryProcessor getSut({ + bool enableLogs = false, + MockTelemetryBuffer? logBuffer, + }) { + options.enableLogs = enableLogs; + return DefaultTelemetryProcessor( + options.log, + logBuffer: logBuffer, + ); + } + + SentryLog createLog({String body = 'test log'}) { + return SentryLog( + timestamp: DateTime.now().toUtc(), + level: SentryLogLevel.info, + body: body, + attributes: {}, + ); + } +} From 6da49c8766c49059c058121236ad46d361934408 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 19:32:58 +0100 Subject: [PATCH 08/79] Update --- .../dart/lib/src/telemetry/processing/in_memory_buffer.dart | 2 -- .../lib/src/telemetry/processing/processor_integration.dart | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart b/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart index 21809f9740..be97f5f0a4 100644 --- a/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart +++ b/packages/dart/lib/src/telemetry/processing/in_memory_buffer.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:meta/meta.dart'; - import '../../utils/internal_logger.dart'; import 'buffer.dart'; import 'buffer_config.dart'; diff --git a/packages/dart/lib/src/telemetry/processing/processor_integration.dart b/packages/dart/lib/src/telemetry/processing/processor_integration.dart index a2796882e5..c3581dd505 100644 --- a/packages/dart/lib/src/telemetry/processing/processor_integration.dart +++ b/packages/dart/lib/src/telemetry/processing/processor_integration.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import '../../../sentry.dart'; import 'in_memory_buffer.dart'; import 'processor.dart'; From 1b97198a1a309d8ff98259302d46bfa5ff68b691 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 19:35:07 +0100 Subject: [PATCH 09/79] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d8bc0255d..e06a63c8d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- Replace log batcher with telemetry processor ([#3448](https://github.com/getsentry/sentry-dart/pull/3448)) + ### Dependencies - Bump Native SDK from v0.10.0 to v0.12.3 ([#3438](https://github.com/getsentry/sentry-dart/pull/3438)) From 82a4374115a0dd72971159f8c43afd7af0186450 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 19:35:46 +0100 Subject: [PATCH 10/79] Update --- packages/dart/test/telemetry/processing/processor_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart index 3dbf1b2887..5429793e3f 100644 --- a/packages/dart/test/telemetry/processing/processor_test.dart +++ b/packages/dart/test/telemetry/processing/processor_test.dart @@ -64,7 +64,6 @@ void main() { () async { final mockLogBuffer = MockTelemetryBuffer(asyncFlush: true); final processor = fixture.getSut(logBuffer: mockLogBuffer); - processor.logBuffer = null; final result = processor.flush(); From 1a48756bcafd29262580f8bc436590fd65392157 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 14 Jan 2026 23:46:21 +0100 Subject: [PATCH 11/79] Update --- packages/dart/lib/src/hub.dart | 33 ++++++++ packages/dart/lib/src/hub_adapter.dart | 5 ++ packages/dart/lib/src/noop_hub.dart | 4 + packages/dart/lib/src/sentry.dart | 3 + packages/dart/lib/src/sentry_client.dart | 31 +++++++ packages/dart/lib/src/sentry_envelope.dart | 29 +++++++ .../dart/lib/src/sentry_envelope_item.dart | 16 ++++ packages/dart/lib/src/sentry_item_type.dart | 1 + packages/dart/lib/src/sentry_options.dart | 18 +++++ .../lib/src/telemetry/metric/metric_type.dart | 14 ++++ .../src/telemetry/metric/sentry_metric.dart | 81 +++++++++++++++++++ .../src/telemetry/metric/sentry_metrics.dart | 79 ++++++++++++++++++ .../src/telemetry/processing/processor.dart | 47 ++++++++--- .../processing/processor_integration.dart | 17 +++- .../test/mocks/mock_telemetry_processor.dart | 7 ++ .../processor_integration_test.dart | 2 +- .../telemetry/processing/processor_test.dart | 54 +++++++++++-- 17 files changed, 421 insertions(+), 20 deletions(-) create mode 100644 packages/dart/lib/src/telemetry/metric/metric_type.dart create mode 100644 packages/dart/lib/src/telemetry/metric/sentry_metric.dart create mode 100644 packages/dart/lib/src/telemetry/metric/sentry_metrics.dart diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index 5346e2cd69..77a2eea307 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -9,6 +9,7 @@ import 'client_reports/discard_reason.dart'; import 'profiling.dart'; import 'sentry_tracer.dart'; import 'sentry_traces_sampler.dart'; +import 'telemetry/metric/sentry_metric.dart'; import 'transport/data_category.dart'; /// Configures the scope through the callback. @@ -317,6 +318,38 @@ class Hub { } } + Future captureMetric(SentryMetric metric) async { + if (!_isEnabled) { + _options.log( + SentryLevel.warning, + "Instance is disabled and this 'captureMetric' call is a no-op.", + ); + } else { + final item = _peek(); + late Scope scope; + final s = _cloneAndRunWithScope(item.scope, null); + if (s is Future) { + scope = await s; + } else { + scope = s; + } + + try { + await item.client.captureMetric( + metric, + scope: scope, + ); + } catch (exception, stacktrace) { + _options.log( + SentryLevel.error, + 'Error while capturing metric', + exception: exception, + stackTrace: stacktrace, + ); + } + } + } + FutureOr _cloneAndRunWithScope( Scope scope, ScopeCallback? withScope) async { if (withScope != null) { diff --git a/packages/dart/lib/src/hub_adapter.dart b/packages/dart/lib/src/hub_adapter.dart index 6491f2a951..ae5a369dc6 100644 --- a/packages/dart/lib/src/hub_adapter.dart +++ b/packages/dart/lib/src/hub_adapter.dart @@ -11,6 +11,7 @@ import 'scope.dart'; import 'sentry.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; +import 'telemetry/metric/sentry_metric.dart'; import 'tracing.dart'; /// Hub adapter to make Integrations testable @@ -200,6 +201,10 @@ class HubAdapter implements Hub { @override FutureOr captureLog(SentryLog log) => Sentry.currentHub.captureLog(log); + @override + Future captureMetric(SentryMetric metric) => + Sentry.currentHub.captureMetric(metric); + @override void setAttributes(Map attributes) => Sentry.currentHub.setAttributes(attributes); diff --git a/packages/dart/lib/src/noop_hub.dart b/packages/dart/lib/src/noop_hub.dart index bb486b9e80..f23f054048 100644 --- a/packages/dart/lib/src/noop_hub.dart +++ b/packages/dart/lib/src/noop_hub.dart @@ -10,6 +10,7 @@ import 'protocol/sentry_feedback.dart'; import 'scope.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; +import 'telemetry/metric/sentry_metric.dart'; import 'tracing.dart'; class NoOpHub implements Hub { @@ -97,6 +98,9 @@ class NoOpHub implements Hub { @override FutureOr captureLog(SentryLog log) async {} + @override + Future captureMetric(SentryMetric metric) async {} + @override ISentrySpan startTransaction( String name, diff --git a/packages/dart/lib/src/sentry.dart b/packages/dart/lib/src/sentry.dart index 06821ccedf..1c0d65af38 100644 --- a/packages/dart/lib/src/sentry.dart +++ b/packages/dart/lib/src/sentry.dart @@ -23,6 +23,7 @@ import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; import 'sentry_run_zoned_guarded.dart'; +import 'telemetry/metric/sentry_metrics.dart'; import 'telemetry/processing/processor_integration.dart'; import 'tracing.dart'; import 'transport/data_category.dart'; @@ -450,4 +451,6 @@ class Sentry { ); static SentryLogger get logger => currentHub.options.logger; + + static SentryMetrics get metrics => currentHub.options.metrics; } diff --git a/packages/dart/lib/src/sentry_client.dart b/packages/dart/lib/src/sentry_client.dart index 02e6841e1d..de0940c4d7 100644 --- a/packages/dart/lib/src/sentry_client.dart +++ b/packages/dart/lib/src/sentry_client.dart @@ -18,6 +18,7 @@ import 'sentry_exception_factory.dart'; import 'sentry_options.dart'; import 'sentry_stack_trace_factory.dart'; import 'sentry_trace_context_header.dart'; +import 'telemetry/metric/sentry_metric.dart'; import 'transport/client_report_transport.dart'; import 'transport/data_category.dart'; import 'transport/http_transport.dart'; @@ -583,6 +584,36 @@ class SentryClient { } } + Future captureMetric(SentryMetric metric, {Scope? scope}) async { + if (!_options.enableLogs) { + return; + } + + final beforeSendMetric = _options.beforeSendMetric; + SentryMetric? processedMetric = metric; + if (beforeSendMetric != null) { + try { + processedMetric = await beforeSendMetric(metric); + } catch (exception, stackTrace) { + _options.log( + SentryLevel.error, + 'The beforeSendLog callback threw an exception', + exception: exception, + stackTrace: stackTrace, + ); + if (_options.automatedTestMode) { + rethrow; + } + } + } + + // TODO: attributes enricher + + if (processedMetric != null) { + _options.telemetryProcessor.addMetric(processedMetric); + } + } + FutureOr close() { final flush = _options.telemetryProcessor.flush(); if (flush is Future) { diff --git a/packages/dart/lib/src/sentry_envelope.dart b/packages/dart/lib/src/sentry_envelope.dart index 23cef605a3..7b2539d1ba 100644 --- a/packages/dart/lib/src/sentry_envelope.dart +++ b/packages/dart/lib/src/sentry_envelope.dart @@ -129,6 +129,21 @@ class SentryEnvelope { ); } + /// Create a [SentryEnvelope] containing raw metric data payload. + /// This is used by the log batcher to send pre-encoded metric batches. + @internal + factory SentryEnvelope.fromMetricsData( + List> encodedMetrics, + SdkVersion sdkVersion, + ) => + SentryEnvelope( + SentryEnvelopeHeader(null, sdkVersion), + [ + SentryEnvelopeItem.fromMetricsData( + _buildItemsPayload(encodedMetrics), encodedMetrics.length) + ], + ); + /// Stream binary data representation of `Envelope` file encoded. Stream> envelopeStream(SentryOptions options) async* { yield utf8JsonEncoder.convert(header.toJson()); @@ -160,6 +175,20 @@ class SentryEnvelope { } } + /// Builds a payload in the format {"items": [item1, item2, ...]} + static Uint8List _buildItemsPayload(List> encodedItems) { + final builder = BytesBuilder(copy: false); + builder.add(utf8.encode('{"items":[')); + for (int i = 0; i < encodedItems.length; i++) { + if (i > 0) { + builder.add(utf8.encode(',')); + } + builder.add(encodedItems[i]); + } + builder.add(utf8.encode(']}')); + return builder.takeBytes(); + } + /// Add an envelope item containing client report data. void addClientReport(ClientReport? clientReport) { if (clientReport != null) { diff --git a/packages/dart/lib/src/sentry_envelope_item.dart b/packages/dart/lib/src/sentry_envelope_item.dart index f626d97882..e633c6cfcc 100644 --- a/packages/dart/lib/src/sentry_envelope_item.dart +++ b/packages/dart/lib/src/sentry_envelope_item.dart @@ -94,6 +94,22 @@ class SentryEnvelopeItem { ); } + /// Create a [SentryEnvelopeItem] which holds pre-encoded metric data. + /// This is used by the buffer to send pre-encoded metric batches. + @internal + factory SentryEnvelopeItem.fromMetricsData( + List payload, int metricsCount) { + return SentryEnvelopeItem( + SentryEnvelopeItemHeader( + SentryItemType.metric, + itemCount: metricsCount, + contentType: 'application/vnd.sentry.items.trace-metric+json', + ), + () => payload, + originalObject: null, + ); + } + /// Header with info about type and length of data in bytes. final SentryEnvelopeItemHeader header; diff --git a/packages/dart/lib/src/sentry_item_type.dart b/packages/dart/lib/src/sentry_item_type.dart index c712ad8793..3d0fe8d956 100644 --- a/packages/dart/lib/src/sentry_item_type.dart +++ b/packages/dart/lib/src/sentry_item_type.dart @@ -6,5 +6,6 @@ class SentryItemType { static const String profile = 'profile'; static const String statsd = 'statsd'; static const String log = 'log'; + static const String metric = 'trace_metric'; static const String unknown = '__unknown__'; } diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index f0b7472f3e..1ffb1ff25b 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -12,6 +12,8 @@ import 'noop_client.dart'; import 'platform/platform.dart'; import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.dart'; +import 'telemetry/metric/sentry_metric.dart'; +import 'telemetry/metric/sentry_metrics.dart'; import 'telemetry/processing/processor.dart'; import 'transport/noop_transport.dart'; import 'version.dart'; @@ -217,6 +219,10 @@ class SentryOptions { /// Can return a modified log or null to drop the log. BeforeSendLogCallback? beforeSendLog; + /// This function is called right before a metric is about to be sent. + /// Can return a modified metric or null to drop the log. + BeforeSendMetricCallback? beforeSendMetric; + /// Sets the release. SDK will try to automatically configure a release out of the box /// See [docs for further information](https://docs.sentry.io/platforms/flutter/configuration/releases/) String? release; @@ -545,6 +551,11 @@ class SentryOptions { /// Disabled by default. bool enableLogs = false; + /// Enable to capture and send metrics to Sentry. + /// + /// Disabled by default. + bool enableMetrics = false; + /// Enables adding the module in [SentryStackFrame.module]. /// This option only has an effect in non-obfuscated builds. /// Enabling this option may change grouping. @@ -552,6 +563,8 @@ class SentryOptions { late final SentryLogger logger = SentryLogger(clock); + late final metrics = SentryMetrics(HubAdapter(), clock); + @internal TelemetryProcessor telemetryProcessor = NoOpTelemetryProcessor(); @@ -688,6 +701,11 @@ typedef BeforeMetricCallback = bool Function( /// Can return a modified log or null to drop the log. typedef BeforeSendLogCallback = FutureOr Function(SentryLog log); +/// This function is called right before a metric is about to be emitted. +/// Can return true to emit the metric, or false to drop it. +typedef BeforeSendMetricCallback = FutureOr Function( + SentryMetric metric); + /// Used to provide timestamp for logging. typedef ClockProvider = DateTime Function(); diff --git a/packages/dart/lib/src/telemetry/metric/metric_type.dart b/packages/dart/lib/src/telemetry/metric/metric_type.dart new file mode 100644 index 0000000000..2e7f86f2bf --- /dev/null +++ b/packages/dart/lib/src/telemetry/metric/metric_type.dart @@ -0,0 +1,14 @@ +/// The type of metric being recorded +enum SentryMetricType { + /// A metric that increments counts + counter('counter'), + + /// A metric that tracks a value that can go up or down + gauge('gauge'), + + /// A metric that tracks statistical distribution of values + distribution('distribution'); + + final String value; + const SentryMetricType(this.value); +} diff --git a/packages/dart/lib/src/telemetry/metric/sentry_metric.dart b/packages/dart/lib/src/telemetry/metric/sentry_metric.dart new file mode 100644 index 0000000000..ecdbdacc4a --- /dev/null +++ b/packages/dart/lib/src/telemetry/metric/sentry_metric.dart @@ -0,0 +1,81 @@ +import 'package:meta/meta.dart'; + +import '../../../sentry.dart'; +import 'metric_type.dart'; + +/// Base sealed class for all Sentry metrics +sealed class SentryMetric { + final DateTime timestamp; + final SentryMetricType type; + final String name; + final num value; + final SentryId traceId; + final SpanId? spanId; + final String? unit; + final Map attributes; + + const SentryMetric({ + required this.timestamp, + required this.type, + required this.name, + required this.value, + required this.traceId, + this.spanId, + this.unit, + this.attributes = const {}, + }); + + @internal + Map toJson() { + return { + 'timestamp': timestamp.millisecondsSinceEpoch / 1000.0, + 'type': type.value, + 'name': name, + 'value': value, + 'trace_id': traceId, + if (spanId != null) 'span_id': spanId, + if (unit != null) 'unit': unit, + if (attributes.isNotEmpty) + 'attributes': attributes.map((k, v) => MapEntry(k, v.toJson())), + }; + } +} + +/// Counter metric - increments counts (only increases) +final class SentryCounterMetric extends SentryMetric { + const SentryCounterMetric({ + required super.timestamp, + required super.name, + required super.value, + required super.traceId, + super.spanId, + super.unit, + super.attributes, + }) : super(type: SentryMetricType.counter); +} + +/// Gauge metric - tracks values that can go up or down +final class SentryGaugeMetric extends SentryMetric { + const SentryGaugeMetric({ + required super.timestamp, + required super.name, + required super.value, + required super.traceId, + super.spanId, + super.unit, + super.attributes, + }) : super(type: SentryMetricType.gauge); +} + +/// Distribution metric - tracks statistical distribution of values +final class SentryDistributionMetric extends SentryMetric { + const SentryDistributionMetric({ + required super.timestamp, + required super.name, + required super.value, + required super.traceId, + super.spanId, + super.unit, + super.attributes, + }) : super(type: SentryMetricType.distribution); +} diff --git a/packages/dart/lib/src/telemetry/metric/sentry_metrics.dart b/packages/dart/lib/src/telemetry/metric/sentry_metrics.dart new file mode 100644 index 0000000000..924539aa51 --- /dev/null +++ b/packages/dart/lib/src/telemetry/metric/sentry_metrics.dart @@ -0,0 +1,79 @@ +import '../../../sentry.dart'; +import 'sentry_metric.dart'; + +/// Public API for recording metrics +final class SentryMetrics { + final Hub _hub; + final ClockProvider _clockProvider; + + SentryMetrics(this._hub, this._clockProvider); + + /// Records a counter metric + void count( + String name, + int value, { + Map? attributes, + Scope? scope, + }) { + if (!_isEnabled) return; + + final metric = SentryCounterMetric( + timestamp: _clockProvider(), + name: name, + value: value, + spanId: _hub.scope.span?.context.spanId, + traceId: _traceIdFromScope(scope), + attributes: attributes ?? {}); + + _hub.captureMetric(metric); + } + + /// Records a gauge metric + void gauge( + String name, + num value, { + String? unit, + Map? attributes, + Scope? scope, + }) { + if (!_isEnabled) return; + + final metric = SentryGaugeMetric( + timestamp: _clockProvider(), + name: name, + value: value, + spanId: _hub.scope.span?.context.spanId, + traceId: _traceIdFromScope(scope), + attributes: attributes ?? {}); + + _hub.captureMetric(metric); + } + + /// Records a distribution metric + void distribution( + String name, + num value, { + String? unit, + Map? attributes, + Scope? scope, + }) { + if (!_isEnabled) return; + + final metric = SentryDistributionMetric( + timestamp: _clockProvider(), + name: name, + value: value, + unit: unit, + spanId: _hub.scope.span?.context.spanId, + traceId: _traceIdFromScope(scope), + attributes: attributes ?? {}); + + _hub.captureMetric(metric); + } + + bool get _isEnabled => _hub.options.enableMetrics; + + SentryId _traceIdFromScope(Scope? scope) => + scope?.propagationContext.traceId ?? + _hub.scope.propagationContext.traceId; +} diff --git a/packages/dart/lib/src/telemetry/processing/processor.dart b/packages/dart/lib/src/telemetry/processing/processor.dart index 2e4268bdb2..81b04694fb 100644 --- a/packages/dart/lib/src/telemetry/processing/processor.dart +++ b/packages/dart/lib/src/telemetry/processing/processor.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:meta/meta.dart'; import '../../../sentry.dart'; +import '../../utils/internal_logger.dart'; +import '../metric/sentry_metric.dart'; import 'buffer.dart'; /// Interface for processing and buffering telemetry data before sending. @@ -13,6 +15,9 @@ abstract class TelemetryProcessor { /// Adds a log to be processed and buffered. void addLog(SentryLog log); + /// Adds a metric to be processed and buffered. + void addMetric(SentryMetric log); + /// Flushes all buffered telemetry data. /// /// Returns a [Future] if any buffer performs async flushing, otherwise @@ -26,22 +31,23 @@ abstract class TelemetryProcessor { /// instances. If no buffer is registered for a telemetry type, items are /// dropped with a warning. class DefaultTelemetryProcessor implements TelemetryProcessor { - final SdkLogCallback _logger; - /// The buffer for log data, or `null` if log buffering is disabled. @visibleForTesting TelemetryBuffer? logBuffer; - DefaultTelemetryProcessor( - this._logger, { + /// The buffer for metric data, or `null` if metric buffering is disabled. + @visibleForTesting + TelemetryBuffer? metricBuffer; + + DefaultTelemetryProcessor({ this.logBuffer, + this.metricBuffer, }); @override void addLog(SentryLog log) { if (logBuffer == null) { - _logger( - SentryLevel.warning, + internalLogger.warning( '$runtimeType: No buffer registered for ${log.runtimeType} - item was dropped', ); return; @@ -50,17 +56,33 @@ class DefaultTelemetryProcessor implements TelemetryProcessor { logBuffer!.add(log); } + @override + void addMetric(SentryMetric metric) { + print('in here'); + if (metricBuffer == null) { + internalLogger.warning( + '$runtimeType: No buffer registered for ${metric.runtimeType} - item was dropped', + ); + return; + } + + print('metricjson: ${metric.toJson()}'); + + metricBuffer!.add(metric); + } + @override FutureOr flush() { - _logger(SentryLevel.debug, '$runtimeType: Clearing buffers'); + internalLogger.debug('$runtimeType: Clearing buffers'); - final result = logBuffer?.flush(); + final results = [logBuffer?.flush(), metricBuffer?.flush()]; - if (result is Future) { - return result; + final futures = results.whereType().toList(); + if (futures.isEmpty) { + return null; } - return null; + return Future.wait(futures).then((_) {}); } } @@ -68,6 +90,9 @@ class NoOpTelemetryProcessor implements TelemetryProcessor { @override void addLog(SentryLog log) {} + @override + void addMetric(SentryMetric log) {} + @override FutureOr flush() {} } diff --git a/packages/dart/lib/src/telemetry/processing/processor_integration.dart b/packages/dart/lib/src/telemetry/processing/processor_integration.dart index c3581dd505..d07e2a6a9f 100644 --- a/packages/dart/lib/src/telemetry/processing/processor_integration.dart +++ b/packages/dart/lib/src/telemetry/processing/processor_integration.dart @@ -1,4 +1,5 @@ import '../../../sentry.dart'; +import '../metric/sentry_metric.dart'; import 'in_memory_buffer.dart'; import 'processor.dart'; @@ -15,8 +16,9 @@ class DefaultTelemetryProcessorIntegration extends Integration { return; } - options.telemetryProcessor = DefaultTelemetryProcessor(options.log, - logBuffer: _createLogBuffer(options)); + options.telemetryProcessor = DefaultTelemetryProcessor( + logBuffer: _createLogBuffer(options), + metricBuffer: _createMetricBuffer(options)); options.sdk.addIntegration(integrationName); } @@ -29,4 +31,15 @@ class DefaultTelemetryProcessorIntegration extends Integration { items.map((item) => item).toList(), options.sdk); return options.transport.send(envelope).then((_) {}); }); + + InMemoryTelemetryBuffer _createMetricBuffer( + SentryOptions options) => + InMemoryTelemetryBuffer( + encoder: (SentryMetric item) => + utf8JsonEncoder.convert(item.toJson()), + onFlush: (items) { + final envelope = SentryEnvelope.fromMetricsData( + items.map((item) => item).toList(), options.sdk); + return options.transport.send(envelope).then((_) {}); + }); } diff --git a/packages/dart/test/mocks/mock_telemetry_processor.dart b/packages/dart/test/mocks/mock_telemetry_processor.dart index a52fd97a2f..e2179bf143 100644 --- a/packages/dart/test/mocks/mock_telemetry_processor.dart +++ b/packages/dart/test/mocks/mock_telemetry_processor.dart @@ -1,8 +1,10 @@ import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/metric/sentry_metric.dart'; import 'package:sentry/src/telemetry/processing/processor.dart'; class MockTelemetryProcessor implements TelemetryProcessor { final List addedLogs = []; + final List addedMetrics = []; int flushCalls = 0; int closeCalls = 0; @@ -11,6 +13,11 @@ class MockTelemetryProcessor implements TelemetryProcessor { addedLogs.add(log); } + @override + void addMetric(SentryMetric metric) { + addedMetrics.add(metric); + } + @override void flush() { flushCalls++; diff --git a/packages/dart/test/telemetry/processing/processor_integration_test.dart b/packages/dart/test/telemetry/processing/processor_integration_test.dart index 009a6c3613..0d13db5dec 100644 --- a/packages/dart/test/telemetry/processing/processor_integration_test.dart +++ b/packages/dart/test/telemetry/processing/processor_integration_test.dart @@ -29,7 +29,7 @@ void main() { test('does not override existing telemetry processor', () { final options = fixture.options; - final existingProcessor = DefaultTelemetryProcessor(options.log); + final existingProcessor = DefaultTelemetryProcessor(); options.telemetryProcessor = existingProcessor; fixture.getSut().call(fixture.hub, options); diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart index 5429793e3f..936ef6c385 100644 --- a/packages/dart/test/telemetry/processing/processor_test.dart +++ b/packages/dart/test/telemetry/processing/processor_test.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/metric/sentry_metric.dart'; import 'package:sentry/src/telemetry/processing/processor.dart'; import 'package:test/test.dart'; @@ -37,23 +38,52 @@ void main() { }); }); + group('addMetric', () { + test('routes metric to metric buffer', () { + final mockMetricBuffer = MockTelemetryBuffer(); + final processor = + fixture.getSut(enableMetrics: true, metricBuffer: mockMetricBuffer); + + final metric = fixture.createMetric(); + processor.addMetric(metric); + + expect(mockMetricBuffer.addedItems.length, 1); + expect(mockMetricBuffer.addedItems.first, metric); + }); + + test('does not throw when no metric buffer registered', () { + final processor = fixture.getSut(); + processor.logBuffer = null; + + final log = fixture.createLog(); + processor.addLog(log); + }); + }); + group('flush', () { test('flushes all registered buffers', () async { final mockLogBuffer = MockTelemetryBuffer(); + final mockMetricBuffer = MockTelemetryBuffer(); + final processor = fixture.getSut( enableLogs: true, logBuffer: mockLogBuffer, + metricBuffer: mockMetricBuffer, ); await processor.flush(); expect(mockLogBuffer.flushCallCount, 1); + expect(mockMetricBuffer.flushCallCount, 1); }); test('returns sync (null) when all buffers flush synchronously', () { final mockLogBuffer = MockTelemetryBuffer(asyncFlush: false); - final processor = fixture.getSut(logBuffer: mockLogBuffer); - processor.logBuffer = null; + final mockMetricBuffer = + MockTelemetryBuffer(asyncFlush: false); + + final processor = fixture.getSut( + logBuffer: mockLogBuffer, metricBuffer: mockMetricBuffer); final result = processor.flush(); @@ -63,7 +93,10 @@ void main() { test('returns Future when at least one buffer flushes asynchronously', () async { final mockLogBuffer = MockTelemetryBuffer(asyncFlush: true); - final processor = fixture.getSut(logBuffer: mockLogBuffer); + final mockMetricBuffer = + MockTelemetryBuffer(asyncFlush: false); + final processor = fixture.getSut( + logBuffer: mockLogBuffer, metricBuffer: mockMetricBuffer); final result = processor.flush(); @@ -83,13 +116,14 @@ class Fixture { DefaultTelemetryProcessor getSut({ bool enableLogs = false, + bool enableMetrics = false, MockTelemetryBuffer? logBuffer, + MockTelemetryBuffer? metricBuffer, }) { options.enableLogs = enableLogs; + options.enableMetrics = enableMetrics; return DefaultTelemetryProcessor( - options.log, - logBuffer: logBuffer, - ); + logBuffer: logBuffer, metricBuffer: metricBuffer); } SentryLog createLog({String body = 'test log'}) { @@ -100,4 +134,12 @@ class Fixture { attributes: {}, ); } + + SentryMetric createMetric() => SentryCounterMetric( + timestamp: DateTime.now().toUtc(), + attributes: {}, + name: 'test-metric', + value: 1, + traceId: SentryId.newId(), + ); } From 1081ca32e0830c35bdf20f010256e21a305dc7f6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 15 Jan 2026 00:51:40 +0100 Subject: [PATCH 12/79] Update --- packages/dart/lib/src/hub.dart | 2 +- packages/dart/lib/src/hub_adapter.dart | 2 +- packages/dart/lib/src/noop_hub.dart | 2 +- packages/dart/lib/src/sentry.dart | 4 +- packages/dart/lib/src/sentry_client.dart | 2 +- packages/dart/lib/src/sentry_options.dart | 7 +- .../{sentry_metric.dart => metric.dart} | 29 ++-- .../lib/src/telemetry/metric/metrics.dart | 139 ++++++++++++++++++ .../metric/metrics_setup_integration.dart | 17 +++ .../src/telemetry/metric/sentry_metrics.dart | 79 ---------- .../src/telemetry/processing/processor.dart | 5 +- .../processing/processor_integration.dart | 2 +- .../test/mocks/mock_telemetry_processor.dart | 2 +- .../telemetry/processing/processor_test.dart | 2 +- 14 files changed, 186 insertions(+), 108 deletions(-) rename packages/dart/lib/src/telemetry/metric/{sentry_metric.dart => metric.dart} (81%) create mode 100644 packages/dart/lib/src/telemetry/metric/metrics.dart create mode 100644 packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart delete mode 100644 packages/dart/lib/src/telemetry/metric/sentry_metrics.dart diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index 77a2eea307..c980419d68 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -9,7 +9,7 @@ import 'client_reports/discard_reason.dart'; import 'profiling.dart'; import 'sentry_tracer.dart'; import 'sentry_traces_sampler.dart'; -import 'telemetry/metric/sentry_metric.dart'; +import 'telemetry/metric/metric.dart'; import 'transport/data_category.dart'; /// Configures the scope through the callback. diff --git a/packages/dart/lib/src/hub_adapter.dart b/packages/dart/lib/src/hub_adapter.dart index ae5a369dc6..73ee33c4cf 100644 --- a/packages/dart/lib/src/hub_adapter.dart +++ b/packages/dart/lib/src/hub_adapter.dart @@ -11,7 +11,7 @@ import 'scope.dart'; import 'sentry.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; -import 'telemetry/metric/sentry_metric.dart'; +import 'telemetry/metric/metric.dart'; import 'tracing.dart'; /// Hub adapter to make Integrations testable diff --git a/packages/dart/lib/src/noop_hub.dart b/packages/dart/lib/src/noop_hub.dart index f23f054048..81855f0bef 100644 --- a/packages/dart/lib/src/noop_hub.dart +++ b/packages/dart/lib/src/noop_hub.dart @@ -10,7 +10,7 @@ import 'protocol/sentry_feedback.dart'; import 'scope.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; -import 'telemetry/metric/sentry_metric.dart'; +import 'telemetry/metric/metric.dart'; import 'tracing.dart'; class NoOpHub implements Hub { diff --git a/packages/dart/lib/src/sentry.dart b/packages/dart/lib/src/sentry.dart index 1c0d65af38..68f6db0b37 100644 --- a/packages/dart/lib/src/sentry.dart +++ b/packages/dart/lib/src/sentry.dart @@ -23,7 +23,8 @@ import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; import 'sentry_run_zoned_guarded.dart'; -import 'telemetry/metric/sentry_metrics.dart'; +import 'telemetry/metric/metrics.dart'; +import 'telemetry/metric/metrics_setup_integration.dart'; import 'telemetry/processing/processor_integration.dart'; import 'tracing.dart'; import 'transport/data_category.dart'; @@ -111,6 +112,7 @@ class Sentry { options.addIntegration(LoadDartDebugImagesIntegration()); } + options.addIntegration(MetricsSetupIntegration()); options.addIntegration(FeatureFlagsIntegration()); options.addIntegration(LogsEnricherIntegration()); options.addIntegration(DefaultTelemetryProcessorIntegration()); diff --git a/packages/dart/lib/src/sentry_client.dart b/packages/dart/lib/src/sentry_client.dart index de0940c4d7..3ba406c5d7 100644 --- a/packages/dart/lib/src/sentry_client.dart +++ b/packages/dart/lib/src/sentry_client.dart @@ -18,7 +18,7 @@ import 'sentry_exception_factory.dart'; import 'sentry_options.dart'; import 'sentry_stack_trace_factory.dart'; import 'sentry_trace_context_header.dart'; -import 'telemetry/metric/sentry_metric.dart'; +import 'telemetry/metric/metric.dart'; import 'transport/client_report_transport.dart'; import 'transport/data_category.dart'; import 'transport/http_transport.dart'; diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 1ffb1ff25b..b682c129e5 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -12,8 +12,8 @@ import 'noop_client.dart'; import 'platform/platform.dart'; import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.dart'; -import 'telemetry/metric/sentry_metric.dart'; -import 'telemetry/metric/sentry_metrics.dart'; +import 'telemetry/metric/metric.dart'; +import 'telemetry/metric/metrics.dart'; import 'telemetry/processing/processor.dart'; import 'transport/noop_transport.dart'; import 'version.dart'; @@ -563,7 +563,8 @@ class SentryOptions { late final SentryLogger logger = SentryLogger(clock); - late final metrics = SentryMetrics(HubAdapter(), clock); + @internal + SentryMetrics metrics = NoOpSentryMetrics.instance; @internal TelemetryProcessor telemetryProcessor = NoOpTelemetryProcessor(); diff --git a/packages/dart/lib/src/telemetry/metric/sentry_metric.dart b/packages/dart/lib/src/telemetry/metric/metric.dart similarity index 81% rename from packages/dart/lib/src/telemetry/metric/sentry_metric.dart rename to packages/dart/lib/src/telemetry/metric/metric.dart index ecdbdacc4a..39ecef6597 100644 --- a/packages/dart/lib/src/telemetry/metric/sentry_metric.dart +++ b/packages/dart/lib/src/telemetry/metric/metric.dart @@ -5,16 +5,17 @@ import 'metric_type.dart'; /// Base sealed class for all Sentry metrics sealed class SentryMetric { - final DateTime timestamp; final SentryMetricType type; - final String name; - final num value; - final SentryId traceId; - final SpanId? spanId; - final String? unit; - final Map attributes; - const SentryMetric({ + DateTime timestamp; + String name; + num value; + SentryId traceId; + SpanId? spanId; + String? unit; + Map attributes; + + SentryMetric({ required this.timestamp, required this.type, required this.name, @@ -22,8 +23,8 @@ sealed class SentryMetric { required this.traceId, this.spanId, this.unit, - this.attributes = const {}, - }); + Map? attributes, + }) : attributes = attributes ?? {}; @internal Map toJson() { @@ -41,9 +42,9 @@ sealed class SentryMetric { } } -/// Counter metric - increments counts (only increases) +/// Counter metric - increments counts final class SentryCounterMetric extends SentryMetric { - const SentryCounterMetric({ + SentryCounterMetric({ required super.timestamp, required super.name, required super.value, @@ -56,7 +57,7 @@ final class SentryCounterMetric extends SentryMetric { /// Gauge metric - tracks values that can go up or down final class SentryGaugeMetric extends SentryMetric { - const SentryGaugeMetric({ + SentryGaugeMetric({ required super.timestamp, required super.name, required super.value, @@ -69,7 +70,7 @@ final class SentryGaugeMetric extends SentryMetric { /// Distribution metric - tracks statistical distribution of values final class SentryDistributionMetric extends SentryMetric { - const SentryDistributionMetric({ + SentryDistributionMetric({ required super.timestamp, required super.name, required super.value, diff --git a/packages/dart/lib/src/telemetry/metric/metrics.dart b/packages/dart/lib/src/telemetry/metric/metrics.dart new file mode 100644 index 0000000000..6788e6c713 --- /dev/null +++ b/packages/dart/lib/src/telemetry/metric/metrics.dart @@ -0,0 +1,139 @@ +import 'package:meta/meta.dart'; + +import '../../../sentry.dart'; +import 'metric.dart'; + +/// Public API for recording metrics +abstract base class SentryMetrics { + void count( + String name, + int value, { + Map? attributes, + Scope? scope, + }); + + void gauge( + String name, + num value, { + String? unit, + Map? attributes, + Scope? scope, + }); + + void distribution( + String name, + num value, { + String? unit, + Map? attributes, + Scope? scope, + }); +} + +typedef CaptureMetricCallback = Future Function(SentryMetric metric); +typedef ScopeProvider = Scope Function(); + +@internal +final class DefaultSentryMetrics implements SentryMetrics { + final bool _isMetricsEnabled; + final CaptureMetricCallback _captureMetricCallback; + final ClockProvider _clockProvider; + final ScopeProvider _defaultScopeProvider; + + DefaultSentryMetrics( + {required bool isMetricsEnabled, + required CaptureMetricCallback captureMetricCallback, + required ClockProvider clockProvider, + required ScopeProvider defaultScopeProvider}) + : _isMetricsEnabled = isMetricsEnabled, + _captureMetricCallback = captureMetricCallback, + _clockProvider = clockProvider, + _defaultScopeProvider = defaultScopeProvider; + + @override + void count( + String name, + int value, { + Map? attributes, + Scope? scope, + }) { + if (!_isMetricsEnabled) return; + + final metric = SentryCounterMetric( + timestamp: _clockProvider(), + name: name, + value: value, + spanId: _activeSpanIdFor(scope), + traceId: _traceIdFor(scope), + attributes: attributes ?? {}); + + _captureMetricCallback(metric); + } + + @override + void gauge( + String name, + num value, { + String? unit, + Map? attributes, + Scope? scope, + }) { + if (!_isMetricsEnabled) return; + + final metric = SentryGaugeMetric( + timestamp: _clockProvider(), + name: name, + value: value, + spanId: _activeSpanIdFor(scope), + traceId: _traceIdFor(scope), + attributes: attributes ?? {}); + + _captureMetricCallback(metric); + } + + @override + void distribution( + String name, + num value, { + String? unit, + Map? attributes, + Scope? scope, + }) { + if (!_isMetricsEnabled) return; + + final metric = SentryDistributionMetric( + timestamp: _clockProvider(), + name: name, + value: value, + unit: unit, + spanId: _activeSpanIdFor(scope), + traceId: _traceIdFor(scope), + attributes: attributes ?? {}); + + _captureMetricCallback(metric); + } + + SentryId _traceIdFor(Scope? scope) => + (scope ?? _defaultScopeProvider()).propagationContext.traceId; + + SpanId? _activeSpanIdFor(Scope? scope) => + (scope ?? _defaultScopeProvider()).span?.context.spanId; +} + +@internal +final class NoOpSentryMetrics implements SentryMetrics { + const NoOpSentryMetrics(); + + static const instance = NoOpSentryMetrics(); + + @override + void count(String name, int value, + {Map? attributes, Scope? scope}) {} + + @override + void distribution(String name, num value, + {String? unit, Map? attributes, Scope? scope}) {} + + @override + void gauge(String name, num value, + {String? unit, Map? attributes, Scope? scope}) {} +} diff --git a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart new file mode 100644 index 0000000000..f87628a6fd --- /dev/null +++ b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart @@ -0,0 +1,17 @@ +import '../../../sentry.dart'; +import 'metrics.dart'; + +class MetricsSetupIntegration extends Integration { + static const integrationName = 'MetricsSetup'; + + @override + void call(Hub hub, SentryOptions options) { + options.metrics = DefaultSentryMetrics( + isMetricsEnabled: options.enableMetrics, + captureMetricCallback: hub.captureMetric, + clockProvider: options.clock, + defaultScopeProvider: () => hub.scope); + + options.sdk.addIntegration(integrationName); + } +} diff --git a/packages/dart/lib/src/telemetry/metric/sentry_metrics.dart b/packages/dart/lib/src/telemetry/metric/sentry_metrics.dart deleted file mode 100644 index 924539aa51..0000000000 --- a/packages/dart/lib/src/telemetry/metric/sentry_metrics.dart +++ /dev/null @@ -1,79 +0,0 @@ -import '../../../sentry.dart'; -import 'sentry_metric.dart'; - -/// Public API for recording metrics -final class SentryMetrics { - final Hub _hub; - final ClockProvider _clockProvider; - - SentryMetrics(this._hub, this._clockProvider); - - /// Records a counter metric - void count( - String name, - int value, { - Map? attributes, - Scope? scope, - }) { - if (!_isEnabled) return; - - final metric = SentryCounterMetric( - timestamp: _clockProvider(), - name: name, - value: value, - spanId: _hub.scope.span?.context.spanId, - traceId: _traceIdFromScope(scope), - attributes: attributes ?? {}); - - _hub.captureMetric(metric); - } - - /// Records a gauge metric - void gauge( - String name, - num value, { - String? unit, - Map? attributes, - Scope? scope, - }) { - if (!_isEnabled) return; - - final metric = SentryGaugeMetric( - timestamp: _clockProvider(), - name: name, - value: value, - spanId: _hub.scope.span?.context.spanId, - traceId: _traceIdFromScope(scope), - attributes: attributes ?? {}); - - _hub.captureMetric(metric); - } - - /// Records a distribution metric - void distribution( - String name, - num value, { - String? unit, - Map? attributes, - Scope? scope, - }) { - if (!_isEnabled) return; - - final metric = SentryDistributionMetric( - timestamp: _clockProvider(), - name: name, - value: value, - unit: unit, - spanId: _hub.scope.span?.context.spanId, - traceId: _traceIdFromScope(scope), - attributes: attributes ?? {}); - - _hub.captureMetric(metric); - } - - bool get _isEnabled => _hub.options.enableMetrics; - - SentryId _traceIdFromScope(Scope? scope) => - scope?.propagationContext.traceId ?? - _hub.scope.propagationContext.traceId; -} diff --git a/packages/dart/lib/src/telemetry/processing/processor.dart b/packages/dart/lib/src/telemetry/processing/processor.dart index 81b04694fb..d0798f75b4 100644 --- a/packages/dart/lib/src/telemetry/processing/processor.dart +++ b/packages/dart/lib/src/telemetry/processing/processor.dart @@ -4,7 +4,7 @@ import 'package:meta/meta.dart'; import '../../../sentry.dart'; import '../../utils/internal_logger.dart'; -import '../metric/sentry_metric.dart'; +import '../metric/metric.dart'; import 'buffer.dart'; /// Interface for processing and buffering telemetry data before sending. @@ -58,7 +58,6 @@ class DefaultTelemetryProcessor implements TelemetryProcessor { @override void addMetric(SentryMetric metric) { - print('in here'); if (metricBuffer == null) { internalLogger.warning( '$runtimeType: No buffer registered for ${metric.runtimeType} - item was dropped', @@ -66,8 +65,6 @@ class DefaultTelemetryProcessor implements TelemetryProcessor { return; } - print('metricjson: ${metric.toJson()}'); - metricBuffer!.add(metric); } diff --git a/packages/dart/lib/src/telemetry/processing/processor_integration.dart b/packages/dart/lib/src/telemetry/processing/processor_integration.dart index d07e2a6a9f..4bfeb0ae54 100644 --- a/packages/dart/lib/src/telemetry/processing/processor_integration.dart +++ b/packages/dart/lib/src/telemetry/processing/processor_integration.dart @@ -1,5 +1,5 @@ import '../../../sentry.dart'; -import '../metric/sentry_metric.dart'; +import '../metric/metric.dart'; import 'in_memory_buffer.dart'; import 'processor.dart'; diff --git a/packages/dart/test/mocks/mock_telemetry_processor.dart b/packages/dart/test/mocks/mock_telemetry_processor.dart index e2179bf143..450f03d0f3 100644 --- a/packages/dart/test/mocks/mock_telemetry_processor.dart +++ b/packages/dart/test/mocks/mock_telemetry_processor.dart @@ -1,5 +1,5 @@ import 'package:sentry/sentry.dart'; -import 'package:sentry/src/telemetry/metric/sentry_metric.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/telemetry/processing/processor.dart'; class MockTelemetryProcessor implements TelemetryProcessor { diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart index 936ef6c385..e754610dc5 100644 --- a/packages/dart/test/telemetry/processing/processor_test.dart +++ b/packages/dart/test/telemetry/processing/processor_test.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; -import 'package:sentry/src/telemetry/metric/sentry_metric.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/telemetry/processing/processor.dart'; import 'package:test/test.dart'; From ffd9fc77b8b7041eafc7dc79b50cc830cfed13da Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 15 Jan 2026 11:17:38 +0100 Subject: [PATCH 13/79] Update --- .../dart/lib/src/telemetry/metric/metric.dart | 16 ++++- .../lib/src/telemetry/metric/metric_type.dart | 14 ----- .../lib/src/telemetry/metric/metrics.dart | 63 ++++++------------- .../metric/metrics_setup_integration.dart | 6 +- 4 files changed, 39 insertions(+), 60 deletions(-) delete mode 100644 packages/dart/lib/src/telemetry/metric/metric_type.dart diff --git a/packages/dart/lib/src/telemetry/metric/metric.dart b/packages/dart/lib/src/telemetry/metric/metric.dart index 39ecef6597..ef6735cb9a 100644 --- a/packages/dart/lib/src/telemetry/metric/metric.dart +++ b/packages/dart/lib/src/telemetry/metric/metric.dart @@ -1,7 +1,21 @@ import 'package:meta/meta.dart'; import '../../../sentry.dart'; -import 'metric_type.dart'; + +/// The type of metric being recorded +enum SentryMetricType { + /// A metric that increments counts + counter('counter'), + + /// A metric that tracks a value that can go up or down + gauge('gauge'), + + /// A metric that tracks statistical distribution of values + distribution('distribution'); + + final String value; + const SentryMetricType(this.value); +} /// Base sealed class for all Sentry metrics sealed class SentryMetric { diff --git a/packages/dart/lib/src/telemetry/metric/metric_type.dart b/packages/dart/lib/src/telemetry/metric/metric_type.dart deleted file mode 100644 index 2e7f86f2bf..0000000000 --- a/packages/dart/lib/src/telemetry/metric/metric_type.dart +++ /dev/null @@ -1,14 +0,0 @@ -/// The type of metric being recorded -enum SentryMetricType { - /// A metric that increments counts - counter('counter'), - - /// A metric that tracks a value that can go up or down - gauge('gauge'), - - /// A metric that tracks statistical distribution of values - distribution('distribution'); - - final String value; - const SentryMetricType(this.value); -} diff --git a/packages/dart/lib/src/telemetry/metric/metrics.dart b/packages/dart/lib/src/telemetry/metric/metrics.dart index 6788e6c713..7ba78adae9 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics.dart @@ -3,61 +3,29 @@ import 'package:meta/meta.dart'; import '../../../sentry.dart'; import 'metric.dart'; -/// Public API for recording metrics -abstract base class SentryMetrics { - void count( - String name, - int value, { - Map? attributes, - Scope? scope, - }); - - void gauge( - String name, - num value, { - String? unit, - Map? attributes, - Scope? scope, - }); - - void distribution( - String name, - num value, { - String? unit, - Map? attributes, - Scope? scope, - }); -} - typedef CaptureMetricCallback = Future Function(SentryMetric metric); typedef ScopeProvider = Scope Function(); @internal -final class DefaultSentryMetrics implements SentryMetrics { - final bool _isMetricsEnabled; +final class SentryMetrics { final CaptureMetricCallback _captureMetricCallback; final ClockProvider _clockProvider; final ScopeProvider _defaultScopeProvider; - DefaultSentryMetrics( - {required bool isMetricsEnabled, - required CaptureMetricCallback captureMetricCallback, + SentryMetrics( + {required CaptureMetricCallback captureMetricCallback, required ClockProvider clockProvider, required ScopeProvider defaultScopeProvider}) - : _isMetricsEnabled = isMetricsEnabled, - _captureMetricCallback = captureMetricCallback, + : _captureMetricCallback = captureMetricCallback, _clockProvider = clockProvider, _defaultScopeProvider = defaultScopeProvider; - @override void count( String name, int value, { Map? attributes, Scope? scope, }) { - if (!_isMetricsEnabled) return; - final metric = SentryCounterMetric( timestamp: _clockProvider(), name: name, @@ -69,7 +37,6 @@ final class DefaultSentryMetrics implements SentryMetrics { _captureMetricCallback(metric); } - @override void gauge( String name, num value, { @@ -77,8 +44,6 @@ final class DefaultSentryMetrics implements SentryMetrics { Map? attributes, Scope? scope, }) { - if (!_isMetricsEnabled) return; - final metric = SentryGaugeMetric( timestamp: _clockProvider(), name: name, @@ -90,7 +55,6 @@ final class DefaultSentryMetrics implements SentryMetrics { _captureMetricCallback(metric); } - @override void distribution( String name, num value, { @@ -98,8 +62,6 @@ final class DefaultSentryMetrics implements SentryMetrics { Map? attributes, Scope? scope, }) { - if (!_isMetricsEnabled) return; - final metric = SentryDistributionMetric( timestamp: _clockProvider(), name: name, @@ -119,7 +81,6 @@ final class DefaultSentryMetrics implements SentryMetrics { (scope ?? _defaultScopeProvider()).span?.context.spanId; } -@internal final class NoOpSentryMetrics implements SentryMetrics { const NoOpSentryMetrics(); @@ -136,4 +97,20 @@ final class NoOpSentryMetrics implements SentryMetrics { @override void gauge(String name, num value, {String? unit, Map? attributes, Scope? scope}) {} + + @override + SpanId? _activeSpanIdFor(Scope? scope) => null; + + @override + CaptureMetricCallback get _captureMetricCallback => (_) async {}; + + @override + ClockProvider get _clockProvider => + () => DateTime.fromMillisecondsSinceEpoch(0); + + @override + ScopeProvider get _defaultScopeProvider => () => Scope(SentryOptions()); + + @override + SentryId _traceIdFor(Scope? scope) => SentryId.empty(); } diff --git a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart index f87628a6fd..5787f17cef 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart @@ -6,8 +6,10 @@ class MetricsSetupIntegration extends Integration { @override void call(Hub hub, SentryOptions options) { - options.metrics = DefaultSentryMetrics( - isMetricsEnabled: options.enableMetrics, + if (!options.enableMetrics) return; + if (options.metrics is! NoOpSentryMetrics) return; + + options.metrics = SentryMetrics( captureMetricCallback: hub.captureMetric, clockProvider: options.clock, defaultScopeProvider: () => hub.scope); From b56a272fc21fffedc0da1aadd7e71ebef5024e92 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 15 Jan 2026 11:21:58 +0100 Subject: [PATCH 14/79] Update --- packages/dart/lib/src/telemetry/processing/processor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dart/lib/src/telemetry/processing/processor.dart b/packages/dart/lib/src/telemetry/processing/processor.dart index d0798f75b4..71e2a1cb5a 100644 --- a/packages/dart/lib/src/telemetry/processing/processor.dart +++ b/packages/dart/lib/src/telemetry/processing/processor.dart @@ -16,7 +16,7 @@ abstract class TelemetryProcessor { void addLog(SentryLog log); /// Adds a metric to be processed and buffered. - void addMetric(SentryMetric log); + void addMetric(SentryMetric metric); /// Flushes all buffered telemetry data. /// From 586ae3d402578b2251a17ffd92a126aa85ae9a35 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 15 Jan 2026 11:35:57 +0100 Subject: [PATCH 15/79] Update --- packages/dart/lib/src/noop_sentry_client.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/dart/lib/src/noop_sentry_client.dart b/packages/dart/lib/src/noop_sentry_client.dart index 05e526e393..a889df5d20 100644 --- a/packages/dart/lib/src/noop_sentry_client.dart +++ b/packages/dart/lib/src/noop_sentry_client.dart @@ -7,6 +7,7 @@ import 'scope.dart'; import 'sentry_client.dart'; import 'sentry_envelope.dart'; import 'sentry_trace_context_header.dart'; +import 'telemetry/metric/metric.dart'; class NoOpSentryClient implements SentryClient { NoOpSentryClient._(); @@ -69,4 +70,7 @@ class NoOpSentryClient implements SentryClient { @override FutureOr captureLog(SentryLog log, {Scope? scope}) async {} + + @override + Future captureMetric(SentryMetric metric, {Scope? scope}) async {} } From 33f991f07969c23319e1a19a249e0c67b93089b5 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Sat, 17 Jan 2026 04:24:03 +0100 Subject: [PATCH 16/79] feat: integrate telemetry metrics into Sentry options and core functionality - Added imports for telemetry metrics in `sentry_options.dart` and `sentry.dart`. - Updated `SentryMetric` class to use string types for metric types instead of an enum. - Adjusted metric constructors to reflect the new string-based type system. - Modified tests to accommodate changes in metric handling. --- packages/dart/lib/src/sentry.dart | 2 ++ packages/dart/lib/src/sentry_options.dart | 2 ++ .../dart/lib/src/telemetry/metric/metric.dart | 27 +++++-------------- .../telemetry/processing/processor_test.dart | 6 ++--- 4 files changed, 12 insertions(+), 25 deletions(-) diff --git a/packages/dart/lib/src/sentry.dart b/packages/dart/lib/src/sentry.dart index 7079c33c08..3f9f69f6a1 100644 --- a/packages/dart/lib/src/sentry.dart +++ b/packages/dart/lib/src/sentry.dart @@ -23,6 +23,8 @@ import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; import 'sentry_run_zoned_guarded.dart'; +import 'telemetry/metric/metrics.dart'; +import 'telemetry/metric/metrics_setup_integration.dart'; import 'telemetry/processing/processor_integration.dart'; import 'tracing.dart'; import 'transport/data_category.dart'; diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 125ea4a4f6..b682c129e5 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -12,6 +12,8 @@ import 'noop_client.dart'; import 'platform/platform.dart'; import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.dart'; +import 'telemetry/metric/metric.dart'; +import 'telemetry/metric/metrics.dart'; import 'telemetry/processing/processor.dart'; import 'transport/noop_transport.dart'; import 'version.dart'; diff --git a/packages/dart/lib/src/telemetry/metric/metric.dart b/packages/dart/lib/src/telemetry/metric/metric.dart index ef6735cb9a..7f00dc5aee 100644 --- a/packages/dart/lib/src/telemetry/metric/metric.dart +++ b/packages/dart/lib/src/telemetry/metric/metric.dart @@ -2,24 +2,9 @@ import 'package:meta/meta.dart'; import '../../../sentry.dart'; -/// The type of metric being recorded -enum SentryMetricType { - /// A metric that increments counts - counter('counter'), - - /// A metric that tracks a value that can go up or down - gauge('gauge'), - - /// A metric that tracks statistical distribution of values - distribution('distribution'); - - final String value; - const SentryMetricType(this.value); -} - -/// Base sealed class for all Sentry metrics +/// The metrics telemetry. sealed class SentryMetric { - final SentryMetricType type; + final String type; DateTime timestamp; String name; @@ -44,7 +29,7 @@ sealed class SentryMetric { Map toJson() { return { 'timestamp': timestamp.millisecondsSinceEpoch / 1000.0, - 'type': type.value, + 'type': type, 'name': name, 'value': value, 'trace_id': traceId, @@ -66,7 +51,7 @@ final class SentryCounterMetric extends SentryMetric { super.spanId, super.unit, super.attributes, - }) : super(type: SentryMetricType.counter); + }) : super(type: 'counter'); } /// Gauge metric - tracks values that can go up or down @@ -79,7 +64,7 @@ final class SentryGaugeMetric extends SentryMetric { super.spanId, super.unit, super.attributes, - }) : super(type: SentryMetricType.gauge); + }) : super(type: 'gauge'); } /// Distribution metric - tracks statistical distribution of values @@ -92,5 +77,5 @@ final class SentryDistributionMetric extends SentryMetric { super.spanId, super.unit, super.attributes, - }) : super(type: SentryMetricType.distribution); + }) : super(type: 'distribution'); } diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart index e754610dc5..dd0d100b70 100644 --- a/packages/dart/test/telemetry/processing/processor_test.dart +++ b/packages/dart/test/telemetry/processing/processor_test.dart @@ -31,7 +31,6 @@ void main() { test('does not throw when no log buffer registered', () { final processor = fixture.getSut(); - processor.logBuffer = null; final log = fixture.createLog(); processor.addLog(log); @@ -53,10 +52,9 @@ void main() { test('does not throw when no metric buffer registered', () { final processor = fixture.getSut(); - processor.logBuffer = null; - final log = fixture.createLog(); - processor.addLog(log); + final metric = fixture.createMetric(); + processor.addMetric(metric); }); }); From b6bff0dc8a7e4cc90b7b8e90ae397a3e3d231399 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Sat, 17 Jan 2026 04:27:55 +0100 Subject: [PATCH 17/79] feat: implement SentryMetrics with Default and NoOp implementations - Introduced DefaultSentryMetrics and NoOpSentryMetrics classes for metric handling. - Updated MetricsSetupIntegration to utilize DefaultSentryMetrics. - Refactored SentryMetrics interface to abstract metric methods. - Added metrics implementation to the Sentry export in sentry.dart. --- packages/dart/lib/sentry.dart | 1 + packages/dart/lib/src/sentry.dart | 2 +- packages/dart/lib/src/sentry_options.dart | 4 +- .../lib/src/telemetry/metric/metrics.dart | 114 +----------------- .../src/telemetry/metric/metrics_impl.dart | 101 ++++++++++++++++ .../metric/metrics_setup_integration.dart | 3 +- 6 files changed, 111 insertions(+), 114 deletions(-) create mode 100644 packages/dart/lib/src/telemetry/metric/metrics_impl.dart diff --git a/packages/dart/lib/sentry.dart b/packages/dart/lib/sentry.dart index 8d01a3181c..46186a1949 100644 --- a/packages/dart/lib/sentry.dart +++ b/packages/dart/lib/sentry.dart @@ -61,5 +61,6 @@ export 'src/utils/url_details.dart'; // ignore: invalid_export_of_internal_element export 'src/utils/breadcrumb_log_level.dart'; export 'src/sentry_logger.dart'; +export 'src/telemetry/metric/metrics.dart'; // ignore: invalid_export_of_internal_element export 'src/utils/internal_logger.dart' show SentryInternalLogger; diff --git a/packages/dart/lib/src/sentry.dart b/packages/dart/lib/src/sentry.dart index 3f9f69f6a1..8e598c9b4b 100644 --- a/packages/dart/lib/src/sentry.dart +++ b/packages/dart/lib/src/sentry.dart @@ -23,8 +23,8 @@ import 'sentry_attachment/sentry_attachment.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; import 'sentry_run_zoned_guarded.dart'; -import 'telemetry/metric/metrics.dart'; import 'telemetry/metric/metrics_setup_integration.dart'; +import 'telemetry/metric/metrics.dart'; import 'telemetry/processing/processor_integration.dart'; import 'tracing.dart'; import 'transport/data_category.dart'; diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index b682c129e5..54bd628d4b 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -13,14 +13,14 @@ import 'platform/platform.dart'; import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.dart'; import 'telemetry/metric/metric.dart'; -import 'telemetry/metric/metrics.dart'; +import 'telemetry/metric/metrics_impl.dart'; import 'telemetry/processing/processor.dart'; import 'transport/noop_transport.dart'; import 'version.dart'; import 'dart:developer' as developer; // TODO: shutdownTimeout, flushTimeoutMillis -// https://api.dart.dev/stable/2.10.2/dart-io/HttpClient/close.html doesn't have a timeout param, we'd need to implement manually +// https://api.dart.dev/stable/2.1w0.2/dart-io/HttpClient/close.html doesn't have a timeout param, we'd need to implement manually /// Sentry SDK options class SentryOptions { diff --git a/packages/dart/lib/src/telemetry/metric/metrics.dart b/packages/dart/lib/src/telemetry/metric/metrics.dart index 7ba78adae9..ae9a826ff9 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics.dart @@ -1,116 +1,10 @@ -import 'package:meta/meta.dart'; - import '../../../sentry.dart'; -import 'metric.dart'; - -typedef CaptureMetricCallback = Future Function(SentryMetric metric); -typedef ScopeProvider = Scope Function(); - -@internal -final class SentryMetrics { - final CaptureMetricCallback _captureMetricCallback; - final ClockProvider _clockProvider; - final ScopeProvider _defaultScopeProvider; - - SentryMetrics( - {required CaptureMetricCallback captureMetricCallback, - required ClockProvider clockProvider, - required ScopeProvider defaultScopeProvider}) - : _captureMetricCallback = captureMetricCallback, - _clockProvider = clockProvider, - _defaultScopeProvider = defaultScopeProvider; - - void count( - String name, - int value, { - Map? attributes, - Scope? scope, - }) { - final metric = SentryCounterMetric( - timestamp: _clockProvider(), - name: name, - value: value, - spanId: _activeSpanIdFor(scope), - traceId: _traceIdFor(scope), - attributes: attributes ?? {}); - - _captureMetricCallback(metric); - } - - void gauge( - String name, - num value, { - String? unit, - Map? attributes, - Scope? scope, - }) { - final metric = SentryGaugeMetric( - timestamp: _clockProvider(), - name: name, - value: value, - spanId: _activeSpanIdFor(scope), - traceId: _traceIdFor(scope), - attributes: attributes ?? {}); - - _captureMetricCallback(metric); - } - - void distribution( - String name, - num value, { - String? unit, - Map? attributes, - Scope? scope, - }) { - final metric = SentryDistributionMetric( - timestamp: _clockProvider(), - name: name, - value: value, - unit: unit, - spanId: _activeSpanIdFor(scope), - traceId: _traceIdFor(scope), - attributes: attributes ?? {}); - - _captureMetricCallback(metric); - } - - SentryId _traceIdFor(Scope? scope) => - (scope ?? _defaultScopeProvider()).propagationContext.traceId; - SpanId? _activeSpanIdFor(Scope? scope) => - (scope ?? _defaultScopeProvider()).span?.context.spanId; -} - -final class NoOpSentryMetrics implements SentryMetrics { - const NoOpSentryMetrics(); - - static const instance = NoOpSentryMetrics(); - - @override +abstract interface class SentryMetrics { void count(String name, int value, - {Map? attributes, Scope? scope}) {} - - @override + {Map? attributes, Scope? scope}); void distribution(String name, num value, - {String? unit, Map? attributes, Scope? scope}) {} - - @override + {String? unit, Map? attributes, Scope? scope}); void gauge(String name, num value, - {String? unit, Map? attributes, Scope? scope}) {} - - @override - SpanId? _activeSpanIdFor(Scope? scope) => null; - - @override - CaptureMetricCallback get _captureMetricCallback => (_) async {}; - - @override - ClockProvider get _clockProvider => - () => DateTime.fromMillisecondsSinceEpoch(0); - - @override - ScopeProvider get _defaultScopeProvider => () => Scope(SentryOptions()); - - @override - SentryId _traceIdFor(Scope? scope) => SentryId.empty(); + {String? unit, Map? attributes, Scope? scope}); } diff --git a/packages/dart/lib/src/telemetry/metric/metrics_impl.dart b/packages/dart/lib/src/telemetry/metric/metrics_impl.dart new file mode 100644 index 0000000000..a62d216eb9 --- /dev/null +++ b/packages/dart/lib/src/telemetry/metric/metrics_impl.dart @@ -0,0 +1,101 @@ +import '../../../sentry.dart'; +import 'metric.dart'; +import 'metrics.dart'; + +typedef CaptureMetricCallback = void Function(SentryMetric metric); +typedef ScopeProvider = Scope Function(); + +final class DefaultSentryMetrics implements SentryMetrics { + final CaptureMetricCallback _captureMetricCallback; + final ClockProvider _clockProvider; + final ScopeProvider _defaultScopeProvider; + + DefaultSentryMetrics( + {required CaptureMetricCallback captureMetricCallback, + required ClockProvider clockProvider, + required ScopeProvider defaultScopeProvider}) + : _captureMetricCallback = captureMetricCallback, + _clockProvider = clockProvider, + _defaultScopeProvider = defaultScopeProvider; + + @override + void count( + String name, + int value, { + Map? attributes, + Scope? scope, + }) { + final metric = SentryCounterMetric( + timestamp: _clockProvider(), + name: name, + value: value, + spanId: _activeSpanIdFor(scope), + traceId: _traceIdFor(scope), + attributes: attributes ?? {}); + + _captureMetricCallback(metric); + } + + @override + void gauge( + String name, + num value, { + String? unit, + Map? attributes, + Scope? scope, + }) { + final metric = SentryGaugeMetric( + timestamp: _clockProvider(), + name: name, + value: value, + spanId: _activeSpanIdFor(scope), + traceId: _traceIdFor(scope), + attributes: attributes ?? {}); + + _captureMetricCallback(metric); + } + + @override + void distribution( + String name, + num value, { + String? unit, + Map? attributes, + Scope? scope, + }) { + final metric = SentryDistributionMetric( + timestamp: _clockProvider(), + name: name, + value: value, + unit: unit, + spanId: _activeSpanIdFor(scope), + traceId: _traceIdFor(scope), + attributes: attributes ?? {}); + + _captureMetricCallback(metric); + } + + SentryId _traceIdFor(Scope? scope) => + (scope ?? _defaultScopeProvider()).propagationContext.traceId; + + SpanId? _activeSpanIdFor(Scope? scope) => + (scope ?? _defaultScopeProvider()).span?.context.spanId; +} + +final class NoOpSentryMetrics implements SentryMetrics { + const NoOpSentryMetrics(); + + static const instance = NoOpSentryMetrics(); + + @override + void count(String name, int value, + {Map? attributes, Scope? scope}) {} + + @override + void distribution(String name, num value, + {String? unit, Map? attributes, Scope? scope}) {} + + @override + void gauge(String name, num value, + {String? unit, Map? attributes, Scope? scope}) {} +} diff --git a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart index 5787f17cef..ccec9dc682 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart @@ -1,5 +1,6 @@ import '../../../sentry.dart'; import 'metrics.dart'; +import 'metrics_impl.dart'; class MetricsSetupIntegration extends Integration { static const integrationName = 'MetricsSetup'; @@ -9,7 +10,7 @@ class MetricsSetupIntegration extends Integration { if (!options.enableMetrics) return; if (options.metrics is! NoOpSentryMetrics) return; - options.metrics = SentryMetrics( + options.metrics = DefaultSentryMetrics( captureMetricCallback: hub.captureMetric, clockProvider: options.clock, defaultScopeProvider: () => hub.scope); From 435917d9145ac9df7318a00cffe09c54512b260c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 19 Jan 2026 17:04:31 +0100 Subject: [PATCH 18/79] feat: enhance telemetry metrics with MetricCapturePipeline and default attributes - Introduced MetricCapturePipeline for capturing and processing metrics. - Added default attributes for telemetry metrics in the new default_attributes.dart file. - Implemented DefaultSentryMetrics for metric handling and integrated it into SentryClient. - Updated SentryClient to utilize the MetricCapturePipeline for capturing metrics. - Added tests to ensure proper functionality of the new metric capturing and processing features. --- packages/dart/lib/src/constants.dart | 70 ++++++ .../dart/lib/src/sdk_lifecycle_hooks.dart | 8 + packages/dart/lib/src/sentry_client.dart | 41 +--- packages/dart/lib/src/sentry_options.dart | 2 +- .../lib/src/telemetry/default_attributes.dart | 55 +++++ ...metrics_impl.dart => default_metrics.dart} | 18 -- .../metric/metric_capture_pipeline.dart | 50 +++++ .../metric/metrics_setup_integration.dart | 4 +- .../src/telemetry/metric/noop_metrics.dart | 19 ++ .../src/telemetry/processing/processor.dart | 13 +- packages/dart/lib/src/utils.dart | 9 + .../mocks/mock_metric_capture_pipeline.dart | 18 ++ packages/dart/test/sentry_client_test.dart | 30 +++ .../metric/metric_capture_pipeline_test.dart | 205 ++++++++++++++++++ .../telemetry/processing/processor_test.dart | 7 - 15 files changed, 482 insertions(+), 67 deletions(-) create mode 100644 packages/dart/lib/src/telemetry/default_attributes.dart rename packages/dart/lib/src/telemetry/metric/{metrics_impl.dart => default_metrics.dart} (81%) create mode 100644 packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart create mode 100644 packages/dart/lib/src/telemetry/metric/noop_metrics.dart create mode 100644 packages/dart/test/mocks/mock_metric_capture_pipeline.dart create mode 100644 packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart diff --git a/packages/dart/lib/src/constants.dart b/packages/dart/lib/src/constants.dart index 640b776832..7cef712c67 100644 --- a/packages/dart/lib/src/constants.dart +++ b/packages/dart/lib/src/constants.dart @@ -39,3 +39,73 @@ class SentrySpanDescriptions { static String dbOpen({required String dbName}) => 'Open database $dbName'; static String dbClose({required String dbName}) => 'Close database $dbName'; } + +/// Semantic attributes for telemetry. +/// +/// Not all attributes apply to every telemetry type. +/// +/// See https://getsentry.github.io/sentry-conventions/generated/attributes/ +/// for more details. +@internal +abstract class SemanticAttributesConstants { + SemanticAttributesConstants._(); + + /// The source of a span, also referred to as transaction source. + /// + /// Known values are: `'custom'`, `'url'`, `'route'`, `'component'`, `'view'`, `'task'`. + static const sentrySpanSource = 'sentry.span.source'; + + /// Attributes that holds the sample rate that was locally applied to a span. + /// If this attribute is not defined, it means that the span inherited a sampling decision. + /// + /// NOTE: Is only defined on root spans. + static const sentrySampleRate = 'sentry.sample_rate'; + + /// Use this attribute to represent the origin of a span. + static const sentryOrigin = 'sentry.origin'; + + /// The release version of the application + static const sentryRelease = 'sentry.release'; + + /// The environment name (e.g., "production", "staging", "development") + static const sentryEnvironment = 'sentry.environment'; + + /// The segment name (e.g., "GET /users") + static const sentrySegmentName = 'sentry.segment.name'; + + /// The span id of the segment that this span belongs to. + static const sentrySegmentId = 'sentry.segment.id'; + + /// The name of the Sentry SDK (e.g., "sentry.dart.flutter") + static const sentrySdkName = 'sentry.sdk.name'; + + /// The version of the Sentry SDK + static const sentrySdkVersion = 'sentry.sdk.version'; + + /// The user ID (gated by `sendDefaultPii`). + static const userId = 'user.id'; + + /// The user email (gated by `sendDefaultPii`). + static const userEmail = 'user.email'; + + /// The user IP address (gated by `sendDefaultPii`). + static const userIpAddress = 'user.ip_address'; + + /// The user username (gated by `sendDefaultPii`). + static const userName = 'user.name'; + + /// The operating system name. + static const osName = 'os.name'; + + /// The operating system version. + static const osVersion = 'os.version'; + + /// The device brand (e.g., "Apple", "Samsung"). + static const deviceBrand = 'device.brand'; + + /// The device model identifier (e.g., "iPhone14,2"). + static const deviceModel = 'device.model'; + + /// The device family (e.g., "iOS", "Android"). + static const deviceFamily = 'device.family'; +} diff --git a/packages/dart/lib/src/sdk_lifecycle_hooks.dart b/packages/dart/lib/src/sdk_lifecycle_hooks.dart index fe803e5e27..b0df1b19b9 100644 --- a/packages/dart/lib/src/sdk_lifecycle_hooks.dart +++ b/packages/dart/lib/src/sdk_lifecycle_hooks.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import '../sentry.dart'; +import 'telemetry/metric/metric.dart'; @internal typedef SdkLifecycleCallback = FutureOr @@ -96,3 +97,10 @@ class OnSpanFinish extends SdkLifecycleEvent { final ISentrySpan span; } + +@internal +class OnProcessMetric extends SdkLifecycleEvent { + final SentryMetric metric; + + OnProcessMetric(this.metric); +} diff --git a/packages/dart/lib/src/sentry_client.dart b/packages/dart/lib/src/sentry_client.dart index 3ba406c5d7..bcd4a45419 100644 --- a/packages/dart/lib/src/sentry_client.dart +++ b/packages/dart/lib/src/sentry_client.dart @@ -19,6 +19,7 @@ import 'sentry_options.dart'; import 'sentry_stack_trace_factory.dart'; import 'sentry_trace_context_header.dart'; import 'telemetry/metric/metric.dart'; +import 'telemetry/metric/metric_capture_pipeline.dart'; import 'transport/client_report_transport.dart'; import 'transport/data_category.dart'; import 'transport/http_transport.dart'; @@ -43,6 +44,7 @@ String get defaultIpAddress => _defaultIpAddress; class SentryClient { final SentryOptions _options; final Random? _random; + final MetricCapturePipeline _metricCapturePipeline; static final _emptySentryId = Future.value(SentryId.empty()); @@ -50,7 +52,8 @@ class SentryClient { SentryStackTraceFactory get _stackTraceFactory => _options.stackTraceFactory; /// Instantiates a client using [SentryOptions] - factory SentryClient(SentryOptions options) { + factory SentryClient(SentryOptions options, + {MetricCapturePipeline? metricCapturePipeline}) { if (options.sendClientReports) { options.recorder = ClientReportRecorder(options.clock); } @@ -75,11 +78,12 @@ class SentryClient { if (enableFlutterSpotlight) { options.transport = SpotlightHttpTransport(options, options.transport); } - return SentryClient._(options); + return SentryClient._( + options, metricCapturePipeline ?? MetricCapturePipeline(options)); } /// Instantiates a client using [SentryOptions] - SentryClient._(this._options) + SentryClient._(this._options, this._metricCapturePipeline) : _random = _options.sampleRate == null ? null : Random(); /// Reports an [event] to Sentry.io. @@ -584,35 +588,8 @@ class SentryClient { } } - Future captureMetric(SentryMetric metric, {Scope? scope}) async { - if (!_options.enableLogs) { - return; - } - - final beforeSendMetric = _options.beforeSendMetric; - SentryMetric? processedMetric = metric; - if (beforeSendMetric != null) { - try { - processedMetric = await beforeSendMetric(metric); - } catch (exception, stackTrace) { - _options.log( - SentryLevel.error, - 'The beforeSendLog callback threw an exception', - exception: exception, - stackTrace: stackTrace, - ); - if (_options.automatedTestMode) { - rethrow; - } - } - } - - // TODO: attributes enricher - - if (processedMetric != null) { - _options.telemetryProcessor.addMetric(processedMetric); - } - } + Future captureMetric(SentryMetric metric, {Scope? scope}) => + _metricCapturePipeline.captureMetric(metric, scope: scope); FutureOr close() { final flush = _options.telemetryProcessor.flush(); diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 54bd628d4b..e5d725d5a9 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -13,7 +13,7 @@ import 'platform/platform.dart'; import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.dart'; import 'telemetry/metric/metric.dart'; -import 'telemetry/metric/metrics_impl.dart'; +import 'telemetry/metric/noop_metrics.dart'; import 'telemetry/processing/processor.dart'; import 'transport/noop_transport.dart'; import 'version.dart'; diff --git a/packages/dart/lib/src/telemetry/default_attributes.dart b/packages/dart/lib/src/telemetry/default_attributes.dart new file mode 100644 index 0000000000..93facb066a --- /dev/null +++ b/packages/dart/lib/src/telemetry/default_attributes.dart @@ -0,0 +1,55 @@ +import '../../sentry.dart'; +import '../utils/os_utils.dart'; + +final _operatingSystem = getSentryOperatingSystem(); + +Map defaultAttributes(SentryOptions options, + {Scope? scope}) { + final attributes = {}; + + attributes[SemanticAttributesConstants.sentrySdkName] = + SentryAttribute.string(options.sdk.name); + + attributes[SemanticAttributesConstants.sentrySdkVersion] = + SentryAttribute.string(options.sdk.version); + + if (options.environment != null) { + attributes[SemanticAttributesConstants.sentryEnvironment] = + SentryAttribute.string(options.environment!); + } + + if (options.release != null) { + attributes[SemanticAttributesConstants.sentryRelease] = + SentryAttribute.string(options.release!); + } + + if (options.sendDefaultPii) { + final user = scope?.user; + if (user != null) { + if (user.id != null) { + attributes[SemanticAttributesConstants.userId] = + SentryAttribute.string(user.id!); + } + if (user.name != null) { + attributes[SemanticAttributesConstants.userName] = + SentryAttribute.string(user.name!); + } + if (user.email != null) { + attributes[SemanticAttributesConstants.userEmail] = + SentryAttribute.string(user.email!); + } + } + } + + if (_operatingSystem.name != null) { + attributes[SemanticAttributesConstants.osName] = + SentryAttribute.string(_operatingSystem.name!); + } + + if (_operatingSystem.version != null) { + attributes[SemanticAttributesConstants.osVersion] = + SentryAttribute.string(_operatingSystem.version!); + } + + return attributes; +} diff --git a/packages/dart/lib/src/telemetry/metric/metrics_impl.dart b/packages/dart/lib/src/telemetry/metric/default_metrics.dart similarity index 81% rename from packages/dart/lib/src/telemetry/metric/metrics_impl.dart rename to packages/dart/lib/src/telemetry/metric/default_metrics.dart index a62d216eb9..6940db9e9e 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics_impl.dart +++ b/packages/dart/lib/src/telemetry/metric/default_metrics.dart @@ -81,21 +81,3 @@ final class DefaultSentryMetrics implements SentryMetrics { SpanId? _activeSpanIdFor(Scope? scope) => (scope ?? _defaultScopeProvider()).span?.context.spanId; } - -final class NoOpSentryMetrics implements SentryMetrics { - const NoOpSentryMetrics(); - - static const instance = NoOpSentryMetrics(); - - @override - void count(String name, int value, - {Map? attributes, Scope? scope}) {} - - @override - void distribution(String name, num value, - {String? unit, Map? attributes, Scope? scope}) {} - - @override - void gauge(String name, num value, - {String? unit, Map? attributes, Scope? scope}) {} -} diff --git a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart new file mode 100644 index 0000000000..b9fc62373b --- /dev/null +++ b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart @@ -0,0 +1,50 @@ +import 'package:meta/meta.dart'; + +import '../../../sentry.dart'; +import '../default_attributes.dart'; +import 'metric.dart'; + +@internal +class MetricCapturePipeline { + final SentryOptions _options; + + MetricCapturePipeline(this._options); + + Future captureMetric(SentryMetric metric, {Scope? scope}) async { + if (!_options.enableMetrics) { + return; + } + + if (scope != null) { + metric.attributes.addAllIfAbsent(scope.attributes); + } + + await _options.lifecycleRegistry + .dispatchCallback(OnProcessMetric(metric)); + + metric.attributes.addAllIfAbsent(defaultAttributes(_options, scope: scope)); + + final beforeSendMetric = _options.beforeSendMetric; + SentryMetric? processedMetric = metric; + if (beforeSendMetric != null) { + try { + processedMetric = await beforeSendMetric(metric); + } catch (exception, stackTrace) { + _options.log( + SentryLevel.error, + 'The beforeSendLog callback threw an exception', + exception: exception, + stackTrace: stackTrace, + ); + if (_options.automatedTestMode) { + rethrow; + } + } + } + if (processedMetric == null) { + return; + } + + _options.telemetryProcessor.addMetric(metric); + } +} diff --git a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart index ccec9dc682..f664eb7b16 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart @@ -1,6 +1,6 @@ import '../../../sentry.dart'; -import 'metrics.dart'; -import 'metrics_impl.dart'; +import 'default_metrics.dart'; +import 'noop_metrics.dart'; class MetricsSetupIntegration extends Integration { static const integrationName = 'MetricsSetup'; diff --git a/packages/dart/lib/src/telemetry/metric/noop_metrics.dart b/packages/dart/lib/src/telemetry/metric/noop_metrics.dart new file mode 100644 index 0000000000..125c8b274b --- /dev/null +++ b/packages/dart/lib/src/telemetry/metric/noop_metrics.dart @@ -0,0 +1,19 @@ +import '../../../sentry.dart'; + +final class NoOpSentryMetrics implements SentryMetrics { + const NoOpSentryMetrics(); + + static const instance = NoOpSentryMetrics(); + + @override + void count(String name, int value, + {Map? attributes, Scope? scope}) {} + + @override + void distribution(String name, num value, + {String? unit, Map? attributes, Scope? scope}) {} + + @override + void gauge(String name, num value, + {String? unit, Map? attributes, Scope? scope}) {} +} diff --git a/packages/dart/lib/src/telemetry/processing/processor.dart b/packages/dart/lib/src/telemetry/processing/processor.dart index 9916e6323b..928dd029fa 100644 --- a/packages/dart/lib/src/telemetry/processing/processor.dart +++ b/packages/dart/lib/src/telemetry/processing/processor.dart @@ -33,12 +33,11 @@ abstract class TelemetryProcessor { class DefaultTelemetryProcessor implements TelemetryProcessor { /// The buffer for metric data, or `null` if metric buffering is disabled. final TelemetryBuffer? _metricBuffer; - - + @visibleForTesting TelemetryBuffer? get metricBuffer => _metricBuffer; - /// The buffer for log data, or `null` if log buffering is disabled. + /// The buffer for log data, or `null` if log buffering is disabled. final TelemetryBuffer? _logBuffer; @visibleForTesting @@ -47,8 +46,8 @@ class DefaultTelemetryProcessor implements TelemetryProcessor { DefaultTelemetryProcessor({ TelemetryBuffer? metricBuffer, TelemetryBuffer? logBuffer, - }) : _metricBuffer = metricBuffer, - _logBuffer = logBuffer; + }) : _metricBuffer = metricBuffer, + _logBuffer = logBuffer; @override void addLog(SentryLog log) { @@ -59,7 +58,7 @@ class DefaultTelemetryProcessor implements TelemetryProcessor { return; } - _logBuffer!.add(log); + _logBuffer.add(log); } @override @@ -71,7 +70,7 @@ class DefaultTelemetryProcessor implements TelemetryProcessor { return; } - _metricBuffer!.add(metric); + _metricBuffer.add(metric); } @override diff --git a/packages/dart/lib/src/utils.dart b/packages/dart/lib/src/utils.dart index db5ca614c2..1d6af4c968 100644 --- a/packages/dart/lib/src/utils.dart +++ b/packages/dart/lib/src/utils.dart @@ -34,3 +34,12 @@ Object? jsonSerializationFallback(Object? nonEncodable) { } return nonEncodable.toString(); } + +@internal +extension AddAllAbsentX on Map { + void addAllIfAbsent(Map other) { + for (final e in other.entries) { + putIfAbsent(e.key, () => e.value); + } + } +} diff --git a/packages/dart/test/mocks/mock_metric_capture_pipeline.dart b/packages/dart/test/mocks/mock_metric_capture_pipeline.dart new file mode 100644 index 0000000000..8f9a5146d7 --- /dev/null +++ b/packages/dart/test/mocks/mock_metric_capture_pipeline.dart @@ -0,0 +1,18 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; +import 'package:sentry/src/telemetry/metric/metric_capture_pipeline.dart'; + +class FakeMetricCapturePipeline extends MetricCapturePipeline { + FakeMetricCapturePipeline(super.options); + + int callCount = 0; + SentryMetric? capturedMetric; + Scope? capturedScope; + + @override + Future captureMetric(SentryMetric metric, {Scope? scope}) async { + callCount++; + capturedMetric = metric; + capturedScope = scope; + } +} diff --git a/packages/dart/test/sentry_client_test.dart b/packages/dart/test/sentry_client_test.dart index e7c46ab55e..b0b5923ce6 100644 --- a/packages/dart/test/sentry_client_test.dart +++ b/packages/dart/test/sentry_client_test.dart @@ -14,6 +14,7 @@ import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry/src/transport/client_report_transport.dart'; import 'package:sentry/src/transport/data_category.dart'; import 'package:sentry/src/transport/noop_transport.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/transport/spotlight_http_transport.dart'; import 'package:sentry/src/utils/iterable_utils.dart'; import 'package:test/test.dart'; @@ -23,6 +24,7 @@ import 'package:http/http.dart' as http; import 'mocks.dart'; import 'mocks/mock_client_report_recorder.dart'; import 'mocks/mock_hub.dart'; +import 'mocks/mock_metric_capture_pipeline.dart'; import 'mocks/mock_telemetry_processor.dart'; import 'mocks/mock_transport.dart'; import 'test_utils.dart'; @@ -2060,6 +2062,34 @@ void main() { }); }); + group('SentryClient captureMetric', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('delegates to metric pipeline', () async { + final pipeline = FakeMetricCapturePipeline(fixture.options); + final client = + SentryClient(fixture.options, metricCapturePipeline: pipeline); + final scope = Scope(fixture.options); + + final metric = SentryCounterMetric( + timestamp: DateTime.now().toUtc(), + name: 'test-metric', + value: 1, + traceId: SentryId.newId(), + ); + + await client.captureMetric(metric, scope: scope); + + expect(pipeline.callCount, 1); + expect(pipeline.capturedMetric, same(metric)); + expect(pipeline.capturedScope, same(scope)); + }); + }); + group('SentryClient captures envelope', () { late Fixture fixture; final fakeEnvelope = getFakeEnvelope(); diff --git a/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart b/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart new file mode 100644 index 0000000000..9a7de99fb2 --- /dev/null +++ b/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart @@ -0,0 +1,205 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; +import 'package:sentry/src/telemetry/metric/metric_capture_pipeline.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_telemetry_processor.dart'; +import '../../test_utils.dart'; + +void main() { + group('$MetricCapturePipeline', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('when capturing a metric', () { + test('forwards to telemetry processor', () async { + final metric = fixture.createMetric(); + + await fixture.pipeline.captureMetric(metric, scope: fixture.scope); + + expect(fixture.processor.addedMetrics.length, 1); + expect(fixture.processor.addedMetrics.first, same(metric)); + }); + + test('adds default attributes', () async { + await fixture.scope.setUser(SentryUser(id: 'user-id')); + fixture.scope.setAttributes({ + 'scope-key': SentryAttribute.string('scope-value'), + }); + + final metric = fixture.createMetric() + ..attributes['custom'] = SentryAttribute.string('metric-value'); + + await fixture.pipeline.captureMetric(metric, scope: fixture.scope); + + final attributes = metric.attributes; + expect(attributes['scope-key']?.value, 'scope-value'); + expect(attributes['custom']?.value, 'metric-value'); + expect(attributes[SemanticAttributesConstants.sentryEnvironment]?.value, + 'test-env'); + expect(attributes[SemanticAttributesConstants.sentryRelease]?.value, + 'test-release'); + expect(attributes[SemanticAttributesConstants.sentrySdkName]?.value, + fixture.options.sdk.name); + expect(attributes[SemanticAttributesConstants.sentrySdkVersion]?.value, + fixture.options.sdk.version); + expect( + attributes[SemanticAttributesConstants.userId]?.value, 'user-id'); + }); + + test('prefers scope attributes over defaults', () async { + fixture.scope.setAttributes({ + SemanticAttributesConstants.sentryEnvironment: + SentryAttribute.string('scope-env'), + }); + + final metric = fixture.createMetric(); + + await fixture.pipeline.captureMetric(metric, scope: fixture.scope); + + final attributes = metric.attributes; + expect(attributes[SemanticAttributesConstants.sentryEnvironment]?.value, + 'scope-env'); + expect(attributes[SemanticAttributesConstants.sentryRelease]?.value, + 'test-release'); + }); + + test( + 'dispatches OnProcessMetric after scope merge but before beforeSendMetric', + () async { + final operations = []; + bool hasScopeAttrInCallback = false; + + fixture.scope.setAttributes({ + 'scope-attr': SentryAttribute.string('scope-value'), + }); + + fixture.options.lifecycleRegistry + .registerCallback((event) { + operations.add('onProcessMetric'); + hasScopeAttrInCallback = + event.metric.attributes.containsKey('scope-attr'); + }); + + fixture.options.beforeSendMetric = (metric) { + operations.add('beforeSendMetric'); + return metric; + }; + + final metric = fixture.createMetric(); + + await fixture.pipeline.captureMetric(metric, scope: fixture.scope); + + expect(operations, ['onProcessMetric', 'beforeSendMetric']); + expect(hasScopeAttrInCallback, isTrue); + }); + + test('keeps attributes added by lifecycle callbacks', () async { + fixture.options.lifecycleRegistry + .registerCallback((event) { + event.metric.attributes['callback-key'] = + SentryAttribute.string('callback-value'); + event.metric + .attributes[SemanticAttributesConstants.sentryEnvironment] = + SentryAttribute.string('callback-env'); + }); + + final metric = fixture.createMetric(); + + await fixture.pipeline.captureMetric(metric, scope: fixture.scope); + + final attributes = metric.attributes; + expect(attributes['callback-key']?.value, 'callback-value'); + expect(attributes[SemanticAttributesConstants.sentryEnvironment]?.value, + 'callback-env'); + }); + + test('does not add user attributes when sendDefaultPii is false', + () async { + fixture.options.sendDefaultPii = false; + await fixture.scope.setUser(SentryUser(id: 'user-id')); + + final metric = fixture.createMetric(); + + await fixture.pipeline.captureMetric(metric, scope: fixture.scope); + + expect( + metric.attributes.containsKey(SemanticAttributesConstants.userId), + isFalse, + ); + }); + }); + + group('when metrics are disabled', () { + test('does not add metrics to processor', () async { + fixture.options.enableMetrics = false; + + final metric = fixture.createMetric(); + + await fixture.pipeline.captureMetric(metric, scope: fixture.scope); + + expect(fixture.processor.addedMetrics, isEmpty); + }); + }); + + group('when beforeSendMetric is configured', () { + test('returning null drops the metric', () async { + fixture.options.beforeSendMetric = (_) => null; + + final metric = fixture.createMetric(); + + await fixture.pipeline.captureMetric(metric, scope: fixture.scope); + + expect(fixture.processor.addedMetrics, isEmpty); + }); + + test('can mutate the metric', () async { + fixture.options.beforeSendMetric = (metric) { + metric.name = 'modified-name'; + metric.attributes['added-key'] = SentryAttribute.string('added'); + return metric; + }; + + final metric = fixture.createMetric(name: 'original-name'); + + await fixture.pipeline.captureMetric(metric, scope: fixture.scope); + + expect(fixture.processor.addedMetrics.length, 1); + final captured = fixture.processor.addedMetrics.first; + expect(captured.name, 'modified-name'); + expect(captured.attributes['added-key']?.value, 'added'); + }); + }); + }); +} + +class Fixture { + final options = defaultTestOptions() + ..environment = 'test-env' + ..release = 'test-release' + ..sendDefaultPii = true + ..enableMetrics = true; + + final processor = MockTelemetryProcessor(); + + late final Scope scope; + late final MetricCapturePipeline pipeline; + + Fixture() { + options.telemetryProcessor = processor; + scope = Scope(options); + pipeline = MetricCapturePipeline(options); + } + + SentryMetric createMetric({String name = 'test-metric'}) { + return SentryCounterMetric( + timestamp: DateTime.now().toUtc(), + name: name, + value: 1, + traceId: SentryId.newId(), + ); + } +} diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart index dd0d100b70..1f81288c43 100644 --- a/packages/dart/test/telemetry/processing/processor_test.dart +++ b/packages/dart/test/telemetry/processing/processor_test.dart @@ -49,13 +49,6 @@ void main() { expect(mockMetricBuffer.addedItems.length, 1); expect(mockMetricBuffer.addedItems.first, metric); }); - - test('does not throw when no metric buffer registered', () { - final processor = fixture.getSut(); - - final metric = fixture.createMetric(); - processor.addMetric(metric); - }); }); group('flush', () { From 07b5051ee25e9b48cc72a17781b0792b7c9beade Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 19 Jan 2026 17:11:41 +0100 Subject: [PATCH 19/79] Add more tests --- packages/dart/lib/src/sentry_options.dart | 2 +- packages/dart/test/sentry_test.dart | 42 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index e5d725d5a9..600e243dd0 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -20,7 +20,7 @@ import 'version.dart'; import 'dart:developer' as developer; // TODO: shutdownTimeout, flushTimeoutMillis -// https://api.dart.dev/stable/2.1w0.2/dart-io/HttpClient/close.html doesn't have a timeout param, we'd need to implement manually +// https://api.dart.dev/stable/2.10.2/dart-io/HttpClient/close.html doesn't have a timeout param, we'd need to implement manually /// Sentry SDK options class SentryOptions { diff --git a/packages/dart/test/sentry_test.dart b/packages/dart/test/sentry_test.dart index ffb609b062..55cf4bc3b4 100644 --- a/packages/dart/test/sentry_test.dart +++ b/packages/dart/test/sentry_test.dart @@ -8,6 +8,7 @@ import 'package:sentry/src/dart_exception_type_identifier.dart'; import 'package:sentry/src/event_processor/deduplication_event_processor.dart'; import 'package:sentry/src/logs_enricher_integration.dart'; import 'package:sentry/src/feature_flags_integration.dart'; +import 'package:sentry/src/telemetry/metric/metrics_setup_integration.dart'; import 'package:sentry/src/telemetry/processing/processor_integration.dart'; import 'package:test/test.dart'; @@ -268,6 +269,27 @@ void main() { expect(integration.callCalls, 1); }); + test('should add $MetricsSetupIntegration', () async { + late SentryOptions optionsReference; + final options = defaultTestOptions(); + + await Sentry.init( + options: options, + (options) { + options.dsn = fakeDsn; + optionsReference = options; + }, + appRunner: appRunner, + ); + + expect( + optionsReference.integrations + .whereType() + .length, + 1, + ); + }); + test('should add default integrations', () async { late SentryOptions optionsReference; final options = defaultTestOptions(); @@ -375,6 +397,26 @@ void main() { ); }); + test('should add feature flag $MetricsSetupIntegration', () async { + await Sentry.init( + options: defaultTestOptions(), + (options) => options.dsn = fakeDsn, + ); + + await Sentry.addFeatureFlag('foo', true); + + expect( + Sentry.currentHub.scope.contexts[SentryFeatureFlags.type]?.values.first + .flag, + equals('foo'), + ); + expect( + Sentry.currentHub.scope.contexts[SentryFeatureFlags.type]?.values.first + .result, + equals(true), + ); + }); + test('addFeatureFlag should ignore non-boolean values', () async { await Sentry.init( options: defaultTestOptions(), From 875ca84622ecb5d719a7d97c29242bda6309370d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 02:37:07 +0100 Subject: [PATCH 20/79] feat: enhance metric capturing and logging in MetricCapturePipeline and MetricsSetupIntegration - Added internal logging for metric capture events in MetricCapturePipeline to provide better visibility on dropped and captured metrics. - Updated MetricsSetupIntegration to log when metrics are disabled or when custom metrics are already configured. - Introduced tests for capturing metrics in the Hub, ensuring proper functionality and scope handling. - Added tests for creating SentryEnvelope and SentryEnvelopeItem from metrics data, verifying correct headers and payloads. --- .../metric/metric_capture_pipeline.dart | 9 +- .../metric/metrics_setup_integration.dart | 15 +- packages/dart/test/hub_test.dart | 56 +++++++ .../dart/test/mocks/mock_sentry_client.dart | 14 ++ .../dart/test/sentry_envelope_item_test.dart | 26 ++++ packages/dart/test/sentry_envelope_test.dart | 31 ++++ .../test/telemetry/metric/metric_test.dart | 73 +++++++++ .../metrics_setup_integration_test.dart | 95 ++++++++++++ .../test/telemetry/metric/metrics_test.dart | 146 ++++++++++++++++++ 9 files changed, 462 insertions(+), 3 deletions(-) create mode 100644 packages/dart/test/telemetry/metric/metric_test.dart create mode 100644 packages/dart/test/telemetry/metric/metrics_setup_integration_test.dart create mode 100644 packages/dart/test/telemetry/metric/metrics_test.dart diff --git a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart index b9fc62373b..a096dd885c 100644 --- a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart +++ b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import '../../../sentry.dart'; +import '../../utils/internal_logger.dart'; import '../default_attributes.dart'; import 'metric.dart'; @@ -12,6 +13,8 @@ class MetricCapturePipeline { Future captureMetric(SentryMetric metric, {Scope? scope}) async { if (!_options.enableMetrics) { + internalLogger.debug( + '$MetricCapturePipeline: Metrics disabled, dropping ${metric.name}'); return; } @@ -32,7 +35,7 @@ class MetricCapturePipeline { } catch (exception, stackTrace) { _options.log( SentryLevel.error, - 'The beforeSendLog callback threw an exception', + 'The beforeSendMetric callback threw an exception', exception: exception, stackTrace: stackTrace, ); @@ -42,9 +45,13 @@ class MetricCapturePipeline { } } if (processedMetric == null) { + internalLogger.debug( + '$MetricCapturePipeline: Metric ${metric.name} dropped by beforeSendMetric'); return; } _options.telemetryProcessor.addMetric(metric); + internalLogger.debug( + '$MetricCapturePipeline: Metric ${metric.name} (${metric.type}) captured'); } } diff --git a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart index f664eb7b16..5c0cfd4742 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart @@ -1,4 +1,5 @@ import '../../../sentry.dart'; +import '../../utils/internal_logger.dart'; import 'default_metrics.dart'; import 'noop_metrics.dart'; @@ -7,8 +8,17 @@ class MetricsSetupIntegration extends Integration { @override void call(Hub hub, SentryOptions options) { - if (!options.enableMetrics) return; - if (options.metrics is! NoOpSentryMetrics) return; + if (!options.enableMetrics) { + internalLogger + .debug('$integrationName: Metrics disabled, skipping setup'); + return; + } + + if (options.metrics is! NoOpSentryMetrics) { + internalLogger.debug( + '$integrationName: Custom metrics already configured, skipping setup'); + return; + } options.metrics = DefaultSentryMetrics( captureMetricCallback: hub.captureMetric, @@ -16,5 +26,6 @@ class MetricsSetupIntegration extends Integration { defaultScopeProvider: () => hub.scope); options.sdk.addIntegration(integrationName); + internalLogger.debug('$integrationName: Metrics configured successfully'); } } diff --git a/packages/dart/test/hub_test.dart b/packages/dart/test/hub_test.dart index ab9f11e048..9c97993ffb 100644 --- a/packages/dart/test/hub_test.dart +++ b/packages/dart/test/hub_test.dart @@ -4,6 +4,7 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/client_reports/discard_reason.dart'; import 'package:sentry/src/propagation_context.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; @@ -943,6 +944,61 @@ void main() { expect(fixture.client.captureLogCalls.first.log, log); }); }); + + group('Hub Metrics', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + SentryMetric givenMetric() { + return SentryCounterMetric( + timestamp: DateTime.now().toUtc(), + name: 'test-metric', + value: 1, + traceId: SentryId.newId(), + attributes: { + 'attribute': SentryAttribute.string('value'), + }, + ); + } + + test('captures metrics', () async { + final hub = fixture.getSut(); + + final metric = givenMetric(); + await hub.captureMetric(metric); + + expect(fixture.client.captureMetricCalls.length, 1); + expect(fixture.client.captureMetricCalls.first.metric, metric); + }); + + test('does not capture metric when hub is disabled', () async { + final hub = fixture.getSut(); + await hub.close(); + + final metric = givenMetric(); + await hub.captureMetric(metric); + + expect(fixture.client.captureMetricCalls, isEmpty); + }); + + test('passes scope to client', () async { + final hub = fixture.getSut(); + hub.configureScope((scope) { + scope.setTag('test-tag', 'test-value'); + }); + + final metric = givenMetric(); + await hub.captureMetric(metric); + + expect(fixture.client.captureMetricCalls.length, 1); + final capturedScope = fixture.client.captureMetricCalls.first.scope; + expect(capturedScope, isNotNull); + expect(capturedScope!.tags['test-tag'], 'test-value'); + }); + }); } class Fixture { diff --git a/packages/dart/test/mocks/mock_sentry_client.dart b/packages/dart/test/mocks/mock_sentry_client.dart index 00a61cc203..0286e3e6b3 100644 --- a/packages/dart/test/mocks/mock_sentry_client.dart +++ b/packages/dart/test/mocks/mock_sentry_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; import 'no_such_method_provider.dart'; @@ -11,6 +12,7 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { List captureTransactionCalls = []; List captureFeedbackCalls = []; List captureLogCalls = []; + List captureMetricCalls = []; int closeCalls = 0; @override @@ -90,6 +92,11 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { captureLogCalls.add(CaptureLogCall(log, scope)); } + @override + Future captureMetric(SentryMetric metric, {Scope? scope}) async { + captureMetricCalls.add(CaptureMetricCall(metric, scope)); + } + @override void close() { closeCalls = closeCalls + 1; @@ -186,3 +193,10 @@ class CaptureLogCall { CaptureLogCall(this.log, this.scope); } + +class CaptureMetricCall { + final SentryMetric metric; + final Scope? scope; + + CaptureMetricCall(this.metric, this.scope); +} diff --git a/packages/dart/test/sentry_envelope_item_test.dart b/packages/dart/test/sentry_envelope_item_test.dart index 54f534de61..cca111b6a0 100644 --- a/packages/dart/test/sentry_envelope_item_test.dart +++ b/packages/dart/test/sentry_envelope_item_test.dart @@ -157,5 +157,31 @@ void main() { expect(sut.originalObject, null); }); + + test('fromMetricsData creates item with correct headers and payload', + () async { + final payload = + utf8.encode('{"items":[{"test":"metric1"},{"test":"metric2"}]}'); + final metricsCount = 2; + + final sut = SentryEnvelopeItem.fromMetricsData(payload, metricsCount); + + expect(sut.header.contentType, + 'application/vnd.sentry.items.trace-metric+json'); + expect(sut.header.type, SentryItemType.metric); + expect(sut.header.itemCount, metricsCount); + + final actualData = await sut.dataFactory(); + expect(actualData, payload); + }); + + test('fromMetricsData does not set originalObject', () async { + final payload = utf8.encode('{"items":[{"test":"metric"}]}'); + final metricsCount = 1; + + final sut = SentryEnvelopeItem.fromMetricsData(payload, metricsCount); + + expect(sut.originalObject, null); + }); }); } diff --git a/packages/dart/test/sentry_envelope_test.dart b/packages/dart/test/sentry_envelope_test.dart index f0953624c7..2225c811e1 100644 --- a/packages/dart/test/sentry_envelope_test.dart +++ b/packages/dart/test/sentry_envelope_test.dart @@ -209,6 +209,37 @@ void main() { expect(actualItem, expectedItem); }); + test('fromMetricsData creates envelope with wrapped metrics payload', + () async { + final encodedMetrics = [ + utf8.encode( + '{"timestamp":1672531200.0,"type":"counter","name":"metric1","value":1,"trace_id":"abc"}'), + utf8.encode( + '{"timestamp":1672531201.0,"type":"gauge","name":"metric2","value":42,"trace_id":"def"}'), + ]; + + final sdkVersion = + SdkVersion(name: 'fixture-name', version: 'fixture-version'); + final sut = SentryEnvelope.fromMetricsData(encodedMetrics, sdkVersion); + + expect(sut.header.eventId, null); + expect(sut.header.sdkVersion, sdkVersion); + expect(sut.items.length, 1); + + expect(sut.items[0].header.contentType, + 'application/vnd.sentry.items.trace-metric+json'); + expect(sut.items[0].header.type, SentryItemType.metric); + expect(sut.items[0].header.itemCount, 2); + + final actualItem = await sut.items[0].dataFactory(); + final expectedPayload = utf8.encode('{"items":[') + + encodedMetrics[0] + + utf8.encode(',') + + encodedMetrics[1] + + utf8.encode(']}'); + expect(actualItem, expectedPayload); + }); + test('max attachment size', () async { final attachment = SentryAttachment.fromLoader( loader: () => Uint8List.fromList([1, 2, 3, 4]), diff --git a/packages/dart/test/telemetry/metric/metric_test.dart b/packages/dart/test/telemetry/metric/metric_test.dart new file mode 100644 index 0000000000..08cf6e80e2 --- /dev/null +++ b/packages/dart/test/telemetry/metric/metric_test.dart @@ -0,0 +1,73 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; +import 'package:test/test.dart'; + +void main() { + group('SentryMetric toJson', () { + test('serializes all fields correctly', () { + final traceId = SentryId.newId(); + final spanId = SpanId.newId(); + final timestamp = DateTime.utc(2024, 1, 15, 10, 30, 0); + + final metric = SentryCounterMetric( + timestamp: timestamp, + name: 'button_clicks', + value: 5, + traceId: traceId, + spanId: spanId, + unit: 'click', + attributes: {'key': SentryAttribute.string('value')}, + ); + + final json = metric.toJson(); + + expect(json['timestamp'], 1705314600.0); + expect(json['type'], 'counter'); + expect(json['name'], 'button_clicks'); + expect(json['value'], 5); + expect(json['trace_id'], traceId); + expect(json['span_id'], spanId); + expect(json['unit'], 'click'); + expect(json['attributes']['key'], {'type': 'string', 'value': 'value'}); + }); + + test('omits optional fields when null', () { + final metric = SentryCounterMetric( + timestamp: DateTime.utc(2024, 1, 15), + name: 'test', + value: 1, + traceId: SentryId.newId(), + ); + + final json = metric.toJson(); + + expect(json.containsKey('span_id'), isFalse); + expect(json.containsKey('unit'), isFalse); + expect(json.containsKey('attributes'), isFalse); + }); + + test('each metric type sets correct type field', () { + final traceId = SentryId.newId(); + final timestamp = DateTime.utc(2024, 1, 15); + + expect( + SentryCounterMetric( + timestamp: timestamp, name: 't', value: 1, traceId: traceId) + .toJson()['type'], + 'counter', + ); + expect( + SentryGaugeMetric( + timestamp: timestamp, name: 't', value: 1, traceId: traceId) + .toJson()['type'], + 'gauge', + ); + expect( + SentryDistributionMetric( + timestamp: timestamp, name: 't', value: 1, traceId: traceId) + .toJson()['type'], + 'distribution', + ); + }); + }); +} diff --git a/packages/dart/test/telemetry/metric/metrics_setup_integration_test.dart b/packages/dart/test/telemetry/metric/metrics_setup_integration_test.dart new file mode 100644 index 0000000000..5dc7aef1d2 --- /dev/null +++ b/packages/dart/test/telemetry/metric/metrics_setup_integration_test.dart @@ -0,0 +1,95 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/metric/default_metrics.dart'; +import 'package:sentry/src/telemetry/metric/metrics_setup_integration.dart'; +import 'package:sentry/src/telemetry/metric/noop_metrics.dart'; +import 'package:test/test.dart'; + +import '../../test_utils.dart'; + +void main() { + group('$MetricsSetupIntegration', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('when metrics are enabled', () { + test('configures DefaultSentryMetrics', () { + fixture.options.enableMetrics = true; + + fixture.sut.call(fixture.hub, fixture.options); + + expect(fixture.options.metrics, isA()); + }); + + test('adds integration to SDK', () { + fixture.options.enableMetrics = true; + + fixture.sut.call(fixture.hub, fixture.options); + + expect( + fixture.options.sdk.integrations, + contains(MetricsSetupIntegration.integrationName), + ); + }); + + test('does not override existing non-noop metrics', () { + fixture.options.enableMetrics = true; + final customMetrics = _CustomSentryMetrics(); + fixture.options.metrics = customMetrics; + + fixture.sut.call(fixture.hub, fixture.options); + + expect(fixture.options.metrics, same(customMetrics)); + }); + }); + + group('when metrics are disabled', () { + test('does not configure metrics', () { + fixture.options.enableMetrics = false; + + fixture.sut.call(fixture.hub, fixture.options); + + expect(fixture.options.metrics, isA()); + }); + + test('does not add integration to SDK', () { + fixture.options.enableMetrics = false; + + fixture.sut.call(fixture.hub, fixture.options); + + expect( + fixture.options.sdk.integrations, + isNot(contains(MetricsSetupIntegration.integrationName)), + ); + }); + }); + }); +} + +class Fixture { + final options = defaultTestOptions(); + + late final Hub hub; + late final MetricsSetupIntegration sut; + + Fixture() { + hub = Hub(options); + sut = MetricsSetupIntegration(); + } +} + +class _CustomSentryMetrics implements SentryMetrics { + @override + void count(String name, int value, + {Map? attributes, Scope? scope}) {} + + @override + void distribution(String name, num value, + {String? unit, Map? attributes, Scope? scope}) {} + + @override + void gauge(String name, num value, + {String? unit, Map? attributes, Scope? scope}) {} +} diff --git a/packages/dart/test/telemetry/metric/metrics_test.dart b/packages/dart/test/telemetry/metric/metrics_test.dart new file mode 100644 index 0000000000..45f2d06cd3 --- /dev/null +++ b/packages/dart/test/telemetry/metric/metrics_test.dart @@ -0,0 +1,146 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/metric/default_metrics.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; +import 'package:sentry/src/telemetry/metric/noop_metrics.dart'; +import 'package:test/test.dart'; + +import '../../test_utils.dart'; + +void main() { + group('$DefaultSentryMetrics', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('when calling count', () { + test('creates counter metric with correct type', () { + fixture.sut.count('test-counter', 5); + + expect(fixture.capturedMetrics.length, 1); + final metric = fixture.capturedMetrics.first; + expect(metric, isA()); + expect(metric.type, 'counter'); + }); + + test('sets name and value', () { + fixture.sut.count('my-counter', 42); + + final metric = fixture.capturedMetrics.first; + expect(metric.name, 'my-counter'); + expect(metric.value, 42); + }); + + test('includes attributes when provided', () { + fixture.sut.count( + 'test-counter', + 1, + attributes: {'key': SentryAttribute.string('value')}, + ); + + final metric = fixture.capturedMetrics.first; + expect(metric.attributes['key']?.value, 'value'); + }); + + test('sets trace id from scope', () { + fixture.sut.count('test-counter', 1); + + final metric = fixture.capturedMetrics.first; + expect(metric.traceId, fixture.scope.propagationContext.traceId); + }); + + test('sets timestamp from clock', () { + fixture.sut.count('test-counter', 1); + + final metric = fixture.capturedMetrics.first; + expect(metric.timestamp, fixture.fixedTimestamp); + }); + }); + + group('when calling gauge', () { + test('creates gauge metric with correct type', () { + fixture.sut.gauge('test-gauge', 42.5); + + expect(fixture.capturedMetrics.length, 1); + final metric = fixture.capturedMetrics.first; + expect(metric, isA()); + expect(metric.type, 'gauge'); + }); + + test('sets name and value', () { + fixture.sut.gauge('memory-usage', 75.5); + + final metric = fixture.capturedMetrics.first; + expect(metric.name, 'memory-usage'); + expect(metric.value, 75.5); + }); + + test('includes attributes when provided', () { + fixture.sut.gauge( + 'test-gauge', + 10, + attributes: {'env': SentryAttribute.string('prod')}, + ); + + final metric = fixture.capturedMetrics.first; + expect(metric.attributes['env']?.value, 'prod'); + }); + }); + + group('when calling distribution', () { + test('creates distribution metric with correct type', () { + fixture.sut.distribution('test-distribution', 100); + + expect(fixture.capturedMetrics.length, 1); + final metric = fixture.capturedMetrics.first; + expect(metric, isA()); + expect(metric.type, 'distribution'); + }); + + test('sets name and value', () { + fixture.sut.distribution('response-time', 250); + + final metric = fixture.capturedMetrics.first; + expect(metric.name, 'response-time'); + expect(metric.value, 250); + }); + + test('includes unit when provided', () { + fixture.sut.distribution('response-time', 250, unit: 'millisecond'); + + final metric = fixture.capturedMetrics.first; + expect(metric.unit, 'millisecond'); + }); + + test('includes attributes when provided', () { + fixture.sut.distribution( + 'test-distribution', + 50, + attributes: {'route': SentryAttribute.string('/api/users')}, + ); + + final metric = fixture.capturedMetrics.first; + expect(metric.attributes['route']?.value, '/api/users'); + }); + }); + }); +} + +class Fixture { + final options = defaultTestOptions(); + final capturedMetrics = []; + final fixedTimestamp = DateTime.utc(2024, 1, 15, 10, 30, 0); + + late final Scope scope; + late final DefaultSentryMetrics sut; + + Fixture() { + scope = Scope(options); + sut = DefaultSentryMetrics( + captureMetricCallback: (metric) => capturedMetrics.add(metric), + clockProvider: () => fixedTimestamp, + defaultScopeProvider: () => scope, + ); + } +} From b5eee19ab9947e85fc98ff73e410e4db34c8ab15 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 03:09:41 +0100 Subject: [PATCH 21/79] feat: enhance Sentry attribute formatting and replay integration - Added new constants for replay ID and buffering state in SemanticAttributesConstants. - Refactored SentryLogger to utilize a new extension for formatting Sentry attributes. - Introduced utility extensions for formatting Sentry attributes and maps of attributes. - Updated MetricCapturePipeline to log metrics with formatted attributes. - Replaced ReplayLogIntegration with ReplayTelemetryIntegration in Flutter integration tests and codebase. - Added tests to ensure proper functionality of the new replay integration and attribute formatting. --- .../src/client_reports/discarded_event.dart | 2 + packages/dart/lib/src/constants.dart | 7 + packages/dart/lib/src/sentry_logger.dart | 44 +-- .../src/telemetry/metric/default_metrics.dart | 18 +- .../metric/metric_capture_pipeline.dart | 4 + .../dart/lib/src/transport/data_category.dart | 3 + packages/dart/lib/src/utils.dart | 56 ++++ .../metric/metric_capture_pipeline_test.dart | 19 ++ .../platform_integrations_test.dart | 10 +- .../integrations/replay_log_integration.dart | 63 ---- .../replay_telemetry_integration.dart | 97 ++++++ packages/flutter/lib/src/sentry_flutter.dart | 6 +- .../replay_log_integration_test.dart | 303 ------------------ .../replay_telemetry_integration_test.dart | 221 +++++++++++++ packages/flutter/test/mocks.dart | 7 + 15 files changed, 443 insertions(+), 417 deletions(-) delete mode 100644 packages/flutter/lib/src/integrations/replay_log_integration.dart create mode 100644 packages/flutter/lib/src/integrations/replay_telemetry_integration.dart delete mode 100644 packages/flutter/test/integrations/replay_log_integration_test.dart create mode 100644 packages/flutter/test/integrations/replay_telemetry_integration_test.dart diff --git a/packages/dart/lib/src/client_reports/discarded_event.dart b/packages/dart/lib/src/client_reports/discarded_event.dart index 24a3471df0..53d8b95bb8 100644 --- a/packages/dart/lib/src/client_reports/discarded_event.dart +++ b/packages/dart/lib/src/client_reports/discarded_event.dart @@ -70,6 +70,8 @@ extension _DataCategoryExtension on DataCategory { return 'feedback'; case DataCategory.metricBucket: return 'metric_bucket'; + case DataCategory.metric: + return 'trace_metric'; } } } diff --git a/packages/dart/lib/src/constants.dart b/packages/dart/lib/src/constants.dart index 7cef712c67..94759424df 100644 --- a/packages/dart/lib/src/constants.dart +++ b/packages/dart/lib/src/constants.dart @@ -82,6 +82,13 @@ abstract class SemanticAttributesConstants { /// The version of the Sentry SDK static const sentrySdkVersion = 'sentry.sdk.version'; + /// The replay ID. + static const sentryReplayId = 'sentry.replay_id'; + + /// Whether the replay is buffering (onErrorSampleRate). + static const sentryInternalReplayIsBuffering = + 'sentry._internal.replay_is_buffering'; + /// The user ID (gated by `sendDefaultPii`). static const userId = 'user.id'; diff --git a/packages/dart/lib/src/sentry_logger.dart b/packages/dart/lib/src/sentry_logger.dart index 1d324e0678..a643ac8eec 100644 --- a/packages/dart/lib/src/sentry_logger.dart +++ b/packages/dart/lib/src/sentry_logger.dart @@ -6,6 +6,7 @@ import 'protocol/sentry_log_level.dart'; import 'protocol/sentry_attribute.dart'; import 'sentry_options.dart'; import 'sentry_logger_formatter.dart'; +import 'utils.dart'; class SentryLogger { SentryLogger(this._clock, {Hub? hub}) : _hub = hub ?? HubAdapter(); @@ -89,47 +90,6 @@ class SentryLogger { if (attributes == null || attributes.isEmpty) { return body; } - - final attrsStr = attributes.entries - .map((e) => '"${e.key}": ${_formatAttributeValue(e.value)}') - .join(', '); - - return '$body {$attrsStr}'; - } - - /// Format attribute value based on its type - String _formatAttributeValue(SentryAttribute attribute) { - switch (attribute.type) { - case 'string': - if (attribute.value is String) { - return '"${attribute.value}"'; - } - break; - case 'boolean': - if (attribute.value is bool) { - return attribute.value.toString(); - } - break; - case 'integer': - if (attribute.value is int) { - return attribute.value.toString(); - } - break; - case 'double': - if (attribute.value is double) { - final value = attribute.value as double; - // Handle special double values - if (value.isNaN || value.isInfinite) { - return value.toString(); - } - // Ensure doubles always show decimal notation to distinguish from ints - // Use toStringAsFixed(1) for whole numbers, toString() for decimals - return value == value.toInt() - ? value.toStringAsFixed(1) - : value.toString(); - } - break; - } - return attribute.value.toString(); + return '$body ${attributes.toFormattedString()}'; } } diff --git a/packages/dart/lib/src/telemetry/metric/default_metrics.dart b/packages/dart/lib/src/telemetry/metric/default_metrics.dart index 6940db9e9e..d8d0d903a9 100644 --- a/packages/dart/lib/src/telemetry/metric/default_metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/default_metrics.dart @@ -1,6 +1,6 @@ import '../../../sentry.dart'; +import '../../utils/internal_logger.dart'; import 'metric.dart'; -import 'metrics.dart'; typedef CaptureMetricCallback = void Function(SentryMetric metric); typedef ScopeProvider = Scope Function(); @@ -25,6 +25,9 @@ final class DefaultSentryMetrics implements SentryMetrics { Map? attributes, Scope? scope, }) { + internalLogger.debug(() => + 'Sentry.metrics.count("$name", $value) called with attributes ${_formatAttributes(attributes)}'); + final metric = SentryCounterMetric( timestamp: _clockProvider(), name: name, @@ -44,6 +47,9 @@ final class DefaultSentryMetrics implements SentryMetrics { Map? attributes, Scope? scope, }) { + internalLogger.debug(() => + 'Sentry.metrics.gauge("$name", $value${_formatUnit(unit)}) called with attributes ${_formatAttributes(attributes)}'); + final metric = SentryGaugeMetric( timestamp: _clockProvider(), name: name, @@ -63,6 +69,9 @@ final class DefaultSentryMetrics implements SentryMetrics { Map? attributes, Scope? scope, }) { + internalLogger.debug(() => + 'Sentry.metrics.distribution("$name", $value${_formatUnit(unit)}) called with attributes ${_formatAttributes(attributes)}'); + final metric = SentryDistributionMetric( timestamp: _clockProvider(), name: name, @@ -80,4 +89,11 @@ final class DefaultSentryMetrics implements SentryMetrics { SpanId? _activeSpanIdFor(Scope? scope) => (scope ?? _defaultScopeProvider()).span?.context.spanId; + + String _formatUnit(String? unit) => unit != null ? ', unit: $unit' : ''; + + String _formatAttributes(Map? attributes) { + final formatted = attributes?.toFormattedString() ?? ''; + return formatted.isEmpty ? '' : ' $formatted'; + } } diff --git a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart index a096dd885c..7c097850ef 100644 --- a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart +++ b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart @@ -1,6 +1,8 @@ import 'package:meta/meta.dart'; import '../../../sentry.dart'; +import '../../client_reports/discard_reason.dart'; +import '../../transport/data_category.dart'; import '../../utils/internal_logger.dart'; import '../default_attributes.dart'; import 'metric.dart'; @@ -45,6 +47,8 @@ class MetricCapturePipeline { } } if (processedMetric == null) { + _options.recorder + .recordLostEvent(DiscardReason.beforeSend, DataCategory.metric); internalLogger.debug( '$MetricCapturePipeline: Metric ${metric.name} dropped by beforeSendMetric'); return; diff --git a/packages/dart/lib/src/transport/data_category.dart b/packages/dart/lib/src/transport/data_category.dart index 89f983f3a7..5dbba0f392 100644 --- a/packages/dart/lib/src/transport/data_category.dart +++ b/packages/dart/lib/src/transport/data_category.dart @@ -11,6 +11,7 @@ enum DataCategory { metricBucket, logItem, feedback, + metric, unknown; static DataCategory fromItemType(String itemType) { @@ -29,6 +30,8 @@ enum DataCategory { return DataCategory.metricBucket; case 'log': return DataCategory.logItem; + case 'trace_metric': + return DataCategory.metric; case 'feedback': return DataCategory.feedback; default: diff --git a/packages/dart/lib/src/utils.dart b/packages/dart/lib/src/utils.dart index 1d6af4c968..407bceab07 100644 --- a/packages/dart/lib/src/utils.dart +++ b/packages/dart/lib/src/utils.dart @@ -6,6 +6,8 @@ import 'dart:convert'; import 'package:meta/meta.dart'; +import 'protocol/sentry_attribute.dart'; + /// Sentry does not take a timezone and instead expects the date-time to be /// submitted in UTC timezone. @internal @@ -43,3 +45,57 @@ extension AddAllAbsentX on Map { } } } + +@internal +extension SentryAttributeFormatting on SentryAttribute { + /// Formats the attribute value for debug/log output. + /// + /// Strings are quoted, numbers and booleans are shown as-is. + String toFormattedString() { + switch (type) { + case 'string': + if (value is String) { + return '"$value"'; + } + break; + case 'boolean': + if (value is bool) { + return value.toString(); + } + break; + case 'integer': + if (value is int) { + return value.toString(); + } + break; + case 'double': + if (value is double) { + final doubleValue = value as double; + // Handle special double values + if (doubleValue.isNaN || doubleValue.isInfinite) { + return doubleValue.toString(); + } + // Ensure doubles always show decimal notation to distinguish from ints + return doubleValue == doubleValue.toInt() + ? doubleValue.toStringAsFixed(1) + : doubleValue.toString(); + } + break; + } + return value.toString(); + } +} + +@internal +extension SentryAttributeMapFormatting on Map { + /// Formats attributes as `{key1: value1, key2: value2}`. + /// + /// Returns an empty string if the map is empty. + String toFormattedString() { + if (isEmpty) return ''; + final attrsStr = entries + .map((e) => '"${e.key}": ${e.value.toFormattedString()}') + .join(', '); + return '{$attrsStr}'; + } +} diff --git a/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart b/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart index 9a7de99fb2..5467756b8c 100644 --- a/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart +++ b/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart @@ -1,8 +1,11 @@ import 'package:sentry/sentry.dart'; +import 'package:sentry/src/client_reports/discard_reason.dart'; import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/telemetry/metric/metric_capture_pipeline.dart'; +import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; +import '../../mocks/mock_client_report_recorder.dart'; import '../../mocks/mock_telemetry_processor.dart'; import '../../test_utils.dart'; @@ -156,6 +159,20 @@ void main() { expect(fixture.processor.addedMetrics, isEmpty); }); + test('returning null records lost event in client report', () async { + fixture.options.beforeSendMetric = (_) => null; + + final metric = fixture.createMetric(); + + await fixture.pipeline.captureMetric(metric, scope: fixture.scope); + + expect(fixture.recorder.discardedEvents.length, 1); + expect(fixture.recorder.discardedEvents.first.reason, + DiscardReason.beforeSend); + expect(fixture.recorder.discardedEvents.first.category, + DataCategory.metric); + }); + test('can mutate the metric', () async { fixture.options.beforeSendMetric = (metric) { metric.name = 'modified-name'; @@ -184,12 +201,14 @@ class Fixture { ..enableMetrics = true; final processor = MockTelemetryProcessor(); + final recorder = MockClientReportRecorder(); late final Scope scope; late final MetricCapturePipeline pipeline; Fixture() { options.telemetryProcessor = processor; + options.recorder = recorder; scope = Scope(options); pipeline = MetricCapturePipeline(options); } diff --git a/packages/flutter/example/integration_test/platform_integrations_test.dart b/packages/flutter/example/integration_test/platform_integrations_test.dart index 75c675e754..d6a760166b 100644 --- a/packages/flutter/example/integration_test/platform_integrations_test.dart +++ b/packages/flutter/example/integration_test/platform_integrations_test.dart @@ -14,7 +14,7 @@ import 'package:sentry_flutter/src/integrations/generic_app_start_integration.da import 'package:sentry_flutter/src/integrations/load_contexts_integration.dart'; import 'package:sentry_flutter/src/integrations/native_load_debug_images_integration.dart'; import 'package:sentry_flutter/src/integrations/native_sdk_integration.dart'; -import 'package:sentry_flutter/src/integrations/replay_log_integration.dart'; +import 'package:sentry_flutter/src/integrations/replay_telemetry_integration.dart'; import 'package:sentry_flutter/src/integrations/screenshot_integration.dart'; import 'package:sentry_flutter/src/integrations/thread_info_integration.dart'; import 'package:sentry_flutter/src/integrations/web_session_integration.dart'; @@ -162,14 +162,14 @@ void main() { isTrue); expect( options.integrations.any((i) => i is ReplayIntegration), isTrue); - expect(options.integrations.any((i) => i is ReplayLogIntegration), + expect(options.integrations.any((i) => i is ReplayTelemetryIntegration), isTrue); } else if (isIOS) { expect(options.integrations.any((i) => i is LoadContextsIntegration), isTrue); expect( options.integrations.any((i) => i is ReplayIntegration), isTrue); - expect(options.integrations.any((i) => i is ReplayLogIntegration), + expect(options.integrations.any((i) => i is ReplayTelemetryIntegration), isTrue); } else if (isMacOS) { expect(options.integrations.any((i) => i is LoadContextsIntegration), @@ -180,7 +180,7 @@ void main() { // still not add it expect( options.integrations.any((i) => i is ReplayIntegration), isTrue); - expect(options.integrations.any((i) => i is ReplayLogIntegration), + expect(options.integrations.any((i) => i is ReplayTelemetryIntegration), isFalse); } }); @@ -235,7 +235,7 @@ void main() { isFalse); expect( options.integrations.any((i) => i is ReplayIntegration), isFalse); - expect(options.integrations.any((i) => i is ReplayLogIntegration), + expect(options.integrations.any((i) => i is ReplayTelemetryIntegration), isFalse); // Ordering: RunZonedGuarded before Widgets diff --git a/packages/flutter/lib/src/integrations/replay_log_integration.dart b/packages/flutter/lib/src/integrations/replay_log_integration.dart deleted file mode 100644 index 2747930cdd..0000000000 --- a/packages/flutter/lib/src/integrations/replay_log_integration.dart +++ /dev/null @@ -1,63 +0,0 @@ -// ignore_for_file: invalid_use_of_internal_member - -import 'package:sentry/sentry.dart'; -import '../sentry_flutter_options.dart'; -import '../native/sentry_native_binding.dart'; - -/// Integration that adds replay-related information to logs using lifecycle callbacks -class ReplayLogIntegration implements Integration { - static const String integrationName = 'ReplayLog'; - - final SentryNativeBinding? _native; - ReplayLogIntegration(this._native); - - SentryFlutterOptions? _options; - SdkLifecycleCallback? _addReplayInformation; - - @override - Future call(Hub hub, SentryFlutterOptions options) async { - if (!options.replay.isEnabled) { - return; - } - final sessionSampleRate = options.replay.sessionSampleRate ?? 0; - final onErrorSampleRate = options.replay.onErrorSampleRate ?? 0; - - _options = options; - _addReplayInformation = (OnBeforeCaptureLog event) { - final scopeReplayId = hub.scope.replayId; - final replayId = scopeReplayId ?? _native?.replayId; - final replayIsBuffering = replayId != null && scopeReplayId == null; - - if (sessionSampleRate > 0 && replayId != null && !replayIsBuffering) { - event.log.attributes['sentry.replay_id'] = SentryAttribute.string( - scopeReplayId.toString(), - ); - } else if (onErrorSampleRate > 0 && - replayId != null && - replayIsBuffering) { - event.log.attributes['sentry.replay_id'] = SentryAttribute.string( - replayId.toString(), - ); - event.log.attributes['sentry._internal.replay_is_buffering'] = - SentryAttribute.bool(true); - } - }; - options.lifecycleRegistry - .registerCallback(_addReplayInformation!); - options.sdk.addIntegration(integrationName); - } - - @override - Future close() async { - final options = _options; - final addReplayInformation = _addReplayInformation; - - if (options != null && addReplayInformation != null) { - options.lifecycleRegistry - .removeCallback(addReplayInformation); - } - - _options = null; - _addReplayInformation = null; - } -} diff --git a/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart new file mode 100644 index 0000000000..4883ad1bb1 --- /dev/null +++ b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart @@ -0,0 +1,97 @@ +// ignore_for_file: invalid_use_of_internal_member + +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; +import '../sentry_flutter_options.dart'; +import '../native/sentry_native_binding.dart'; + +/// Integration that adds replay-related information to logs and metrics +/// using lifecycle callbacks. +@internal +class ReplayTelemetryIntegration implements Integration { + static const String integrationName = 'ReplayTelemetry'; + + final SentryNativeBinding? _native; + ReplayTelemetryIntegration(this._native); + + SentryFlutterOptions? _options; + SdkLifecycleCallback? _onBeforeCaptureLog; + SdkLifecycleCallback? _onProcessMetric; + + @override + Future call(Hub hub, SentryFlutterOptions options) async { + if (!options.replay.isEnabled) { + return; + } + final sessionSampleRate = options.replay.sessionSampleRate ?? 0; + final onErrorSampleRate = options.replay.onErrorSampleRate ?? 0; + + _options = options; + + _onBeforeCaptureLog = (OnBeforeCaptureLog event) { + _addReplayAttributes( + hub.scope.replayId, + event.log.attributes, + sessionSampleRate: sessionSampleRate, + onErrorSampleRate: onErrorSampleRate, + ); + }; + + _onProcessMetric = (OnProcessMetric event) { + _addReplayAttributes( + hub.scope.replayId, + event.metric.attributes, + sessionSampleRate: sessionSampleRate, + onErrorSampleRate: onErrorSampleRate, + ); + }; + + options.lifecycleRegistry + .registerCallback(_onBeforeCaptureLog!); + options.lifecycleRegistry + .registerCallback(_onProcessMetric!); + options.sdk.addIntegration(integrationName); + } + + void _addReplayAttributes( + SentryId? scopeReplayId, + Map attributes, { + required double sessionSampleRate, + required double onErrorSampleRate, + }) { + final replayId = scopeReplayId ?? _native?.replayId; + final replayIsBuffering = replayId != null && scopeReplayId == null; + + if (sessionSampleRate > 0 && replayId != null && !replayIsBuffering) { + attributes[SemanticAttributesConstants.sentryReplayId] = + SentryAttribute.string(scopeReplayId.toString()); + } else if (onErrorSampleRate > 0 && replayId != null && replayIsBuffering) { + attributes[SemanticAttributesConstants.sentryReplayId] = + SentryAttribute.string(replayId.toString()); + attributes[SemanticAttributesConstants.sentryInternalReplayIsBuffering] = + SentryAttribute.bool(true); + } + } + + @override + Future close() async { + final options = _options; + final onBeforeCaptureLog = _onBeforeCaptureLog; + final onProcessMetric = _onProcessMetric; + + if (options != null) { + if (onBeforeCaptureLog != null) { + options.lifecycleRegistry + .removeCallback(onBeforeCaptureLog); + } + if (onProcessMetric != null) { + options.lifecycleRegistry + .removeCallback(onProcessMetric); + } + } + + _options = null; + _onBeforeCaptureLog = null; + _onProcessMetric = null; + } +} diff --git a/packages/flutter/lib/src/sentry_flutter.dart b/packages/flutter/lib/src/sentry_flutter.dart index 0609d087bf..3ec4d43cd9 100644 --- a/packages/flutter/lib/src/sentry_flutter.dart +++ b/packages/flutter/lib/src/sentry_flutter.dart @@ -22,7 +22,7 @@ import 'integrations/flutter_framework_feature_flag_integration.dart'; import 'integrations/frames_tracking_integration.dart'; import 'integrations/integrations.dart'; import 'integrations/native_app_start_handler.dart'; -import 'integrations/replay_log_integration.dart'; +import 'integrations/replay_telemetry_integration.dart'; import 'integrations/screenshot_integration.dart'; import 'integrations/generic_app_start_integration.dart'; import 'integrations/thread_info_integration.dart'; @@ -232,9 +232,9 @@ mixin SentryFlutter { integrations.add(DebugPrintIntegration()); - // Only add ReplayLogIntegration on platforms that support replay + // Only add ReplayTelemetryIntegration on platforms that support replay if (native != null && native.supportsReplay) { - integrations.add(ReplayLogIntegration(native)); + integrations.add(ReplayTelemetryIntegration(native)); } if (!platform.isWeb) { diff --git a/packages/flutter/test/integrations/replay_log_integration_test.dart b/packages/flutter/test/integrations/replay_log_integration_test.dart deleted file mode 100644 index 7a667a050e..0000000000 --- a/packages/flutter/test/integrations/replay_log_integration_test.dart +++ /dev/null @@ -1,303 +0,0 @@ -// ignore_for_file: invalid_use_of_internal_member - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/integrations/replay_log_integration.dart'; - -import '../mocks.mocks.dart'; - -void main() { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - group('ReplayLogIntegration', () { - test('does not register when replay is disabled', () async { - final integration = fixture.getSut(); - fixture.options.replay.sessionSampleRate = 0.0; - fixture.options.replay.onErrorSampleRate = 0.0; - - await integration.call(fixture.hub, fixture.options); - - // Integration should not be registered when replay is disabled - expect(fixture.options.sdk.integrations.contains('ReplayLog'), false); - }); - - test( - 'adds replay_id attribute when sessionSampleRate > 0 and scope replayId is set', - () async { - final integration = fixture.getSut(); - - fixture.options.replay.sessionSampleRate = 0.5; - fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); - - await integration.call(fixture.hub, fixture.options); - - final log = fixture.createTestLog(); - await fixture.hub.captureLog(log); - - expect(log.attributes['sentry.replay_id']?.value, 'testreplayid'); - expect(log.attributes['sentry.replay_id']?.type, 'string'); - - // When scope replayId is set via session sample rate, no buffering flag should be added - expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), - false); - }); - - test( - 'does not add replay_id when sessionSampleRate is 0 even if scope replayId is set', - () async { - final integration = fixture.getSut(); - - fixture.options.replay.sessionSampleRate = 0.0; - fixture.options.replay.onErrorSampleRate = 0.5; // Needed to enable replay - fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); - - await integration.call(fixture.hub, fixture.options); - - final log = fixture.createTestLog(); - await fixture.hub.captureLog(log); - - // With sessionSampleRate = 0, scope replay ID should not be used - // (even though it's set, we're not in session mode) - expect(log.attributes.containsKey('sentry.replay_id'), false); - expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), - false); - }); - - test( - 'does not add replay_id when sessionSampleRate is null even if scope replayId is set', - () async { - final integration = fixture.getSut(); - - fixture.options.replay.sessionSampleRate = null; - fixture.options.replay.onErrorSampleRate = 0.5; // Needed to enable replay - fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); - - await integration.call(fixture.hub, fixture.options); - - final log = fixture.createTestLog(); - await fixture.hub.captureLog(log); - - // With sessionSampleRate = null (treated as 0), scope replay ID should not be used - expect(log.attributes.containsKey('sentry.replay_id'), false); - expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), - false); - }); - - test( - 'uses replay_id when set on scope and sessionSampleRate > 0 (active session mode)', - () async { - final integration = fixture.getSut(); - - fixture.options.replay.sessionSampleRate = 0.5; - fixture.options.replay.onErrorSampleRate = 0.5; - final replayId = SentryId.fromId('test-replay-id'); - fixture.hub.scope.replayId = replayId; - - // Mock native replayId with the same value (same replay, just also set on scope) - when(fixture.nativeBinding.replayId).thenReturn(replayId); - - await integration.call(fixture.hub, fixture.options); - - final log = fixture.createTestLog(); - await fixture.hub.captureLog(log); - - // Should use the replay ID from scope (active session mode) - expect(log.attributes['sentry.replay_id']?.value, 'testreplayid'); - expect(log.attributes['sentry.replay_id']?.type, 'string'); - - // Should NOT add buffering flag when replay is active on scope - expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), - false); - }); - - test( - 'adds replay_id and buffering flag when replay is in buffer mode (scope null, native has ID)', - () async { - final integration = fixture.getSut(); - fixture.options.replay.onErrorSampleRate = 0.5; - // Scope replay ID is null (default), so we're in buffer mode - - // Mock native replayId to simulate buffering mode (same replay, not on scope yet) - final replayId = SentryId.fromId('test-replay-id'); - when(fixture.nativeBinding.replayId).thenReturn(replayId); - - await integration.call(fixture.hub, fixture.options); - - final log = fixture.createTestLog(); - await fixture.hub.captureLog(log); - - // In buffering mode, use native replay ID and add buffering flag - expect(log.attributes['sentry.replay_id']?.value, 'testreplayid'); - expect(log.attributes['sentry.replay_id']?.type, 'string'); - - expect( - log.attributes['sentry._internal.replay_is_buffering']?.value, true); - expect(log.attributes['sentry._internal.replay_is_buffering']?.type, - 'boolean'); - }); - - test( - 'does not add anything when onErrorSampleRate is 0 and no scope replayId', - () async { - final integration = fixture.getSut(); - fixture.options.replay.sessionSampleRate = 0.5; // Needed to enable replay - fixture.options.replay.onErrorSampleRate = 0.0; - // Scope replay ID is null (default) - - // Mock native replayId to simulate what would be buffering mode - final replayId = SentryId.fromId('test-replay-id'); - when(fixture.nativeBinding.replayId).thenReturn(replayId); - - await integration.call(fixture.hub, fixture.options); - - final log = fixture.createTestLog(); - await fixture.hub.captureLog(log); - - // When onErrorSampleRate is 0, native replayId should be ignored even if it exists - expect(log.attributes.containsKey('sentry.replay_id'), false); - expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), - false); - }); - - test( - 'does not add anything when onErrorSampleRate is null and no scope replayId', - () async { - final integration = fixture.getSut(); - fixture.options.replay.sessionSampleRate = 0.5; // Needed to enable replay - fixture.options.replay.onErrorSampleRate = null; - // Scope replay ID is null (default) - - // Mock native replayId to simulate what would be buffering mode - final replayId = SentryId.fromId('test-replay-id'); - when(fixture.nativeBinding.replayId).thenReturn(replayId); - - await integration.call(fixture.hub, fixture.options); - - final log = fixture.createTestLog(); - await fixture.hub.captureLog(log); - - // When onErrorSampleRate is null (treated as 0), native replayId should be ignored - expect(log.attributes.containsKey('sentry.replay_id'), false); - expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), - false); - }); - - test( - 'adds replay_id when scope is null but native has ID and onErrorSampleRate > 0 (buffer mode)', - () async { - final integration = fixture.getSut(); - fixture.options.replay.sessionSampleRate = 0.0; - fixture.options.replay.onErrorSampleRate = 0.5; - // Scope replay ID is null (default), so we're in buffer mode - - // Mock native replayId to simulate buffering mode (replay exists but not on scope) - final replayId = SentryId.fromId('test-replay-id'); - when(fixture.nativeBinding.replayId).thenReturn(replayId); - - await integration.call(fixture.hub, fixture.options); - - final log = fixture.createTestLog(); - await fixture.hub.captureLog(log); - - // When scope is null but native has replay ID, use it in buffer mode - expect(log.attributes['sentry.replay_id']?.value, 'testreplayid'); - expect(log.attributes['sentry.replay_id']?.type, 'string'); - expect( - log.attributes['sentry._internal.replay_is_buffering']?.value, true); - expect(log.attributes['sentry._internal.replay_is_buffering']?.type, - 'boolean'); - }); - - test('registers integration name in SDK with sessionSampleRate', () async { - final integration = fixture.getSut(); - - fixture.options.replay.sessionSampleRate = 0.5; - fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); - - await integration.call(fixture.hub, fixture.options); - - // Integration name is registered in SDK - expect(fixture.options.sdk.integrations.contains('ReplayLog'), true); - }); - - test('registers integration name in SDK with onErrorSampleRate', () async { - final integration = fixture.getSut(); - - fixture.options.replay.onErrorSampleRate = 0.5; - - // Mock native replayId - final replayId = SentryId.fromId('test-replay-id'); - when(fixture.nativeBinding.replayId).thenReturn(replayId); - - await integration.call(fixture.hub, fixture.options); - - // Integration name is registered in SDK - expect(fixture.options.sdk.integrations.contains('ReplayLog'), true); - }); - - test('removes callback on close', () async { - final integration = fixture.getSut(); - - fixture.options.replay.sessionSampleRate = 0.5; - fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); - - await integration.call(fixture.hub, fixture.options); - await integration.close(); - - final log = fixture.createTestLog(); - await fixture.hub.captureLog(log); - - expect(log.attributes.containsKey('sentry.replay_id'), false); - expect(log.attributes.containsKey('sentry._internal.replay_is_buffering'), - false); - }); - - test('integration name is correct', () { - expect(ReplayLogIntegration.integrationName, 'ReplayLog'); - }); - }); -} - -class Fixture { - final options = - SentryFlutterOptions(dsn: 'https://abc@def.ingest.sentry.io/1234567'); - final hub = MockHub(); - final nativeBinding = MockSentryNativeBinding(); - - Fixture() { - options.enableLogs = true; - options.environment = 'test'; - options.release = 'test-release'; - - final scope = Scope(options); - when(hub.options).thenReturn(options); - when(hub.scope).thenReturn(scope); - when(hub.captureLog(any)).thenAnswer((invocation) async { - final log = invocation.positionalArguments.first as SentryLog; - // Trigger the lifecycle callback - await options.lifecycleRegistry.dispatchCallback(OnBeforeCaptureLog(log)); - }); - - // Default: no native replayId - when(nativeBinding.replayId).thenReturn(null); - } - - SentryLog createTestLog() { - return SentryLog( - timestamp: DateTime.now(), - traceId: SentryId.newId(), - level: SentryLogLevel.info, - body: 'test log message', - attributes: {}, - ); - } - - ReplayLogIntegration getSut() { - return ReplayLogIntegration(nativeBinding); - } -} diff --git a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart new file mode 100644 index 0000000000..190625966d --- /dev/null +++ b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart @@ -0,0 +1,221 @@ +// ignore_for_file: invalid_use_of_internal_member + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/integrations/replay_telemetry_integration.dart'; + +import '../mocks.mocks.dart'; + +const _replayId = SemanticAttributesConstants.sentryReplayId; +const _replayIsBuffering = + SemanticAttributesConstants.sentryInternalReplayIsBuffering; + +void main() { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('$ReplayTelemetryIntegration', () { + group('when replay is disabled', () { + test('does not register', () async { + fixture.options.replay.sessionSampleRate = 0.0; + fixture.options.replay.onErrorSampleRate = 0.0; + + await fixture.getSut().call(fixture.hub, fixture.options); + + expect(fixture.options.sdk.integrations, + isNot(contains('ReplayTelemetry'))); + }); + }); + + group('when replay is enabled', () { + test('registers integration', () async { + fixture.options.replay.sessionSampleRate = 0.5; + + await fixture.getSut().call(fixture.hub, fixture.options); + + expect(fixture.options.sdk.integrations, contains('ReplayTelemetry')); + }); + }); + + group('in session mode', () { + setUp(() { + fixture.options.replay.sessionSampleRate = 0.5; + fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); + }); + + test('adds replay_id to logs', () async { + await fixture.getSut().call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes[_replayId]?.value, 'testreplayid'); + }); + + test('adds replay_id to metrics', () async { + await fixture.getSut().call(fixture.hub, fixture.options); + + final metric = fixture.createTestMetric(); + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessMetric(metric)); + + expect(metric.attributes[_replayId]?.value, 'testreplayid'); + }); + + test('does not add buffering flag', () async { + await fixture.getSut().call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes.containsKey(_replayIsBuffering), false); + }); + }); + + group('in buffer mode', () { + setUp(() { + fixture.options.replay.onErrorSampleRate = 0.5; + when(fixture.nativeBinding.replayId) + .thenReturn(SentryId.fromId('test-replay-id')); + }); + + test('adds replay_id to logs', () async { + await fixture.getSut().call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes[_replayId]?.value, 'testreplayid'); + }); + + test('adds replay_id to metrics', () async { + await fixture.getSut().call(fixture.hub, fixture.options); + + final metric = fixture.createTestMetric(); + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessMetric(metric)); + + expect(metric.attributes[_replayId]?.value, 'testreplayid'); + }); + + test('adds buffering flag', () async { + await fixture.getSut().call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes[_replayIsBuffering]?.value, true); + }); + }); + + group('with zero or null sample rates', () { + for (final rate in [0.0, null]) { + test('ignores scope replayId when sessionSampleRate is $rate', + () async { + fixture.options.replay.sessionSampleRate = rate; + fixture.options.replay.onErrorSampleRate = 0.5; + fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); + + await fixture.getSut().call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes.containsKey(_replayId), false); + }); + + test('ignores native replayId when onErrorSampleRate is $rate', + () async { + fixture.options.replay.sessionSampleRate = 0.5; + fixture.options.replay.onErrorSampleRate = rate; + when(fixture.nativeBinding.replayId) + .thenReturn(SentryId.fromId('test-replay-id')); + + await fixture.getSut().call(fixture.hub, fixture.options); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes.containsKey(_replayId), false); + }); + } + }); + + group('when closed', () { + test('removes log callback', () async { + fixture.options.replay.sessionSampleRate = 0.5; + fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); + + final sut = fixture.getSut(); + await sut.call(fixture.hub, fixture.options); + await sut.close(); + + final log = fixture.createTestLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes.containsKey(_replayId), false); + }); + + test('removes metric callback', () async { + fixture.options.replay.sessionSampleRate = 0.5; + fixture.hub.scope.replayId = SentryId.fromId('test-replay-id'); + + final sut = fixture.getSut(); + await sut.call(fixture.hub, fixture.options); + await sut.close(); + + final metric = fixture.createTestMetric(); + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessMetric(metric)); + + expect(metric.attributes.containsKey(_replayId), false); + }); + }); + }); +} + +class Fixture { + final options = + SentryFlutterOptions(dsn: 'https://abc@def.ingest.sentry.io/1234567'); + final hub = MockHub(); + final nativeBinding = MockSentryNativeBinding(); + + Fixture() { + options.enableLogs = true; + options.environment = 'test'; + options.release = 'test-release'; + + final scope = Scope(options); + when(hub.options).thenReturn(options); + when(hub.scope).thenReturn(scope); + when(hub.captureLog(any)).thenAnswer((invocation) async { + final log = invocation.positionalArguments.first as SentryLog; + await options.lifecycleRegistry.dispatchCallback(OnBeforeCaptureLog(log)); + }); + when(nativeBinding.replayId).thenReturn(null); + } + + SentryLog createTestLog() => SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.info, + body: 'test log message', + attributes: {}, + ); + + SentryMetric createTestMetric() => SentryCounterMetric( + timestamp: DateTime.now(), + name: 'test-metric', + value: 1, + traceId: SentryId.newId(), + attributes: {}, + ); + + ReplayTelemetryIntegration getSut() => + ReplayTelemetryIntegration(nativeBinding); +} diff --git a/packages/flutter/test/mocks.dart b/packages/flutter/test/mocks.dart index af190bf78e..2230a545c9 100644 --- a/packages/flutter/test/mocks.dart +++ b/packages/flutter/test/mocks.dart @@ -8,6 +8,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry/src/platform/platform.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/telemetry/processing/processor.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/frames_tracking/sentry_delayed_frames_tracker.dart'; @@ -240,6 +241,7 @@ class MockLogItem { class MockTelemetryProcessor implements TelemetryProcessor { final List addedLogs = []; + final List addedMetrics = []; int flushCalls = 0; int closeCalls = 0; @@ -248,6 +250,11 @@ class MockTelemetryProcessor implements TelemetryProcessor { addedLogs.add(log); } + @override + void addMetric(SentryMetric metric) { + addedMetrics.add(metric); + } + @override void flush() { flushCalls++; From 61bb4bcb3b16e760c6b60067d27ccbd95fd857c9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 03:16:17 +0100 Subject: [PATCH 22/79] feat: include unit in DefaultSentryMetrics and update tests - Added a unit field to the DefaultSentryMetrics for better metric representation. - Updated the metrics test to verify that the unit is included when provided. --- .../dart/lib/src/telemetry/metric/default_metrics.dart | 1 + packages/dart/test/telemetry/metric/metrics_test.dart | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/packages/dart/lib/src/telemetry/metric/default_metrics.dart b/packages/dart/lib/src/telemetry/metric/default_metrics.dart index d8d0d903a9..c54ae034f3 100644 --- a/packages/dart/lib/src/telemetry/metric/default_metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/default_metrics.dart @@ -54,6 +54,7 @@ final class DefaultSentryMetrics implements SentryMetrics { timestamp: _clockProvider(), name: name, value: value, + unit: unit, spanId: _activeSpanIdFor(scope), traceId: _traceIdFor(scope), attributes: attributes ?? {}); diff --git a/packages/dart/test/telemetry/metric/metrics_test.dart b/packages/dart/test/telemetry/metric/metrics_test.dart index 45f2d06cd3..925213136c 100644 --- a/packages/dart/test/telemetry/metric/metrics_test.dart +++ b/packages/dart/test/telemetry/metric/metrics_test.dart @@ -76,6 +76,13 @@ void main() { expect(metric.value, 75.5); }); + test('includes unit when provided', () { + fixture.sut.gauge('response-time', 250, unit: 'millisecond'); + + final metric = fixture.capturedMetrics.first; + expect(metric.unit, 'millisecond'); + }); + test('includes attributes when provided', () { fixture.sut.gauge( 'test-gauge', From 5b55d0af6ee8aeb8b4c4dde0735aad4496826006 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 03:20:17 +0100 Subject: [PATCH 23/79] fix: update metric logging to use processed metrics - Changed the MetricCapturePipeline to log the processed metric instead of the original metric. - Removed unused import from metrics_test.dart to clean up the codebase. --- .../lib/src/telemetry/metric/metric_capture_pipeline.dart | 4 ++-- packages/dart/test/telemetry/metric/metrics_test.dart | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart index 7c097850ef..bf8de7dc33 100644 --- a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart +++ b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart @@ -54,8 +54,8 @@ class MetricCapturePipeline { return; } - _options.telemetryProcessor.addMetric(metric); + _options.telemetryProcessor.addMetric(processedMetric); internalLogger.debug( - '$MetricCapturePipeline: Metric ${metric.name} (${metric.type}) captured'); + '$MetricCapturePipeline: Metric ${processedMetric.name} (${processedMetric.type}) captured'); } } diff --git a/packages/dart/test/telemetry/metric/metrics_test.dart b/packages/dart/test/telemetry/metric/metrics_test.dart index 925213136c..134b1fd390 100644 --- a/packages/dart/test/telemetry/metric/metrics_test.dart +++ b/packages/dart/test/telemetry/metric/metrics_test.dart @@ -1,7 +1,6 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/telemetry/metric/default_metrics.dart'; import 'package:sentry/src/telemetry/metric/metric.dart'; -import 'package:sentry/src/telemetry/metric/noop_metrics.dart'; import 'package:test/test.dart'; import '../../test_utils.dart'; From 264f72bc8d0d7ca42818ab3129e07a36f0d57596 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 11:13:02 +0100 Subject: [PATCH 24/79] Fix missing spec --- packages/dart/lib/src/sentry_options.dart | 2 +- packages/dart/test/sentry_options_test.dart | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 600e243dd0..8591c859de 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -554,7 +554,7 @@ class SentryOptions { /// Enable to capture and send metrics to Sentry. /// /// Disabled by default. - bool enableMetrics = false; + bool enableMetrics = true; /// Enables adding the module in [SentryStackFrame.module]. /// This option only has an effect in non-obfuscated builds. diff --git a/packages/dart/test/sentry_options_test.dart b/packages/dart/test/sentry_options_test.dart index 25402e2f74..cb20034987 100644 --- a/packages/dart/test/sentry_options_test.dart +++ b/packages/dart/test/sentry_options_test.dart @@ -183,4 +183,10 @@ void main() { expect(() => options.parsedDsn, throwsA(isA())); }); + + test('enableMetrics is true by default', () { + final options = defaultTestOptions(); + + expect(options.enableMetrics, true); + }); } From f49e37141e554866dd16993ea142004cab775b84 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 11:18:16 +0100 Subject: [PATCH 25/79] Update CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc9328eefd..953cd3e886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Unreleased + +### Features + +- Trace connected metrics ([#3450](https://github.com/getsentry/sentry-dart/pull/3450)) + - This feature is enabled by default. + - To send metrics use the following APIs: + - `Sentry.metrics.gauge(...)` + - `Sentry.metrics.count(...)` + - `Sentry.metrics.distribution(...)` + + ## 9.10.0 ### Fixes From 5cfeba64db1601219d19f35a0b3e2df9c973bdff Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 11:34:05 +0100 Subject: [PATCH 26/79] Add examples --- .../platform_integrations_test.dart | 9 ++++-- packages/flutter/example/lib/main.dart | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/flutter/example/integration_test/platform_integrations_test.dart b/packages/flutter/example/integration_test/platform_integrations_test.dart index d6a760166b..a76cd94cc6 100644 --- a/packages/flutter/example/integration_test/platform_integrations_test.dart +++ b/packages/flutter/example/integration_test/platform_integrations_test.dart @@ -162,14 +162,16 @@ void main() { isTrue); expect( options.integrations.any((i) => i is ReplayIntegration), isTrue); - expect(options.integrations.any((i) => i is ReplayTelemetryIntegration), + expect( + options.integrations.any((i) => i is ReplayTelemetryIntegration), isTrue); } else if (isIOS) { expect(options.integrations.any((i) => i is LoadContextsIntegration), isTrue); expect( options.integrations.any((i) => i is ReplayIntegration), isTrue); - expect(options.integrations.any((i) => i is ReplayTelemetryIntegration), + expect( + options.integrations.any((i) => i is ReplayTelemetryIntegration), isTrue); } else if (isMacOS) { expect(options.integrations.any((i) => i is LoadContextsIntegration), @@ -180,7 +182,8 @@ void main() { // still not add it expect( options.integrations.any((i) => i is ReplayIntegration), isTrue); - expect(options.integrations.any((i) => i is ReplayTelemetryIntegration), + expect( + options.integrations.any((i) => i is ReplayTelemetryIntegration), isFalse); } }); diff --git a/packages/flutter/example/lib/main.dart b/packages/flutter/example/lib/main.dart index d1e2a18fef..d528c46659 100644 --- a/packages/flutter/example/lib/main.dart +++ b/packages/flutter/example/lib/main.dart @@ -544,6 +544,37 @@ class MainScaffold extends StatelessWidget { text: 'Demonstrates the feature flags.', buttonTitle: 'Add "feature-one" flag', ), + TooltipButton( + onPressed: () { + Sentry.metrics.count( + 'screen.view', + 1, + attributes: { + 'screen': SentryAttribute.string('HomeScreen'), + 'source': SentryAttribute.string('navigation'), + }, + ); + Sentry.metrics.gauge( + 'app.memory_usage', + 128, + unit: 'megabyte', + attributes: { + 'state': SentryAttribute.string('foreground'), + }, + ); + Sentry.metrics.distribution( + 'ui.render_time', + 16.7, + unit: 'millisecond', + attributes: { + 'widget': SentryAttribute.string('ListView'), + 'item_count': SentryAttribute.int(50), + }, + ); + }, + text: 'Demonstrates Sentry Metrics.', + buttonTitle: 'Send Metrics', + ), TooltipButton( onPressed: () { Sentry.logger From fbfcc745500f37ad10b5e5245cd1bf9e343d5d17 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 11:41:56 +0100 Subject: [PATCH 27/79] Update doc --- packages/dart/lib/src/sentry_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 8591c859de..ede49a8e05 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -220,7 +220,7 @@ class SentryOptions { BeforeSendLogCallback? beforeSendLog; /// This function is called right before a metric is about to be sent. - /// Can return a modified metric or null to drop the log. + /// Can return a modified metric or null to drop the metric. BeforeSendMetricCallback? beforeSendMetric; /// Sets the release. SDK will try to automatically configure a release out of the box From c7ca90cf5c88a9beb9af4c0b3b4bb2a215d2d2c0 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 11:42:56 +0100 Subject: [PATCH 28/79] Update doc --- packages/dart/lib/src/sentry_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index ede49a8e05..9c202caf07 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -553,7 +553,7 @@ class SentryOptions { /// Enable to capture and send metrics to Sentry. /// - /// Disabled by default. + /// Enabled by default. bool enableMetrics = true; /// Enables adding the module in [SentryStackFrame.module]. From 7f3b4916b92c28cad3618f238064caab8e2a6240 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 11:52:04 +0100 Subject: [PATCH 29/79] Update --- .../dart/lib/src/telemetry/metric/metric.dart | 2 +- .../load_contexts_integration.dart | 109 +++++++++++------- .../load_contexts_integrations_test.dart | 49 +++++++- 3 files changed, 114 insertions(+), 46 deletions(-) diff --git a/packages/dart/lib/src/telemetry/metric/metric.dart b/packages/dart/lib/src/telemetry/metric/metric.dart index 7f00dc5aee..03381e753e 100644 --- a/packages/dart/lib/src/telemetry/metric/metric.dart +++ b/packages/dart/lib/src/telemetry/metric/metric.dart @@ -3,7 +3,7 @@ import 'package:meta/meta.dart'; import '../../../sentry.dart'; /// The metrics telemetry. -sealed class SentryMetric { +abstract class SentryMetric { final String type; DateTime timestamp; diff --git a/packages/flutter/lib/src/integrations/load_contexts_integration.dart b/packages/flutter/lib/src/integrations/load_contexts_integration.dart index fa9b625ab5..822a1d6182 100644 --- a/packages/flutter/lib/src/integrations/load_contexts_integration.dart +++ b/packages/flutter/lib/src/integrations/load_contexts_integration.dart @@ -1,13 +1,14 @@ +// ignore_for_file: implementation_imports, invalid_use_of_internal_member + import 'dart:async'; import 'package:sentry/sentry.dart'; import 'package:collection/collection.dart'; -// ignore: implementation_imports import 'package:sentry/src/event_processor/enricher/enricher_event_processor.dart'; -// ignore: implementation_imports import 'package:sentry/src/logs_enricher_integration.dart'; import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; +import '../utils/internal_logger.dart'; /// Load Device's Contexts from the iOS & Android SDKs. /// @@ -21,6 +22,7 @@ import '../sentry_flutter_options.dart'; /// This integration is only executed on iOS, macOS & Android Apps. class LoadContextsIntegration extends Integration { final SentryNativeBinding _native; + Map? _cachedAttributes; LoadContextsIntegration(this._native); @@ -45,7 +47,6 @@ class LoadContextsIntegration extends Integration { } if (options.enableLogs) { final logsEnricherIntegration = options.integrations.firstWhereOrNull( - // ignore: invalid_use_of_internal_member (element) => element is LogsEnricherIntegration, ); if (logsEnricherIntegration != null) { @@ -54,55 +55,78 @@ class LoadContextsIntegration extends Integration { options.removeIntegration(logsEnricherIntegration); } - // ignore: invalid_use_of_internal_member options.lifecycleRegistry.registerCallback( (event) async { try { - final infos = await _native.loadContexts() ?? {}; - - final contextsMap = infos['contexts'] as Map?; - final contexts = - Contexts(); // We just need the the native contexts. - _mergeNativeWithLocalContexts(contextsMap, contexts); - - if (contexts.operatingSystem?.name != null) { - event.log.attributes['os.name'] = SentryAttribute.string( - contexts.operatingSystem?.name ?? '', - ); - } - if (contexts.operatingSystem?.version != null) { - event.log.attributes['os.version'] = SentryAttribute.string( - contexts.operatingSystem?.version ?? '', - ); - } - if (contexts.device?.brand != null) { - event.log.attributes['device.brand'] = SentryAttribute.string( - contexts.device?.brand ?? '', - ); - } - if (contexts.device?.model != null) { - event.log.attributes['device.model'] = SentryAttribute.string( - contexts.device?.model ?? '', - ); - } - if (contexts.device?.family != null) { - event.log.attributes['device.family'] = SentryAttribute.string( - contexts.device?.family ?? '', - ); - } + final attributes = await _nativeContextAttributes(); + event.log.attributes.addAllIfAbsent(attributes); } catch (exception, stackTrace) { - options.log( - SentryLevel.error, - 'LoadContextsIntegration failed to load contexts', - exception: exception, + internalLogger.error( + 'LoadContextsIntegration failed to load contexts for $OnBeforeCaptureLog', + error: exception, stackTrace: stackTrace, ); } }, ); } + + if (options.enableMetrics) { + options.lifecycleRegistry + .registerCallback((event) async { + try { + final attributes = await _nativeContextAttributes(); + event.metric.attributes.addAllIfAbsent(attributes); + } catch (exception, stackTrace) { + internalLogger.error( + 'LoadContextsIntegration failed to load contexts for $OnProcessMetric', + error: exception, + stackTrace: stackTrace, + ); + } + }); + } + options.sdk.addIntegration('loadContextsIntegration'); } + + Future> _nativeContextAttributes() async { + if (_cachedAttributes != null) { + return _cachedAttributes!; + } + + final nativeContexts = await _native.loadContexts() ?? {}; + + final contextsMap = nativeContexts['contexts'] as Map?; + final contexts = Contexts(); + _mergeNativeWithLocalContexts(contextsMap, contexts); + + final attributes = {}; + if (contexts.operatingSystem?.name != null) { + attributes[SemanticAttributesConstants.osName] = + SentryAttribute.string(contexts.operatingSystem!.name!); + } + if (contexts.operatingSystem?.version != null) { + attributes[SemanticAttributesConstants.osVersion] = + SentryAttribute.string(contexts.operatingSystem!.version!); + } + if (contexts.device?.brand != null) { + attributes[SemanticAttributesConstants.deviceBrand] = + SentryAttribute.string(contexts.device!.brand!); + } + if (contexts.device?.model != null) { + attributes[SemanticAttributesConstants.deviceModel] = + SentryAttribute.string(contexts.device!.model!); + } + if (contexts.device?.family != null) { + attributes[SemanticAttributesConstants.deviceFamily] = + SentryAttribute.string(contexts.device!.family!); + } + + _cachedAttributes = attributes; + + return attributes; + } } class _LoadContextsIntegrationEventProcessor implements EventProcessor { @@ -247,10 +271,9 @@ class _LoadContextsIntegrationEventProcessor implements EventProcessor { event.tags = tags; } } catch (exception, stackTrace) { - _options.log( - SentryLevel.error, + internalLogger.error( 'loadContextsIntegration failed', - exception: exception, + error: exception, stackTrace: stackTrace, ); if (_options.automatedTestMode) { diff --git a/packages/flutter/test/integrations/load_contexts_integrations_test.dart b/packages/flutter/test/integrations/load_contexts_integrations_test.dart index 4022d7e8cf..b00a96ed88 100644 --- a/packages/flutter/test/integrations/load_contexts_integrations_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integrations_test.dart @@ -1,10 +1,13 @@ @TestOn('vm') library; +// ignore_for_file: invalid_use_of_internal_member + import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/integrations/load_contexts_integration.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; import '../mocks.dart'; import '../mocks.mocks.dart'; @@ -422,6 +425,43 @@ void main() { expect(event?.level, SentryLevel.fatal); }); + + test('with metrics enabled adds native attributes to metric', () async { + fixture.options.enableMetrics = true; + final integration = fixture.getSut(); + integration(fixture.hub, fixture.options); + + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 1); + + final metric = SentryCounterMetric( + timestamp: DateTime.now(), + name: 'random', + value: 1, + traceId: SentryId.newId()); + + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessMetric(metric)); + + verify(fixture.binding.loadContexts()).called(1); + final attributes = metric.attributes; + expect(attributes[SemanticAttributesConstants.osName]?.value, 'os1'); + expect(attributes[SemanticAttributesConstants.osVersion]?.value, + 'fixture-os-version'); + expect(attributes[SemanticAttributesConstants.deviceBrand]?.value, + 'fixture-brand'); + expect(attributes[SemanticAttributesConstants.deviceModel]?.value, + 'fixture-model'); + expect(attributes[SemanticAttributesConstants.deviceFamily]?.value, + 'fixture-family'); + }); + + test('with metrics disabled does not register callback', () async { + fixture.options.enableMetrics = false; + final integration = fixture.getSut(); + integration(fixture.hub, fixture.options); + + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 0); + }); } class Fixture { @@ -434,9 +474,14 @@ class Fixture { 'integrations': ['NativeIntegration'], 'package': {'sdk_name': 'native-package', 'version': '1.0'}, 'contexts': { - 'device': {'name': 'Device1'}, + 'device': { + 'name': 'Device1', + 'brand': 'fixture-brand', + 'model': 'fixture-model', + 'family': 'fixture-family', + }, 'app': {'app_name': 'test-app'}, - 'os': {'name': 'os1'}, + 'os': {'name': 'os1', 'version': 'fixture-os-version'}, 'gpu': {'name': 'gpu1'}, 'browser': {'name': 'browser1'}, 'runtime': {'name': 'RT1'}, From c4221aa2866c38476482bf22ccb412fdbb092ddb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 11:57:10 +0100 Subject: [PATCH 30/79] Update --- .../metric/metric_capture_pipeline.dart | 76 +++++++++++-------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart index bf8de7dc33..e8ace638cf 100644 --- a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart +++ b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart @@ -20,42 +20,54 @@ class MetricCapturePipeline { return; } - if (scope != null) { - metric.attributes.addAllIfAbsent(scope.attributes); - } + try { + if (scope != null) { + metric.attributes.addAllIfAbsent(scope.attributes); + } + + await _options.lifecycleRegistry + .dispatchCallback(OnProcessMetric(metric)); - await _options.lifecycleRegistry - .dispatchCallback(OnProcessMetric(metric)); - - metric.attributes.addAllIfAbsent(defaultAttributes(_options, scope: scope)); - - final beforeSendMetric = _options.beforeSendMetric; - SentryMetric? processedMetric = metric; - if (beforeSendMetric != null) { - try { - processedMetric = await beforeSendMetric(metric); - } catch (exception, stackTrace) { - _options.log( - SentryLevel.error, - 'The beforeSendMetric callback threw an exception', - exception: exception, - stackTrace: stackTrace, - ); - if (_options.automatedTestMode) { - rethrow; + metric.attributes + .addAllIfAbsent(defaultAttributes(_options, scope: scope)); + + final beforeSendMetric = _options.beforeSendMetric; + SentryMetric? processedMetric = metric; + if (beforeSendMetric != null) { + try { + processedMetric = await beforeSendMetric(metric); + } catch (exception, stackTrace) { + _options.log( + SentryLevel.error, + 'The beforeSendMetric callback threw an exception', + exception: exception, + stackTrace: stackTrace, + ); + if (_options.automatedTestMode) { + rethrow; + } } } - } - if (processedMetric == null) { - _options.recorder - .recordLostEvent(DiscardReason.beforeSend, DataCategory.metric); + if (processedMetric == null) { + _options.recorder + .recordLostEvent(DiscardReason.beforeSend, DataCategory.metric); + internalLogger.debug( + '$MetricCapturePipeline: Metric ${metric.name} dropped by beforeSendMetric'); + return; + } + + _options.telemetryProcessor.addMetric(processedMetric); internalLogger.debug( - '$MetricCapturePipeline: Metric ${metric.name} dropped by beforeSendMetric'); - return; + '$MetricCapturePipeline: Metric ${processedMetric.name} (${processedMetric.type}) captured'); + } catch (exception, stackTrace) { + internalLogger.error( + 'Error capturing metric ${metric.name}', + error: exception, + stackTrace: stackTrace, + ); + if (_options.automatedTestMode) { + rethrow; + } } - - _options.telemetryProcessor.addMetric(processedMetric); - internalLogger.debug( - '$MetricCapturePipeline: Metric ${processedMetric.name} (${processedMetric.type}) captured'); } } From 2a553a2006a5b252670184eb0313c66b009db92b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 12:02:27 +0100 Subject: [PATCH 31/79] Update docs --- packages/dart/lib/src/telemetry/metric/metric.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/dart/lib/src/telemetry/metric/metric.dart b/packages/dart/lib/src/telemetry/metric/metric.dart index 03381e753e..6e247eec38 100644 --- a/packages/dart/lib/src/telemetry/metric/metric.dart +++ b/packages/dart/lib/src/telemetry/metric/metric.dart @@ -2,7 +2,10 @@ import 'package:meta/meta.dart'; import '../../../sentry.dart'; -/// The metrics telemetry. +/// Base class for metric data points sent to Sentry. +/// +/// See [SentryCounterMetric], [SentryGaugeMetric], and [SentryDistributionMetric] +/// for concrete metric types. abstract class SentryMetric { final String type; @@ -41,7 +44,7 @@ abstract class SentryMetric { } } -/// Counter metric - increments counts +/// A metric that tracks the number of times an event occurs. final class SentryCounterMetric extends SentryMetric { SentryCounterMetric({ required super.timestamp, @@ -54,7 +57,7 @@ final class SentryCounterMetric extends SentryMetric { }) : super(type: 'counter'); } -/// Gauge metric - tracks values that can go up or down +/// A metric that tracks a value which can increase or decrease over time. final class SentryGaugeMetric extends SentryMetric { SentryGaugeMetric({ required super.timestamp, @@ -67,7 +70,7 @@ final class SentryGaugeMetric extends SentryMetric { }) : super(type: 'gauge'); } -/// Distribution metric - tracks statistical distribution of values +/// A metric that tracks the statistical distribution of a set of values. final class SentryDistributionMetric extends SentryMetric { SentryDistributionMetric({ required super.timestamp, From ba9930cfa9f1138f014c49d235a88d0a80fd66dc Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 12:36:02 +0100 Subject: [PATCH 32/79] Add support for 'trace_metric' category in RateLimitParser and corresponding test case --- packages/dart/lib/src/transport/rate_limit_parser.dart | 2 ++ packages/dart/test/protocol/rate_limit_parser_test.dart | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/packages/dart/lib/src/transport/rate_limit_parser.dart b/packages/dart/lib/src/transport/rate_limit_parser.dart index f0fea1dfde..642f53815a 100644 --- a/packages/dart/lib/src/transport/rate_limit_parser.dart +++ b/packages/dart/lib/src/transport/rate_limit_parser.dart @@ -89,6 +89,8 @@ extension _DataCategoryExtension on DataCategory { return DataCategory.metricBucket; case 'log_item': return DataCategory.logItem; + case 'trace_metric': + return DataCategory.metric; } return DataCategory.unknown; } diff --git a/packages/dart/test/protocol/rate_limit_parser_test.dart b/packages/dart/test/protocol/rate_limit_parser_test.dart index 567dec34f0..cf62c03d5c 100644 --- a/packages/dart/test/protocol/rate_limit_parser_test.dart +++ b/packages/dart/test/protocol/rate_limit_parser_test.dart @@ -114,6 +114,14 @@ void main() { expect(sut[0].category, DataCategory.metricBucket); expect(sut[0].namespaces, isEmpty); }); + + test('parse trace_metric category', () { + final sut = RateLimitParser('60:trace_metric').parseRateLimitHeader(); + + expect(sut.length, 1); + expect(sut[0].category, DataCategory.metric); + expect(sut[0].duration.inMilliseconds, 60000); + }); }); group('parseRetryAfterHeader', () { From e3e55a5e4fb9e1b069eb6952fda32c0ff9e554f6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 12:40:54 +0100 Subject: [PATCH 33/79] Update comments --- .../dart/lib/src/telemetry/metric/metric.dart | 16 ++++++++++++++++ .../dart/lib/src/telemetry/metric/metrics.dart | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/packages/dart/lib/src/telemetry/metric/metric.dart b/packages/dart/lib/src/telemetry/metric/metric.dart index 6e247eec38..b902bba828 100644 --- a/packages/dart/lib/src/telemetry/metric/metric.dart +++ b/packages/dart/lib/src/telemetry/metric/metric.dart @@ -7,14 +7,30 @@ import '../../../sentry.dart'; /// See [SentryCounterMetric], [SentryGaugeMetric], and [SentryDistributionMetric] /// for concrete metric types. abstract class SentryMetric { + /// The metric type identifier (e.g., 'counter', 'gauge', 'distribution'). final String type; + /// The time when the metric was recorded. DateTime timestamp; + + /// The metric name, typically using dot notation (e.g., 'app.memory_usage'). String name; + + /// The numeric value of the metric. num value; + + /// The trace ID from the current propagation context. SentryId traceId; + + /// The span ID of the active span when the metric was recorded. SpanId? spanId; + + /// The unit of measurement (e.g., 'millisecond', 'byte'). + /// + /// For a list of supported units, see https://develop.sentry.dev/sdk/telemetry/attributes/#units. String? unit; + + /// Custom key-value pairs attached to the metric. Map attributes; SentryMetric({ diff --git a/packages/dart/lib/src/telemetry/metric/metrics.dart b/packages/dart/lib/src/telemetry/metric/metrics.dart index ae9a826ff9..032b2f0db4 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics.dart @@ -1,10 +1,26 @@ import '../../../sentry.dart'; +/// Interface for emitting custom metrics to Sentry. +/// +/// Access via [Sentry.metrics]. abstract interface class SentryMetrics { + /// Increments a counter metric by the given [value]. + /// + /// Use counters to track the number of times an event occurs. void count(String name, int value, {Map? attributes, Scope? scope}); + + /// Records a value in a distribution metric. + /// + /// Use distributions to track the statistical distribution of values, + /// such as response times or file sizes. void distribution(String name, num value, {String? unit, Map? attributes, Scope? scope}); + + /// Sets the current value of a gauge metric. + /// + /// Use gauges to track values that can increase or decrease over time, + /// such as memory usage or queue depth. void gauge(String name, num value, {String? unit, Map? attributes, Scope? scope}); } From e92efaf202014569e232e9f9f1a87fad527a6430 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 14:32:52 +0100 Subject: [PATCH 34/79] Add SentryMetricUnit constants --- packages/dart/lib/src/sentry_envelope.dart | 32 ++----- packages/dart/lib/src/sentry_options.dart | 2 +- .../dart/lib/src/telemetry/metric/metric.dart | 84 ++++++++++++++++++- .../lib/src/telemetry/metric/metrics.dart | 4 + .../src/telemetry/metric/noop_metrics.dart | 2 - .../dart/lib/src/transport/data_category.dart | 5 -- .../mocks/mock_metric_capture_pipeline.dart | 16 ++-- packages/dart/test/sentry_client_test.dart | 6 +- 8 files changed, 107 insertions(+), 44 deletions(-) diff --git a/packages/dart/lib/src/sentry_envelope.dart b/packages/dart/lib/src/sentry_envelope.dart index 7b2539d1ba..7be52b243a 100644 --- a/packages/dart/lib/src/sentry_envelope.dart +++ b/packages/dart/lib/src/sentry_envelope.dart @@ -104,30 +104,14 @@ class SentryEnvelope { factory SentryEnvelope.fromLogsData( List> encodedLogs, SdkVersion sdkVersion, - ) { - // Create the payload in the format expected by Sentry - // Format: {"items": [log1, log2, ...]} - final builder = BytesBuilder(copy: false); - builder.add(utf8.encode('{"items":[')); - for (int i = 0; i < encodedLogs.length; i++) { - if (i > 0) { - builder.add(utf8.encode(',')); - } - builder.add(encodedLogs[i]); - } - builder.add(utf8.encode(']}')); - - return SentryEnvelope( - SentryEnvelopeHeader( - null, - sdkVersion, - ), - [ - SentryEnvelopeItem.fromLogsData( - builder.takeBytes(), encodedLogs.length), - ], - ); - } + ) => + SentryEnvelope( + SentryEnvelopeHeader(null, sdkVersion), + [ + SentryEnvelopeItem.fromLogsData( + _buildItemsPayload(encodedLogs), encodedLogs.length) + ], + ); /// Create a [SentryEnvelope] containing raw metric data payload. /// This is used by the log batcher to send pre-encoded metric batches. diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 9c202caf07..7ec9b4e424 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -564,7 +564,7 @@ class SentryOptions { late final SentryLogger logger = SentryLogger(clock); @internal - SentryMetrics metrics = NoOpSentryMetrics.instance; + late SentryMetrics metrics = NoOpSentryMetrics(); @internal TelemetryProcessor telemetryProcessor = NoOpTelemetryProcessor(); diff --git a/packages/dart/lib/src/telemetry/metric/metric.dart b/packages/dart/lib/src/telemetry/metric/metric.dart index b902bba828..da3ed4c90c 100644 --- a/packages/dart/lib/src/telemetry/metric/metric.dart +++ b/packages/dart/lib/src/telemetry/metric/metric.dart @@ -27,7 +27,7 @@ abstract class SentryMetric { /// The unit of measurement (e.g., 'millisecond', 'byte'). /// - /// For a list of supported units, see https://develop.sentry.dev/sdk/telemetry/attributes/#units. + /// See [SentryMetricUnit] for predefined unit constants. String? unit; /// Custom key-value pairs attached to the metric. @@ -74,6 +74,8 @@ final class SentryCounterMetric extends SentryMetric { } /// A metric that tracks a value which can increase or decrease over time. +/// +/// See [SentryMetricUnit] for predefined unit constants. final class SentryGaugeMetric extends SentryMetric { SentryGaugeMetric({ required super.timestamp, @@ -87,6 +89,8 @@ final class SentryGaugeMetric extends SentryMetric { } /// A metric that tracks the statistical distribution of a set of values. +/// +/// See [SentryMetricUnit] for predefined unit constants. final class SentryDistributionMetric extends SentryMetric { SentryDistributionMetric({ required super.timestamp, @@ -98,3 +102,81 @@ final class SentryDistributionMetric extends SentryMetric { super.attributes, }) : super(type: 'distribution'); } + +/// String constants for metric units. +/// +/// These constants represent the API names of measurement units that can be +/// used with metrics. +abstract final class SentryMetricUnit { + /// Nanosecond, 10^-9 seconds. + static const String nanosecond = 'nanosecond'; + + /// Microsecond, 10^-6 seconds. + static const String microsecond = 'microsecond'; + + /// Millisecond, 10^-3 seconds. + static const String millisecond = 'millisecond'; + + /// Full second. + static const String second = 'second'; + + /// Minute, 60 seconds. + static const String minute = 'minute'; + + /// Hour, 3600 seconds. + static const String hour = 'hour'; + + /// Day, 86,400 seconds. + static const String day = 'day'; + + /// Week, 604,800 seconds. + static const String week = 'week'; + + /// Bit, corresponding to 1/8 of a byte. + static const String bit = 'bit'; + + /// Byte. + static const String byte = 'byte'; + + /// Kilobyte, 10^3 bytes. + static const String kilobyte = 'kilobyte'; + + /// Kibibyte, 2^10 bytes. + static const String kibibyte = 'kibibyte'; + + /// Megabyte, 10^6 bytes. + static const String megabyte = 'megabyte'; + + /// Mebibyte, 2^20 bytes. + static const String mebibyte = 'mebibyte'; + + /// Gigabyte, 10^9 bytes. + static const String gigabyte = 'gigabyte'; + + /// Gibibyte, 2^30 bytes. + static const String gibibyte = 'gibibyte'; + + /// Terabyte, 10^12 bytes. + static const String terabyte = 'terabyte'; + + /// Tebibyte, 2^40 bytes. + static const String tebibyte = 'tebibyte'; + + /// Petabyte, 10^15 bytes. + static const String petabyte = 'petabyte'; + + /// Pebibyte, 2^50 bytes. + static const String pebibyte = 'pebibyte'; + + /// Exabyte, 10^18 bytes. + static const String exabyte = 'exabyte'; + + /// Exbibyte, 2^60 bytes. + static const String exbibyte = 'exbibyte'; + + /// Floating point fraction of `1`. + static const String ratio = 'ratio'; + + /// Ratio expressed as a fraction of `100`. `100%` equals a ratio of `1.0`. + static const String percent = 'percent'; +} diff --git a/packages/dart/lib/src/telemetry/metric/metrics.dart b/packages/dart/lib/src/telemetry/metric/metrics.dart index 032b2f0db4..10b3a37901 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics.dart @@ -14,6 +14,8 @@ abstract interface class SentryMetrics { /// /// Use distributions to track the statistical distribution of values, /// such as response times or file sizes. + /// + /// See [SentryMetricUnit] for predefined unit constants. void distribution(String name, num value, {String? unit, Map? attributes, Scope? scope}); @@ -21,6 +23,8 @@ abstract interface class SentryMetrics { /// /// Use gauges to track values that can increase or decrease over time, /// such as memory usage or queue depth. + /// + /// See [SentryMetricUnit] for predefined unit constants. void gauge(String name, num value, {String? unit, Map? attributes, Scope? scope}); } diff --git a/packages/dart/lib/src/telemetry/metric/noop_metrics.dart b/packages/dart/lib/src/telemetry/metric/noop_metrics.dart index 125c8b274b..de71339af8 100644 --- a/packages/dart/lib/src/telemetry/metric/noop_metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/noop_metrics.dart @@ -3,8 +3,6 @@ import '../../../sentry.dart'; final class NoOpSentryMetrics implements SentryMetrics { const NoOpSentryMetrics(); - static const instance = NoOpSentryMetrics(); - @override void count(String name, int value, {Map? attributes, Scope? scope}) {} diff --git a/packages/dart/lib/src/transport/data_category.dart b/packages/dart/lib/src/transport/data_category.dart index 5dbba0f392..f271733c6a 100644 --- a/packages/dart/lib/src/transport/data_category.dart +++ b/packages/dart/lib/src/transport/data_category.dart @@ -8,7 +8,6 @@ enum DataCategory { span, attachment, security, - metricBucket, logItem, feedback, metric, @@ -24,10 +23,6 @@ enum DataCategory { return DataCategory.attachment; case 'transaction': return DataCategory.transaction; - // The envelope item type used for metrics is statsd, - // whereas the client report category is metric_bucket - case 'statsd': - return DataCategory.metricBucket; case 'log': return DataCategory.logItem; case 'trace_metric': diff --git a/packages/dart/test/mocks/mock_metric_capture_pipeline.dart b/packages/dart/test/mocks/mock_metric_capture_pipeline.dart index 8f9a5146d7..79de9b5d8a 100644 --- a/packages/dart/test/mocks/mock_metric_capture_pipeline.dart +++ b/packages/dart/test/mocks/mock_metric_capture_pipeline.dart @@ -2,17 +2,17 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/telemetry/metric/metric_capture_pipeline.dart'; -class FakeMetricCapturePipeline extends MetricCapturePipeline { - FakeMetricCapturePipeline(super.options); +import 'mock_sentry_client.dart'; - int callCount = 0; - SentryMetric? capturedMetric; - Scope? capturedScope; +class MockMetricCapturePipeline extends MetricCapturePipeline { + MockMetricCapturePipeline(super.options); + + final List captureMetricCalls = []; + + int get callCount => captureMetricCalls.length; @override Future captureMetric(SentryMetric metric, {Scope? scope}) async { - callCount++; - capturedMetric = metric; - capturedScope = scope; + captureMetricCalls.add(CaptureMetricCall(metric, scope)); } } diff --git a/packages/dart/test/sentry_client_test.dart b/packages/dart/test/sentry_client_test.dart index b0b5923ce6..7016e59be0 100644 --- a/packages/dart/test/sentry_client_test.dart +++ b/packages/dart/test/sentry_client_test.dart @@ -2070,7 +2070,7 @@ void main() { }); test('delegates to metric pipeline', () async { - final pipeline = FakeMetricCapturePipeline(fixture.options); + final pipeline = MockMetricCapturePipeline(fixture.options); final client = SentryClient(fixture.options, metricCapturePipeline: pipeline); final scope = Scope(fixture.options); @@ -2085,8 +2085,8 @@ void main() { await client.captureMetric(metric, scope: scope); expect(pipeline.callCount, 1); - expect(pipeline.capturedMetric, same(metric)); - expect(pipeline.capturedScope, same(scope)); + expect(pipeline.captureMetricCalls.first.metric, same(metric)); + expect(pipeline.captureMetricCalls.first.scope, same(scope)); }); }); From d530afc55addd8dac82d2d1253dfb3b73d5757ea Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 14:36:36 +0100 Subject: [PATCH 35/79] Fix compilation --- packages/dart/lib/src/transport/data_category.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/dart/lib/src/transport/data_category.dart b/packages/dart/lib/src/transport/data_category.dart index f271733c6a..5dbba0f392 100644 --- a/packages/dart/lib/src/transport/data_category.dart +++ b/packages/dart/lib/src/transport/data_category.dart @@ -8,6 +8,7 @@ enum DataCategory { span, attachment, security, + metricBucket, logItem, feedback, metric, @@ -23,6 +24,10 @@ enum DataCategory { return DataCategory.attachment; case 'transaction': return DataCategory.transaction; + // The envelope item type used for metrics is statsd, + // whereas the client report category is metric_bucket + case 'statsd': + return DataCategory.metricBucket; case 'log': return DataCategory.logItem; case 'trace_metric': From eb2cad901c3d99558bd7d160332d897122fba5aa Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 14:41:37 +0100 Subject: [PATCH 36/79] Fix analyze --- packages/dart/lib/src/telemetry/metric/metrics.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dart/lib/src/telemetry/metric/metrics.dart b/packages/dart/lib/src/telemetry/metric/metrics.dart index 10b3a37901..82f0d8f644 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics.dart @@ -1,4 +1,5 @@ import '../../../sentry.dart'; +import 'metric.dart'; /// Interface for emitting custom metrics to Sentry. /// From 3282f7a1b162ed8482e97d5e60c1cad4db0ef8e5 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 14:42:39 +0100 Subject: [PATCH 37/79] Update --- packages/dart/lib/src/sentry_options.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 7ec9b4e424..f64b2dafc8 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -563,7 +563,6 @@ class SentryOptions { late final SentryLogger logger = SentryLogger(clock); - @internal late SentryMetrics metrics = NoOpSentryMetrics(); @internal From 71e67b734c29b4e11586e51fd1c26ff3aeebd5fc Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 14:45:23 +0100 Subject: [PATCH 38/79] Refactor SentryMetrics handling in SentryOptions to use a private variable with getter and setter --- packages/dart/lib/src/sentry_options.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index f64b2dafc8..6816124023 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -563,7 +563,12 @@ class SentryOptions { late final SentryLogger logger = SentryLogger(clock); - late SentryMetrics metrics = NoOpSentryMetrics(); + late SentryMetrics _metrics = NoOpSentryMetrics(); + + SentryMetrics get metrics => _metrics; + + @internal + set metrics(SentryMetrics value) => _metrics = value; @internal TelemetryProcessor telemetryProcessor = NoOpTelemetryProcessor(); From cd836b524d0c54de8d6a3eb4f062a71ec28d4dd7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 14:50:01 +0100 Subject: [PATCH 39/79] Refactor SentryMetrics in SentryOptions to use a late variable for improved initialization --- packages/dart/lib/src/sentry_options.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 6816124023..7ec9b4e424 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -563,12 +563,8 @@ class SentryOptions { late final SentryLogger logger = SentryLogger(clock); - late SentryMetrics _metrics = NoOpSentryMetrics(); - - SentryMetrics get metrics => _metrics; - @internal - set metrics(SentryMetrics value) => _metrics = value; + late SentryMetrics metrics = NoOpSentryMetrics(); @internal TelemetryProcessor telemetryProcessor = NoOpTelemetryProcessor(); From 26482138a18f2881d5ee4a70ec974ddd23720974 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 15:20:32 +0100 Subject: [PATCH 40/79] Add close --- .../load_contexts_integration.dart | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/flutter/lib/src/integrations/load_contexts_integration.dart b/packages/flutter/lib/src/integrations/load_contexts_integration.dart index 822a1d6182..3d4791d5b1 100644 --- a/packages/flutter/lib/src/integrations/load_contexts_integration.dart +++ b/packages/flutter/lib/src/integrations/load_contexts_integration.dart @@ -20,14 +20,19 @@ import '../utils/internal_logger.dart'; /// App, Device and OS. /// /// This integration is only executed on iOS, macOS & Android Apps. -class LoadContextsIntegration extends Integration { +class LoadContextsIntegration implements Integration { final SentryNativeBinding _native; Map? _cachedAttributes; + SentryFlutterOptions? _options; + SdkLifecycleCallback? _logCallback; + SdkLifecycleCallback? _metricCallback; LoadContextsIntegration(this._native); @override void call(Hub hub, SentryFlutterOptions options) { + _options = options; + options.addEventProcessor( _LoadContextsIntegrationEventProcessor(_native, options), ); @@ -55,25 +60,25 @@ class LoadContextsIntegration extends Integration { options.removeIntegration(logsEnricherIntegration); } + _logCallback = (event) async { + try { + final attributes = await _nativeContextAttributes(); + event.log.attributes.addAllIfAbsent(attributes); + } catch (exception, stackTrace) { + internalLogger.error( + 'LoadContextsIntegration failed to load contexts for $OnBeforeCaptureLog', + error: exception, + stackTrace: stackTrace, + ); + } + }; options.lifecycleRegistry.registerCallback( - (event) async { - try { - final attributes = await _nativeContextAttributes(); - event.log.attributes.addAllIfAbsent(attributes); - } catch (exception, stackTrace) { - internalLogger.error( - 'LoadContextsIntegration failed to load contexts for $OnBeforeCaptureLog', - error: exception, - stackTrace: stackTrace, - ); - } - }, + _logCallback!, ); } if (options.enableMetrics) { - options.lifecycleRegistry - .registerCallback((event) async { + _metricCallback = (event) async { try { final attributes = await _nativeContextAttributes(); event.metric.attributes.addAllIfAbsent(attributes); @@ -84,12 +89,33 @@ class LoadContextsIntegration extends Integration { stackTrace: stackTrace, ); } - }); + }; + options.lifecycleRegistry.registerCallback( + _metricCallback!, + ); } options.sdk.addIntegration('loadContextsIntegration'); } + @override + void close() { + final options = _options; + if (options == null) return; + + if (_logCallback != null) { + options.lifecycleRegistry + .removeCallback(_logCallback!); + _logCallback = null; + } + if (_metricCallback != null) { + options.lifecycleRegistry + .removeCallback(_metricCallback!); + _metricCallback = null; + } + _cachedAttributes = null; + } + Future> _nativeContextAttributes() async { if (_cachedAttributes != null) { return _cachedAttributes!; From 98f48fd5aada4921bedc07d44174e81f85c6606b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 15:25:04 +0100 Subject: [PATCH 41/79] Add close test --- .../load_contexts_integrations_test.dart | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/packages/flutter/test/integrations/load_contexts_integrations_test.dart b/packages/flutter/test/integrations/load_contexts_integrations_test.dart index b00a96ed88..ada1f5bfa9 100644 --- a/packages/flutter/test/integrations/load_contexts_integrations_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integrations_test.dart @@ -462,6 +462,78 @@ void main() { expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 0); }); + + test('close removes metric callback from lifecycle registry', () async { + fixture.options.enableMetrics = true; + final integration = fixture.getSut(); + integration(fixture.hub, fixture.options); + + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 1); + + integration.close(); + + expect( + fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessMetric], + isEmpty); + }); + + test('close removes log callback from lifecycle registry', () async { + fixture.options.enableLogs = true; + final integration = fixture.getSut(); + integration(fixture.hub, fixture.options); + + expect( + fixture + .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + isNotEmpty); + + integration.close(); + + expect( + fixture + .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + isEmpty); + }); + + test('close removes both callbacks when both features enabled', () async { + fixture.options.enableMetrics = true; + fixture.options.enableLogs = true; + final integration = fixture.getSut(); + integration(fixture.hub, fixture.options); + + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 2); + + integration.close(); + + expect( + fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessMetric], + isEmpty); + expect( + fixture + .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + isEmpty); + }); + + test('callback is not invoked after close', () async { + fixture.options.enableMetrics = true; + final integration = fixture.getSut(); + integration(fixture.hub, fixture.options); + + integration.close(); + + final metric = SentryCounterMetric( + timestamp: DateTime.now(), + name: 'random', + value: 1, + traceId: SentryId.newId()); + + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessMetric(metric)); + + // loadContexts should not be called since callback was removed + verifyNever(fixture.binding.loadContexts()); + expect(metric.attributes, isEmpty); + }); } class Fixture { From 756de8a99bb49a6fa5a333d7d056795b18cfe5df Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 18:06:00 +0100 Subject: [PATCH 42/79] Refactor logging integration by replacing LogsEnricherIntegration with a new logging pipeline and default logger implementation. Update related tests and remove deprecated code. --- packages/dart/lib/sentry.dart | 2 +- .../lib/src/logs_enricher_integration.dart | 37 -- .../dart/lib/src/protocol/sentry_log.dart | 6 + .../dart/lib/src/sdk_lifecycle_hooks.dart | 4 +- packages/dart/lib/src/sentry.dart | 6 +- packages/dart/lib/src/sentry_client.dart | 107 +---- packages/dart/lib/src/sentry_logger.dart | 95 ---- .../dart/lib/src/sentry_logger_formatter.dart | 157 ------- packages/dart/lib/src/sentry_options.dart | 3 +- .../lib/src/telemetry/log/default_logger.dart | 302 ++++++++++++ .../telemetry/log/log_capture_pipeline.dart | 79 ++++ .../dart/lib/src/telemetry/log/logger.dart | 106 +++++ .../telemetry/log/logs_setup_integration.dart | 31 ++ .../lib/src/telemetry/log/noop_logger.dart | 66 +++ .../test/logs_enricher_integration_test.dart | 99 ---- .../test/mocks/mock_log_capture_pipeline.dart | 19 + .../dart/test/protocol/sentry_log_test.dart | 21 + .../test/sentry_client_lifecycle_test.dart | 43 -- .../sentry_client_sdk_lifecycle_test.dart | 4 +- packages/dart/test/sentry_client_test.dart | 338 +------------- .../test/sentry_logger_formatter_test.dart | 429 ------------------ packages/dart/test/sentry_logger_test.dart | 252 ---------- packages/dart/test/sentry_test.dart | 22 - .../log/log_capture_pipeline_test.dart | 258 +++++++++++ .../telemetry/log/logger_formatter_test.dart | 295 ++++++++++++ .../dart/test/telemetry/log/logger_test.dart | 172 +++++++ .../log/logs_setup_integration_test.dart | 138 ++++++ .../load_contexts_integration.dart | 19 +- .../replay_telemetry_integration.dart | 17 +- .../load_contexts_integrations_test.dart | 12 +- .../replay_telemetry_integration_test.dart | 2 +- .../test/logging_integration_test.dart | 6 + 32 files changed, 1548 insertions(+), 1599 deletions(-) delete mode 100644 packages/dart/lib/src/logs_enricher_integration.dart delete mode 100644 packages/dart/lib/src/sentry_logger.dart delete mode 100644 packages/dart/lib/src/sentry_logger_formatter.dart create mode 100644 packages/dart/lib/src/telemetry/log/default_logger.dart create mode 100644 packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart create mode 100644 packages/dart/lib/src/telemetry/log/logger.dart create mode 100644 packages/dart/lib/src/telemetry/log/logs_setup_integration.dart create mode 100644 packages/dart/lib/src/telemetry/log/noop_logger.dart delete mode 100644 packages/dart/test/logs_enricher_integration_test.dart create mode 100644 packages/dart/test/mocks/mock_log_capture_pipeline.dart delete mode 100644 packages/dart/test/sentry_logger_formatter_test.dart delete mode 100644 packages/dart/test/sentry_logger_test.dart create mode 100644 packages/dart/test/telemetry/log/log_capture_pipeline_test.dart create mode 100644 packages/dart/test/telemetry/log/logger_formatter_test.dart create mode 100644 packages/dart/test/telemetry/log/logger_test.dart create mode 100644 packages/dart/test/telemetry/log/logs_setup_integration_test.dart diff --git a/packages/dart/lib/sentry.dart b/packages/dart/lib/sentry.dart index 46186a1949..aa05956317 100644 --- a/packages/dart/lib/sentry.dart +++ b/packages/dart/lib/sentry.dart @@ -60,7 +60,7 @@ export 'src/utils/tracing_utils.dart'; export 'src/utils/url_details.dart'; // ignore: invalid_export_of_internal_element export 'src/utils/breadcrumb_log_level.dart'; -export 'src/sentry_logger.dart'; +export 'src/telemetry/log/logger.dart'; export 'src/telemetry/metric/metrics.dart'; // ignore: invalid_export_of_internal_element export 'src/utils/internal_logger.dart' show SentryInternalLogger; diff --git a/packages/dart/lib/src/logs_enricher_integration.dart b/packages/dart/lib/src/logs_enricher_integration.dart deleted file mode 100644 index ee39b41a96..0000000000 --- a/packages/dart/lib/src/logs_enricher_integration.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:async'; -import 'package:meta/meta.dart'; - -import 'sdk_lifecycle_hooks.dart'; -import 'utils/os_utils.dart'; -import 'integration.dart'; -import 'hub.dart'; -import 'protocol/sentry_attribute.dart'; -import 'sentry_options.dart'; - -@internal -class LogsEnricherIntegration extends Integration { - static const integrationName = 'LogsEnricher'; - - @override - FutureOr call(Hub hub, SentryOptions options) { - if (options.enableLogs) { - options.lifecycleRegistry.registerCallback( - (event) async { - final os = getSentryOperatingSystem(); - - if (os.name != null) { - event.log.attributes['os.name'] = SentryAttribute.string( - os.name ?? '', - ); - } - if (os.version != null) { - event.log.attributes['os.version'] = SentryAttribute.string( - os.version ?? '', - ); - } - }, - ); - options.sdk.addIntegration(integrationName); - } - } -} diff --git a/packages/dart/lib/src/protocol/sentry_log.dart b/packages/dart/lib/src/protocol/sentry_log.dart index 6612b8d700..05966d05c6 100644 --- a/packages/dart/lib/src/protocol/sentry_log.dart +++ b/packages/dart/lib/src/protocol/sentry_log.dart @@ -1,10 +1,14 @@ import 'sentry_attribute.dart'; import 'sentry_id.dart'; import 'sentry_log_level.dart'; +import 'span_id.dart'; class SentryLog { DateTime timestamp; SentryId traceId; + + /// The span ID of the active span when the log was recorded. + SpanId? spanId; SentryLogLevel level; String body; Map attributes; @@ -15,6 +19,7 @@ class SentryLog { SentryLog({ required this.timestamp, SentryId? traceId, + this.spanId, required this.level, required this.body, required this.attributes, @@ -25,6 +30,7 @@ class SentryLog { return { 'timestamp': timestamp.toIso8601String(), 'trace_id': traceId.toString(), + if (spanId != null) 'span_id': spanId.toString(), 'level': level.value, 'body': body, 'attributes': diff --git a/packages/dart/lib/src/sdk_lifecycle_hooks.dart b/packages/dart/lib/src/sdk_lifecycle_hooks.dart index b0df1b19b9..bc87b903ca 100644 --- a/packages/dart/lib/src/sdk_lifecycle_hooks.dart +++ b/packages/dart/lib/src/sdk_lifecycle_hooks.dart @@ -68,8 +68,8 @@ class SdkLifecycleRegistry { } @internal -class OnBeforeCaptureLog extends SdkLifecycleEvent { - OnBeforeCaptureLog(this.log); +class OnProcessLog extends SdkLifecycleEvent { + OnProcessLog(this.log); final SentryLog log; } diff --git a/packages/dart/lib/src/sentry.dart b/packages/dart/lib/src/sentry.dart index 8e598c9b4b..f4baa17f8d 100644 --- a/packages/dart/lib/src/sentry.dart +++ b/packages/dart/lib/src/sentry.dart @@ -30,8 +30,8 @@ import 'tracing.dart'; import 'transport/data_category.dart'; import 'transport/task_queue.dart'; import 'feature_flags_integration.dart'; -import 'sentry_logger.dart'; -import 'logs_enricher_integration.dart'; +import 'telemetry/log/logger.dart'; +import 'telemetry/log/logs_setup_integration.dart'; /// Configuration options callback typedef OptionsConfiguration = FutureOr Function(SentryOptions); @@ -113,8 +113,8 @@ class Sentry { } options.addIntegration(MetricsSetupIntegration()); + options.addIntegration(LogsSetupIntegration()); options.addIntegration(FeatureFlagsIntegration()); - options.addIntegration(LogsEnricherIntegration()); options.addIntegration(InMemoryTelemetryProcessorIntegration()); options.addEventProcessor(EnricherEventProcessor(options)); diff --git a/packages/dart/lib/src/sentry_client.dart b/packages/dart/lib/src/sentry_client.dart index bcd4a45419..af9850f847 100644 --- a/packages/dart/lib/src/sentry_client.dart +++ b/packages/dart/lib/src/sentry_client.dart @@ -18,6 +18,7 @@ import 'sentry_exception_factory.dart'; import 'sentry_options.dart'; import 'sentry_stack_trace_factory.dart'; import 'sentry_trace_context_header.dart'; +import 'telemetry/log/log_capture_pipeline.dart'; import 'telemetry/metric/metric.dart'; import 'telemetry/metric/metric_capture_pipeline.dart'; import 'transport/client_report_transport.dart'; @@ -44,6 +45,7 @@ String get defaultIpAddress => _defaultIpAddress; class SentryClient { final SentryOptions _options; final Random? _random; + final LogCapturePipeline _logCapturePipeline; final MetricCapturePipeline _metricCapturePipeline; static final _emptySentryId = Future.value(SentryId.empty()); @@ -53,7 +55,8 @@ class SentryClient { /// Instantiates a client using [SentryOptions] factory SentryClient(SentryOptions options, - {MetricCapturePipeline? metricCapturePipeline}) { + {LogCapturePipeline? logCapturePipeline, + MetricCapturePipeline? metricCapturePipeline}) { if (options.sendClientReports) { options.recorder = ClientReportRecorder(options.clock); } @@ -79,11 +82,15 @@ class SentryClient { options.transport = SpotlightHttpTransport(options, options.transport); } return SentryClient._( - options, metricCapturePipeline ?? MetricCapturePipeline(options)); + options, + logCapturePipeline ?? LogCapturePipeline(options), + metricCapturePipeline ?? MetricCapturePipeline(options), + ); } /// Instantiates a client using [SentryOptions] - SentryClient._(this._options, this._metricCapturePipeline) + SentryClient._( + this._options, this._logCapturePipeline, this._metricCapturePipeline) : _random = _options.sampleRate == null ? null : Random(); /// Reports an [event] to Sentry.io. @@ -495,98 +502,8 @@ class SentryClient { } @internal - FutureOr captureLog( - SentryLog log, { - Scope? scope, - }) async { - if (!_options.enableLogs) { - return; - } - - if (scope != null) { - final merged = Map.of(scope.attributes)..addAll(log.attributes); - log.attributes = merged; - } - - log.attributes['sentry.sdk.name'] = SentryAttribute.string( - _options.sdk.name, - ); - log.attributes['sentry.sdk.version'] = SentryAttribute.string( - _options.sdk.version, - ); - final environment = _options.environment; - if (environment != null) { - log.attributes['sentry.environment'] = SentryAttribute.string( - environment, - ); - } - final release = _options.release; - if (release != null) { - log.attributes['sentry.release'] = SentryAttribute.string( - release, - ); - } - - final propagationContext = scope?.propagationContext; - if (propagationContext != null) { - log.traceId = propagationContext.traceId; - } - final span = scope?.span; - if (span != null) { - log.attributes['sentry.trace.parent_span_id'] = SentryAttribute.string( - span.context.spanId.toString(), - ); - } - - final user = scope?.user; - final id = user?.id; - final email = user?.email; - final name = user?.name; - if (id != null) { - log.attributes['user.id'] = SentryAttribute.string(id); - } - if (name != null) { - log.attributes['user.name'] = SentryAttribute.string(name); - } - if (email != null) { - log.attributes['user.email'] = SentryAttribute.string(email); - } - - final beforeSendLog = _options.beforeSendLog; - SentryLog? processedLog = log; - if (beforeSendLog != null) { - try { - final callbackResult = beforeSendLog(log); - - if (callbackResult is Future) { - processedLog = await callbackResult; - } else { - processedLog = callbackResult; - } - } catch (exception, stackTrace) { - _options.log( - SentryLevel.error, - 'The beforeSendLog callback threw an exception', - exception: exception, - stackTrace: stackTrace, - ); - if (_options.automatedTestMode) { - rethrow; - } - } - } - - if (processedLog != null) { - await _options.lifecycleRegistry - .dispatchCallback(OnBeforeCaptureLog(processedLog)); - _options.telemetryProcessor.addLog(processedLog); - } else { - _options.recorder.recordLostEvent( - DiscardReason.beforeSend, - DataCategory.logItem, - ); - } - } + FutureOr captureLog(SentryLog log, {Scope? scope}) => + _logCapturePipeline.captureLog(log, scope: scope); Future captureMetric(SentryMetric metric, {Scope? scope}) => _metricCapturePipeline.captureMetric(metric, scope: scope); diff --git a/packages/dart/lib/src/sentry_logger.dart b/packages/dart/lib/src/sentry_logger.dart deleted file mode 100644 index a643ac8eec..0000000000 --- a/packages/dart/lib/src/sentry_logger.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:async'; -import 'hub.dart'; -import 'hub_adapter.dart'; -import 'protocol/sentry_log.dart'; -import 'protocol/sentry_log_level.dart'; -import 'protocol/sentry_attribute.dart'; -import 'sentry_options.dart'; -import 'sentry_logger_formatter.dart'; -import 'utils.dart'; - -class SentryLogger { - SentryLogger(this._clock, {Hub? hub}) : _hub = hub ?? HubAdapter(); - - final ClockProvider _clock; - final Hub _hub; - - late final fmt = SentryLoggerFormatter(this); - - FutureOr trace( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.trace, body, attributes: attributes); - } - - FutureOr debug( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.debug, body, attributes: attributes); - } - - FutureOr info( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.info, body, attributes: attributes); - } - - FutureOr warn( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.warn, body, attributes: attributes); - } - - FutureOr error( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.error, body, attributes: attributes); - } - - FutureOr fatal( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.fatal, body, attributes: attributes); - } - - // Helper - - FutureOr _captureLog( - SentryLogLevel level, - String body, { - Map? attributes, - }) { - final log = SentryLog( - timestamp: _clock(), - level: level, - body: body, - attributes: attributes ?? {}, - ); - - _hub.options.log( - level.toSentryLevel(), - _formatLogMessage(level, body, attributes ?? {}), - logger: 'sentry_logger', - ); - - return _hub.captureLog(log); - } - - /// Format log message with level and attributes - String _formatLogMessage( - SentryLogLevel level, - String body, - Map? attributes, - ) { - if (attributes == null || attributes.isEmpty) { - return body; - } - return '$body ${attributes.toFormattedString()}'; - } -} diff --git a/packages/dart/lib/src/sentry_logger_formatter.dart b/packages/dart/lib/src/sentry_logger_formatter.dart deleted file mode 100644 index aff7d21e51..0000000000 --- a/packages/dart/lib/src/sentry_logger_formatter.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'dart:async'; -import 'protocol/sentry_attribute.dart'; -import 'sentry_template_string.dart'; -import 'sentry_logger.dart'; - -class SentryLoggerFormatter { - SentryLoggerFormatter(this._logger); - - final SentryLogger _logger; - - FutureOr trace( - String templateBody, - List arguments, { - Map? attributes, - }) { - return _format( - templateBody, - arguments, - attributes, - (formattedBody, allAttributes) { - return _logger.trace(formattedBody, attributes: allAttributes); - }, - ); - } - - FutureOr debug( - String templateBody, - List arguments, { - Map? attributes, - }) { - return _format( - templateBody, - arguments, - attributes, - (formattedBody, allAttributes) { - return _logger.debug(formattedBody, attributes: allAttributes); - }, - ); - } - - FutureOr info( - String templateBody, - List arguments, { - Map? attributes, - }) { - return _format( - templateBody, - arguments, - attributes, - (formattedBody, allAttributes) { - return _logger.info(formattedBody, attributes: allAttributes); - }, - ); - } - - FutureOr warn( - String templateBody, - List arguments, { - Map? attributes, - }) { - return _format( - templateBody, - arguments, - attributes, - (formattedBody, allAttributes) { - return _logger.warn(formattedBody, attributes: allAttributes); - }, - ); - } - - FutureOr error( - String templateBody, - List arguments, { - Map? attributes, - }) { - return _format( - templateBody, - arguments, - attributes, - (formattedBody, allAttributes) { - return _logger.error(formattedBody, attributes: allAttributes); - }, - ); - } - - FutureOr fatal( - String templateBody, - List arguments, { - Map? attributes, - }) { - return _format( - templateBody, - arguments, - attributes, - (formattedBody, allAttributes) { - return _logger.fatal(formattedBody, attributes: allAttributes); - }, - ); - } - - // Helper - - FutureOr _format( - String templateBody, - List arguments, - Map? attributes, - FutureOr Function(String, Map) callback, - ) { - String formattedBody; - Map templateAttributes; - - if (arguments.isEmpty) { - // No arguments means no template processing needed - formattedBody = templateBody; - templateAttributes = {}; - } else { - // Process template with arguments - final templateString = SentryTemplateString(templateBody, arguments); - formattedBody = templateString.format(); - templateAttributes = _getAllAttributes(templateBody, arguments); - } - - if (attributes != null) { - templateAttributes.addAll(attributes); - } - return callback(formattedBody, templateAttributes); - } - - Map _getAllAttributes( - String templateBody, - List args, - ) { - final templateAttributes = { - 'sentry.message.template': SentryAttribute.string(templateBody), - }; - for (var i = 0; i < args.length; i++) { - final argument = args[i]; - final key = 'sentry.message.parameter.$i'; - if (argument is String) { - templateAttributes[key] = SentryAttribute.string(argument); - } else if (argument is int) { - templateAttributes[key] = SentryAttribute.int(argument); - } else if (argument is bool) { - templateAttributes[key] = SentryAttribute.bool(argument); - } else if (argument is double) { - templateAttributes[key] = SentryAttribute.double(argument); - } else { - try { - templateAttributes[key] = SentryAttribute.string(argument.toString()); - } catch (e) { - templateAttributes[key] = SentryAttribute.string(""); - } - } - } - return templateAttributes; - } -} diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 7ec9b4e424..48ee37e624 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -12,6 +12,7 @@ import 'noop_client.dart'; import 'platform/platform.dart'; import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.dart'; +import 'telemetry/log/noop_logger.dart'; import 'telemetry/metric/metric.dart'; import 'telemetry/metric/noop_metrics.dart'; import 'telemetry/processing/processor.dart'; @@ -561,7 +562,7 @@ class SentryOptions { /// Enabling this option may change grouping. bool includeModuleInStackTrace = false; - late final SentryLogger logger = SentryLogger(clock); + late SentryLogger logger = const NoOpSentryLogger(); @internal late SentryMetrics metrics = NoOpSentryMetrics(); diff --git a/packages/dart/lib/src/telemetry/log/default_logger.dart b/packages/dart/lib/src/telemetry/log/default_logger.dart new file mode 100644 index 0000000000..e729c74c02 --- /dev/null +++ b/packages/dart/lib/src/telemetry/log/default_logger.dart @@ -0,0 +1,302 @@ +import 'dart:async'; + +import '../../../sentry.dart'; +import '../../sentry_template_string.dart'; +import '../../utils/internal_logger.dart'; + +typedef CaptureLogCallback = FutureOr Function(SentryLog log); +typedef ScopeProvider = Scope Function(); + +final class DefaultSentryLogger implements SentryLogger { + final CaptureLogCallback _captureLogCallback; + final ClockProvider _clockProvider; + final ScopeProvider _defaultScopeProvider; + + late final SentryLoggerFormatter _formatter = + _DefaultSentryLoggerFormatter(this); + + DefaultSentryLogger({ + required CaptureLogCallback captureLogCallback, + required ClockProvider clockProvider, + required ScopeProvider defaultScopeProvider, + }) : _captureLogCallback = captureLogCallback, + _clockProvider = clockProvider, + _defaultScopeProvider = defaultScopeProvider; + + @override + FutureOr trace( + String body, { + Map? attributes, + Scope? scope, + }) { + internalLogger.debug(() => + 'Sentry.logger.trace("$body") called with attributes ${_formatAttributes(attributes)}'); + return _captureLog(SentryLogLevel.trace, body, + attributes: attributes, scope: scope); + } + + @override + FutureOr debug( + String body, { + Map? attributes, + Scope? scope, + }) { + internalLogger.debug(() => + 'Sentry.logger.debug("$body") called with attributes ${_formatAttributes(attributes)}'); + return _captureLog(SentryLogLevel.debug, body, + attributes: attributes, scope: scope); + } + + @override + FutureOr info( + String body, { + Map? attributes, + Scope? scope, + }) { + internalLogger.debug(() => + 'Sentry.logger.info("$body") called with attributes ${_formatAttributes(attributes)}'); + return _captureLog(SentryLogLevel.info, body, + attributes: attributes, scope: scope); + } + + @override + FutureOr warn( + String body, { + Map? attributes, + Scope? scope, + }) { + internalLogger.debug(() => + 'Sentry.logger.warn("$body") called with attributes ${_formatAttributes(attributes)}'); + return _captureLog(SentryLogLevel.warn, body, + attributes: attributes, scope: scope); + } + + @override + FutureOr error( + String body, { + Map? attributes, + Scope? scope, + }) { + internalLogger.debug(() => + 'Sentry.logger.error("$body") called with attributes ${_formatAttributes(attributes)}'); + return _captureLog(SentryLogLevel.error, body, + attributes: attributes, scope: scope); + } + + @override + FutureOr fatal( + String body, { + Map? attributes, + Scope? scope, + }) { + internalLogger.debug(() => + 'Sentry.logger.fatal("$body") called with attributes ${_formatAttributes(attributes)}'); + return _captureLog(SentryLogLevel.fatal, body, + attributes: attributes, scope: scope); + } + + @override + SentryLoggerFormatter get fmt => _formatter; + + // Helpers + + FutureOr _captureLog( + SentryLogLevel level, + String body, { + Map? attributes, + Scope? scope, + }) { + final log = SentryLog( + timestamp: _clockProvider(), + level: level, + body: body, + traceId: _traceIdFor(scope), + spanId: _activeSpanIdFor(scope), + attributes: attributes ?? {}, + ); + + return _captureLogCallback(log); + } + + SentryId _traceIdFor(Scope? scope) => + (scope ?? _defaultScopeProvider()).propagationContext.traceId; + + SpanId? _activeSpanIdFor(Scope? scope) => + (scope ?? _defaultScopeProvider()).span?.context.spanId; + + String _formatAttributes(Map? attributes) { + final formatted = attributes?.toFormattedString() ?? ''; + return formatted.isEmpty ? '' : ' $formatted'; + } +} + +final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { + _DefaultSentryLoggerFormatter(this._logger); + + final DefaultSentryLogger _logger; + + @override + FutureOr trace( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }) { + return _format( + templateBody, + arguments, + attributes, + (formattedBody, allAttributes) { + return _logger.trace(formattedBody, + attributes: allAttributes, scope: scope); + }, + ); + } + + @override + FutureOr debug( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }) { + return _format( + templateBody, + arguments, + attributes, + (formattedBody, allAttributes) { + return _logger.debug(formattedBody, + attributes: allAttributes, scope: scope); + }, + ); + } + + @override + FutureOr info( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }) { + return _format( + templateBody, + arguments, + attributes, + (formattedBody, allAttributes) { + return _logger.info(formattedBody, + attributes: allAttributes, scope: scope); + }, + ); + } + + @override + FutureOr warn( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }) { + return _format( + templateBody, + arguments, + attributes, + (formattedBody, allAttributes) { + return _logger.warn(formattedBody, + attributes: allAttributes, scope: scope); + }, + ); + } + + @override + FutureOr error( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }) { + return _format( + templateBody, + arguments, + attributes, + (formattedBody, allAttributes) { + return _logger.error(formattedBody, + attributes: allAttributes, scope: scope); + }, + ); + } + + @override + FutureOr fatal( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }) { + return _format( + templateBody, + arguments, + attributes, + (formattedBody, allAttributes) { + return _logger.fatal(formattedBody, + attributes: allAttributes, scope: scope); + }, + ); + } + + // Helper + + FutureOr _format( + String templateBody, + List arguments, + Map? attributes, + FutureOr Function(String, Map) callback, + ) { + String formattedBody; + Map templateAttributes; + + if (arguments.isEmpty) { + // No arguments means no template processing needed + formattedBody = templateBody; + templateAttributes = {}; + } else { + // Process template with arguments + final templateString = SentryTemplateString(templateBody, arguments); + formattedBody = templateString.format(); + templateAttributes = _getAllAttributes(templateBody, arguments); + } + + if (attributes != null) { + templateAttributes.addAll(attributes); + } + return callback(formattedBody, templateAttributes); + } + + Map _getAllAttributes( + String templateBody, + List args, + ) { + final templateAttributes = { + 'sentry.message.template': SentryAttribute.string(templateBody), + }; + for (var i = 0; i < args.length; i++) { + final argument = args[i]; + final key = 'sentry.message.parameter.$i'; + if (argument is String) { + templateAttributes[key] = SentryAttribute.string(argument); + } else if (argument is int) { + templateAttributes[key] = SentryAttribute.int(argument); + } else if (argument is bool) { + templateAttributes[key] = SentryAttribute.bool(argument); + } else if (argument is double) { + templateAttributes[key] = SentryAttribute.double(argument); + } else { + try { + templateAttributes[key] = SentryAttribute.string(argument.toString()); + } catch (e) { + templateAttributes[key] = SentryAttribute.string(""); + } + } + } + return templateAttributes; + } +} diff --git a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart new file mode 100644 index 0000000000..46b4af7ea5 --- /dev/null +++ b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart @@ -0,0 +1,79 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../../../sentry.dart'; +import '../../client_reports/discard_reason.dart'; +import '../../transport/data_category.dart'; +import '../../utils/internal_logger.dart'; +import '../default_attributes.dart'; + +@internal +class LogCapturePipeline { + final SentryOptions _options; + + LogCapturePipeline(this._options); + + FutureOr captureLog(SentryLog log, {Scope? scope}) async { + if (!_options.enableLogs) { + internalLogger + .debug('$LogCapturePipeline: Logs disabled, dropping ${log.body}'); + return; + } + + try { + if (scope != null) { + log.attributes.addAllIfAbsent(scope.attributes); + } + + await _options.lifecycleRegistry + .dispatchCallback(OnProcessLog(log)); + + log.attributes.addAllIfAbsent(defaultAttributes(_options, scope: scope)); + + final beforeSendLog = _options.beforeSendLog; + SentryLog? processedLog = log; + if (beforeSendLog != null) { + try { + final callbackResult = beforeSendLog(log); + + if (callbackResult is Future) { + processedLog = await callbackResult; + } else { + processedLog = callbackResult; + } + } catch (exception, stackTrace) { + internalLogger.error( + 'The beforeSendLog callback threw an exception', + error: exception, + stackTrace: stackTrace, + ); + if (_options.automatedTestMode) { + rethrow; + } + } + } + + if (processedLog == null) { + _options.recorder + .recordLostEvent(DiscardReason.beforeSend, DataCategory.logItem); + internalLogger.debug( + '$LogCapturePipeline: Log "${log.body}" dropped by beforeSendLog'); + return; + } + + _options.telemetryProcessor.addLog(processedLog); + internalLogger.debug( + '$LogCapturePipeline: Log "${processedLog.body}" (${processedLog.level.name}) captured'); + } catch (exception, stackTrace) { + internalLogger.error( + 'Error capturing log "${log.body}"', + error: exception, + stackTrace: stackTrace, + ); + if (_options.automatedTestMode) { + rethrow; + } + } + } +} diff --git a/packages/dart/lib/src/telemetry/log/logger.dart b/packages/dart/lib/src/telemetry/log/logger.dart new file mode 100644 index 0000000000..df88b90e90 --- /dev/null +++ b/packages/dart/lib/src/telemetry/log/logger.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import '../../../sentry.dart'; + +/// Interface for emitting custom logs to Sentry. +/// +/// Access via [Sentry.logger]. +abstract interface class SentryLogger { + /// Logs a message at TRACE level. + FutureOr trace( + String body, { + Map? attributes, + Scope? scope, + }); + + /// Logs a message at DEBUG level. + FutureOr debug( + String body, { + Map? attributes, + Scope? scope, + }); + + /// Logs a message at INFO level. + FutureOr info( + String body, { + Map? attributes, + Scope? scope, + }); + + /// Logs a message at WARN level. + FutureOr warn( + String body, { + Map? attributes, + Scope? scope, + }); + + /// Logs a message at ERROR level. + FutureOr error( + String body, { + Map? attributes, + Scope? scope, + }); + + /// Logs a message at FATAL level. + FutureOr fatal( + String body, { + Map? attributes, + Scope? scope, + }); + + /// Provides formatted logging with template strings. + SentryLoggerFormatter get fmt; +} + +/// Interface for formatted logging with template strings. +/// +/// Access via [SentryLogger.fmt]. +abstract interface class SentryLoggerFormatter { + /// Logs a formatted message at TRACE level. + FutureOr trace( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }); + + /// Logs a formatted message at DEBUG level. + FutureOr debug( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }); + + /// Logs a formatted message at INFO level. + FutureOr info( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }); + + /// Logs a formatted message at WARN level. + FutureOr warn( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }); + + /// Logs a formatted message at ERROR level. + FutureOr error( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }); + + /// Logs a formatted message at FATAL level. + FutureOr fatal( + String templateBody, + List arguments, { + Map? attributes, + Scope? scope, + }); +} diff --git a/packages/dart/lib/src/telemetry/log/logs_setup_integration.dart b/packages/dart/lib/src/telemetry/log/logs_setup_integration.dart new file mode 100644 index 0000000000..ef5780d51b --- /dev/null +++ b/packages/dart/lib/src/telemetry/log/logs_setup_integration.dart @@ -0,0 +1,31 @@ +import '../../../sentry.dart'; +import '../../utils/internal_logger.dart'; +import 'default_logger.dart'; +import 'noop_logger.dart'; + +class LogsSetupIntegration extends Integration { + static const integrationName = 'LogsSetup'; + + @override + void call(Hub hub, SentryOptions options) { + if (!options.enableLogs) { + internalLogger.debug('$integrationName: Logs disabled, skipping setup'); + return; + } + + if (options.logger is! NoOpSentryLogger) { + internalLogger.debug( + '$integrationName: Custom logger already configured, skipping setup'); + return; + } + + options.logger = DefaultSentryLogger( + captureLogCallback: hub.captureLog, + clockProvider: options.clock, + defaultScopeProvider: () => hub.scope, + ); + + options.sdk.addIntegration(integrationName); + internalLogger.debug('$integrationName: Logger configured successfully'); + } +} diff --git a/packages/dart/lib/src/telemetry/log/noop_logger.dart b/packages/dart/lib/src/telemetry/log/noop_logger.dart new file mode 100644 index 0000000000..784f48054d --- /dev/null +++ b/packages/dart/lib/src/telemetry/log/noop_logger.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import '../../protocol/sentry_attribute.dart'; +import '../../scope.dart'; +import 'logger.dart'; + +final class NoOpSentryLogger implements SentryLogger { + const NoOpSentryLogger(); + + static const _formatter = _NoOpSentryLoggerFormatter(); + + @override + FutureOr trace(String body, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr debug(String body, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr info(String body, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr warn(String body, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr error(String body, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr fatal(String body, + {Map? attributes, Scope? scope}) {} + + @override + SentryLoggerFormatter get fmt => _formatter; +} + +final class _NoOpSentryLoggerFormatter implements SentryLoggerFormatter { + const _NoOpSentryLoggerFormatter(); + + @override + FutureOr trace(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr debug(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr info(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr warn(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr error(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr fatal(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} +} diff --git a/packages/dart/test/logs_enricher_integration_test.dart b/packages/dart/test/logs_enricher_integration_test.dart deleted file mode 100644 index 38644c1c74..0000000000 --- a/packages/dart/test/logs_enricher_integration_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -@TestOn('vm') -library; - -import 'package:sentry/src/logs_enricher_integration.dart'; -import 'package:test/test.dart'; -import 'package:sentry/src/hub.dart'; -import 'package:sentry/src/protocol/sentry_log.dart'; -import 'package:sentry/src/protocol/sentry_attribute.dart'; -import 'package:sentry/src/protocol/sentry_id.dart'; -import 'package:sentry/src/protocol/sentry_log_level.dart'; -import 'test_utils.dart'; -import 'package:sentry/src/utils/os_utils.dart'; - -void main() { - SentryLog givenLog() { - return SentryLog( - timestamp: DateTime.now(), - traceId: SentryId.newId(), - level: SentryLogLevel.info, - body: 'test', - attributes: { - 'attribute': SentryAttribute.string('value'), - }, - ); - } - - group('LogsEnricherIntegration', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('adds itself to sdk.integrations if enableLogs is true', () { - fixture.options.enableLogs = true; - fixture.addIntegration(); - - expect( - fixture.options.sdk.integrations - .contains(LogsEnricherIntegration.integrationName), - true, - ); - }); - - test('does not add itself to sdk.integrations if enableLogs is false', () { - fixture.options.enableLogs = false; - fixture.addIntegration(); - - expect( - fixture.options.sdk.integrations - .contains(LogsEnricherIntegration.integrationName), - false, - ); - }); - - test( - 'adds os.name and os.version to log attributes on OnBeforeCaptureLog lifecycle event', - () async { - fixture.options.enableLogs = true; - fixture.addIntegration(); - - final log = givenLog(); - await fixture.hub.captureLog(log); - - final os = getSentryOperatingSystem(); - - expect(log.attributes['os.name']?.value, os.name); - expect(log.attributes['os.version']?.value, os.version); - }); - - test( - 'does not add os.name and os.version to log attributes if enableLogs is false', - () async { - fixture.options.enableLogs = false; - fixture.addIntegration(); - - final log = givenLog(); - await fixture.hub.captureLog(log); - - expect(log.attributes['os.name'], isNull); - expect(log.attributes['os.version'], isNull); - }); - }); -} - -class Fixture { - final options = defaultTestOptions(); - late final hub = Hub(options); - late final LogsEnricherIntegration integration; - - Fixture() { - options.enableLogs = true; - integration = LogsEnricherIntegration(); - } - - void addIntegration() { - integration.call(hub, options); - } -} diff --git a/packages/dart/test/mocks/mock_log_capture_pipeline.dart b/packages/dart/test/mocks/mock_log_capture_pipeline.dart new file mode 100644 index 0000000000..0e88e6b4da --- /dev/null +++ b/packages/dart/test/mocks/mock_log_capture_pipeline.dart @@ -0,0 +1,19 @@ +import 'dart:async'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/log/log_capture_pipeline.dart'; + +import 'mock_sentry_client.dart'; + +class MockLogCapturePipeline extends LogCapturePipeline { + MockLogCapturePipeline(super.options); + + final List captureLogCalls = []; + + int get callCount => captureLogCalls.length; + + @override + FutureOr captureLog(SentryLog log, {Scope? scope}) async { + captureLogCalls.add(CaptureLogCall(log, scope)); + } +} diff --git a/packages/dart/test/protocol/sentry_log_test.dart b/packages/dart/test/protocol/sentry_log_test.dart index b138e5340a..4095dd7424 100644 --- a/packages/dart/test/protocol/sentry_log_test.dart +++ b/packages/dart/test/protocol/sentry_log_test.dart @@ -5,10 +5,12 @@ void main() { test('$SentryLog to json', () { final timestamp = DateTime.now(); final traceId = SentryId.newId(); + final spanId = SpanId.newId(); final logItem = SentryLog( timestamp: timestamp, traceId: traceId, + spanId: spanId, level: SentryLogLevel.info, body: 'fixture-body', attributes: { @@ -25,6 +27,7 @@ void main() { expect(json, { 'timestamp': timestamp.toIso8601String(), 'trace_id': traceId.toString(), + 'span_id': spanId.toString(), 'level': 'info', 'body': 'fixture-body', 'attributes': { @@ -49,6 +52,24 @@ void main() { }); }); + test('$SentryLog to json without spanId', () { + final timestamp = DateTime.now(); + final traceId = SentryId.newId(); + + final logItem = SentryLog( + timestamp: timestamp, + traceId: traceId, + level: SentryLogLevel.info, + body: 'fixture-body', + attributes: {}, + severityNumber: 1, + ); + + final json = logItem.toJson(); + + expect(json.containsKey('span_id'), isFalse); + }); + test('$SentryLevel without severity number infers from level in toJson', () { final logItem = SentryLog( timestamp: DateTime.now(), diff --git a/packages/dart/test/sentry_client_lifecycle_test.dart b/packages/dart/test/sentry_client_lifecycle_test.dart index 060cb6e261..9e94217e6c 100644 --- a/packages/dart/test/sentry_client_lifecycle_test.dart +++ b/packages/dart/test/sentry_client_lifecycle_test.dart @@ -16,49 +16,6 @@ void main() { setUp(() => fixture = Fixture()); - group('Logs', () { - SentryLog givenLog() { - return SentryLog( - timestamp: DateTime.now(), - traceId: SentryId.newId(), - level: SentryLogLevel.info, - body: 'test', - attributes: { - 'attribute': SentryAttribute.string('value'), - }, - ); - } - - test('captureLog triggers OnBeforeCaptureLog', () async { - fixture.options.enableLogs = true; - fixture.options.environment = 'test-environment'; - fixture.options.release = 'test-release'; - - final log = givenLog(); - - final scope = Scope(fixture.options); - final span = MockSpan(); - scope.span = span; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - fixture.options.lifecycleRegistry - .registerCallback((event) { - event.log.attributes['test'] = SentryAttribute.string('test-value'); - }); - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.attributes['test']?.value, "test-value"); - expect(capturedLog.attributes['test']?.type, 'string'); - }); - }); - group('SentryEvent', () { test('captureEvent triggers OnBeforeSendEvent', () async { fixture.options.enableLogs = true; diff --git a/packages/dart/test/sentry_client_sdk_lifecycle_test.dart b/packages/dart/test/sentry_client_sdk_lifecycle_test.dart index 060cb6e261..9de1f753c7 100644 --- a/packages/dart/test/sentry_client_sdk_lifecycle_test.dart +++ b/packages/dart/test/sentry_client_sdk_lifecycle_test.dart @@ -29,7 +29,7 @@ void main() { ); } - test('captureLog triggers OnBeforeCaptureLog', () async { + test('captureLog triggers OnProcessLog', () async { fixture.options.enableLogs = true; fixture.options.environment = 'test-environment'; fixture.options.release = 'test-release'; @@ -45,7 +45,7 @@ void main() { fixture.options.telemetryProcessor = mockProcessor; fixture.options.lifecycleRegistry - .registerCallback((event) { + .registerCallback((event) { event.log.attributes['test'] = SentryAttribute.string('test-value'); }); diff --git a/packages/dart/test/sentry_client_test.dart b/packages/dart/test/sentry_client_test.dart index 7016e59be0..057fba1649 100644 --- a/packages/dart/test/sentry_client_test.dart +++ b/packages/dart/test/sentry_client_test.dart @@ -24,11 +24,11 @@ import 'package:http/http.dart' as http; import 'mocks.dart'; import 'mocks/mock_client_report_recorder.dart'; import 'mocks/mock_hub.dart'; +import 'mocks/mock_log_capture_pipeline.dart'; import 'mocks/mock_metric_capture_pipeline.dart'; import 'mocks/mock_telemetry_processor.dart'; import 'mocks/mock_transport.dart'; import 'test_utils.dart'; -import 'utils/url_details_test.dart'; void main() { group('SentryClient captures message', () { @@ -1722,8 +1722,13 @@ void main() { fixture = Fixture(); }); - SentryLog givenLog() { - return SentryLog( + test('delegates to log pipeline', () async { + final pipeline = MockLogCapturePipeline(fixture.options); + final client = + SentryClient(fixture.options, logCapturePipeline: pipeline); + final scope = Scope(fixture.options); + + final log = SentryLog( timestamp: DateTime.now(), traceId: SentryId.newId(), level: SentryLogLevel.info, @@ -1732,333 +1737,12 @@ void main() { 'attribute': SentryAttribute.string('value'), }, ); - } - - test('disabled by default', () async { - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - - await client.captureLog(log); - - expect(mockProcessor.addedLogs, isEmpty); - }); - - test('should capture logs as envelope', () async { - fixture.options.enableLogs = true; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - - await client.captureLog(log); - - expect(mockProcessor.addedLogs.length, 1); - - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.traceId, log.traceId); - expect(capturedLog.level, log.level); - expect(capturedLog.body, log.body); - expect(capturedLog.attributes['attribute']?.value, - log.attributes['attribute']?.value); - }); - - test('should add additional info to attributes', () async { - fixture.options.enableLogs = true; - fixture.options.environment = 'test-environment'; - fixture.options.release = 'test-release'; - - final log = givenLog(); - - final scope = Scope(fixture.options); - final span = MockSpan(); - scope.span = span; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect( - capturedLog.attributes['sentry.sdk.name']?.value, - fixture.options.sdk.name, - ); - expect( - capturedLog.attributes['sentry.sdk.name']?.type, - 'string', - ); - expect( - capturedLog.attributes['sentry.sdk.version']?.value, - fixture.options.sdk.version, - ); - expect( - capturedLog.attributes['sentry.sdk.version']?.type, - 'string', - ); - expect( - capturedLog.attributes['sentry.environment']?.value, - fixture.options.environment, - ); - expect( - capturedLog.attributes['sentry.environment']?.type, - 'string', - ); - expect( - capturedLog.attributes['sentry.release']?.value, - fixture.options.release, - ); - expect( - capturedLog.attributes['sentry.release']?.type, - 'string', - ); - expect( - capturedLog.attributes['sentry.trace.parent_span_id']?.value, - span.context.spanId.toString(), - ); - expect( - capturedLog.attributes['sentry.trace.parent_span_id']?.type, - 'string', - ); - }); - - test('should use attributes from given scope', () async { - fixture.options.enableLogs = true; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - final log = givenLog(); - - final scope = Scope(fixture.options); - scope.setAttributes({'from_scope': SentryAttribute.int(12)}); - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - expect(capturedLog.attributes['from_scope']?.value, 12); - }); - - test('per-log attributes override scope on same key', () async { - fixture.options.enableLogs = true; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - final log = givenLog(); - - final scope = Scope(fixture.options); - scope.setAttributes({ - 'overridden': SentryAttribute.string('fromScope'), - 'kept': SentryAttribute.bool(true), - }); - - log.attributes['overridden'] = SentryAttribute.string('fromLog'); - log.attributes['logOnly'] = SentryAttribute.double(1.23); - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final captured = mockProcessor.addedLogs.first; - - expect(captured.attributes['overridden']?.value, 'fromLog'); - expect(captured.attributes['kept']?.value, true); - expect(captured.attributes['logOnly']?.type, 'double'); - }); - - test('should add user info to attributes', () async { - fixture.options.enableLogs = true; - - final log = givenLog(); - final scope = Scope(fixture.options); - final user = SentryUser( - id: '123', - email: 'test@test.com', - name: 'test-name', - ); - await scope.setUser(user); - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect( - capturedLog.attributes['user.id']?.value, - user.id, - ); - expect( - capturedLog.attributes['user.id']?.type, - 'string', - ); - - expect( - capturedLog.attributes['user.name']?.value, - user.name, - ); - expect( - capturedLog.attributes['user.name']?.type, - 'string', - ); - - expect( - capturedLog.attributes['user.email']?.value, - user.email, - ); - expect( - capturedLog.attributes['user.email']?.type, - 'string', - ); - }); - - test('should set trace id from propagation context', () async { - fixture.options.enableLogs = true; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - final scope = Scope(fixture.options); await client.captureLog(log, scope: scope); - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.traceId, scope.propagationContext.traceId); - }); - - test( - '$BeforeSendLogCallback returning null drops the log and record it as lost', - () async { - fixture.options.enableLogs = true; - fixture.options.beforeSendLog = (log) => null; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - - await client.captureLog(log); - - expect(mockProcessor.addedLogs.length, 0); - - expect( - fixture.recorder.discardedEvents.first.reason, - DiscardReason.beforeSend, - ); - expect( - fixture.recorder.discardedEvents.first.category, - DataCategory.logItem, - ); - }); - - test('$BeforeSendLogCallback returning a log modifies it', () async { - fixture.options.enableLogs = true; - fixture.options.beforeSendLog = (log) { - log.body = 'modified'; - return log; - }; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - - await client.captureLog(log); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.body, 'modified'); - }); - - test('$BeforeSendLogCallback returning a log async modifies it', () async { - fixture.options.enableLogs = true; - fixture.options.beforeSendLog = (log) async { - await Future.delayed(Duration(milliseconds: 100)); - log.body = 'modified'; - return log; - }; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - - await client.captureLog(log); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.body, 'modified'); - }); - - test('$BeforeSendLogCallback throwing is caught', () async { - fixture.options.enableLogs = true; - fixture.options.automatedTestMode = false; - - fixture.options.beforeSendLog = (log) { - throw Exception('test'); - }; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - await client.captureLog(log); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.body, 'test'); - }); - - test('OnBeforeCaptureLog lifecycle event is called', () async { - fixture.options.enableLogs = true; - fixture.options.environment = 'test-environment'; - fixture.options.release = 'test-release'; - - final log = givenLog(); - - final scope = Scope(fixture.options); - final span = MockSpan(); - scope.span = span; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - fixture.options.lifecycleRegistry - .registerCallback((event) { - event.log.attributes['test'] = SentryAttribute.string('test-value'); - }); - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.attributes['test']?.value, "test-value"); - expect(capturedLog.attributes['test']?.type, 'string'); + expect(pipeline.callCount, 1); + expect(pipeline.captureLogCalls.first.log, same(log)); + expect(pipeline.captureLogCalls.first.scope, same(scope)); }); }); diff --git a/packages/dart/test/sentry_logger_formatter_test.dart b/packages/dart/test/sentry_logger_formatter_test.dart deleted file mode 100644 index 443fade82a..0000000000 --- a/packages/dart/test/sentry_logger_formatter_test.dart +++ /dev/null @@ -1,429 +0,0 @@ -import 'package:test/test.dart'; -import 'package:sentry/src/sentry_logger_formatter.dart'; -import 'package:sentry/src/sentry_logger.dart'; -import 'package:sentry/src/protocol/sentry_attribute.dart'; - -void main() { - final fixture = Fixture(); - - void verifyPassedAttributes(Map attributes) { - expect(attributes['foo'].type, 'string'); - expect(attributes['foo'].value, 'bar'); - } - - void verifyBasicTemplate(String body, Map attributes) { - expect(body, 'Hello, World!'); - expect(attributes['sentry.message.template'].type, 'string'); - expect(attributes['sentry.message.template'].value, 'Hello, %s!'); - expect(attributes['sentry.message.parameter.0'].type, 'string'); - expect(attributes['sentry.message.parameter.0'].value, 'World'); - verifyPassedAttributes(attributes); - } - - void verifyTemplateWithMultipleArguments( - String body, Map attributes) { - expect(body, 'Name: Alice, Age: 30, Active: true, Score: 95.5'); - expect(attributes['sentry.message.template'].type, 'string'); - expect(attributes['sentry.message.template'].value, - 'Name: %s, Age: %s, Active: %s, Score: %s'); - expect(attributes['sentry.message.parameter.0'].type, 'string'); - expect(attributes['sentry.message.parameter.0'].value, 'Alice'); - expect(attributes['sentry.message.parameter.1'].type, 'integer'); - expect(attributes['sentry.message.parameter.1'].value, 30); - expect(attributes['sentry.message.parameter.2'].type, 'boolean'); - expect(attributes['sentry.message.parameter.2'].value, true); - expect(attributes['sentry.message.parameter.3'].type, 'double'); - expect(attributes['sentry.message.parameter.3'].value, 95.5); - verifyPassedAttributes(attributes); - } - - group('format basic template', () { - test('for trace', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.trace( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.traceCalls.length, 1); - final message = logger.traceCalls[0].message; - final attributes = logger.traceCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - - test('for debug', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.debug( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.debugCalls.length, 1); - final message = logger.debugCalls[0].message; - final attributes = logger.debugCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - - test('for info', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.info( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.infoCalls.length, 1); - final message = logger.infoCalls[0].message; - final attributes = logger.infoCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - - test('for warn', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.warn( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.warnCalls.length, 1); - final message = logger.warnCalls[0].message; - final attributes = logger.warnCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - - test('for error', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.error( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.errorCalls.length, 1); - final message = logger.errorCalls[0].message; - final attributes = logger.errorCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - - test('for fatal', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.fatal( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.fatalCalls.length, 1); - final message = logger.fatalCalls[0].message; - final attributes = logger.fatalCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - }); - - group('template with multiple arguments', () { - test('for trace', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.trace( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.traceCalls.length, 1); - final message = logger.traceCalls[0].message; - final attributes = logger.traceCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for trace', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.trace( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.traceCalls.length, 1); - final message = logger.traceCalls[0].message; - final attributes = logger.traceCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for debug', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.debug( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.debugCalls.length, 1); - final message = logger.debugCalls[0].message; - final attributes = logger.debugCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for info', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.info( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.infoCalls.length, 1); - final message = logger.infoCalls[0].message; - final attributes = logger.infoCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for warn', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.warn( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.warnCalls.length, 1); - final message = logger.warnCalls[0].message; - final attributes = logger.warnCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for error', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.error( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.errorCalls.length, 1); - final message = logger.errorCalls[0].message; - final attributes = logger.errorCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for fatal', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.fatal( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.fatalCalls.length, 1); - final message = logger.fatalCalls[0].message; - final attributes = logger.fatalCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - }); - - group('template with no arguments', () { - test('for trace', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.trace( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.traceCalls.length, 1); - final message = logger.traceCalls[0].message; - final attributes = logger.traceCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - - test('for debug', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.debug( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.debugCalls.length, 1); - final message = logger.debugCalls[0].message; - final attributes = logger.debugCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - - test('for info', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.info( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.infoCalls.length, 1); - final message = logger.infoCalls[0].message; - final attributes = logger.infoCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - - test('for warn', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.warn( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.warnCalls.length, 1); - final message = logger.warnCalls[0].message; - final attributes = logger.warnCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - - test('for error', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.error( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.errorCalls.length, 1); - final message = logger.errorCalls[0].message; - final attributes = logger.errorCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - - test('for fatal', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.fatal( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.fatalCalls.length, 1); - final message = logger.fatalCalls[0].message; - final attributes = logger.fatalCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - }); -} - -class Fixture { - SentryLoggerFormatter getSut(SentryLogger logger) { - return SentryLoggerFormatter(logger); - } -} - -class MockLogger implements SentryLogger { - var traceCalls = []; - var debugCalls = []; - var infoCalls = []; - var warnCalls = []; - var errorCalls = []; - var fatalCalls = []; - - @override - SentryLoggerFormatter get fmt => throw UnimplementedError(); - - @override - Future trace(String message, {Map? attributes}) async { - traceCalls.add((message: message, attributes: attributes)); - return; - } - - @override - Future debug(String message, {Map? attributes}) async { - debugCalls.add((message: message, attributes: attributes)); - return; - } - - @override - Future info(String message, {Map? attributes}) async { - infoCalls.add((message: message, attributes: attributes)); - return; - } - - @override - Future warn(String message, {Map? attributes}) async { - warnCalls.add((message: message, attributes: attributes)); - return; - } - - @override - Future error(String message, {Map? attributes}) async { - errorCalls.add((message: message, attributes: attributes)); - return; - } - - @override - Future fatal(String message, {Map? attributes}) async { - fatalCalls.add((message: message, attributes: attributes)); - return; - } -} - -typedef LoggerCall = ({String message, Map? attributes}); diff --git a/packages/dart/test/sentry_logger_test.dart b/packages/dart/test/sentry_logger_test.dart deleted file mode 100644 index 37e760a204..0000000000 --- a/packages/dart/test/sentry_logger_test.dart +++ /dev/null @@ -1,252 +0,0 @@ -import 'package:sentry/sentry.dart'; -import 'package:test/test.dart'; -import 'test_utils.dart'; -import 'mocks/mock_hub.dart'; - -void main() { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - void verifyCaptureLog(SentryLogLevel level) { - expect(fixture.hub.captureLogCalls.length, 1); - final capturedLog = fixture.hub.captureLogCalls[0].log; - - expect(capturedLog.timestamp, fixture.timestamp); - expect(capturedLog.level, level); - expect(capturedLog.body, 'test'); - expect(capturedLog.attributes, fixture.attributes); - } - - test('sentry logger', () { - final logger = fixture.getSut(); - - logger.info('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.info); - }); - - test('trace', () { - final logger = fixture.getSut(); - - logger.info('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.info); - }); - - test('debug', () { - final logger = fixture.getSut(); - - logger.debug('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.debug); - }); - - test('info', () { - final logger = fixture.getSut(); - - logger.info('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.info); - }); - - test('warn', () { - final logger = fixture.getSut(); - - logger.warn('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.warn); - }); - - test('error', () { - final logger = fixture.getSut(); - - logger.error('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.error); - }); - - test('fatal', () { - final logger = fixture.getSut(); - - logger.fatal('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.fatal); - }); - - test('logs to hub options when provided', () { - final mockLogCallback = _MockSdkLogCallback(); - - // Set the mock log callback on the fixture hub - fixture.hub.options.log = mockLogCallback.call; - fixture.hub.options.debug = true; - fixture.hub.options.diagnosticLevel = SentryLevel.debug; - - final logger = SentryLogger( - () => fixture.timestamp, - hub: fixture.hub, - ); - - logger.trace('test message', attributes: fixture.attributes); - - // Verify that both hub.captureLog and our callback were called - expect(fixture.hub.captureLogCalls.length, 1); - expect(mockLogCallback.calls.length, 1); - - // Verify the captured log has the right content - final capturedLog = fixture.hub.captureLogCalls[0].log; - expect(capturedLog.level, SentryLogLevel.trace); - expect(capturedLog.body, 'test message'); - expect(capturedLog.attributes, fixture.attributes); - - // Verify the log callback was called with the right parameters - final logCall = mockLogCallback.calls[0]; - expect(logCall.level, SentryLevel.debug); // trace maps to debug - expect(logCall.message, - 'test message {"string": "string", "int": 1, "double": 1.23456789, "bool": true, "double_int": 1.0, "nan": NaN, "positive_infinity": Infinity, "negative_infinity": -Infinity}'); - expect(logCall.logger, 'sentry_logger'); - }); - - test('bridges SentryLogLevel to SentryLevel correctly', () { - final mockLogCallback = _MockSdkLogCallback(); - - // Set the mock log callback on the fixture hub's options - fixture.hub.options.log = mockLogCallback.call; - fixture.hub.options.debug = true; - fixture.hub.options.diagnosticLevel = SentryLevel.debug; - - final logger = SentryLogger( - () => fixture.timestamp, - hub: fixture.hub, - ); - - // Test all log levels to ensure proper bridging - logger.trace('trace message'); - logger.debug('debug message'); - logger.info('info message'); - logger.warn('warn message'); - logger.error('error message'); - logger.fatal('fatal message'); - - // Verify that all calls were made to both the hub and the log callback - expect(fixture.hub.captureLogCalls.length, 6); - expect(mockLogCallback.calls.length, 6); - - // Verify the bridging is correct - expect(mockLogCallback.calls[0].level, SentryLevel.debug); // trace -> debug - expect(mockLogCallback.calls[1].level, SentryLevel.debug); // debug -> debug - expect(mockLogCallback.calls[2].level, SentryLevel.info); // info -> info - expect( - mockLogCallback.calls[3].level, SentryLevel.warning); // warn -> warning - expect(mockLogCallback.calls[4].level, SentryLevel.error); // error -> error - expect(mockLogCallback.calls[5].level, SentryLevel.fatal); // fatal -> fatal - }); - - test('handles NaN and infinite values correctly', () { - final mockLogCallback = _MockSdkLogCallback(); - - // Set the mock log callback on the fixture hub's options - fixture.hub.options.log = mockLogCallback.call; - fixture.hub.options.debug = true; - fixture.hub.options.diagnosticLevel = SentryLevel.debug; - - final logger = SentryLogger( - () => fixture.timestamp, - hub: fixture.hub, - ); - - // Test with special double values - final specialAttributes = { - 'nan': SentryAttribute.double(double.nan), - 'positive_infinity': SentryAttribute.double(double.infinity), - 'negative_infinity': SentryAttribute.double(double.negativeInfinity), - }; - - logger.info('special values', attributes: specialAttributes); - - // Verify that both hub.captureLog and our callback were called - expect(fixture.hub.captureLogCalls.length, 1); - expect(mockLogCallback.calls.length, 1); - - // Verify the captured log has the right content - final capturedLog = fixture.hub.captureLogCalls[0].log; - expect(capturedLog.level, SentryLogLevel.info); - expect(capturedLog.body, 'special values'); - expect(capturedLog.attributes, specialAttributes); - - // Verify the log callback was called with the right parameters - final logCall = mockLogCallback.calls[0]; - expect(logCall.level, SentryLevel.info); - expect(logCall.message, - 'special values {"nan": NaN, "positive_infinity": Infinity, "negative_infinity": -Infinity}'); - expect(logCall.logger, 'sentry_logger'); - }); - - // This is mostly an edge case but let's cover it just in case - test('per-log attributes override fmt template attributes on same key', () { - final logger = fixture.getSut(); - - logger.fmt.info( - 'Hello, %s!', - ['World'], - attributes: { - 'sentry.message.template': SentryAttribute.string('OVERRIDE'), - 'sentry.message.parameter.0': SentryAttribute.string('Earth'), - }, - ); - - final attrs = fixture.hub.captureLogCalls[0].log.attributes; - expect(attrs['sentry.message.template']?.value, 'OVERRIDE'); - expect(attrs['sentry.message.parameter.0']?.value, 'Earth'); - }); -} - -class Fixture { - final options = defaultTestOptions(); - final hub = MockHub(); - final timestamp = DateTime.fromMicrosecondsSinceEpoch(0); - - final attributes = { - 'string': SentryAttribute.string('string'), - 'int': SentryAttribute.int(1), - 'double': SentryAttribute.double(1.23456789), - 'bool': SentryAttribute.bool(true), - 'double_int': SentryAttribute.double(1.0), - 'nan': SentryAttribute.double(double.nan), - 'positive_infinity': SentryAttribute.double(double.infinity), - 'negative_infinity': SentryAttribute.double(double.negativeInfinity), - }; - - SentryLogger getSut() { - return SentryLogger(() => timestamp, hub: hub); - } -} - -/// Simple mock for SdkLogCallback to track calls -class _MockSdkLogCallback { - final List<_LogCall> calls = []; - - void call( - SentryLevel level, - String message, { - String? logger, - Object? exception, - StackTrace? stackTrace, - }) { - calls.add(_LogCall(level, message, logger, exception, stackTrace)); - } -} - -/// Data class to store log call information -class _LogCall { - final SentryLevel level; - final String message; - final String? logger; - final Object? exception; - final StackTrace? stackTrace; - - _LogCall( - this.level, this.message, this.logger, this.exception, this.stackTrace); -} diff --git a/packages/dart/test/sentry_test.dart b/packages/dart/test/sentry_test.dart index 55cf4bc3b4..663a3b38fb 100644 --- a/packages/dart/test/sentry_test.dart +++ b/packages/dart/test/sentry_test.dart @@ -6,7 +6,6 @@ import 'dart:isolate'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/dart_exception_type_identifier.dart'; import 'package:sentry/src/event_processor/deduplication_event_processor.dart'; -import 'package:sentry/src/logs_enricher_integration.dart'; import 'package:sentry/src/feature_flags_integration.dart'; import 'package:sentry/src/telemetry/metric/metrics_setup_integration.dart'; import 'package:sentry/src/telemetry/processing/processor_integration.dart'; @@ -321,27 +320,6 @@ void main() { ); }, onPlatform: {'browser': Skip()}); - test('should add logsEnricherIntegration', () async { - late SentryOptions optionsReference; - final options = defaultTestOptions(); - - await Sentry.init( - options: options, - (options) { - options.dsn = fakeDsn; - optionsReference = options; - }, - appRunner: appRunner, - ); - - expect( - optionsReference.integrations - .whereType() - .length, - 1, - ); - }); - test('should add $InMemoryTelemetryProcessorIntegration', () async { late SentryOptions optionsReference; final options = defaultTestOptions(); diff --git a/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart b/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart new file mode 100644 index 0000000000..9d574031e8 --- /dev/null +++ b/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart @@ -0,0 +1,258 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/client_reports/discard_reason.dart'; +import 'package:sentry/src/telemetry/log/log_capture_pipeline.dart'; +import 'package:sentry/src/transport/data_category.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_client_report_recorder.dart'; +import '../../mocks/mock_telemetry_processor.dart'; +import '../../test_utils.dart'; + +void main() { + group('$LogCapturePipeline', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + SentryLog givenLog() { + return SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.info, + body: 'test', + attributes: { + 'attribute': SentryAttribute.string('value'), + }, + ); + } + + group('when capturing a log', () { + test('forwards to telemetry processor', () async { + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs.length, 1); + expect(fixture.processor.addedLogs.first, same(log)); + }); + + test('adds default attributes', () async { + await fixture.scope.setUser(SentryUser(id: 'user-id')); + fixture.scope.setAttributes({ + 'scope-key': SentryAttribute.string('scope-value'), + }); + + final log = givenLog() + ..attributes['custom'] = SentryAttribute.string('log-value'); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + final attributes = log.attributes; + expect(attributes['scope-key']?.value, 'scope-value'); + expect(attributes['custom']?.value, 'log-value'); + expect(attributes[SemanticAttributesConstants.sentryEnvironment]?.value, + 'test-env'); + expect(attributes[SemanticAttributesConstants.sentryRelease]?.value, + 'test-release'); + expect(attributes[SemanticAttributesConstants.sentrySdkName]?.value, + fixture.options.sdk.name); + expect(attributes[SemanticAttributesConstants.sentrySdkVersion]?.value, + fixture.options.sdk.version); + expect( + attributes[SemanticAttributesConstants.userId]?.value, 'user-id'); + }); + + test('prefers log attributes over scope attributes', () async { + fixture.scope.setAttributes({ + 'overridden': SentryAttribute.string('scope-value'), + 'kept': SentryAttribute.bool(true), + }); + + final log = givenLog() + ..attributes['overridden'] = SentryAttribute.string('log-value') + ..attributes['logOnly'] = SentryAttribute.double(1.23); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + final attributes = log.attributes; + expect(attributes['overridden']?.value, 'log-value'); + expect(attributes['kept']?.value, true); + expect(attributes['logOnly']?.type, 'double'); + }); + + test( + 'dispatches OnProcessLog after scope merge but before beforeSendLog', + () async { + final operations = []; + bool hasScopeAttrInCallback = false; + + fixture.scope.setAttributes({ + 'scope-attr': SentryAttribute.string('scope-value'), + }); + + fixture.options.lifecycleRegistry + .registerCallback((event) { + operations.add('onProcessLog'); + hasScopeAttrInCallback = + event.log.attributes.containsKey('scope-attr'); + }); + + fixture.options.beforeSendLog = (log) { + operations.add('beforeSendLog'); + return log; + }; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(operations, ['onProcessLog', 'beforeSendLog']); + expect(hasScopeAttrInCallback, isTrue); + }); + + test('keeps attributes added by lifecycle callbacks', () async { + fixture.options.lifecycleRegistry + .registerCallback((event) { + event.log.attributes['callback-key'] = + SentryAttribute.string('callback-value'); + event.log.attributes[SemanticAttributesConstants.sentryEnvironment] = + SentryAttribute.string('callback-env'); + }); + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + final attributes = log.attributes; + expect(attributes['callback-key']?.value, 'callback-value'); + expect(attributes[SemanticAttributesConstants.sentryEnvironment]?.value, + 'callback-env'); + }); + + test('does not add user attributes when sendDefaultPii is false', + () async { + fixture.options.sendDefaultPii = false; + await fixture.scope.setUser(SentryUser(id: 'user-id')); + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect( + log.attributes.containsKey(SemanticAttributesConstants.userId), + isFalse, + ); + }); + }); + + group('when logs are disabled', () { + test('does not add logs to processor', () async { + fixture.options.enableLogs = false; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs, isEmpty); + }); + }); + + group('when beforeSendLog is configured', () { + test('returning null drops the log', () async { + fixture.options.beforeSendLog = (_) => null; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs, isEmpty); + }); + + test('returning null records lost event in client report', () async { + fixture.options.beforeSendLog = (_) => null; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.recorder.discardedEvents.length, 1); + expect(fixture.recorder.discardedEvents.first.reason, + DiscardReason.beforeSend); + expect(fixture.recorder.discardedEvents.first.category, + DataCategory.logItem); + }); + + test('can mutate the log', () async { + fixture.options.beforeSendLog = (log) { + log.body = 'modified-body'; + log.attributes['added-key'] = SentryAttribute.string('added'); + return log; + }; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs.length, 1); + final captured = fixture.processor.addedLogs.first; + expect(captured.body, 'modified-body'); + expect(captured.attributes['added-key']?.value, 'added'); + }); + + test('async callback is awaited', () async { + fixture.options.beforeSendLog = (log) async { + await Future.delayed(Duration(milliseconds: 10)); + log.body = 'async-modified'; + return log; + }; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs.length, 1); + final captured = fixture.processor.addedLogs.first; + expect(captured.body, 'async-modified'); + }); + + test('exception in callback is caught and log is still captured', + () async { + fixture.options.automatedTestMode = false; + fixture.options.beforeSendLog = (log) { + throw Exception('test'); + }; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs.length, 1); + final captured = fixture.processor.addedLogs.first; + expect(captured.body, 'test'); + }); + }); + }); +} + +class Fixture { + final options = defaultTestOptions() + ..environment = 'test-env' + ..release = 'test-release' + ..sendDefaultPii = true + ..enableLogs = true; + + final processor = MockTelemetryProcessor(); + final recorder = MockClientReportRecorder(); + + late final Scope scope; + late final LogCapturePipeline pipeline; + + Fixture() { + options.telemetryProcessor = processor; + options.recorder = recorder; + scope = Scope(options); + pipeline = LogCapturePipeline(options); + } +} diff --git a/packages/dart/test/telemetry/log/logger_formatter_test.dart b/packages/dart/test/telemetry/log/logger_formatter_test.dart new file mode 100644 index 0000000000..8190bdb0e5 --- /dev/null +++ b/packages/dart/test/telemetry/log/logger_formatter_test.dart @@ -0,0 +1,295 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/log/default_logger.dart'; +import 'package:test/test.dart'; + +import '../../test_utils.dart'; + +void main() { + group('$_DefaultSentryLoggerFormatter', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void verifyPassedAttributes(Map attributes) { + expect(attributes['foo']?.type, 'string'); + expect(attributes['foo']?.value, 'bar'); + } + + void verifyBasicTemplate(SentryLog log) { + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template']?.type, 'string'); + expect(log.attributes['sentry.message.template']?.value, 'Hello, %s!'); + expect(log.attributes['sentry.message.parameter.0']?.type, 'string'); + expect(log.attributes['sentry.message.parameter.0']?.value, 'World'); + verifyPassedAttributes(log.attributes); + } + + void verifyTemplateWithMultipleArguments(SentryLog log) { + expect(log.body, 'Name: Alice, Age: 30, Active: true, Score: 95.5'); + expect(log.attributes['sentry.message.template']?.type, 'string'); + expect(log.attributes['sentry.message.template']?.value, + 'Name: %s, Age: %s, Active: %s, Score: %s'); + expect(log.attributes['sentry.message.parameter.0']?.type, 'string'); + expect(log.attributes['sentry.message.parameter.0']?.value, 'Alice'); + expect(log.attributes['sentry.message.parameter.1']?.type, 'integer'); + expect(log.attributes['sentry.message.parameter.1']?.value, 30); + expect(log.attributes['sentry.message.parameter.2']?.type, 'boolean'); + expect(log.attributes['sentry.message.parameter.2']?.value, true); + expect(log.attributes['sentry.message.parameter.3']?.type, 'double'); + expect(log.attributes['sentry.message.parameter.3']?.value, 95.5); + verifyPassedAttributes(log.attributes); + } + + group('format basic template', () { + test('for trace', () async { + await fixture.logger.fmt.trace( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + + test('for debug', () async { + await fixture.logger.fmt.debug( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + + test('for info', () async { + await fixture.logger.fmt.info( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + + test('for warn', () async { + await fixture.logger.fmt.warn( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + + test('for error', () async { + await fixture.logger.fmt.error( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + + test('for fatal', () async { + await fixture.logger.fmt.fatal( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + }); + + group('template with multiple arguments', () { + test('for trace', () async { + await fixture.logger.fmt.trace( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + + test('for debug', () async { + await fixture.logger.fmt.debug( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + + test('for info', () async { + await fixture.logger.fmt.info( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + + test('for warn', () async { + await fixture.logger.fmt.warn( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + + test('for error', () async { + await fixture.logger.fmt.error( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + + test('for fatal', () async { + await fixture.logger.fmt.fatal( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + }); + + group('template with no arguments', () { + test('for trace', () async { + await fixture.logger.fmt.trace( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + + test('for debug', () async { + await fixture.logger.fmt.debug( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + + test('for info', () async { + await fixture.logger.fmt.info( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + + test('for warn', () async { + await fixture.logger.fmt.warn( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + + test('for error', () async { + await fixture.logger.fmt.error( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + + test('for fatal', () async { + await fixture.logger.fmt.fatal( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + }); + }); +} + +// Used to reference the private class in the group name +// ignore: camel_case_types +class _DefaultSentryLoggerFormatter {} + +class Fixture { + final capturedLogs = []; + final options = defaultTestOptions(); + late final SentryLogger logger; + late final Scope scope; + + Fixture() { + scope = Scope(options); + logger = DefaultSentryLogger( + captureLogCallback: (log) { + capturedLogs.add(log); + }, + clockProvider: () => DateTime.now(), + defaultScopeProvider: () => scope, + ); + } +} diff --git a/packages/dart/test/telemetry/log/logger_test.dart b/packages/dart/test/telemetry/log/logger_test.dart new file mode 100644 index 0000000000..b1db0b305f --- /dev/null +++ b/packages/dart/test/telemetry/log/logger_test.dart @@ -0,0 +1,172 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/log/default_logger.dart'; +import 'package:test/test.dart'; + +import '../../test_utils.dart'; + +void main() { + group('$DefaultSentryLogger', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void verifyCaptureLog(SentryLogLevel level) { + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + + expect(capturedLog.level, level); + expect(capturedLog.body, 'test'); + // The attributes might have additional trace context, so check key attributes + expect(capturedLog.attributes['string']?.value, 'string'); + } + + test('info', () async { + final logger = fixture.getSut(); + + await logger.info('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.info); + }); + + test('trace', () async { + final logger = fixture.getSut(); + + await logger.trace('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.trace); + }); + + test('debug', () async { + final logger = fixture.getSut(); + + await logger.debug('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.debug); + }); + + test('warn', () async { + final logger = fixture.getSut(); + + await logger.warn('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.warn); + }); + + test('error', () async { + final logger = fixture.getSut(); + + await logger.error('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.error); + }); + + test('fatal', () async { + final logger = fixture.getSut(); + + await logger.fatal('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.fatal); + }); + + // This is mostly an edge case but let's cover it just in case + test('per-log attributes override fmt template attributes on same key', + () async { + final logger = fixture.getSut(); + + await logger.fmt.info( + 'Hello, %s!', + ['World'], + attributes: { + 'sentry.message.template': SentryAttribute.string('OVERRIDE'), + 'sentry.message.parameter.0': SentryAttribute.string('Earth'), + }, + ); + + final attrs = fixture.capturedLogs[0].attributes; + expect(attrs['sentry.message.template']?.value, 'OVERRIDE'); + expect(attrs['sentry.message.parameter.0']?.value, 'Earth'); + }); + + test('sets trace id from default scope propagation context', () async { + final logger = fixture.getSut(); + + await logger.info('test'); + + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + expect(capturedLog.traceId, fixture.scope.propagationContext.traceId); + }); + + test('sets span id when span is active on default scope', () async { + final span = _MockSpan(); + fixture.scope.span = span; + + final logger = fixture.getSut(); + + await logger.info('test'); + + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + expect(capturedLog.spanId, span.context.spanId); + }); + + test('uses provided scope for trace id and span id', () async { + final customScope = Scope(fixture.options); + final span = _MockSpan(); + customScope.span = span; + + final logger = fixture.getSut(); + + await logger.info('test', scope: customScope); + + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + expect(capturedLog.traceId, customScope.propagationContext.traceId); + expect(capturedLog.spanId, span.context.spanId); + }); + }); +} + +class Fixture { + final options = defaultTestOptions(); + final timestamp = DateTime.fromMicrosecondsSinceEpoch(0); + final capturedLogs = []; + late final Scope scope; + + final attributes = { + 'string': SentryAttribute.string('string'), + 'int': SentryAttribute.int(1), + 'double': SentryAttribute.double(1.23456789), + 'bool': SentryAttribute.bool(true), + 'double_int': SentryAttribute.double(1.0), + 'nan': SentryAttribute.double(double.nan), + 'positive_infinity': SentryAttribute.double(double.infinity), + 'negative_infinity': SentryAttribute.double(double.negativeInfinity), + }; + + Fixture() { + scope = Scope(options); + } + + SentryLogger getSut() { + return DefaultSentryLogger( + captureLogCallback: (log) { + capturedLogs.add(log); + }, + clockProvider: () => timestamp, + defaultScopeProvider: () => scope, + ); + } +} + +class _MockSpan implements ISentrySpan { + final SentrySpanContext _context = SentrySpanContext(operation: 'test'); + + @override + SentrySpanContext get context => _context; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/packages/dart/test/telemetry/log/logs_setup_integration_test.dart b/packages/dart/test/telemetry/log/logs_setup_integration_test.dart new file mode 100644 index 0000000000..ffa022a45b --- /dev/null +++ b/packages/dart/test/telemetry/log/logs_setup_integration_test.dart @@ -0,0 +1,138 @@ +import 'dart:async'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/log/default_logger.dart'; +import 'package:sentry/src/telemetry/log/logs_setup_integration.dart'; +import 'package:sentry/src/telemetry/log/noop_logger.dart'; +import 'package:test/test.dart'; + +import '../../test_utils.dart'; + +void main() { + group('$LogsSetupIntegration', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('when logs are enabled', () { + test('configures DefaultSentryLogger', () { + fixture.options.enableLogs = true; + + fixture.sut.call(fixture.hub, fixture.options); + + expect(fixture.options.logger, isA()); + }); + + test('adds integration to SDK', () { + fixture.options.enableLogs = true; + + fixture.sut.call(fixture.hub, fixture.options); + + expect( + fixture.options.sdk.integrations, + contains(LogsSetupIntegration.integrationName), + ); + }); + + test('does not override existing non-noop logger', () { + fixture.options.enableLogs = true; + final customLogger = _CustomSentryLogger(); + fixture.options.logger = customLogger; + + fixture.sut.call(fixture.hub, fixture.options); + + expect(fixture.options.logger, same(customLogger)); + }); + }); + + group('when logs are disabled', () { + test('does not configure logger', () { + fixture.options.enableLogs = false; + + fixture.sut.call(fixture.hub, fixture.options); + + expect(fixture.options.logger, isA()); + }); + + test('does not add integration to SDK', () { + fixture.options.enableLogs = false; + + fixture.sut.call(fixture.hub, fixture.options); + + expect( + fixture.options.sdk.integrations, + isNot(contains(LogsSetupIntegration.integrationName)), + ); + }); + }); + }); +} + +class Fixture { + final options = defaultTestOptions(); + + late final Hub hub; + late final LogsSetupIntegration sut; + + Fixture() { + hub = Hub(options); + sut = LogsSetupIntegration(); + } +} + +class _CustomSentryLogger implements SentryLogger { + @override + FutureOr trace(String body, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr debug(String body, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr info(String body, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr warn(String body, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr error(String body, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr fatal(String body, + {Map? attributes, Scope? scope}) {} + + @override + SentryLoggerFormatter get fmt => _CustomSentryLoggerFormatter(); +} + +class _CustomSentryLoggerFormatter implements SentryLoggerFormatter { + @override + FutureOr trace(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr debug(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr info(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr warn(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr error(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} + + @override + FutureOr fatal(String templateBody, List arguments, + {Map? attributes, Scope? scope}) {} +} diff --git a/packages/flutter/lib/src/integrations/load_contexts_integration.dart b/packages/flutter/lib/src/integrations/load_contexts_integration.dart index 3d4791d5b1..b46a7a7494 100644 --- a/packages/flutter/lib/src/integrations/load_contexts_integration.dart +++ b/packages/flutter/lib/src/integrations/load_contexts_integration.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; import 'package:collection/collection.dart'; import 'package:sentry/src/event_processor/enricher/enricher_event_processor.dart'; -import 'package:sentry/src/logs_enricher_integration.dart'; import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; import '../utils/internal_logger.dart'; @@ -24,7 +23,7 @@ class LoadContextsIntegration implements Integration { final SentryNativeBinding _native; Map? _cachedAttributes; SentryFlutterOptions? _options; - SdkLifecycleCallback? _logCallback; + SdkLifecycleCallback? _logCallback; SdkLifecycleCallback? _metricCallback; LoadContextsIntegration(this._native); @@ -51,28 +50,19 @@ class LoadContextsIntegration implements Integration { options.addEventProcessor(enricherEventProcessor); } if (options.enableLogs) { - final logsEnricherIntegration = options.integrations.firstWhereOrNull( - (element) => element is LogsEnricherIntegration, - ); - if (logsEnricherIntegration != null) { - // Contexts from native cover the os.name and os.version attributes, - // so we can remove the logsEnricherIntegration. - options.removeIntegration(logsEnricherIntegration); - } - _logCallback = (event) async { try { final attributes = await _nativeContextAttributes(); event.log.attributes.addAllIfAbsent(attributes); } catch (exception, stackTrace) { internalLogger.error( - 'LoadContextsIntegration failed to load contexts for $OnBeforeCaptureLog', + 'LoadContextsIntegration failed to load contexts for $OnProcessLog', error: exception, stackTrace: stackTrace, ); } }; - options.lifecycleRegistry.registerCallback( + options.lifecycleRegistry.registerCallback( _logCallback!, ); } @@ -104,8 +94,7 @@ class LoadContextsIntegration implements Integration { if (options == null) return; if (_logCallback != null) { - options.lifecycleRegistry - .removeCallback(_logCallback!); + options.lifecycleRegistry.removeCallback(_logCallback!); _logCallback = null; } if (_metricCallback != null) { diff --git a/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart index 4883ad1bb1..81421bdb4d 100644 --- a/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart +++ b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart @@ -15,7 +15,7 @@ class ReplayTelemetryIntegration implements Integration { ReplayTelemetryIntegration(this._native); SentryFlutterOptions? _options; - SdkLifecycleCallback? _onBeforeCaptureLog; + SdkLifecycleCallback? _onProcessLog; SdkLifecycleCallback? _onProcessMetric; @override @@ -28,7 +28,7 @@ class ReplayTelemetryIntegration implements Integration { _options = options; - _onBeforeCaptureLog = (OnBeforeCaptureLog event) { + _onProcessLog = (OnProcessLog event) { _addReplayAttributes( hub.scope.replayId, event.log.attributes, @@ -46,10 +46,10 @@ class ReplayTelemetryIntegration implements Integration { ); }; - options.lifecycleRegistry - .registerCallback(_onBeforeCaptureLog!); + options.lifecycleRegistry.registerCallback(_onProcessLog!); options.lifecycleRegistry .registerCallback(_onProcessMetric!); + options.sdk.addIntegration(integrationName); } @@ -76,13 +76,12 @@ class ReplayTelemetryIntegration implements Integration { @override Future close() async { final options = _options; - final onBeforeCaptureLog = _onBeforeCaptureLog; + final onProcessLog = _onProcessLog; final onProcessMetric = _onProcessMetric; if (options != null) { - if (onBeforeCaptureLog != null) { - options.lifecycleRegistry - .removeCallback(onBeforeCaptureLog); + if (onProcessLog != null) { + options.lifecycleRegistry.removeCallback(onProcessLog); } if (onProcessMetric != null) { options.lifecycleRegistry @@ -91,7 +90,7 @@ class ReplayTelemetryIntegration implements Integration { } _options = null; - _onBeforeCaptureLog = null; + _onProcessLog = null; _onProcessMetric = null; } } diff --git a/packages/flutter/test/integrations/load_contexts_integrations_test.dart b/packages/flutter/test/integrations/load_contexts_integrations_test.dart index ada1f5bfa9..53816ab852 100644 --- a/packages/flutter/test/integrations/load_contexts_integrations_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integrations_test.dart @@ -482,16 +482,12 @@ void main() { final integration = fixture.getSut(); integration(fixture.hub, fixture.options); - expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessLog], isNotEmpty); integration.close(); - expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessLog], isEmpty); }); @@ -508,9 +504,7 @@ void main() { expect( fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessMetric], isEmpty); - expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessLog], isEmpty); }); diff --git a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart index 190625966d..8d66733c7c 100644 --- a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart +++ b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart @@ -195,7 +195,7 @@ class Fixture { when(hub.scope).thenReturn(scope); when(hub.captureLog(any)).thenAnswer((invocation) async { final log = invocation.positionalArguments.first as SentryLog; - await options.lifecycleRegistry.dispatchCallback(OnBeforeCaptureLog(log)); + await options.lifecycleRegistry.dispatchCallback(OnProcessLog(log)); }); when(nativeBinding.replayId).thenReturn(null); } diff --git a/packages/logging/test/logging_integration_test.dart b/packages/logging/test/logging_integration_test.dart index 1147f6099c..fb96a6cecd 100644 --- a/packages/logging/test/logging_integration_test.dart +++ b/packages/logging/test/logging_integration_test.dart @@ -492,6 +492,7 @@ class MockSentryLogger implements SentryLogger { Future trace( String body, { Map? attributes, + Scope? scope, }) async { traceCalls.add(MockLogCall(body, attributes)); } @@ -500,6 +501,7 @@ class MockSentryLogger implements SentryLogger { Future debug( String body, { Map? attributes, + Scope? scope, }) async { debugCalls.add(MockLogCall(body, attributes)); } @@ -508,6 +510,7 @@ class MockSentryLogger implements SentryLogger { Future info( String body, { Map? attributes, + Scope? scope, }) async { infoCalls.add(MockLogCall(body, attributes)); } @@ -516,6 +519,7 @@ class MockSentryLogger implements SentryLogger { Future warn( String body, { Map? attributes, + Scope? scope, }) async { warnCalls.add(MockLogCall(body, attributes)); } @@ -524,6 +528,7 @@ class MockSentryLogger implements SentryLogger { Future error( String body, { Map? attributes, + Scope? scope, }) async { errorCalls.add(MockLogCall(body, attributes)); } @@ -532,6 +537,7 @@ class MockSentryLogger implements SentryLogger { Future fatal( String body, { Map? attributes, + Scope? scope, }) async { fatalCalls.add(MockLogCall(body, attributes)); } From 1f2dc29f5b75964c9c603d11576960271997a730 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 18:06:11 +0100 Subject: [PATCH 43/79] Enhance logging tests by adding timestamp verification and attribute inclusion checks. Simplify test case formatting for clarity. --- .../log/log_capture_pipeline_test.dart | 3 +-- .../dart/test/telemetry/log/logger_test.dart | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart b/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart index 9d574031e8..5c6179f99e 100644 --- a/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart +++ b/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart @@ -82,8 +82,7 @@ void main() { expect(attributes['logOnly']?.type, 'double'); }); - test( - 'dispatches OnProcessLog after scope merge but before beforeSendLog', + test('dispatches OnProcessLog after scope merge but before beforeSendLog', () async { final operations = []; bool hasScopeAttrInCallback = false; diff --git a/packages/dart/test/telemetry/log/logger_test.dart b/packages/dart/test/telemetry/log/logger_test.dart index b1db0b305f..a4dbeec8e6 100644 --- a/packages/dart/test/telemetry/log/logger_test.dart +++ b/packages/dart/test/telemetry/log/logger_test.dart @@ -126,6 +126,26 @@ void main() { expect(capturedLog.traceId, customScope.propagationContext.traceId); expect(capturedLog.spanId, span.context.spanId); }); + + test('sets timestamp from clock provider', () async { + final logger = fixture.getSut(); + + await logger.info('test'); + + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + expect(capturedLog.timestamp, fixture.timestamp); + }); + + test('includes attributes when provided', () async { + final logger = fixture.getSut(); + + await logger.info('test', attributes: {'key': SentryAttribute.string('value')}); + + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + expect(capturedLog.attributes['key']?.value, 'value'); + }); }); } From 25b0de7877f08d1606fdefaae1946069e9de426b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 18:09:01 +0100 Subject: [PATCH 44/79] Refactor SentryLog constructor to require traceId and adjust spanId initialization. Remove outdated comment for clarity. --- packages/dart/lib/src/protocol/sentry_log.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/dart/lib/src/protocol/sentry_log.dart b/packages/dart/lib/src/protocol/sentry_log.dart index 05966d05c6..9c1b479ff3 100644 --- a/packages/dart/lib/src/protocol/sentry_log.dart +++ b/packages/dart/lib/src/protocol/sentry_log.dart @@ -6,8 +6,6 @@ import 'span_id.dart'; class SentryLog { DateTime timestamp; SentryId traceId; - - /// The span ID of the active span when the log was recorded. SpanId? spanId; SentryLogLevel level; String body; @@ -18,13 +16,13 @@ class SentryLog { /// by the time processing completes, it is guaranteed to be a valid non-empty trace id. SentryLog({ required this.timestamp, - SentryId? traceId, - this.spanId, + required this.traceId, required this.level, required this.body, required this.attributes, + this.spanId, this.severityNumber, - }) : traceId = traceId ?? SentryId.empty(); + }); Map toJson() { return { From 747259d236bd2ff42dd217e4757f2b70e8c8f650 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 18:10:13 +0100 Subject: [PATCH 45/79] Update SentryMetrics initialization in SentryOptions to use a constant instance of NoOpSentryMetrics --- packages/dart/lib/src/sentry_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 7ec9b4e424..0db9f3b527 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -564,7 +564,7 @@ class SentryOptions { late final SentryLogger logger = SentryLogger(clock); @internal - late SentryMetrics metrics = NoOpSentryMetrics(); + late SentryMetrics metrics = const NoOpSentryMetrics(); @internal TelemetryProcessor telemetryProcessor = NoOpTelemetryProcessor(); From 0919930ba52ae74363c8fce97fd487bdc43f92a3 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 18:11:42 +0100 Subject: [PATCH 46/79] Refactor logger tests to improve readability by formatting logger.info calls. Add traceId initialization to SentryLog in processing tests for consistency. --- packages/dart/test/telemetry/log/logger_test.dart | 3 ++- .../test/telemetry/processing/processor_integration_test.dart | 1 + packages/dart/test/telemetry/processing/processor_test.dart | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/dart/test/telemetry/log/logger_test.dart b/packages/dart/test/telemetry/log/logger_test.dart index a4dbeec8e6..4a256cc534 100644 --- a/packages/dart/test/telemetry/log/logger_test.dart +++ b/packages/dart/test/telemetry/log/logger_test.dart @@ -140,7 +140,8 @@ void main() { test('includes attributes when provided', () async { final logger = fixture.getSut(); - await logger.info('test', attributes: {'key': SentryAttribute.string('value')}); + await logger + .info('test', attributes: {'key': SentryAttribute.string('value')}); expect(fixture.capturedLogs.length, 1); final capturedLog = fixture.capturedLogs[0]; diff --git a/packages/dart/test/telemetry/processing/processor_integration_test.dart b/packages/dart/test/telemetry/processing/processor_integration_test.dart index b0a41d07f4..169ecf9f97 100644 --- a/packages/dart/test/telemetry/processing/processor_integration_test.dart +++ b/packages/dart/test/telemetry/processing/processor_integration_test.dart @@ -88,6 +88,7 @@ class _Fixture { SentryLog createLog() { return SentryLog( timestamp: DateTime.now().toUtc(), + traceId: SentryId.newId(), level: SentryLogLevel.info, body: 'test log', attributes: {}, diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart index 1f81288c43..49ade18331 100644 --- a/packages/dart/test/telemetry/processing/processor_test.dart +++ b/packages/dart/test/telemetry/processing/processor_test.dart @@ -120,6 +120,7 @@ class Fixture { SentryLog createLog({String body = 'test log'}) { return SentryLog( timestamp: DateTime.now().toUtc(), + traceId: SentryId.newId(), level: SentryLogLevel.info, body: body, attributes: {}, From 5055a7e811a1320e5f1c2f7e7106d2eff1aa457a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 18:15:34 +0100 Subject: [PATCH 47/79] Remove deprecated test for LogsEnricherIntegration from load_contexts_integration_test.dart to streamline logger tests. --- .../load_contexts_integration_test.dart | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index 4dd6c5b00b..bb4b3cd949 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -274,21 +274,6 @@ void main() { expect(log.attributes['device.model']?.value, 'fixture-device-model'); expect(log.attributes['device.family']?.value, 'fixture-device-family'); }); - - test('removes logsEnricherIntegration', () async { - final integration = LogsEnricherIntegration(); - fixture.options.addIntegration(integration); - - fixture.options.enableLogs = true; - await fixture.registerIntegration(); - - expect( - fixture.options.integrations - // ignore: invalid_use_of_internal_member - .any((element) => element is LogsEnricherIntegration), - isFalse); - }); - test('does not add os and device attributes to log if enableLogs is false', () async { fixture.options.enableLogs = false; From d2538684d888d50ada34e8b79bcb0a35ac15a57e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 20 Jan 2026 18:15:51 +0100 Subject: [PATCH 48/79] Remove unused import for LogsEnricherIntegration in load_contexts_integration_test.dart to clean up the test file. --- .../test/integrations/load_contexts_integration_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index bb4b3cd949..fbfcfab162 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -7,7 +7,6 @@ import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/integrations/load_contexts_integration.dart'; import 'fixture.dart'; -import 'package:sentry/src/logs_enricher_integration.dart'; void main() { final infosJson = { From 6cc1513a29420fd351c4663c44c90d30479110d3 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 10:35:40 +0100 Subject: [PATCH 49/79] Use unawaited --- .../dart/lib/src/telemetry/metric/default_metrics.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/dart/lib/src/telemetry/metric/default_metrics.dart b/packages/dart/lib/src/telemetry/metric/default_metrics.dart index c54ae034f3..0b6c3ca9bf 100644 --- a/packages/dart/lib/src/telemetry/metric/default_metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/default_metrics.dart @@ -1,8 +1,10 @@ +import 'dart:async'; + import '../../../sentry.dart'; import '../../utils/internal_logger.dart'; import 'metric.dart'; -typedef CaptureMetricCallback = void Function(SentryMetric metric); +typedef CaptureMetricCallback = Future Function(SentryMetric metric); typedef ScopeProvider = Scope Function(); final class DefaultSentryMetrics implements SentryMetrics { @@ -36,7 +38,7 @@ final class DefaultSentryMetrics implements SentryMetrics { traceId: _traceIdFor(scope), attributes: attributes ?? {}); - _captureMetricCallback(metric); + unawaited(_captureMetricCallback(metric)); } @override @@ -59,7 +61,7 @@ final class DefaultSentryMetrics implements SentryMetrics { traceId: _traceIdFor(scope), attributes: attributes ?? {}); - _captureMetricCallback(metric); + unawaited(_captureMetricCallback(metric)); } @override @@ -82,7 +84,7 @@ final class DefaultSentryMetrics implements SentryMetrics { traceId: _traceIdFor(scope), attributes: attributes ?? {}); - _captureMetricCallback(metric); + unawaited(_captureMetricCallback(metric)); } SentryId _traceIdFor(Scope? scope) => From f83fc08af26cbc942a174867e3717c0869d76f8e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 10:38:09 +0100 Subject: [PATCH 50/79] Update comment --- packages/dart/lib/src/telemetry/log/logger.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/dart/lib/src/telemetry/log/logger.dart b/packages/dart/lib/src/telemetry/log/logger.dart index df88b90e90..40d330c744 100644 --- a/packages/dart/lib/src/telemetry/log/logger.dart +++ b/packages/dart/lib/src/telemetry/log/logger.dart @@ -2,6 +2,8 @@ import 'dart:async'; import '../../../sentry.dart'; +// TODO(major-v10): refactor FutureOr to void + /// Interface for emitting custom logs to Sentry. /// /// Access via [Sentry.logger]. From f8bb40911cb5c98692ca7655192c67f47cd865ef Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 10:57:25 +0100 Subject: [PATCH 51/79] Update test --- .../load_contexts_integration_test.dart | 961 +++++++++++++---- .../load_contexts_integrations_test.dart | 585 ----------- packages/flutter/test/mocks.mocks.dart | 965 ++++++++++-------- .../sentry_screenshot_widget_test.mocks.dart | 1 + 4 files changed, 1273 insertions(+), 1239 deletions(-) delete mode 100644 packages/flutter/test/integrations/load_contexts_integrations_test.dart diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index 4dd6c5b00b..954793752f 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -1,27 +1,58 @@ @TestOn('vm') library; +// ignore_for_file: invalid_use_of_internal_member + import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; +import 'package:sentry/src/logs_enricher_integration.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/integrations/load_contexts_integration.dart'; + import 'fixture.dart'; -import 'package:sentry/src/logs_enricher_integration.dart'; void main() { - final infosJson = { + final defaultContexts = { + 'integrations': ['NativeIntegration'], + 'package': {'sdk_name': 'native-package', 'version': '1.0'}, 'contexts': { 'device': { + 'name': 'Device1', 'family': 'fixture-device-family', 'model': 'fixture-device-model', 'brand': 'fixture-device-brand', }, + 'app': {'app_name': 'test-app'}, 'os': { 'name': 'fixture-os-name', 'version': 'fixture-os-version', }, - } + 'gpu': {'name': 'gpu1'}, + 'browser': {'name': 'browser1'}, + 'runtime': {'name': 'RT1'}, + 'theme': 'material', + }, + 'user': { + 'id': '196E065A-AAF7-409A-9A6C-A81F40274CB9', + 'username': 'fixture-username', + 'email': 'fixture-email', + 'ip_address': 'fixture-ip_address', + 'data': {'key': 'value'}, + }, + 'tags': {'key-a': 'native', 'key-b': 'native'}, + 'extra': {'key-a': 'native', 'key-b': 'native'}, + 'dist': 'fixture-dist', + 'environment': 'fixture-environment', + 'fingerprint': ['fingerprint-a'], + 'level': 'error', + 'breadcrumbs': [ + { + 'timestamp': '1970-01-01T00:00:00.000Z', + 'message': 'native-crumb', + } + ] }; SentryLog givenLog() { @@ -36,290 +67,798 @@ void main() { ); } + SdkVersion getSdkVersion({ + String name = 'sentry.dart', + List integrations = const [], + List packages = const [], + }) { + return SdkVersion( + name: name, + version: '1.0', + integrations: integrations, + packages: packages, + ); + } + + SentryEvent getEvent({ + SdkVersion? sdk, + Map? tags, + Map? extra, + SentryUser? user, + String? dist, + String? environment, + List? fingerprint, + SentryLevel? level, + List? breadcrumbs, + Contexts? contexts, + List integrations = const ['EventIntegration'], + List packages = const [], + }) { + if (packages.isEmpty) { + packages = [SentryPackage('event-package', '2.0')]; + } + return SentryEvent( + sdk: sdk ?? + getSdkVersion( + integrations: integrations, + packages: packages, + ), + tags: tags, + // ignore: deprecated_member_use + extra: extra, + user: user, + dist: dist, + environment: environment, + fingerprint: fingerprint, + level: level, + breadcrumbs: breadcrumbs, + contexts: contexts, + ); + } + group(LoadContextsIntegration, () { - late IntegrationTestFixture fixture; + late IntegrationTestFixture fixture; setUp(() { fixture = IntegrationTestFixture(LoadContextsIntegration.new); + // Default stub - tests can override this + when(fixture.binding.loadContexts()).thenAnswer((_) async => null); }); - test('loadContextsIntegration adds integration', () async { + void mockLoadContexts([Map? contexts]) { + when(fixture.binding.loadContexts()) + .thenAnswer((_) async => contexts ?? defaultContexts); + } + + test('adds integration to sdk.integrations', () async { await fixture.registerIntegration(); expect( - fixture.options.sdk.integrations.contains('loadContextsIntegration'), - true); + fixture.options.sdk.integrations.contains('loadContextsIntegration'), + true, + ); }); - test('take breadcrumbs from native if scope sync is enabled', () async { + test('applies loadContextsIntegration eventProcessor', () async { + mockLoadContexts(); await fixture.registerIntegration(); - fixture.options.enableScopeSync = true; - - final eventBreadcrumb = Breadcrumb(message: 'event'); - var event = SentryEvent(breadcrumbs: [eventBreadcrumb]); + expect(fixture.options.eventProcessors.length, 1); - when(fixture.binding.loadContexts()).thenAnswer((_) async => { - 'breadcrumbs': [Breadcrumb(message: 'native').toJson()] - }); + final e = SentryEvent(); + e.contexts.operatingSystem = SentryOperatingSystem(theme: 'theme1'); + e.contexts.app = SentryApp(inForeground: true); - event = - (await fixture.options.eventProcessors.first.apply(event, Hint()))!; + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - expect(event.breadcrumbs!.length, 1); - expect(event.breadcrumbs!.first.message, 'native'); + verify(fixture.binding.loadContexts()).called(1); + expect(event?.contexts.device?.name, 'Device1'); + expect(event?.contexts.app?.name, 'test-app'); + expect(event?.contexts.app?.inForeground, true); + expect(event?.contexts.operatingSystem?.name, 'fixture-os-name'); + expect(event?.contexts.operatingSystem?.theme, 'theme1'); + expect(event?.contexts.gpu?.name, 'gpu1'); + expect(event?.contexts.browser?.name, 'browser1'); + expect( + event?.contexts.runtimes.any((element) => element.name == 'RT1'), + true); + expect(event?.contexts['theme'], 'material'); + expect( + event?.sdk?.packages.any((element) => element.name == 'native-package'), + true, + ); + expect(event?.sdk?.integrations.contains('NativeIntegration'), true); + expect(event?.user?.id, '196E065A-AAF7-409A-9A6C-A81F40274CB9'); }); - test('take breadcrumbs from event if scope sync is disabled', () async { + test('does not override event contexts with loadContextsIntegration infos', + () async { + mockLoadContexts(); await fixture.registerIntegration(); - fixture.options.enableScopeSync = false; - - final eventBreadcrumb = Breadcrumb(message: 'event'); - var event = SentryEvent(breadcrumbs: [eventBreadcrumb]); - - when(fixture.binding.loadContexts()).thenAnswer((_) async => { - 'breadcrumbs': [Breadcrumb(message: 'native').toJson()] - }); - - event = - (await fixture.options.eventProcessors.first.apply(event, Hint()))!; - expect(event.breadcrumbs!.length, 1); - expect(event.breadcrumbs!.first.message, 'event'); - }); - - test('apply beforeBreadcrumb to native breadcrumbs', () async { - await fixture.registerIntegration(); - fixture.options.enableScopeSync = true; - fixture.options.beforeBreadcrumb = (breadcrumb, hint) { - if (breadcrumb?.message == 'native-mutated') { - breadcrumb?.message = 'native-mutated-applied'; - return breadcrumb; - } else { - return null; - } - }; - - final eventBreadcrumb = Breadcrumb(message: 'event'); - var event = SentryEvent(breadcrumbs: [eventBreadcrumb]); - - when(fixture.binding.loadContexts()).thenAnswer((_) async => { - 'breadcrumbs': [ - Breadcrumb(message: 'native-mutated').toJson(), - Breadcrumb(message: 'native-deleted').toJson(), - ] - }); - - event = - (await fixture.options.eventProcessors.first.apply(event, Hint()))!; - - expect(event.breadcrumbs!.length, 1); - expect(event.breadcrumbs!.first.message, 'native-mutated-applied'); + final eventContexts = Contexts( + device: SentryDevice(name: 'eDevice'), + app: SentryApp(name: 'eApp', inForeground: true), + operatingSystem: SentryOperatingSystem(name: 'eOS'), + gpu: SentryGpu(name: 'eGpu'), + browser: SentryBrowser(name: 'eBrowser'), + runtimes: [SentryRuntime(name: 'eRT')], + )..['theme'] = 'cuppertino'; + + final e = getEvent( + contexts: eventContexts, + user: SentryUser(id: 'myId'), + ); + + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); + + verify(fixture.binding.loadContexts()).called(1); + expect(event?.contexts.device?.name, 'eDevice'); + expect(event?.contexts.app?.name, 'eApp'); + expect(event?.contexts.app?.inForeground, true); + expect(event?.contexts.operatingSystem?.name, 'eOS'); + expect(event?.contexts.gpu?.name, 'eGpu'); + expect(event?.contexts.browser?.name, 'eBrowser'); + expect( + event?.contexts.runtimes.any((element) => element.name == 'RT1'), + true); + expect( + event?.contexts.runtimes.any((element) => element.name == 'eRT'), + true); + expect(event?.contexts['theme'], 'cuppertino'); + expect(event?.user?.id, 'myId'); }); - test( - 'apply default IP to user during captureEvent after loading context if ip is null and sendDefaultPii is true', + test('merges event and loadContextsIntegration sdk packages and integration', () async { + mockLoadContexts(); await fixture.registerIntegration(); - fixture.options.enableScopeSync = true; - fixture.options.sendDefaultPii = true; + final e = getEvent(); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - const expectedIp = '{{auto}}'; - String? actualIp; - - const expectedId = '1'; - String? actualId; + expect( + event?.sdk?.packages.any((element) => element.name == 'native-package'), + true, + ); + expect( + event?.sdk?.packages.any((element) => element.name == 'event-package'), + true, + ); + expect(event?.sdk?.integrations.contains('NativeIntegration'), true); + expect(event?.sdk?.integrations.contains('EventIntegration'), true); + }); - fixture.options.beforeSend = (event, hint) { - actualIp = event.user?.ipAddress; - actualId = event.user?.id; - return event; - }; + test('does not duplicate integration if already there', () async { + mockLoadContexts({ + 'integrations': ['EventIntegration'] + }); + await fixture.registerIntegration(); - final options = fixture.options; + final e = getEvent(); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - final user = SentryUser(id: expectedId); - when(fixture.binding.loadContexts()) - .thenAnswer((_) async => {'user': user.toJson()}); + expect( + event?.sdk?.integrations + .where((element) => element == 'EventIntegration') + .length, + 1, + ); + }); - final client = SentryClient(options); - final event = SentryEvent(); + test('does not duplicate package if already there', () async { + mockLoadContexts({ + 'package': {'sdk_name': 'event-package', 'version': '2.0'} + }); + await fixture.registerIntegration(); - await client.captureEvent(event); + final e = getEvent(); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - expect(actualIp, expectedIp); - expect(actualId, expectedId); + expect( + event?.sdk?.packages + .where((element) => + element.name == 'event-package' && element.version == '2.0') + .length, + 1, + ); }); - test( - 'does not apply default IP to user during captureEvent after loading context if ip is null and sendDefaultPii is false', - () async { + test('adds package if different version', () async { + mockLoadContexts({ + 'package': {'sdk_name': 'event-package', 'version': '3.0'} + }); await fixture.registerIntegration(); - fixture.options.enableScopeSync = true; - // sendDefaultPii false is by default - String? actualIp; + final e = getEvent(); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - const expectedId = '1'; - String? actualId; + expect( + event?.sdk?.packages + .where((element) => + element.name == 'event-package' && element.version == '2.0') + .length, + 1, + ); + expect( + event?.sdk?.packages + .where((element) => + element.name == 'event-package' && element.version == '3.0') + .length, + 1, + ); + }); - fixture.options.beforeSend = (event, hint) { - actualIp = event.user?.ipAddress; - actualId = event.user?.id; - return event; - }; + group('breadcrumbs', () { + test('takes breadcrumbs from native if scope sync is enabled', () async { + await fixture.registerIntegration(); + fixture.options.enableScopeSync = true; + + final eventBreadcrumb = Breadcrumb(message: 'event'); + var event = SentryEvent(breadcrumbs: [eventBreadcrumb]); + + when(fixture.binding.loadContexts()).thenAnswer((_) async => { + 'breadcrumbs': [Breadcrumb(message: 'native').toJson()] + }); + + event = (await fixture.options.eventProcessors.first + .apply(event, Hint()))!; + + expect(event.breadcrumbs!.length, 1); + expect(event.breadcrumbs!.first.message, 'native'); + }); + + test('takes breadcrumbs from event if scope sync is disabled', () async { + await fixture.registerIntegration(); + fixture.options.enableScopeSync = false; + + final eventBreadcrumb = Breadcrumb(message: 'event'); + var event = SentryEvent(breadcrumbs: [eventBreadcrumb]); + + when(fixture.binding.loadContexts()).thenAnswer((_) async => { + 'breadcrumbs': [Breadcrumb(message: 'native').toJson()] + }); + + event = (await fixture.options.eventProcessors.first + .apply(event, Hint()))!; + + expect(event.breadcrumbs!.length, 1); + expect(event.breadcrumbs!.first.message, 'event'); + }); + + test('applies beforeBreadcrumb to native breadcrumbs', () async { + await fixture.registerIntegration(); + fixture.options.enableScopeSync = true; + fixture.options.beforeBreadcrumb = (breadcrumb, hint) { + if (breadcrumb?.message == 'native-mutated') { + breadcrumb?.message = 'native-mutated-applied'; + return breadcrumb; + } else { + return null; + } + }; + + final eventBreadcrumb = Breadcrumb(message: 'event'); + var event = SentryEvent(breadcrumbs: [eventBreadcrumb]); + + when(fixture.binding.loadContexts()).thenAnswer((_) async => { + 'breadcrumbs': [ + Breadcrumb(message: 'native-mutated').toJson(), + Breadcrumb(message: 'native-deleted').toJson(), + ] + }); + + event = (await fixture.options.eventProcessors.first + .apply(event, Hint()))!; + + expect(event.breadcrumbs!.length, 1); + expect(event.breadcrumbs!.first.message, 'native-mutated-applied'); + }); + }); - final options = fixture.options; + group('tags', () { + test('adds origin and environment tags if tags is null', () async { + mockLoadContexts(); + await fixture.registerIntegration(); + + final eventSdk = getSdkVersion(name: 'sentry.dart.flutter'); + final e = getEvent(sdk: eventSdk); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); + + expect(event?.tags?['event.origin'], 'flutter'); + expect(event?.tags?['event.environment'], 'dart'); + }); + + test('merges origin and environment tags', () async { + mockLoadContexts(); + await fixture.registerIntegration(); + + final eventSdk = getSdkVersion(name: 'sentry.dart.flutter'); + final e = getEvent(sdk: eventSdk, tags: {'a': 'b'}); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); + + expect(event?.tags?['event.origin'], 'flutter'); + expect(event?.tags?['event.environment'], 'dart'); + expect(event?.tags?['a'], 'b'); + }); + + test('does not add origin and environment tags if not flutter sdk', + () async { + mockLoadContexts(); + await fixture.registerIntegration(); + + final e = getEvent(tags: {}); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); + + expect(event?.tags?.containsKey('event.origin'), false); + expect(event?.tags?.containsKey('event.environment'), false); + }); + + test('merges tags from native without overriding flutter keys', () async { + mockLoadContexts(); + await fixture.registerIntegration(); + + final e = getEvent(tags: {'key': 'flutter', 'key-a': 'flutter'}); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); + + expect(event?.tags?['key'], 'flutter'); + expect(event?.tags?['key-a'], 'flutter'); + expect(event?.tags?['key-b'], 'native'); + }); + }); - final user = SentryUser(id: expectedId); - when(fixture.binding.loadContexts()) - .thenAnswer((_) async => {'user': user.toJson()}); + group('extra', () { + test('merges extra from native without overriding flutter keys', () async { + mockLoadContexts(); + await fixture.registerIntegration(); + + final e = getEvent(extra: {'key': 'flutter', 'key-a': 'flutter'}); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); + + // ignore: deprecated_member_use + expect(event?.extra?['key'], 'flutter'); + // ignore: deprecated_member_use + expect(event?.extra?['key-a'], 'flutter'); + // ignore: deprecated_member_use + expect(event?.extra?['key-b'], 'native'); + }); + }); - final client = SentryClient(options); - final event = SentryEvent(); + group('user', () { + test('sets user from native', () async { + mockLoadContexts(); + await fixture.registerIntegration(); + + final e = getEvent(); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); + + expect(event?.user?.id, '196E065A-AAF7-409A-9A6C-A81F40274CB9'); + expect(event?.user?.username, 'fixture-username'); + expect(event?.user?.email, 'fixture-email'); + expect(event?.user?.ipAddress, 'fixture-ip_address'); + expect(event?.user?.data?['key'], 'value'); + }); + + test('does not override user with native', () async { + mockLoadContexts(); + await fixture.registerIntegration(); + + final e = getEvent(user: SentryUser(id: 'abc')); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); + + expect(event?.user?.id, 'abc'); + }); + + test( + 'applies default IP to user during captureEvent if ip is null and sendDefaultPii is true', + () async { + await fixture.registerIntegration(); + fixture.options.enableScopeSync = true; + fixture.options.sendDefaultPii = true; + + const expectedIp = '{{auto}}'; + String? actualIp; + const expectedId = '1'; + String? actualId; + + fixture.options.beforeSend = (event, hint) { + actualIp = event.user?.ipAddress; + actualId = event.user?.id; + return event; + }; + + final user = SentryUser(id: expectedId); + when(fixture.binding.loadContexts()) + .thenAnswer((_) async => {'user': user.toJson()}); + + final client = SentryClient(fixture.options); + final event = SentryEvent(); + + await client.captureEvent(event); + + expect(actualIp, expectedIp); + expect(actualId, expectedId); + }); + + test( + 'does not apply default IP to user during captureEvent if ip is null and sendDefaultPii is false', + () async { + await fixture.registerIntegration(); + fixture.options.enableScopeSync = true; + + String? actualIp; + const expectedId = '1'; + String? actualId; + + fixture.options.beforeSend = (event, hint) { + actualIp = event.user?.ipAddress; + actualId = event.user?.id; + return event; + }; + + final user = SentryUser(id: expectedId); + when(fixture.binding.loadContexts()) + .thenAnswer((_) async => {'user': user.toJson()}); + + final client = SentryClient(fixture.options); + final event = SentryEvent(); + + await client.captureEvent(event); + + expect(actualIp, isNull); + expect(actualId, expectedId); + }); + + test( + 'applies default IP to user during captureTransaction if ip is null and sendDefaultPii is true', + () async { + await fixture.registerIntegration(); + fixture.options.enableScopeSync = true; + fixture.options.sendDefaultPii = true; + + const expectedIp = '{{auto}}'; + String? actualIp; + const expectedId = '1'; + String? actualId; + + fixture.options.beforeSendTransaction = (transaction, hint) { + actualIp = transaction.user?.ipAddress; + actualId = transaction.user?.id; + return transaction; + }; + + final user = SentryUser(id: expectedId); + when(fixture.binding.loadContexts()) + .thenAnswer((_) async => {'user': user.toJson()}); + + final client = SentryClient(fixture.options); + final tracer = + SentryTracer(SentryTransactionContext('name', 'op'), fixture.hub); + final transaction = SentryTransaction(tracer); + + await client.captureTransaction(transaction); + + expect(actualIp, expectedIp); + expect(actualId, expectedId); + }); + + test( + 'does not apply default IP to user during captureTransaction if ip is null and sendDefaultPii is false', + () async { + await fixture.registerIntegration(); + fixture.options.enableScopeSync = true; + + String? actualIp; + const expectedId = '1'; + String? actualId; + + fixture.options.beforeSendTransaction = (transaction, hint) { + actualIp = transaction.user?.ipAddress; + actualId = transaction.user?.id; + return transaction; + }; + + final user = SentryUser(id: expectedId); + when(fixture.binding.loadContexts()) + .thenAnswer((_) async => {'user': user.toJson()}); + + final client = SentryClient(fixture.options); + final tracer = + SentryTracer(SentryTransactionContext('name', 'op'), fixture.hub); + final transaction = SentryTransaction(tracer); + + await client.captureTransaction(transaction); + + expect(actualIp, isNull); + expect(actualId, expectedId); + }); + }); - await client.captureEvent(event); + group('dist', () { + test('sets dist from native', () async { + mockLoadContexts(); + await fixture.registerIntegration(); - expect(actualIp, isNull); - expect(actualId, expectedId); - }); + final e = getEvent(); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - test( - 'apply default IP to user during captureTransaction after loading context if ip is null and sendDefaultPii is true', - () async { - await fixture.registerIntegration(); - fixture.options.enableScopeSync = true; - fixture.options.sendDefaultPii = true; + expect(event?.dist, 'fixture-dist'); + }); + + test('does not override dist with native', () async { + mockLoadContexts(); + await fixture.registerIntegration(); - const expectedIp = '{{auto}}'; - String? actualIp; + final e = getEvent(dist: 'abc'); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - const expectedId = '1'; - String? actualId; + expect(event?.dist, 'abc'); + }); + }); - fixture.options.beforeSendTransaction = (transaction, hint) { - actualIp = transaction.user?.ipAddress; - actualId = transaction.user?.id; - return transaction; - }; + group('environment', () { + test('sets environment from native', () async { + mockLoadContexts(); + await fixture.registerIntegration(); - final options = fixture.options; + final e = getEvent(); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - final user = SentryUser(id: expectedId); - when(fixture.binding.loadContexts()) - .thenAnswer((_) async => {'user': user.toJson()}); + expect(event?.environment, 'fixture-environment'); + }); - final client = SentryClient(options); - final tracer = - SentryTracer(SentryTransactionContext('name', 'op'), fixture.hub); - final transaction = SentryTransaction(tracer); + test('does not override environment with native', () async { + mockLoadContexts(); + await fixture.registerIntegration(); - // ignore: invalid_use_of_internal_member - await client.captureTransaction(transaction); + final e = getEvent(environment: 'abc'); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - expect(actualIp, expectedIp); - expect(actualId, expectedId); + expect(event?.environment, 'abc'); + }); }); - test( - 'does not apply default IP to user during captureTransaction after loading context if ip is null and sendDefaultPii is false', - () async { - await fixture.registerIntegration(); - fixture.options.enableScopeSync = true; - // sendDefaultPii false is by default + group('fingerprint', () { + test('merges fingerprint from native without duplicating entries', + () async { + mockLoadContexts(); + await fixture.registerIntegration(); - String? actualIp; + final e = getEvent(fingerprint: ['fingerprint-a', 'fingerprint-b']); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - const expectedId = '1'; - String? actualId; + expect(event?.fingerprint, ['fingerprint-a', 'fingerprint-b']); + }); + }); - fixture.options.beforeSendTransaction = (transaction, hint) { - actualIp = transaction.user?.ipAddress; - actualId = transaction.user?.id; - return transaction; - }; + group('level', () { + test('sets level from native', () async { + mockLoadContexts(); + await fixture.registerIntegration(); - final options = fixture.options; + final e = getEvent(); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - final user = SentryUser(id: expectedId); - when(fixture.binding.loadContexts()) - .thenAnswer((_) async => {'user': user.toJson()}); + expect(event?.level, SentryLevel.error); + }); - final client = SentryClient(options); - final tracer = - SentryTracer(SentryTransactionContext('name', 'op'), fixture.hub); - final transaction = SentryTransaction(tracer); + test('does not override level with native', () async { + mockLoadContexts(); + await fixture.registerIntegration(); - // ignore: invalid_use_of_internal_member - await client.captureTransaction(transaction); + final e = getEvent(level: SentryLevel.fatal); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); - expect(actualIp, isNull); - expect(actualId, expectedId); + expect(event?.level, SentryLevel.fatal); + }); }); - test('add os and device attributes to log', () async { - fixture.options.enableLogs = true; + group('logs', () { + test('adds os and device attributes to log', () async { + fixture.options.enableLogs = true; + await fixture.registerIntegration(); - await fixture.registerIntegration(); + when(fixture.binding.loadContexts()) + .thenAnswer((_) async => defaultContexts); - when(fixture.binding.loadContexts()).thenAnswer((_) async => infosJson); + final log = givenLog(); + await fixture.hub.captureLog(log); - final log = givenLog(); - await fixture.hub.captureLog(log); + expect(log.attributes['os.name']?.value, 'fixture-os-name'); + expect(log.attributes['os.version']?.value, 'fixture-os-version'); + expect(log.attributes['device.brand']?.value, 'fixture-device-brand'); + expect(log.attributes['device.model']?.value, 'fixture-device-model'); + expect(log.attributes['device.family']?.value, 'fixture-device-family'); + }); - expect(log.attributes['os.name']?.value, 'fixture-os-name'); - expect(log.attributes['os.version']?.value, 'fixture-os-version'); - expect(log.attributes['device.brand']?.value, 'fixture-device-brand'); - expect(log.attributes['device.model']?.value, 'fixture-device-model'); - expect(log.attributes['device.family']?.value, 'fixture-device-family'); - }); - - test('removes logsEnricherIntegration', () async { - final integration = LogsEnricherIntegration(); - fixture.options.addIntegration(integration); + test('removes logsEnricherIntegration', () async { + final integration = LogsEnricherIntegration(); + fixture.options.addIntegration(integration); - fixture.options.enableLogs = true; - await fixture.registerIntegration(); + fixture.options.enableLogs = true; + await fixture.registerIntegration(); - expect( + expect( fixture.options.integrations - // ignore: invalid_use_of_internal_member .any((element) => element is LogsEnricherIntegration), - isFalse); + isFalse, + ); + }); + + test('does not add os and device attributes to log if enableLogs is false', + () async { + fixture.options.enableLogs = false; + await fixture.registerIntegration(); + + when(fixture.binding.loadContexts()) + .thenAnswer((_) async => defaultContexts); + + final log = givenLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes['os.name'], isNull); + expect(log.attributes['os.version'], isNull); + expect(log.attributes['device.brand'], isNull); + expect(log.attributes['device.model'], isNull); + expect(log.attributes['device.family'], isNull); + }); + + test('handles throw during loadContexts', () async { + fixture.options.enableLogs = true; + await fixture.registerIntegration(); + + when(fixture.binding.loadContexts()).thenThrow(Exception('test')); + + final log = givenLog(); + await fixture.hub.captureLog(log); + + expect(log.attributes['os.name'], isNull); + expect(log.attributes['os.version'], isNull); + expect(log.attributes['device.brand'], isNull); + expect(log.attributes['device.model'], isNull); + expect(log.attributes['device.family'], isNull); + }); }); - test('does not add os and device attributes to log if enableLogs is false', - () async { - fixture.options.enableLogs = false; - await fixture.registerIntegration(); - - when(fixture.binding.loadContexts()).thenAnswer((_) async => infosJson); - - final log = givenLog(); - await fixture.hub.captureLog(log); - - expect(log.attributes['os.name'], isNull); - expect(log.attributes['os.version'], isNull); - expect(log.attributes['device.brand'], isNull); - expect(log.attributes['device.model'], isNull); - expect(log.attributes['device.family'], isNull); + group('metrics', () { + test('adds native attributes to metric when metrics enabled', () async { + fixture.options.enableMetrics = true; + mockLoadContexts(); + await fixture.registerIntegration(); + + expect( + fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 1); + + final metric = SentryCounterMetric( + timestamp: DateTime.now(), + name: 'random', + value: 1, + traceId: SentryId.newId(), + ); + + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessMetric(metric)); + + verify(fixture.binding.loadContexts()).called(1); + final attributes = metric.attributes; + expect(attributes[SemanticAttributesConstants.osName]?.value, + 'fixture-os-name'); + expect(attributes[SemanticAttributesConstants.osVersion]?.value, + 'fixture-os-version'); + expect(attributes[SemanticAttributesConstants.deviceBrand]?.value, + 'fixture-device-brand'); + expect(attributes[SemanticAttributesConstants.deviceModel]?.value, + 'fixture-device-model'); + expect(attributes[SemanticAttributesConstants.deviceFamily]?.value, + 'fixture-device-family'); + }); + + test('does not register callback when metrics disabled', () async { + fixture.options.enableMetrics = false; + await fixture.registerIntegration(); + + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 0); + }); }); - test('handles throw during loadContexts', () async { - fixture.options.enableLogs = true; - await fixture.registerIntegration(); - - when(fixture.binding.loadContexts()).thenThrow(Exception('test')); - - final log = givenLog(); - await fixture.hub.captureLog(log); - - expect(log.attributes['os.name'], isNull); - expect(log.attributes['os.version'], isNull); - expect(log.attributes['device.brand'], isNull); - expect(log.attributes['device.model'], isNull); - expect(log.attributes['device.family'], isNull); + group('close', () { + test('removes metric callback from lifecycle registry', () async { + fixture.options.enableMetrics = true; + mockLoadContexts(); + await fixture.registerIntegration(); + + expect( + fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 1); + + fixture.sut.close(); + + expect( + fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessMetric], + isEmpty, + ); + }); + + test('removes log callback from lifecycle registry', () async { + fixture.options.enableLogs = true; + await fixture.registerIntegration(); + + expect( + fixture.options.lifecycleRegistry + .lifecycleCallbacks[OnBeforeCaptureLog], + isNotEmpty, + ); + + fixture.sut.close(); + + expect( + fixture.options.lifecycleRegistry + .lifecycleCallbacks[OnBeforeCaptureLog], + isEmpty, + ); + }); + + test('removes both callbacks when both features enabled', () async { + fixture.options.enableMetrics = true; + fixture.options.enableLogs = true; + mockLoadContexts(); + await fixture.registerIntegration(); + + expect( + fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 2); + + fixture.sut.close(); + + expect( + fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessMetric], + isEmpty, + ); + expect( + fixture.options.lifecycleRegistry + .lifecycleCallbacks[OnBeforeCaptureLog], + isEmpty, + ); + }); + + test('callback is not invoked after close', () async { + fixture.options.enableMetrics = true; + mockLoadContexts(); + await fixture.registerIntegration(); + + fixture.sut.close(); + + final metric = SentryCounterMetric( + timestamp: DateTime.now(), + name: 'random', + value: 1, + traceId: SentryId.newId(), + ); + + await fixture.options.lifecycleRegistry + .dispatchCallback(OnProcessMetric(metric)); + + verifyNever(fixture.binding.loadContexts()); + expect(metric.attributes, isEmpty); + }); }); }); } diff --git a/packages/flutter/test/integrations/load_contexts_integrations_test.dart b/packages/flutter/test/integrations/load_contexts_integrations_test.dart deleted file mode 100644 index ada1f5bfa9..0000000000 --- a/packages/flutter/test/integrations/load_contexts_integrations_test.dart +++ /dev/null @@ -1,585 +0,0 @@ -@TestOn('vm') -library; - -// ignore_for_file: invalid_use_of_internal_member - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/integrations/load_contexts_integration.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; - -import '../mocks.dart'; -import '../mocks.mocks.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - SdkVersion getSdkVersion({ - String name = 'sentry.dart', - List integrations = const [], - List packages = const [], - }) { - return SdkVersion( - name: name, - version: '1.0', - integrations: integrations, - packages: packages); - } - - SentryEvent getEvent({ - SdkVersion? sdk, - Map? tags, - Map? extra, - SentryUser? user, - String? dist, - String? environment, - List? fingerprint, - SentryLevel? level, - List? breadcrumbs, - List integrations = const ['EventIntegration'], - List packages = const [], - }) { - if (packages.isEmpty) { - packages = [SentryPackage('event-package', '2.0')]; - } - return SentryEvent( - sdk: sdk ?? - getSdkVersion( - integrations: integrations, - packages: packages, - ), - tags: tags, - // ignore: deprecated_member_use - extra: extra, - user: user, - dist: dist, - environment: environment, - fingerprint: fingerprint, - level: level, - breadcrumbs: breadcrumbs, - ); - } - - test('$LoadContextsIntegration adds itself to sdk.integrations', () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - expect( - fixture.options.sdk.integrations.contains('loadContextsIntegration'), - true, - ); - }); - - test('should apply the loadContextsIntegration eventProcessor', () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - expect(fixture.options.eventProcessors.length, 1); - - final e = SentryEvent(); - e.contexts.operatingSystem = SentryOperatingSystem(theme: 'theme1'); - e.contexts.app = SentryApp(inForeground: true); - - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - verify(fixture.binding.loadContexts()).called(1); - expect(event?.contexts.device?.name, 'Device1'); - expect(event?.contexts.app?.name, 'test-app'); - expect(event?.contexts.app?.inForeground, true); - expect(event?.contexts.operatingSystem?.name, 'os1'); - expect(event?.contexts.operatingSystem?.theme, 'theme1'); - expect(event?.contexts.gpu?.name, 'gpu1'); - expect(event?.contexts.browser?.name, 'browser1'); - expect( - event?.contexts.runtimes.any((element) => element.name == 'RT1'), true); - expect(event?.contexts['theme'], 'material'); - expect( - event?.sdk?.packages.any((element) => element.name == 'native-package'), - true, - ); - expect(event?.sdk?.integrations.contains('NativeIntegration'), true); - expect(event?.user?.id, '196E065A-AAF7-409A-9A6C-A81F40274CB9'); - }); - - test( - 'should not override event contexts with the loadContextsIntegration infos', - () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - expect(fixture.options.eventProcessors.length, 1); - - final eventContexts = Contexts( - device: SentryDevice(name: 'eDevice'), - app: SentryApp(name: 'eApp', inForeground: true), - operatingSystem: SentryOperatingSystem(name: 'eOS'), - gpu: SentryGpu(name: 'eGpu'), - browser: SentryBrowser(name: 'eBrowser'), - runtimes: [SentryRuntime(name: 'eRT')]) - ..['theme'] = 'cuppertino'; - final e = - SentryEvent(contexts: eventContexts, user: SentryUser(id: 'myId')); - - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - verify(fixture.binding.loadContexts()).called(1); - expect(event?.contexts.device?.name, 'eDevice'); - expect(event?.contexts.app?.name, 'eApp'); - expect(event?.contexts.app?.inForeground, true); - expect(event?.contexts.operatingSystem?.name, 'eOS'); - expect(event?.contexts.gpu?.name, 'eGpu'); - expect(event?.contexts.browser?.name, 'eBrowser'); - expect( - event?.contexts.runtimes.any((element) => element.name == 'RT1'), true); - expect( - event?.contexts.runtimes.any((element) => element.name == 'eRT'), true); - expect(event?.contexts['theme'], 'cuppertino'); - expect(event?.user?.id, 'myId'); - }); - - test( - 'should merge event and loadContextsIntegration sdk packages and integration', - () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(); - final event = - await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect( - event?.sdk?.packages.any((element) => element.name == 'native-package'), - true, - ); - expect( - event?.sdk?.packages.any((element) => element.name == 'event-package'), - true, - ); - expect(event?.sdk?.integrations.contains('NativeIntegration'), true); - expect(event?.sdk?.integrations.contains('EventIntegration'), true); - }, - ); - - test( - 'should not duplicate integration if already there', - () async { - final integration = fixture.getSut(contexts: { - 'integrations': ['EventIntegration'] - }); - integration(fixture.hub, fixture.options); - - final e = getEvent(); - final event = - await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect( - event?.sdk?.integrations - .where((element) => element == 'EventIntegration') - .toList(growable: false) - .length, - 1); - }, - ); - - test( - 'should not duplicate package if already there', - () async { - final integration = fixture.getSut(contexts: { - 'package': {'sdk_name': 'event-package', 'version': '2.0'} - }); - integration(fixture.hub, fixture.options); - - final e = getEvent(); - final event = - await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect( - event?.sdk?.packages - .where((element) => - element.name == 'event-package' && element.version == '2.0') - .toList(growable: false) - .length, - 1); - }, - ); - - test( - 'adds package if different version', - () async { - final integration = fixture.getSut(contexts: { - 'package': {'sdk_name': 'event-package', 'version': '3.0'} - }); - integration(fixture.hub, fixture.options); - - final e = getEvent(); - final event = - await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect( - event?.sdk?.packages - .where((element) => - element.name == 'event-package' && element.version == '2.0') - .toList(growable: false) - .length, - 1); - - expect( - event?.sdk?.packages - .where((element) => - element.name == 'event-package' && element.version == '3.0') - .toList(growable: false) - .length, - 1); - }, - ); - - test('should not throw on loadContextsIntegration exception', () async { - when(fixture.binding.loadContexts()).thenThrow(Exception()); - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = SentryEvent(); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event, isNotNull); - }); - - test( - 'should add origin and environment tags if tags is null', - () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final eventSdk = getSdkVersion(name: 'sentry.dart.flutter'); - final e = getEvent(sdk: eventSdk); - final event = - await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.tags?['event.origin'], 'flutter'); - expect(event?.tags?['event.environment'], 'dart'); - }, - ); - - test( - 'should merge origin and environment tags', - () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final eventSdk = getSdkVersion(name: 'sentry.dart.flutter'); - final e = getEvent( - sdk: eventSdk, - tags: {'a': 'b'}, - ); - final event = - await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.tags?['event.origin'], 'flutter'); - expect(event?.tags?['event.environment'], 'dart'); - expect(event?.tags?['a'], 'b'); - }, - ); - - test( - 'should not add origin and environment tags if not flutter sdk', - () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(tags: {}); - final event = - await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.tags?.containsKey('event.origin'), false); - expect(event?.tags?.containsKey('event.environment'), false); - }, - ); - - test('should merge in tags from native without overriding flutter keys', - () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(tags: {'key': 'flutter', 'key-a': 'flutter'}); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.tags?['key'], 'flutter'); - expect(event?.tags?['key-a'], 'flutter'); - expect(event?.tags?['key-b'], 'native'); - }); - - test('should merge in extra from native without overriding flutter keys', - () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(extra: {'key': 'flutter', 'key-a': 'flutter'}); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - // ignore: deprecated_member_use - expect(event?.extra?['key'], 'flutter'); - // ignore: deprecated_member_use - expect(event?.extra?['key-a'], 'flutter'); - // ignore: deprecated_member_use - expect(event?.extra?['key-b'], 'native'); - }); - - test('should set user from native', () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.user?.id, '196E065A-AAF7-409A-9A6C-A81F40274CB9'); - expect(event?.user?.username, 'fixture-username'); - expect(event?.user?.email, 'fixture-email'); - expect(event?.user?.ipAddress, 'fixture-ip_address'); - expect(event?.user?.data?['key'], 'value'); - }); - - test('should not override user with native', () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(user: SentryUser(id: 'abc')); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.user?.id, 'abc'); - }); - - test('should set dist from native', () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.dist, 'fixture-dist'); - }); - - test('should not override dist with native', () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(dist: 'abc'); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.dist, 'abc'); - }); - - test('should set environment from native', () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.environment, 'fixture-environment'); - }); - - test('should not override environment with native', () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(environment: 'abc'); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.environment, 'abc'); - }); - - test('should merge in fingerprint from native without duplicating entries', - () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(fingerprint: ['fingerprint-a', 'fingerprint-b']); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.fingerprint, ['fingerprint-a', 'fingerprint-b']); - }); - - test('should set level from native', () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.level, SentryLevel.error); - }); - - test('should not override level with native', () async { - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - final e = getEvent(level: SentryLevel.fatal); - final event = await fixture.options.eventProcessors.first.apply(e, Hint()); - - expect(event?.level, SentryLevel.fatal); - }); - - test('with metrics enabled adds native attributes to metric', () async { - fixture.options.enableMetrics = true; - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 1); - - final metric = SentryCounterMetric( - timestamp: DateTime.now(), - name: 'random', - value: 1, - traceId: SentryId.newId()); - - await fixture.options.lifecycleRegistry - .dispatchCallback(OnProcessMetric(metric)); - - verify(fixture.binding.loadContexts()).called(1); - final attributes = metric.attributes; - expect(attributes[SemanticAttributesConstants.osName]?.value, 'os1'); - expect(attributes[SemanticAttributesConstants.osVersion]?.value, - 'fixture-os-version'); - expect(attributes[SemanticAttributesConstants.deviceBrand]?.value, - 'fixture-brand'); - expect(attributes[SemanticAttributesConstants.deviceModel]?.value, - 'fixture-model'); - expect(attributes[SemanticAttributesConstants.deviceFamily]?.value, - 'fixture-family'); - }); - - test('with metrics disabled does not register callback', () async { - fixture.options.enableMetrics = false; - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 0); - }); - - test('close removes metric callback from lifecycle registry', () async { - fixture.options.enableMetrics = true; - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 1); - - integration.close(); - - expect( - fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessMetric], - isEmpty); - }); - - test('close removes log callback from lifecycle registry', () async { - fixture.options.enableLogs = true; - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], - isNotEmpty); - - integration.close(); - - expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], - isEmpty); - }); - - test('close removes both callbacks when both features enabled', () async { - fixture.options.enableMetrics = true; - fixture.options.enableLogs = true; - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 2); - - integration.close(); - - expect( - fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessMetric], - isEmpty); - expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], - isEmpty); - }); - - test('callback is not invoked after close', () async { - fixture.options.enableMetrics = true; - final integration = fixture.getSut(); - integration(fixture.hub, fixture.options); - - integration.close(); - - final metric = SentryCounterMetric( - timestamp: DateTime.now(), - name: 'random', - value: 1, - traceId: SentryId.newId()); - - await fixture.options.lifecycleRegistry - .dispatchCallback(OnProcessMetric(metric)); - - // loadContexts should not be called since callback was removed - verifyNever(fixture.binding.loadContexts()); - expect(metric.attributes, isEmpty); - }); -} - -class Fixture { - final hub = MockHub(); - final options = defaultTestOptions(); - final binding = MockSentryNativeBinding(); - - LoadContextsIntegration getSut( - {Map contexts = const { - 'integrations': ['NativeIntegration'], - 'package': {'sdk_name': 'native-package', 'version': '1.0'}, - 'contexts': { - 'device': { - 'name': 'Device1', - 'brand': 'fixture-brand', - 'model': 'fixture-model', - 'family': 'fixture-family', - }, - 'app': {'app_name': 'test-app'}, - 'os': {'name': 'os1', 'version': 'fixture-os-version'}, - 'gpu': {'name': 'gpu1'}, - 'browser': {'name': 'browser1'}, - 'runtime': {'name': 'RT1'}, - 'theme': 'material', - }, - 'user': { - 'id': '196E065A-AAF7-409A-9A6C-A81F40274CB9', - 'username': 'fixture-username', - 'email': 'fixture-email', - 'ip_address': 'fixture-ip_address', - 'data': {'key': 'value'}, - }, - 'tags': {'key-a': 'native', 'key-b': 'native'}, - 'extra': {'key-a': 'native', 'key-b': 'native'}, - 'dist': 'fixture-dist', - 'environment': 'fixture-environment', - 'fingerprint': ['fingerprint-a'], - 'level': 'error', - 'breadcrumbs': [ - { - 'timestamp': '1970-01-01T00:00:00.000Z', - 'message': 'native-crumb', - } - ] - }}) { - when(binding.loadContexts()).thenAnswer((_) async => contexts); - return LoadContextsIntegration(binding); - } -} diff --git a/packages/flutter/test/mocks.mocks.dart b/packages/flutter/test/mocks.mocks.dart index 9c0fd9755a..db45428c78 100644 --- a/packages/flutter/test/mocks.mocks.dart +++ b/packages/flutter/test/mocks.mocks.dart @@ -3,37 +3,39 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i11; -import 'dart:developer' as _i21; -import 'dart:typed_data' as _i17; +import 'dart:async' as _i12; +import 'dart:developer' as _i23; +import 'dart:typed_data' as _i19; import 'dart:ui' as _i6; import 'package:flutter/foundation.dart' as _i8; import 'package:flutter/gestures.dart' as _i7; import 'package:flutter/rendering.dart' as _i10; -import 'package:flutter/scheduler.dart' as _i20; +import 'package:flutter/scheduler.dart' as _i22; import 'package:flutter/services.dart' as _i4; +import 'package:flutter/src/widgets/_window.dart' as _i11; import 'package:flutter/src/widgets/binding.dart' as _i5; import 'package:flutter/widgets.dart' as _i9; -import 'package:flutter_test/flutter_test.dart' as _i12; +import 'package:flutter_test/flutter_test.dart' as _i13; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i14; -import 'package:sentry/src/profiling.dart' as _i15; +import 'package:mockito/src/dummies.dart' as _i15; +import 'package:sentry/src/profiling.dart' as _i16; import 'package:sentry/src/sentry_tracer.dart' as _i3; +import 'package:sentry/src/telemetry/metric/metric.dart' as _i17; import 'package:sentry_flutter/sentry_flutter.dart' as _i2; import 'package:sentry_flutter/src/frames_tracking/sentry_delayed_frames_tracker.dart' - as _i19; -import 'package:sentry_flutter/src/native/sentry_native_binding.dart' as _i16; + as _i21; +import 'package:sentry_flutter/src/native/sentry_native_binding.dart' as _i18; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart' - as _i23; -import 'package:sentry_flutter/src/navigation/time_to_full_display_tracker.dart' as _i25; +import 'package:sentry_flutter/src/navigation/time_to_full_display_tracker.dart' + as _i27; import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart' - as _i24; -import 'package:sentry_flutter/src/replay/replay_config.dart' as _i18; -import 'package:sentry_flutter/src/web/sentry_js_binding.dart' as _i22; + as _i26; +import 'package:sentry_flutter/src/replay/replay_config.dart' as _i20; +import 'package:sentry_flutter/src/web/sentry_js_binding.dart' as _i24; -import 'mocks.dart' as _i13; +import 'mocks.dart' as _i14; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -48,6 +50,7 @@ import 'mocks.dart' as _i13; // 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 { @@ -218,8 +221,8 @@ class _FakePointerSignalResolver_15 extends _i1.SmartFake ); } -class _FakeDuration_16 extends _i1.SmartFake implements Duration { - _FakeDuration_16( +class _FakeSamplingClock_16 extends _i1.SmartFake implements _i7.SamplingClock { + _FakeSamplingClock_16( Object parent, Invocation parentInvocation, ) : super( @@ -228,8 +231,8 @@ class _FakeDuration_16 extends _i1.SmartFake implements Duration { ); } -class _FakeSamplingClock_17 extends _i1.SmartFake implements _i7.SamplingClock { - _FakeSamplingClock_17( +class _FakeDuration_17 extends _i1.SmartFake implements Duration { + _FakeDuration_17( Object parent, Invocation parentInvocation, ) : super( @@ -238,9 +241,9 @@ class _FakeSamplingClock_17 extends _i1.SmartFake implements _i7.SamplingClock { ); } -class _FakeValueNotifier_18 extends _i1.SmartFake - implements _i8.ValueNotifier { - _FakeValueNotifier_18( +class _FakeHardwareKeyboard_18 extends _i1.SmartFake + implements _i4.HardwareKeyboard { + _FakeHardwareKeyboard_18( Object parent, Invocation parentInvocation, ) : super( @@ -249,9 +252,9 @@ class _FakeValueNotifier_18 extends _i1.SmartFake ); } -class _FakeHardwareKeyboard_19 extends _i1.SmartFake - implements _i4.HardwareKeyboard { - _FakeHardwareKeyboard_19( +class _FakeKeyEventManager_19 extends _i1.SmartFake + implements _i4.KeyEventManager { + _FakeKeyEventManager_19( Object parent, Invocation parentInvocation, ) : super( @@ -260,9 +263,9 @@ class _FakeHardwareKeyboard_19 extends _i1.SmartFake ); } -class _FakeKeyEventManager_20 extends _i1.SmartFake - implements _i4.KeyEventManager { - _FakeKeyEventManager_20( +class _FakeChannelBuffers_20 extends _i1.SmartFake + implements _i6.ChannelBuffers { + _FakeChannelBuffers_20( Object parent, Invocation parentInvocation, ) : super( @@ -271,9 +274,9 @@ class _FakeKeyEventManager_20 extends _i1.SmartFake ); } -class _FakeChannelBuffers_21 extends _i1.SmartFake - implements _i6.ChannelBuffers { - _FakeChannelBuffers_21( +class _FakeValueNotifier_21 extends _i1.SmartFake + implements _i8.ValueNotifier { + _FakeValueNotifier_21( Object parent, Invocation parentInvocation, ) : super( @@ -324,8 +327,18 @@ class _FakeAccessibilityFeatures_25 extends _i1.SmartFake ); } -class _FakeRenderView_26 extends _i1.SmartFake implements _i10.RenderView { - _FakeRenderView_26( +class _FakeMouseTracker_26 extends _i1.SmartFake implements _i10.MouseTracker { + _FakeMouseTracker_26( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRenderView_27 extends _i1.SmartFake implements _i10.RenderView { + _FakeRenderView_27( Object parent, Invocation parentInvocation, ) : super( @@ -338,19 +351,23 @@ class _FakeRenderView_26 extends _i1.SmartFake implements _i10.RenderView { super.toString(); } -class _FakeMouseTracker_27 extends _i1.SmartFake implements _i10.MouseTracker { - _FakeMouseTracker_27( +class _FakeFocusManager_28 extends _i1.SmartFake implements _i9.FocusManager { + _FakeFocusManager_28( Object parent, Invocation parentInvocation, ) : super( parent, parentInvocation, ); + + @override + String toString({_i4.DiagnosticLevel? minLevel = _i4.DiagnosticLevel.info}) => + super.toString(); } -class _FakePlatformMenuDelegate_28 extends _i1.SmartFake +class _FakePlatformMenuDelegate_29 extends _i1.SmartFake implements _i9.PlatformMenuDelegate { - _FakePlatformMenuDelegate_28( + _FakePlatformMenuDelegate_29( Object parent, Invocation parentInvocation, ) : super( @@ -359,22 +376,19 @@ class _FakePlatformMenuDelegate_28 extends _i1.SmartFake ); } -class _FakeFocusManager_29 extends _i1.SmartFake implements _i9.FocusManager { - _FakeFocusManager_29( +class _FakeWindowingOwner_30 extends _i1.SmartFake + implements _i11.WindowingOwner { + _FakeWindowingOwner_30( Object parent, Invocation parentInvocation, ) : super( parent, parentInvocation, ); - - @override - String toString({_i4.DiagnosticLevel? minLevel = _i4.DiagnosticLevel.info}) => - super.toString(); } -class _FakeFuture_30 extends _i1.SmartFake implements _i11.Future { - _FakeFuture_30( +class _FakeFuture_31 extends _i1.SmartFake implements _i12.Future { + _FakeFuture_31( Object parent, Invocation parentInvocation, ) : super( @@ -383,8 +397,8 @@ class _FakeFuture_30 extends _i1.SmartFake implements _i11.Future { ); } -class _FakeCodec_31 extends _i1.SmartFake implements _i6.Codec { - _FakeCodec_31( +class _FakeCodec_32 extends _i1.SmartFake implements _i6.Codec { + _FakeCodec_32( Object parent, Invocation parentInvocation, ) : super( @@ -393,9 +407,9 @@ class _FakeCodec_31 extends _i1.SmartFake implements _i6.Codec { ); } -class _FakeSemanticsHandle_32 extends _i1.SmartFake - implements _i12.SemanticsHandle { - _FakeSemanticsHandle_32( +class _FakeSemanticsHandle_33 extends _i1.SmartFake + implements _i13.SemanticsHandle { + _FakeSemanticsHandle_33( Object parent, Invocation parentInvocation, ) : super( @@ -404,9 +418,9 @@ class _FakeSemanticsHandle_32 extends _i1.SmartFake ); } -class _FakeSemanticsUpdateBuilder_33 extends _i1.SmartFake +class _FakeSemanticsUpdateBuilder_34 extends _i1.SmartFake implements _i6.SemanticsUpdateBuilder { - _FakeSemanticsUpdateBuilder_33( + _FakeSemanticsUpdateBuilder_34( Object parent, Invocation parentInvocation, ) : super( @@ -415,9 +429,9 @@ class _FakeSemanticsUpdateBuilder_33 extends _i1.SmartFake ); } -class _FakeViewConfiguration_34 extends _i1.SmartFake +class _FakeViewConfiguration_35 extends _i1.SmartFake implements _i10.ViewConfiguration { - _FakeViewConfiguration_34( + _FakeViewConfiguration_35( Object parent, Invocation parentInvocation, ) : super( @@ -426,8 +440,8 @@ class _FakeViewConfiguration_34 extends _i1.SmartFake ); } -class _FakeSceneBuilder_35 extends _i1.SmartFake implements _i6.SceneBuilder { - _FakeSceneBuilder_35( +class _FakeSceneBuilder_36 extends _i1.SmartFake implements _i6.SceneBuilder { + _FakeSceneBuilder_36( Object parent, Invocation parentInvocation, ) : super( @@ -436,9 +450,9 @@ class _FakeSceneBuilder_35 extends _i1.SmartFake implements _i6.SceneBuilder { ); } -class _FakePictureRecorder_36 extends _i1.SmartFake +class _FakePictureRecorder_37 extends _i1.SmartFake implements _i6.PictureRecorder { - _FakePictureRecorder_36( + _FakePictureRecorder_37( Object parent, Invocation parentInvocation, ) : super( @@ -447,8 +461,8 @@ class _FakePictureRecorder_36 extends _i1.SmartFake ); } -class _FakeCanvas_37 extends _i1.SmartFake implements _i6.Canvas { - _FakeCanvas_37( +class _FakeCanvas_38 extends _i1.SmartFake implements _i6.Canvas { + _FakeCanvas_38( Object parent, Invocation parentInvocation, ) : super( @@ -457,8 +471,8 @@ class _FakeCanvas_37 extends _i1.SmartFake implements _i6.Canvas { ); } -class _FakeWidget_38 extends _i1.SmartFake implements _i9.Widget { - _FakeWidget_38( +class _FakeWidget_39 extends _i1.SmartFake implements _i9.Widget { + _FakeWidget_39( Object parent, Invocation parentInvocation, ) : super( @@ -471,9 +485,9 @@ class _FakeWidget_38 extends _i1.SmartFake implements _i9.Widget { super.toString(); } -class _FakeSentryFlutterOptions_39 extends _i1.SmartFake +class _FakeSentryFlutterOptions_40 extends _i1.SmartFake implements _i2.SentryFlutterOptions { - _FakeSentryFlutterOptions_39( + _FakeSentryFlutterOptions_40( Object parent, Invocation parentInvocation, ) : super( @@ -482,8 +496,8 @@ class _FakeSentryFlutterOptions_39 extends _i1.SmartFake ); } -class _FakeSentryOptions_40 extends _i1.SmartFake implements _i2.SentryOptions { - _FakeSentryOptions_40( +class _FakeSentryOptions_41 extends _i1.SmartFake implements _i2.SentryOptions { + _FakeSentryOptions_41( Object parent, Invocation parentInvocation, ) : super( @@ -492,8 +506,8 @@ class _FakeSentryOptions_40 extends _i1.SmartFake implements _i2.SentryOptions { ); } -class _FakeScope_41 extends _i1.SmartFake implements _i2.Scope { - _FakeScope_41( +class _FakeScope_42 extends _i1.SmartFake implements _i2.Scope { + _FakeScope_42( Object parent, Invocation parentInvocation, ) : super( @@ -502,8 +516,8 @@ class _FakeScope_41 extends _i1.SmartFake implements _i2.Scope { ); } -class _FakeHub_42 extends _i1.SmartFake implements _i2.Hub { - _FakeHub_42( +class _FakeHub_43 extends _i1.SmartFake implements _i2.Hub { + _FakeHub_43( Object parent, Invocation parentInvocation, ) : super( @@ -515,13 +529,13 @@ class _FakeHub_42 extends _i1.SmartFake implements _i2.Hub { /// A class which mocks [Callbacks]. /// /// See the documentation for Mockito's code generation for more information. -class MockCallbacks extends _i1.Mock implements _i13.Callbacks { +class MockCallbacks extends _i1.Mock implements _i14.Callbacks { MockCallbacks() { _i1.throwOnMissingStub(this); } @override - _i11.Future? methodCallHandler( + _i12.Future? methodCallHandler( String? method, [ dynamic arguments, ]) => @@ -531,7 +545,7 @@ class MockCallbacks extends _i1.Mock implements _i13.Callbacks { method, arguments, ], - )) as _i11.Future?); + )) as _i12.Future?); } /// A class which mocks [Transport]. @@ -543,14 +557,14 @@ class MockTransport extends _i1.Mock implements _i2.Transport { } @override - _i11.Future<_i2.SentryId?> send(_i2.SentryEnvelope? envelope) => + _i12.Future<_i2.SentryId?> send(_i2.SentryEnvelope? envelope) => (super.noSuchMethod( Invocation.method( #send, [envelope], ), - returnValue: _i11.Future<_i2.SentryId?>.value(), - ) as _i11.Future<_i2.SentryId?>); + returnValue: _i12.Future<_i2.SentryId?>.value(), + ) as _i12.Future<_i2.SentryId?>); } /// A class which mocks [SentryTracer]. @@ -564,12 +578,18 @@ class MockSentryTracer extends _i1.Mock implements _i3.SentryTracer { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: _i14.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#name), ), ) as String); + @override + Map get measurements => (super.noSuchMethod( + Invocation.getter(#measurements), + returnValue: {}, + ) as Map); + @override _i2.SentryTransactionNameSource get transactionNameSource => (super.noSuchMethod( @@ -577,12 +597,6 @@ class MockSentryTracer extends _i1.Mock implements _i3.SentryTracer { returnValue: _i2.SentryTransactionNameSource.custom, ) as _i2.SentryTransactionNameSource); - @override - Map get measurements => (super.noSuchMethod( - Invocation.getter(#measurements), - returnValue: {}, - ) as Map); - @override _i2.SentrySpanContext get context => (super.noSuchMethod( Invocation.getter(#context), @@ -626,39 +640,38 @@ class MockSentryTracer extends _i1.Mock implements _i3.SentryTracer { ) as Map); @override - set name(String? _name) => super.noSuchMethod( + set name(String? value) => super.noSuchMethod( Invocation.setter( #name, - _name, + value, ), returnValueForMissingStub: null, ); @override - set transactionNameSource( - _i2.SentryTransactionNameSource? _transactionNameSource) => + set transactionNameSource(_i2.SentryTransactionNameSource? value) => super.noSuchMethod( Invocation.setter( #transactionNameSource, - _transactionNameSource, + value, ), returnValueForMissingStub: null, ); @override - set profiler(_i15.SentryProfiler? _profiler) => super.noSuchMethod( + set profiler(_i16.SentryProfiler? value) => super.noSuchMethod( Invocation.setter( #profiler, - _profiler, + value, ), returnValueForMissingStub: null, ); @override - set profileInfo(_i15.SentryProfileInfo? _profileInfo) => super.noSuchMethod( + set profileInfo(_i16.SentryProfileInfo? value) => super.noSuchMethod( Invocation.setter( #profileInfo, - _profileInfo, + value, ), returnValueForMissingStub: null, ); @@ -691,7 +704,7 @@ class MockSentryTracer extends _i1.Mock implements _i3.SentryTracer { ); @override - _i11.Future finish({ + _i12.Future finish({ _i2.SpanStatus? status, DateTime? endTimestamp, _i2.Hint? hint, @@ -706,9 +719,9 @@ class MockSentryTracer extends _i1.Mock implements _i3.SentryTracer { #hint: hint, }, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override void removeData(String? key) => super.noSuchMethod( @@ -935,39 +948,38 @@ class MockSentryTransaction extends _i1.Mock implements _i2.SentryTransaction { ) as bool); @override - set startTimestamp(DateTime? _startTimestamp) => super.noSuchMethod( + set startTimestamp(DateTime? value) => super.noSuchMethod( Invocation.setter( #startTimestamp, - _startTimestamp, + value, ), returnValueForMissingStub: null, ); @override - set spans(List<_i2.SentrySpan>? _spans) => super.noSuchMethod( + set spans(List<_i2.SentrySpan>? value) => super.noSuchMethod( Invocation.setter( #spans, - _spans, + value, ), returnValueForMissingStub: null, ); @override - set measurements(Map? _measurements) => + set measurements(Map? value) => super.noSuchMethod( Invocation.setter( #measurements, - _measurements, + value, ), returnValueForMissingStub: null, ); @override - set transactionInfo(_i2.SentryTransactionInfo? _transactionInfo) => - super.noSuchMethod( + set transactionInfo(_i2.SentryTransactionInfo? value) => super.noSuchMethod( Invocation.setter( #transactionInfo, - _transactionInfo, + value, ), returnValueForMissingStub: null, ); @@ -991,226 +1003,226 @@ class MockSentryTransaction extends _i1.Mock implements _i2.SentryTransaction { ) as _i2.Contexts); @override - set eventId(_i2.SentryId? _eventId) => super.noSuchMethod( + set eventId(_i2.SentryId? value) => super.noSuchMethod( Invocation.setter( #eventId, - _eventId, + value, ), returnValueForMissingStub: null, ); @override - set timestamp(DateTime? _timestamp) => super.noSuchMethod( + set timestamp(DateTime? value) => super.noSuchMethod( Invocation.setter( #timestamp, - _timestamp, + value, ), returnValueForMissingStub: null, ); @override - set platform(String? _platform) => super.noSuchMethod( + set platform(String? value) => super.noSuchMethod( Invocation.setter( #platform, - _platform, + value, ), returnValueForMissingStub: null, ); @override - set logger(String? _logger) => super.noSuchMethod( + set logger(String? value) => super.noSuchMethod( Invocation.setter( #logger, - _logger, + value, ), returnValueForMissingStub: null, ); @override - set serverName(String? _serverName) => super.noSuchMethod( + set serverName(String? value) => super.noSuchMethod( Invocation.setter( #serverName, - _serverName, + value, ), returnValueForMissingStub: null, ); @override - set release(String? _release) => super.noSuchMethod( + set release(String? value) => super.noSuchMethod( Invocation.setter( #release, - _release, + value, ), returnValueForMissingStub: null, ); @override - set dist(String? _dist) => super.noSuchMethod( + set dist(String? value) => super.noSuchMethod( Invocation.setter( #dist, - _dist, + value, ), returnValueForMissingStub: null, ); @override - set environment(String? _environment) => super.noSuchMethod( + set environment(String? value) => super.noSuchMethod( Invocation.setter( #environment, - _environment, + value, ), returnValueForMissingStub: null, ); @override - set modules(Map? _modules) => super.noSuchMethod( + set modules(Map? value) => super.noSuchMethod( Invocation.setter( #modules, - _modules, + value, ), returnValueForMissingStub: null, ); @override - set message(_i2.SentryMessage? _message) => super.noSuchMethod( + set message(_i2.SentryMessage? value) => super.noSuchMethod( Invocation.setter( #message, - _message, + value, ), returnValueForMissingStub: null, ); @override - set exceptions(List<_i2.SentryException>? _exceptions) => super.noSuchMethod( + set exceptions(List<_i2.SentryException>? value) => super.noSuchMethod( Invocation.setter( #exceptions, - _exceptions, + value, ), returnValueForMissingStub: null, ); @override - set threads(List<_i2.SentryThread>? _threads) => super.noSuchMethod( + set threads(List<_i2.SentryThread>? value) => super.noSuchMethod( Invocation.setter( #threads, - _threads, + value, ), returnValueForMissingStub: null, ); @override - set transaction(String? _transaction) => super.noSuchMethod( + set transaction(String? value) => super.noSuchMethod( Invocation.setter( #transaction, - _transaction, + value, ), returnValueForMissingStub: null, ); @override - set level(_i2.SentryLevel? _level) => super.noSuchMethod( + set level(_i2.SentryLevel? value) => super.noSuchMethod( Invocation.setter( #level, - _level, + value, ), returnValueForMissingStub: null, ); @override - set culprit(String? _culprit) => super.noSuchMethod( + set culprit(String? value) => super.noSuchMethod( Invocation.setter( #culprit, - _culprit, + value, ), returnValueForMissingStub: null, ); @override - set tags(Map? _tags) => super.noSuchMethod( + set tags(Map? value) => super.noSuchMethod( Invocation.setter( #tags, - _tags, + value, ), returnValueForMissingStub: null, ); @override - set extra(Map? _extra) => super.noSuchMethod( + set extra(Map? value) => super.noSuchMethod( Invocation.setter( #extra, - _extra, + value, ), returnValueForMissingStub: null, ); @override - set breadcrumbs(List<_i2.Breadcrumb>? _breadcrumbs) => super.noSuchMethod( + set breadcrumbs(List<_i2.Breadcrumb>? value) => super.noSuchMethod( Invocation.setter( #breadcrumbs, - _breadcrumbs, + value, ), returnValueForMissingStub: null, ); @override - set user(_i2.SentryUser? _user) => super.noSuchMethod( + set user(_i2.SentryUser? value) => super.noSuchMethod( Invocation.setter( #user, - _user, + value, ), returnValueForMissingStub: null, ); @override - set contexts(_i2.Contexts? _contexts) => super.noSuchMethod( + set contexts(_i2.Contexts? value) => super.noSuchMethod( Invocation.setter( #contexts, - _contexts, + value, ), returnValueForMissingStub: null, ); @override - set fingerprint(List? _fingerprint) => super.noSuchMethod( + set fingerprint(List? value) => super.noSuchMethod( Invocation.setter( #fingerprint, - _fingerprint, + value, ), returnValueForMissingStub: null, ); @override - set sdk(_i2.SdkVersion? _sdk) => super.noSuchMethod( + set sdk(_i2.SdkVersion? value) => super.noSuchMethod( Invocation.setter( #sdk, - _sdk, + value, ), returnValueForMissingStub: null, ); @override - set request(_i2.SentryRequest? _request) => super.noSuchMethod( + set request(_i2.SentryRequest? value) => super.noSuchMethod( Invocation.setter( #request, - _request, + value, ), returnValueForMissingStub: null, ); @override - set debugMeta(_i2.DebugMeta? _debugMeta) => super.noSuchMethod( + set debugMeta(_i2.DebugMeta? value) => super.noSuchMethod( Invocation.setter( #debugMeta, - _debugMeta, + value, ), returnValueForMissingStub: null, ); @override - set type(String? _type) => super.noSuchMethod( + set type(String? value) => super.noSuchMethod( Invocation.setter( #type, - _type, + value, ), returnValueForMissingStub: null, ); @@ -1417,7 +1429,7 @@ class MockSentrySpan extends _i1.Mock implements _i2.SentrySpan { ); @override - _i11.Future finish({ + _i12.Future finish({ _i2.SpanStatus? status, DateTime? endTimestamp, _i2.Hint? hint, @@ -1432,9 +1444,9 @@ class MockSentrySpan extends _i1.Mock implements _i2.SentrySpan { #hint: hint, }, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override void removeData(String? key) => super.noSuchMethod( @@ -1575,7 +1587,7 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { } @override - _i11.Future<_i2.SentryId> captureEvent( + _i12.Future<_i2.SentryId> captureEvent( _i2.SentryEvent? event, { _i2.Scope? scope, dynamic stackTrace, @@ -1591,7 +1603,7 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { #hint: hint, }, ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureEvent, @@ -1603,10 +1615,10 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { }, ), )), - ) as _i11.Future<_i2.SentryId>); + ) as _i12.Future<_i2.SentryId>); @override - _i11.Future<_i2.SentryId> captureException( + _i12.Future<_i2.SentryId> captureException( dynamic throwable, { dynamic stackTrace, _i2.Scope? scope, @@ -1622,7 +1634,7 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { #hint: hint, }, ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureException, @@ -1634,10 +1646,10 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { }, ), )), - ) as _i11.Future<_i2.SentryId>); + ) as _i12.Future<_i2.SentryId>); @override - _i11.Future<_i2.SentryId> captureMessage( + _i12.Future<_i2.SentryId> captureMessage( String? formatted, { _i2.SentryLevel? level, String? template, @@ -1657,7 +1669,7 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { #hint: hint, }, ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureMessage, @@ -1671,10 +1683,10 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { }, ), )), - ) as _i11.Future<_i2.SentryId>); + ) as _i12.Future<_i2.SentryId>); @override - _i11.Future<_i2.SentryId> captureTransaction( + _i12.Future<_i2.SentryId> captureTransaction( _i2.SentryTransaction? transaction, { _i2.Scope? scope, _i2.SentryTraceContextHeader? traceContext, @@ -1690,7 +1702,7 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { #hint: hint, }, ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureTransaction, @@ -1702,20 +1714,20 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { }, ), )), - ) as _i11.Future<_i2.SentryId>); + ) as _i12.Future<_i2.SentryId>); @override - _i11.Future<_i2.SentryId?> captureEnvelope(_i2.SentryEnvelope? envelope) => + _i12.Future<_i2.SentryId?> captureEnvelope(_i2.SentryEnvelope? envelope) => (super.noSuchMethod( Invocation.method( #captureEnvelope, [envelope], ), - returnValue: _i11.Future<_i2.SentryId?>.value(), - ) as _i11.Future<_i2.SentryId?>); + returnValue: _i12.Future<_i2.SentryId?>.value(), + ) as _i12.Future<_i2.SentryId?>); @override - _i11.Future<_i2.SentryId> captureFeedback( + _i12.Future<_i2.SentryId> captureFeedback( _i2.SentryFeedback? feedback, { _i2.Scope? scope, _i2.Hint? hint, @@ -1729,7 +1741,7 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { #hint: hint, }, ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureFeedback, @@ -1740,10 +1752,10 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { }, ), )), - ) as _i11.Future<_i2.SentryId>); + ) as _i12.Future<_i2.SentryId>); @override - _i11.FutureOr captureLog( + _i12.FutureOr captureLog( _i2.SentryLog? log, { _i2.Scope? scope, }) => @@ -1751,16 +1763,22 @@ class MockSentryClient extends _i1.Mock implements _i2.SentryClient { #captureLog, [log], {#scope: scope}, - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - void close() => super.noSuchMethod( + _i12.Future captureMetric( + _i17.SentryMetric? metric, { + _i2.Scope? scope, + }) => + (super.noSuchMethod( Invocation.method( - #close, - [], + #captureMetric, + [metric], + {#scope: scope}, ), - returnValueForMissingStub: null, - ); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); } /// A class which mocks [MethodChannel]. @@ -1774,7 +1792,7 @@ class MockMethodChannel extends _i1.Mock implements _i4.MethodChannel { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: _i14.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#name), ), @@ -1799,7 +1817,7 @@ class MockMethodChannel extends _i1.Mock implements _i4.MethodChannel { ) as _i4.BinaryMessenger); @override - _i11.Future invokeMethod( + _i12.Future invokeMethod( String? method, [ dynamic arguments, ]) => @@ -1811,11 +1829,11 @@ class MockMethodChannel extends _i1.Mock implements _i4.MethodChannel { arguments, ], ), - returnValue: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + ) as _i12.Future); @override - _i11.Future?> invokeListMethod( + _i12.Future?> invokeListMethod( String? method, [ dynamic arguments, ]) => @@ -1827,11 +1845,11 @@ class MockMethodChannel extends _i1.Mock implements _i4.MethodChannel { arguments, ], ), - returnValue: _i11.Future?>.value(), - ) as _i11.Future?>); + returnValue: _i12.Future?>.value(), + ) as _i12.Future?>); @override - _i11.Future?> invokeMapMethod( + _i12.Future?> invokeMapMethod( String? method, [ dynamic arguments, ]) => @@ -1843,12 +1861,12 @@ class MockMethodChannel extends _i1.Mock implements _i4.MethodChannel { arguments, ], ), - returnValue: _i11.Future?>.value(), - ) as _i11.Future?>); + returnValue: _i12.Future?>.value(), + ) as _i12.Future?>); @override void setMethodCallHandler( - _i11.Future Function(_i4.MethodCall)? handler) => + _i12.Future Function(_i4.MethodCall)? handler) => super.noSuchMethod( Invocation.method( #setMethodCallHandler, @@ -1862,7 +1880,7 @@ class MockMethodChannel extends _i1.Mock implements _i4.MethodChannel { /// /// See the documentation for Mockito's code generation for more information. class MockSentryNativeBinding extends _i1.Mock - implements _i16.SentryNativeBinding { + implements _i18.SentryNativeBinding { MockSentryNativeBinding() { _i1.throwOnMissingStub(this); } @@ -1886,15 +1904,15 @@ class MockSentryNativeBinding extends _i1.Mock ) as bool); @override - _i11.FutureOr init(_i2.Hub? hub) => + _i12.FutureOr init(_i2.Hub? hub) => (super.noSuchMethod(Invocation.method( #init, [hub], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr captureEnvelope( - _i17.Uint8List? envelopeData, + _i12.FutureOr captureEnvelope( + _i19.Uint8List? envelopeData, bool? containsUnhandledException, ) => (super.noSuchMethod(Invocation.method( @@ -1903,24 +1921,24 @@ class MockSentryNativeBinding extends _i1.Mock envelopeData, containsUnhandledException, ], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr captureStructuredEnvelope(_i2.SentryEnvelope? envelope) => + _i12.FutureOr captureStructuredEnvelope(_i2.SentryEnvelope? envelope) => (super.noSuchMethod(Invocation.method( #captureStructuredEnvelope, [envelope], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr addBreadcrumb(_i2.Breadcrumb? breadcrumb) => + _i12.FutureOr addBreadcrumb(_i2.Breadcrumb? breadcrumb) => (super.noSuchMethod(Invocation.method( #addBreadcrumb, [breadcrumb], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr setContexts( + _i12.FutureOr setContexts( String? key, dynamic value, ) => @@ -1930,17 +1948,17 @@ class MockSentryNativeBinding extends _i1.Mock key, value, ], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr removeContexts(String? key) => + _i12.FutureOr removeContexts(String? key) => (super.noSuchMethod(Invocation.method( #removeContexts, [key], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr setExtra( + _i12.FutureOr setExtra( String? key, dynamic value, ) => @@ -1950,17 +1968,17 @@ class MockSentryNativeBinding extends _i1.Mock key, value, ], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr removeExtra(String? key) => + _i12.FutureOr removeExtra(String? key) => (super.noSuchMethod(Invocation.method( #removeExtra, [key], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr setTag( + _i12.FutureOr setTag( String? key, String? value, ) => @@ -1970,14 +1988,14 @@ class MockSentryNativeBinding extends _i1.Mock key, value, ], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr removeTag(String? key) => + _i12.FutureOr removeTag(String? key) => (super.noSuchMethod(Invocation.method( #removeTag, [key], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override int? startProfiler(_i2.SentryId? traceId) => @@ -1987,14 +2005,14 @@ class MockSentryNativeBinding extends _i1.Mock )) as int?); @override - _i11.FutureOr discardProfiler(_i2.SentryId? traceId) => + _i12.FutureOr discardProfiler(_i2.SentryId? traceId) => (super.noSuchMethod(Invocation.method( #discardProfiler, [traceId], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr?> collectProfile( + _i12.FutureOr?> collectProfile( _i2.SentryId? traceId, int? startTimeNs, int? endTimeNs, @@ -2006,64 +2024,64 @@ class MockSentryNativeBinding extends _i1.Mock startTimeNs, endTimeNs, ], - )) as _i11.FutureOr?>); + )) as _i12.FutureOr?>); @override - _i11.FutureOr?> loadDebugImages( + _i12.FutureOr?> loadDebugImages( _i2.SentryStackTrace? stackTrace) => (super.noSuchMethod(Invocation.method( #loadDebugImages, [stackTrace], - )) as _i11.FutureOr?>); + )) as _i12.FutureOr?>); @override - _i11.FutureOr setReplayConfig(_i18.ReplayConfig? config) => + _i12.FutureOr setReplayConfig(_i20.ReplayConfig? config) => (super.noSuchMethod(Invocation.method( #setReplayConfig, [config], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override - _i11.FutureOr<_i2.SentryId> captureReplay() => (super.noSuchMethod( + _i12.FutureOr<_i2.SentryId> captureReplay() => (super.noSuchMethod( Invocation.method( #captureReplay, [], ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureReplay, [], ), )), - ) as _i11.FutureOr<_i2.SentryId>); + ) as _i12.FutureOr<_i2.SentryId>); @override - _i11.FutureOr startSession({bool? ignoreDuration = false}) => + _i12.FutureOr startSession({bool? ignoreDuration = false}) => (super.noSuchMethod(Invocation.method( #startSession, [], {#ignoreDuration: ignoreDuration}, - )) as _i11.FutureOr); + )) as _i12.FutureOr); } /// A class which mocks [SentryDelayedFramesTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockSentryDelayedFramesTracker extends _i1.Mock - implements _i19.SentryDelayedFramesTracker { + implements _i21.SentryDelayedFramesTracker { MockSentryDelayedFramesTracker() { _i1.throwOnMissingStub(this); } @override - List<_i19.SentryFrameTiming> get delayedFrames => (super.noSuchMethod( + List<_i21.SentryFrameTiming> get delayedFrames => (super.noSuchMethod( Invocation.getter(#delayedFrames), - returnValue: <_i19.SentryFrameTiming>[], - ) as List<_i19.SentryFrameTiming>); + returnValue: <_i21.SentryFrameTiming>[], + ) as List<_i21.SentryFrameTiming>); @override - List<_i19.SentryFrameTiming> getFramesIntersecting({ + List<_i21.SentryFrameTiming> getFramesIntersecting({ required DateTime? startTimestamp, required DateTime? endTimestamp, }) => @@ -2076,8 +2094,8 @@ class MockSentryDelayedFramesTracker extends _i1.Mock #endTimestamp: endTimestamp, }, ), - returnValue: <_i19.SentryFrameTiming>[], - ) as List<_i19.SentryFrameTiming>); + returnValue: <_i21.SentryFrameTiming>[], + ) as List<_i21.SentryFrameTiming>); @override void addDelayedFrame( @@ -2106,7 +2124,7 @@ class MockSentryDelayedFramesTracker extends _i1.Mock ); @override - _i19.SpanFrameMetrics? getFrameMetrics({ + _i21.SpanFrameMetrics? getFrameMetrics({ required DateTime? spanStartTimestamp, required DateTime? spanEndTimestamp, }) => @@ -2117,7 +2135,7 @@ class MockSentryDelayedFramesTracker extends _i1.Mock #spanStartTimestamp: spanStartTimestamp, #spanEndTimestamp: spanEndTimestamp, }, - )) as _i19.SpanFrameMetrics?); + )) as _i21.SpanFrameMetrics?); @override void clear() => super.noSuchMethod( @@ -2213,6 +2231,15 @@ class MockWidgetsFlutterBinding extends _i1.Mock ), ) as _i7.PointerSignalResolver); + @override + _i7.SamplingClock get samplingClock => (super.noSuchMethod( + Invocation.getter(#samplingClock), + returnValue: _FakeSamplingClock_16( + this, + Invocation.getter(#samplingClock), + ), + ) as _i7.SamplingClock); + @override bool get resamplingEnabled => (super.noSuchMethod( Invocation.getter(#resamplingEnabled), @@ -2222,48 +2249,39 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override Duration get samplingOffset => (super.noSuchMethod( Invocation.getter(#samplingOffset), - returnValue: _FakeDuration_16( + returnValue: _FakeDuration_17( this, Invocation.getter(#samplingOffset), ), ) as Duration); @override - _i7.SamplingClock get samplingClock => (super.noSuchMethod( - Invocation.getter(#samplingClock), - returnValue: _FakeSamplingClock_17( - this, - Invocation.getter(#samplingClock), - ), - ) as _i7.SamplingClock); - - @override - set resamplingEnabled(bool? _resamplingEnabled) => super.noSuchMethod( + set resamplingEnabled(bool? value) => super.noSuchMethod( Invocation.setter( #resamplingEnabled, - _resamplingEnabled, + value, ), returnValueForMissingStub: null, ); @override - set samplingOffset(Duration? _samplingOffset) => super.noSuchMethod( + set samplingOffset(Duration? value) => super.noSuchMethod( Invocation.setter( #samplingOffset, - _samplingOffset, + value, ), returnValueForMissingStub: null, ); @override - _i20.SchedulingStrategy get schedulingStrategy => (super.noSuchMethod( + _i22.SchedulingStrategy get schedulingStrategy => (super.noSuchMethod( Invocation.getter(#schedulingStrategy), returnValue: ({ required int priority, - required _i20.SchedulerBinding scheduler, + required _i22.SchedulerBinding scheduler, }) => false, - ) as _i20.SchedulingStrategy); + ) as _i22.SchedulingStrategy); @override int get transientCallbackCount => (super.noSuchMethod( @@ -2272,10 +2290,10 @@ class MockWidgetsFlutterBinding extends _i1.Mock ) as int); @override - _i11.Future get endOfFrame => (super.noSuchMethod( + _i12.Future get endOfFrame => (super.noSuchMethod( Invocation.getter(#endOfFrame), - returnValue: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + ) as _i12.Future); @override bool get hasScheduledFrame => (super.noSuchMethod( @@ -2284,10 +2302,10 @@ class MockWidgetsFlutterBinding extends _i1.Mock ) as bool); @override - _i20.SchedulerPhase get schedulerPhase => (super.noSuchMethod( + _i22.SchedulerPhase get schedulerPhase => (super.noSuchMethod( Invocation.getter(#schedulerPhase), - returnValue: _i20.SchedulerPhase.idle, - ) as _i20.SchedulerPhase); + returnValue: _i22.SchedulerPhase.idle, + ) as _i22.SchedulerPhase); @override bool get framesEnabled => (super.noSuchMethod( @@ -2298,7 +2316,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override Duration get currentFrameTimeStamp => (super.noSuchMethod( Invocation.getter(#currentFrameTimeStamp), - returnValue: _FakeDuration_16( + returnValue: _FakeDuration_17( this, Invocation.getter(#currentFrameTimeStamp), ), @@ -2307,35 +2325,25 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override Duration get currentSystemFrameTimeStamp => (super.noSuchMethod( Invocation.getter(#currentSystemFrameTimeStamp), - returnValue: _FakeDuration_16( + returnValue: _FakeDuration_17( this, Invocation.getter(#currentSystemFrameTimeStamp), ), ) as Duration); @override - set schedulingStrategy(_i20.SchedulingStrategy? _schedulingStrategy) => - super.noSuchMethod( + set schedulingStrategy(_i22.SchedulingStrategy? value) => super.noSuchMethod( Invocation.setter( #schedulingStrategy, - _schedulingStrategy, + value, ), returnValueForMissingStub: null, ); - @override - _i8.ValueNotifier get accessibilityFocus => (super.noSuchMethod( - Invocation.getter(#accessibilityFocus), - returnValue: _FakeValueNotifier_18( - this, - Invocation.getter(#accessibilityFocus), - ), - ) as _i8.ValueNotifier); - @override _i4.HardwareKeyboard get keyboard => (super.noSuchMethod( Invocation.getter(#keyboard), - returnValue: _FakeHardwareKeyboard_19( + returnValue: _FakeHardwareKeyboard_18( this, Invocation.getter(#keyboard), ), @@ -2344,7 +2352,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override _i4.KeyEventManager get keyEventManager => (super.noSuchMethod( Invocation.getter(#keyEventManager), - returnValue: _FakeKeyEventManager_20( + returnValue: _FakeKeyEventManager_19( this, Invocation.getter(#keyEventManager), ), @@ -2362,12 +2370,21 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override _i6.ChannelBuffers get channelBuffers => (super.noSuchMethod( Invocation.getter(#channelBuffers), - returnValue: _FakeChannelBuffers_21( + returnValue: _FakeChannelBuffers_20( this, Invocation.getter(#channelBuffers), ), ) as _i6.ChannelBuffers); + @override + _i8.ValueNotifier get accessibilityFocus => (super.noSuchMethod( + Invocation.getter(#accessibilityFocus), + returnValue: _FakeValueNotifier_21( + this, + Invocation.getter(#accessibilityFocus), + ), + ) as _i8.ValueNotifier); + @override _i4.RestorationManager get restorationManager => (super.noSuchMethod( Invocation.getter(#restorationManager), @@ -2422,10 +2439,19 @@ class MockWidgetsFlutterBinding extends _i1.Mock returnValue: false, ) as bool); + @override + _i10.MouseTracker get mouseTracker => (super.noSuchMethod( + Invocation.getter(#mouseTracker), + returnValue: _FakeMouseTracker_26( + this, + Invocation.getter(#mouseTracker), + ), + ) as _i10.MouseTracker); + @override _i10.PipelineOwner get pipelineOwner => (super.noSuchMethod( Invocation.getter(#pipelineOwner), - returnValue: _i14.dummyValue<_i10.PipelineOwner>( + returnValue: _i15.dummyValue<_i10.PipelineOwner>( this, Invocation.getter(#pipelineOwner), ), @@ -2434,25 +2460,16 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override _i10.RenderView get renderView => (super.noSuchMethod( Invocation.getter(#renderView), - returnValue: _FakeRenderView_26( + returnValue: _FakeRenderView_27( this, Invocation.getter(#renderView), ), ) as _i10.RenderView); - @override - _i10.MouseTracker get mouseTracker => (super.noSuchMethod( - Invocation.getter(#mouseTracker), - returnValue: _FakeMouseTracker_27( - this, - Invocation.getter(#mouseTracker), - ), - ) as _i10.MouseTracker); - @override _i10.PipelineOwner get rootPipelineOwner => (super.noSuchMethod( Invocation.getter(#rootPipelineOwner), - returnValue: _i14.dummyValue<_i10.PipelineOwner>( + returnValue: _i15.dummyValue<_i10.PipelineOwner>( this, Invocation.getter(#rootPipelineOwner), ), @@ -2470,21 +2487,6 @@ class MockWidgetsFlutterBinding extends _i1.Mock returnValue: false, ) as bool); - @override - _i9.PlatformMenuDelegate get platformMenuDelegate => (super.noSuchMethod( - Invocation.getter(#platformMenuDelegate), - returnValue: _FakePlatformMenuDelegate_28( - this, - Invocation.getter(#platformMenuDelegate), - ), - ) as _i9.PlatformMenuDelegate); - - @override - bool get debugBuildingDirtyElements => (super.noSuchMethod( - Invocation.getter(#debugBuildingDirtyElements), - returnValue: false, - ) as bool); - @override bool get debugShowWidgetInspectorOverride => (super.noSuchMethod( Invocation.getter(#debugShowWidgetInspectorOverride), @@ -2495,7 +2497,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock _i8.ValueNotifier get debugShowWidgetInspectorOverrideNotifier => (super.noSuchMethod( Invocation.getter(#debugShowWidgetInspectorOverrideNotifier), - returnValue: _FakeValueNotifier_18( + returnValue: _FakeValueNotifier_21( this, Invocation.getter(#debugShowWidgetInspectorOverrideNotifier), ), @@ -2505,21 +2507,36 @@ class MockWidgetsFlutterBinding extends _i1.Mock _i8.ValueNotifier get debugWidgetInspectorSelectionOnTapEnabled => (super.noSuchMethod( Invocation.getter(#debugWidgetInspectorSelectionOnTapEnabled), - returnValue: _FakeValueNotifier_18( + returnValue: _FakeValueNotifier_21( this, Invocation.getter(#debugWidgetInspectorSelectionOnTapEnabled), ), ) as _i8.ValueNotifier); + @override + bool get debugExcludeRootWidgetInspector => (super.noSuchMethod( + Invocation.getter(#debugExcludeRootWidgetInspector), + returnValue: false, + ) as bool); + @override _i9.FocusManager get focusManager => (super.noSuchMethod( Invocation.getter(#focusManager), - returnValue: _FakeFocusManager_29( + returnValue: _FakeFocusManager_28( this, Invocation.getter(#focusManager), ), ) as _i9.FocusManager); + @override + _i9.PlatformMenuDelegate get platformMenuDelegate => (super.noSuchMethod( + Invocation.getter(#platformMenuDelegate), + returnValue: _FakePlatformMenuDelegate_29( + this, + Invocation.getter(#platformMenuDelegate), + ), + ) as _i9.PlatformMenuDelegate); + @override bool get firstFrameRasterized => (super.noSuchMethod( Invocation.getter(#firstFrameRasterized), @@ -2527,10 +2544,10 @@ class MockWidgetsFlutterBinding extends _i1.Mock ) as bool); @override - _i11.Future get waitUntilFirstFrameRasterized => (super.noSuchMethod( + _i12.Future get waitUntilFirstFrameRasterized => (super.noSuchMethod( Invocation.getter(#waitUntilFirstFrameRasterized), - returnValue: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + ) as _i12.Future); @override bool get debugDidSendFirstFrameEvent => (super.noSuchMethod( @@ -2538,6 +2555,12 @@ class MockWidgetsFlutterBinding extends _i1.Mock returnValue: false, ) as bool); + @override + bool get debugBuildingDirtyElements => (super.noSuchMethod( + Invocation.getter(#debugBuildingDirtyElements), + returnValue: false, + ) as bool); + @override bool get isRootWidgetAttached => (super.noSuchMethod( Invocation.getter(#isRootWidgetAttached), @@ -2545,30 +2568,56 @@ class MockWidgetsFlutterBinding extends _i1.Mock ) as bool); @override - set platformMenuDelegate(_i9.PlatformMenuDelegate? _platformMenuDelegate) => + _i11.WindowingOwner get windowingOwner => (super.noSuchMethod( + Invocation.getter(#windowingOwner), + returnValue: _FakeWindowingOwner_30( + this, + Invocation.getter(#windowingOwner), + ), + ) as _i11.WindowingOwner); + + @override + set debugShowWidgetInspectorOverride(bool? value) => super.noSuchMethod( + Invocation.setter( + #debugShowWidgetInspectorOverride, + value, + ), + returnValueForMissingStub: null, + ); + + @override + set debugExcludeRootWidgetInspector(bool? value) => super.noSuchMethod( + Invocation.setter( + #debugExcludeRootWidgetInspector, + value, + ), + returnValueForMissingStub: null, + ); + + @override + set platformMenuDelegate(_i9.PlatformMenuDelegate? value) => super.noSuchMethod( Invocation.setter( #platformMenuDelegate, - _platformMenuDelegate, + value, ), returnValueForMissingStub: null, ); @override - set debugBuildingDirtyElements(bool? _debugBuildingDirtyElements) => - super.noSuchMethod( + set debugBuildingDirtyElements(bool? value) => super.noSuchMethod( Invocation.setter( #debugBuildingDirtyElements, - _debugBuildingDirtyElements, + value, ), returnValueForMissingStub: null, ); @override - set debugShowWidgetInspectorOverride(bool? value) => super.noSuchMethod( + set windowingOwner(_i11.WindowingOwner? owner) => super.noSuchMethod( Invocation.setter( - #debugShowWidgetInspectorOverride, - value, + #windowingOwner, + owner, ), returnValueForMissingStub: null, ); @@ -2601,15 +2650,15 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i11.Future lockEvents(_i11.Future Function()? callback) => + _i12.Future lockEvents(_i12.Future Function()? callback) => (super.noSuchMethod( Invocation.method( #lockEvents, [callback], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override void unlocked() => super.noSuchMethod( @@ -2621,24 +2670,24 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i11.Future reassembleApplication() => (super.noSuchMethod( + _i12.Future reassembleApplication() => (super.noSuchMethod( Invocation.method( #reassembleApplication, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override - _i11.Future performReassemble() => (super.noSuchMethod( + _i12.Future performReassemble() => (super.noSuchMethod( Invocation.method( #performReassemble, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override void registerSignalServiceExtension({ @@ -2879,11 +2928,11 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i11.Future scheduleTask( - _i20.TaskCallback? task, - _i20.Priority? priority, { + _i12.Future scheduleTask( + _i22.TaskCallback? task, + _i22.Priority? priority, { String? debugLabel, - _i21.Flow? flow, + _i23.Flow? flow, }) => (super.noSuchMethod( Invocation.method( @@ -2897,8 +2946,8 @@ class MockWidgetsFlutterBinding extends _i1.Mock #flow: flow, }, ), - returnValue: _i14.ifNotNull( - _i14.dummyValueOrNull( + returnValue: _i15.ifNotNull( + _i15.dummyValueOrNull( this, Invocation.method( #scheduleTask, @@ -2912,9 +2961,9 @@ class MockWidgetsFlutterBinding extends _i1.Mock }, ), ), - (T v) => _i11.Future.value(v), + (T v) => _i12.Future.value(v), ) ?? - _FakeFuture_30( + _FakeFuture_31( this, Invocation.method( #scheduleTask, @@ -2928,7 +2977,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock }, ), ), - ) as _i11.Future); + ) as _i12.Future); @override bool handleEventLoopCallback() => (super.noSuchMethod( @@ -2941,7 +2990,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override int scheduleFrameCallback( - _i20.FrameCallback? callback, { + _i22.FrameCallback? callback, { bool? rescheduling = false, bool? scheduleNewFrame = true, }) => @@ -2995,7 +3044,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock ) as bool); @override - void addPersistentFrameCallback(_i20.FrameCallback? callback) => + void addPersistentFrameCallback(_i22.FrameCallback? callback) => super.noSuchMethod( Invocation.method( #addPersistentFrameCallback, @@ -3006,7 +3055,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock @override void addPostFrameCallback( - _i20.FrameCallback? callback, { + _i22.FrameCallback? callback, { String? debugLabel = 'callback', }) => super.noSuchMethod( @@ -3082,12 +3131,12 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i20.PerformanceModeRequestHandle? requestPerformanceMode( + _i22.PerformanceModeRequestHandle? requestPerformanceMode( _i6.DartPerformanceMode? mode) => (super.noSuchMethod(Invocation.method( #requestPerformanceMode, [mode], - )) as _i20.PerformanceModeRequestHandle?); + )) as _i22.PerformanceModeRequestHandle?); @override void handleDrawFrame() => super.noSuchMethod( @@ -3123,15 +3172,15 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i11.Future handleSystemMessage(Object? systemMessage) => + _i12.Future handleSystemMessage(Object? systemMessage) => (super.noSuchMethod( Invocation.method( #handleSystemMessage, [systemMessage], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override void initLicenses() => super.noSuchMethod( @@ -3170,18 +3219,18 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i11.Future<_i6.AppExitResponse> handleRequestAppExit() => + _i12.Future<_i6.AppExitResponse> handleRequestAppExit() => (super.noSuchMethod( Invocation.method( #handleRequestAppExit, [], ), returnValue: - _i11.Future<_i6.AppExitResponse>.value(_i6.AppExitResponse.exit), - ) as _i11.Future<_i6.AppExitResponse>); + _i12.Future<_i6.AppExitResponse>.value(_i6.AppExitResponse.exit), + ) as _i12.Future<_i6.AppExitResponse>); @override - _i11.Future<_i6.AppExitResponse> exitApplication( + _i12.Future<_i6.AppExitResponse> exitApplication( _i6.AppExitType? exitType, [ int? exitCode = 0, ]) => @@ -3194,8 +3243,8 @@ class MockWidgetsFlutterBinding extends _i1.Mock ], ), returnValue: - _i11.Future<_i6.AppExitResponse>.value(_i6.AppExitResponse.exit), - ) as _i11.Future<_i6.AppExitResponse>); + _i12.Future<_i6.AppExitResponse>.value(_i6.AppExitResponse.exit), + ) as _i12.Future<_i6.AppExitResponse>); @override _i4.RestorationManager createRestorationManager() => (super.noSuchMethod( @@ -3223,14 +3272,14 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i11.Future initializationComplete() => (super.noSuchMethod( + _i12.Future initializationComplete() => (super.noSuchMethod( Invocation.method( #initializationComplete, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override _i9.ImageCache createImageCache() => (super.noSuchMethod( @@ -3248,7 +3297,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock ) as _i9.ImageCache); @override - _i11.Future<_i6.Codec> instantiateImageCodecFromBuffer( + _i12.Future<_i6.Codec> instantiateImageCodecFromBuffer( _i6.ImmutableBuffer? buffer, { int? cacheWidth, int? cacheHeight, @@ -3264,7 +3313,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock #allowUpscaling: allowUpscaling, }, ), - returnValue: _i11.Future<_i6.Codec>.value(_FakeCodec_31( + returnValue: _i12.Future<_i6.Codec>.value(_FakeCodec_32( this, Invocation.method( #instantiateImageCodecFromBuffer, @@ -3276,10 +3325,10 @@ class MockWidgetsFlutterBinding extends _i1.Mock }, ), )), - ) as _i11.Future<_i6.Codec>); + ) as _i12.Future<_i6.Codec>); @override - _i11.Future<_i6.Codec> instantiateImageCodecWithSize( + _i12.Future<_i6.Codec> instantiateImageCodecWithSize( _i6.ImmutableBuffer? buffer, { _i6.TargetImageSizeCallback? getTargetSize, }) => @@ -3289,7 +3338,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock [buffer], {#getTargetSize: getTargetSize}, ), - returnValue: _i11.Future<_i6.Codec>.value(_FakeCodec_31( + returnValue: _i12.Future<_i6.Codec>.value(_FakeCodec_32( this, Invocation.method( #instantiateImageCodecWithSize, @@ -3297,7 +3346,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock {#getTargetSize: getTargetSize}, ), )), - ) as _i11.Future<_i6.Codec>); + ) as _i12.Future<_i6.Codec>); @override void addSemanticsEnabledListener(_i6.VoidCallback? listener) => @@ -3342,19 +3391,19 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i12.SemanticsHandle ensureSemantics() => (super.noSuchMethod( + _i13.SemanticsHandle ensureSemantics() => (super.noSuchMethod( Invocation.method( #ensureSemantics, [], ), - returnValue: _FakeSemanticsHandle_32( + returnValue: _FakeSemanticsHandle_33( this, Invocation.method( #ensureSemantics, [], ), ), - ) as _i12.SemanticsHandle); + ) as _i13.SemanticsHandle); @override void performSemanticsAction(_i6.SemanticsActionEvent? action) => @@ -3382,7 +3431,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock #createSemanticsUpdateBuilder, [], ), - returnValue: _FakeSemanticsUpdateBuilder_33( + returnValue: _FakeSemanticsUpdateBuilder_34( this, Invocation.method( #createSemanticsUpdateBuilder, @@ -3397,7 +3446,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock #createRootPipelineOwner, [], ), - returnValue: _i14.dummyValue<_i10.PipelineOwner>( + returnValue: _i15.dummyValue<_i10.PipelineOwner>( this, Invocation.method( #createRootPipelineOwner, @@ -3432,7 +3481,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock #createViewConfigurationFor, [renderView], ), - returnValue: _FakeViewConfiguration_34( + returnValue: _FakeViewConfiguration_35( this, Invocation.method( #createViewConfigurationFor, @@ -3447,7 +3496,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock #createSceneBuilder, [], ), - returnValue: _FakeSceneBuilder_35( + returnValue: _FakeSceneBuilder_36( this, Invocation.method( #createSceneBuilder, @@ -3462,7 +3511,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock #createPictureRecorder, [], ), - returnValue: _FakePictureRecorder_36( + returnValue: _FakePictureRecorder_37( this, Invocation.method( #createPictureRecorder, @@ -3477,7 +3526,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock #createCanvas, [recorder], ), - returnValue: _FakeCanvas_37( + returnValue: _FakeCanvas_38( this, Invocation.method( #createCanvas, @@ -3605,22 +3654,22 @@ class MockWidgetsFlutterBinding extends _i1.Mock ); @override - _i11.Future handlePopRoute() => (super.noSuchMethod( + _i12.Future handlePopRoute() => (super.noSuchMethod( Invocation.method( #handlePopRoute, [], ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i12.Future.value(false), + ) as _i12.Future); @override - _i11.Future handlePushRoute(String? route) => (super.noSuchMethod( + _i12.Future handlePushRoute(String? route) => (super.noSuchMethod( Invocation.method( #handlePushRoute, [route], ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i12.Future.value(false), + ) as _i12.Future); @override _i9.Widget wrapWithDefaultView(_i9.Widget? rootWidget) => (super.noSuchMethod( @@ -3628,7 +3677,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock #wrapWithDefaultView, [rootWidget], ), - returnValue: _FakeWidget_38( + returnValue: _FakeWidget_39( this, Invocation.method( #wrapWithDefaultView, @@ -3676,7 +3725,7 @@ class MockWidgetsFlutterBinding extends _i1.Mock /// A class which mocks [SentryJsBinding]. /// /// See the documentation for Mockito's code generation for more information. -class MockSentryJsBinding extends _i1.Mock implements _i22.SentryJsBinding { +class MockSentryJsBinding extends _i1.Mock implements _i24.SentryJsBinding { MockSentryJsBinding() { _i1.throwOnMissingStub(this); } @@ -3748,7 +3797,7 @@ class MockSentryJsBinding extends _i1.Mock implements _i22.SentryJsBinding { /// /// See the documentation for Mockito's code generation for more information. class MockTimeToDisplayTracker extends _i1.Mock - implements _i23.TimeToDisplayTracker { + implements _i25.TimeToDisplayTracker { MockTimeToDisplayTracker() { _i1.throwOnMissingStub(this); } @@ -3756,23 +3805,23 @@ class MockTimeToDisplayTracker extends _i1.Mock @override _i2.SentryFlutterOptions get options => (super.noSuchMethod( Invocation.getter(#options), - returnValue: _FakeSentryFlutterOptions_39( + returnValue: _FakeSentryFlutterOptions_40( this, Invocation.getter(#options), ), ) as _i2.SentryFlutterOptions); @override - set transactionId(_i2.SpanId? _transactionId) => super.noSuchMethod( + set transactionId(_i2.SpanId? value) => super.noSuchMethod( Invocation.setter( #transactionId, - _transactionId, + value, ), returnValueForMissingStub: null, ); @override - _i11.Future track( + _i12.Future track( _i2.ISentrySpan? transaction, { DateTime? ttidEndTimestamp, }) => @@ -3782,12 +3831,12 @@ class MockTimeToDisplayTracker extends _i1.Mock [transaction], {#ttidEndTimestamp: ttidEndTimestamp}, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override - _i11.Future reportFullyDisplayed({ + _i12.Future reportFullyDisplayed({ _i2.SpanId? spanId, DateTime? endTimestamp, }) => @@ -3800,12 +3849,12 @@ class MockTimeToDisplayTracker extends _i1.Mock #endTimestamp: endTimestamp, }, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override - _i11.Future cancelUnfinishedSpans( + _i12.Future cancelUnfinishedSpans( _i3.SentryTracer? transaction, DateTime? endTimestamp, ) => @@ -3817,9 +3866,9 @@ class MockTimeToDisplayTracker extends _i1.Mock endTimestamp, ], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override void clear() => super.noSuchMethod( @@ -3835,13 +3884,13 @@ class MockTimeToDisplayTracker extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTimeToInitialDisplayTracker extends _i1.Mock - implements _i24.TimeToInitialDisplayTracker { + implements _i26.TimeToInitialDisplayTracker { MockTimeToInitialDisplayTracker() { _i1.throwOnMissingStub(this); } @override - _i11.Future<_i2.ISentrySpan?> track({ + _i12.Future<_i2.ISentrySpan?> track({ required _i3.SentryTracer? transaction, DateTime? endTimestamp, }) => @@ -3854,8 +3903,8 @@ class MockTimeToInitialDisplayTracker extends _i1.Mock #endTimestamp: endTimestamp, }, ), - returnValue: _i11.Future<_i2.ISentrySpan?>.value(), - ) as _i11.Future<_i2.ISentrySpan?>); + returnValue: _i12.Future<_i2.ISentrySpan?>.value(), + ) as _i12.Future<_i2.ISentrySpan?>); @override void clear() => super.noSuchMethod( @@ -3871,13 +3920,13 @@ class MockTimeToInitialDisplayTracker extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTimeToFullDisplayTracker extends _i1.Mock - implements _i25.TimeToFullDisplayTracker { + implements _i27.TimeToFullDisplayTracker { MockTimeToFullDisplayTracker() { _i1.throwOnMissingStub(this); } @override - _i11.Future track({ + _i12.Future track({ required _i3.SentryTracer? transaction, DateTime? ttidEndTimestamp, DateTime? ttfdEndTimestamp, @@ -3892,12 +3941,12 @@ class MockTimeToFullDisplayTracker extends _i1.Mock #ttfdEndTimestamp: ttfdEndTimestamp, }, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override - _i11.Future reportFullyDisplayed({ + _i12.Future reportFullyDisplayed({ _i2.SpanId? spanId, DateTime? endTimestamp, }) => @@ -3910,8 +3959,8 @@ class MockTimeToFullDisplayTracker extends _i1.Mock #endTimestamp: endTimestamp, }, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i12.Future.value(false), + ) as _i12.Future); @override void clear() => super.noSuchMethod( @@ -3934,7 +3983,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { @override _i2.SentryOptions get options => (super.noSuchMethod( Invocation.getter(#options), - returnValue: _FakeSentryOptions_40( + returnValue: _FakeSentryOptions_41( this, Invocation.getter(#options), ), @@ -3958,14 +4007,14 @@ class MockHub extends _i1.Mock implements _i2.Hub { @override _i2.Scope get scope => (super.noSuchMethod( Invocation.getter(#scope), - returnValue: _FakeScope_41( + returnValue: _FakeScope_42( this, Invocation.getter(#scope), ), ) as _i2.Scope); @override - set profilerFactory(_i15.SentryProfilerFactory? value) => super.noSuchMethod( + set profilerFactory(_i16.SentryProfilerFactory? value) => super.noSuchMethod( Invocation.setter( #profilerFactory, value, @@ -3974,7 +4023,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ); @override - _i11.Future<_i2.SentryId> captureEvent( + _i12.Future<_i2.SentryId> captureEvent( _i2.SentryEvent? event, { dynamic stackTrace, _i2.Hint? hint, @@ -3990,7 +4039,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureEvent, @@ -4002,10 +4051,10 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i11.Future<_i2.SentryId>); + ) as _i12.Future<_i2.SentryId>); @override - _i11.Future<_i2.SentryId> captureException( + _i12.Future<_i2.SentryId> captureException( dynamic throwable, { dynamic stackTrace, _i2.Hint? hint, @@ -4023,7 +4072,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureException, @@ -4036,10 +4085,10 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i11.Future<_i2.SentryId>); + ) as _i12.Future<_i2.SentryId>); @override - _i11.Future<_i2.SentryId> captureMessage( + _i12.Future<_i2.SentryId> captureMessage( String? message, { _i2.SentryLevel? level, String? template, @@ -4059,7 +4108,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureMessage, @@ -4073,10 +4122,10 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i11.Future<_i2.SentryId>); + ) as _i12.Future<_i2.SentryId>); @override - _i11.Future<_i2.SentryId> captureFeedback( + _i12.Future<_i2.SentryId> captureFeedback( _i2.SentryFeedback? feedback, { _i2.Hint? hint, _i2.ScopeCallback? withScope, @@ -4090,7 +4139,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureFeedback, @@ -4101,17 +4150,47 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i11.Future<_i2.SentryId>); + ) as _i12.Future<_i2.SentryId>); @override - _i11.FutureOr captureLog(_i2.SentryLog? log) => + _i12.FutureOr captureLog(_i2.SentryLog? log) => (super.noSuchMethod(Invocation.method( #captureLog, [log], - )) as _i11.FutureOr); + )) as _i12.FutureOr); + + @override + _i12.Future captureMetric(_i17.SentryMetric? metric) => + (super.noSuchMethod( + Invocation.method( + #captureMetric, + [metric], + ), + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); + + @override + void setAttributes(Map? attributes) => + super.noSuchMethod( + Invocation.method( + #setAttributes, + [attributes], + ), + returnValueForMissingStub: null, + ); + + @override + void removeAttribute(String? key) => super.noSuchMethod( + Invocation.method( + #removeAttribute, + [key], + ), + returnValueForMissingStub: null, + ); @override - _i11.Future addBreadcrumb( + _i12.Future addBreadcrumb( _i2.Breadcrumb? crumb, { _i2.Hint? hint, }) => @@ -4121,9 +4200,9 @@ class MockHub extends _i1.Mock implements _i2.Hub { [crumb], {#hint: hint}, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override void bindClient(_i2.SentryClient? client) => super.noSuchMethod( @@ -4140,7 +4219,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #clone, [], ), - returnValue: _FakeHub_42( + returnValue: _FakeHub_43( this, Invocation.method( #clone, @@ -4150,21 +4229,21 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.Hub); @override - _i11.Future close() => (super.noSuchMethod( + _i12.Future close() => (super.noSuchMethod( Invocation.method( #close, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i12.Future.value(), + returnValueForMissingStub: _i12.Future.value(), + ) as _i12.Future); @override - _i11.FutureOr configureScope(_i2.ScopeCallback? callback) => + _i12.FutureOr configureScope(_i2.ScopeCallback? callback) => (super.noSuchMethod(Invocation.method( #configureScope, [callback], - )) as _i11.FutureOr); + )) as _i12.FutureOr); @override _i2.ISentrySpan startTransaction( @@ -4197,7 +4276,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #customSamplingContext: customSamplingContext, }, ), - returnValue: _i13.startTransactionShim( + returnValue: _i14.startTransactionShim( name, operation, description: description, @@ -4264,7 +4343,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ); @override - _i11.Future<_i2.SentryId> captureTransaction( + _i12.Future<_i2.SentryId> captureTransaction( _i2.SentryTransaction? transaction, { _i2.SentryTraceContextHeader? traceContext, _i2.Hint? hint, @@ -4278,7 +4357,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #hint: hint, }, ), - returnValue: _i11.Future<_i2.SentryId>.value(_FakeSentryId_5( + returnValue: _i12.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureTransaction, @@ -4289,7 +4368,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i11.Future<_i2.SentryId>); + ) as _i12.Future<_i2.SentryId>); @override void setSpanContext( diff --git a/packages/flutter/test/screenshot/sentry_screenshot_widget_test.mocks.dart b/packages/flutter/test/screenshot/sentry_screenshot_widget_test.mocks.dart index 8a5e5a357e..4b5bd71e70 100644 --- a/packages/flutter/test/screenshot/sentry_screenshot_widget_test.mocks.dart +++ b/packages/flutter/test/screenshot/sentry_screenshot_widget_test.mocks.dart @@ -21,6 +21,7 @@ import 'sentry_screenshot_widget_test.dart' as _i2; // 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 /// A class which mocks [Callbacks]. /// From b4f511583259cb15ef08d0ea5609797c0c2ddb24 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 11:03:24 +0100 Subject: [PATCH 52/79] Update test --- packages/dart/test/telemetry/metric/metrics_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dart/test/telemetry/metric/metrics_test.dart b/packages/dart/test/telemetry/metric/metrics_test.dart index 134b1fd390..d1785d921b 100644 --- a/packages/dart/test/telemetry/metric/metrics_test.dart +++ b/packages/dart/test/telemetry/metric/metrics_test.dart @@ -144,7 +144,7 @@ class Fixture { Fixture() { scope = Scope(options); sut = DefaultSentryMetrics( - captureMetricCallback: (metric) => capturedMetrics.add(metric), + captureMetricCallback: (metric) async => capturedMetrics.add(metric), clockProvider: () => fixedTimestamp, defaultScopeProvider: () => scope, ); From 7991bea6fa2ec0a770155189762fea7845e26a50 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 11:04:34 +0100 Subject: [PATCH 53/79] Update test --- .../load_contexts_integration_test.dart | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index 954793752f..2a8c0719d8 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -160,8 +160,7 @@ void main() { expect(event?.contexts.operatingSystem?.theme, 'theme1'); expect(event?.contexts.gpu?.name, 'gpu1'); expect(event?.contexts.browser?.name, 'browser1'); - expect( - event?.contexts.runtimes.any((element) => element.name == 'RT1'), + expect(event?.contexts.runtimes.any((element) => element.name == 'RT1'), true); expect(event?.contexts['theme'], 'material'); expect( @@ -201,17 +200,16 @@ void main() { expect(event?.contexts.operatingSystem?.name, 'eOS'); expect(event?.contexts.gpu?.name, 'eGpu'); expect(event?.contexts.browser?.name, 'eBrowser'); - expect( - event?.contexts.runtimes.any((element) => element.name == 'RT1'), + expect(event?.contexts.runtimes.any((element) => element.name == 'RT1'), true); - expect( - event?.contexts.runtimes.any((element) => element.name == 'eRT'), + expect(event?.contexts.runtimes.any((element) => element.name == 'eRT'), true); expect(event?.contexts['theme'], 'cuppertino'); expect(event?.user?.id, 'myId'); }); - test('merges event and loadContextsIntegration sdk packages and integration', + test( + 'merges event and loadContextsIntegration sdk packages and integration', () async { mockLoadContexts(); await fixture.registerIntegration(); @@ -307,8 +305,8 @@ void main() { 'breadcrumbs': [Breadcrumb(message: 'native').toJson()] }); - event = (await fixture.options.eventProcessors.first - .apply(event, Hint()))!; + event = + (await fixture.options.eventProcessors.first.apply(event, Hint()))!; expect(event.breadcrumbs!.length, 1); expect(event.breadcrumbs!.first.message, 'native'); @@ -325,8 +323,8 @@ void main() { 'breadcrumbs': [Breadcrumb(message: 'native').toJson()] }); - event = (await fixture.options.eventProcessors.first - .apply(event, Hint()))!; + event = + (await fixture.options.eventProcessors.first.apply(event, Hint()))!; expect(event.breadcrumbs!.length, 1); expect(event.breadcrumbs!.first.message, 'event'); @@ -354,8 +352,8 @@ void main() { ] }); - event = (await fixture.options.eventProcessors.first - .apply(event, Hint()))!; + event = + (await fixture.options.eventProcessors.first.apply(event, Hint()))!; expect(event.breadcrumbs!.length, 1); expect(event.breadcrumbs!.first.message, 'native-mutated-applied'); @@ -418,7 +416,8 @@ void main() { }); group('extra', () { - test('merges extra from native without overriding flutter keys', () async { + test('merges extra from native without overriding flutter keys', + () async { mockLoadContexts(); await fixture.registerIntegration(); @@ -705,7 +704,8 @@ void main() { ); }); - test('does not add os and device attributes to log if enableLogs is false', + test( + 'does not add os and device attributes to log if enableLogs is false', () async { fixture.options.enableLogs = false; await fixture.registerIntegration(); @@ -746,8 +746,7 @@ void main() { mockLoadContexts(); await fixture.registerIntegration(); - expect( - fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 1); + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 1); final metric = SentryCounterMetric( timestamp: DateTime.now(), @@ -787,8 +786,7 @@ void main() { mockLoadContexts(); await fixture.registerIntegration(); - expect( - fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 1); + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 1); fixture.sut.close(); @@ -803,16 +801,16 @@ void main() { await fixture.registerIntegration(); expect( - fixture.options.lifecycleRegistry - .lifecycleCallbacks[OnBeforeCaptureLog], + fixture + .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], isNotEmpty, ); fixture.sut.close(); expect( - fixture.options.lifecycleRegistry - .lifecycleCallbacks[OnBeforeCaptureLog], + fixture + .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], isEmpty, ); }); @@ -823,8 +821,7 @@ void main() { mockLoadContexts(); await fixture.registerIntegration(); - expect( - fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 2); + expect(fixture.options.lifecycleRegistry.lifecycleCallbacks.length, 2); fixture.sut.close(); @@ -833,8 +830,8 @@ void main() { isEmpty, ); expect( - fixture.options.lifecycleRegistry - .lifecycleCallbacks[OnBeforeCaptureLog], + fixture + .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], isEmpty, ); }); From 8534d99577eefca6706c8fe92289e784ed21f300 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 11:32:05 +0100 Subject: [PATCH 54/79] Remove logs enricher --- .../load_contexts_integration_test.dart | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index 2a8c0719d8..989cc9e850 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -5,7 +5,6 @@ library; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:sentry/src/logs_enricher_integration.dart'; import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -690,20 +689,6 @@ void main() { expect(log.attributes['device.family']?.value, 'fixture-device-family'); }); - test('removes logsEnricherIntegration', () async { - final integration = LogsEnricherIntegration(); - fixture.options.addIntegration(integration); - - fixture.options.enableLogs = true; - await fixture.registerIntegration(); - - expect( - fixture.options.integrations - .any((element) => element is LogsEnricherIntegration), - isFalse, - ); - }); - test( 'does not add os and device attributes to log if enableLogs is false', () async { @@ -801,16 +786,14 @@ void main() { await fixture.registerIntegration(); expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessLog], isNotEmpty, ); fixture.sut.close(); expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessLog], isEmpty, ); }); @@ -830,8 +813,7 @@ void main() { isEmpty, ); expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessLog], isEmpty, ); }); From a2674f5c9f278326e834e3661ae9b27deabf25a3 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 11:38:29 +0100 Subject: [PATCH 55/79] Update tests --- .../load_contexts_integration_test.dart | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index 989cc9e850..fd3888b2b9 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -707,22 +707,6 @@ void main() { expect(log.attributes['device.model'], isNull); expect(log.attributes['device.family'], isNull); }); - - test('handles throw during loadContexts', () async { - fixture.options.enableLogs = true; - await fixture.registerIntegration(); - - when(fixture.binding.loadContexts()).thenThrow(Exception('test')); - - final log = givenLog(); - await fixture.hub.captureLog(log); - - expect(log.attributes['os.name'], isNull); - expect(log.attributes['os.version'], isNull); - expect(log.attributes['device.brand'], isNull); - expect(log.attributes['device.model'], isNull); - expect(log.attributes['device.family'], isNull); - }); }); group('metrics', () { From 6a658cb94afa59e6be5b5042bad91c375c00908b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 11:56:21 +0100 Subject: [PATCH 56/79] Update tests --- .../load_contexts_integration_test.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index fd3888b2b9..72946d4a69 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -707,6 +707,22 @@ void main() { expect(log.attributes['device.model'], isNull); expect(log.attributes['device.family'], isNull); }); + + test('handles throw during loadContexts', () async { + fixture.options.enableLogs = true; + await fixture.registerIntegration(); + + when(fixture.binding.loadContexts()).thenThrow(Exception('test')); + + final log = givenLog(); + await fixture.hub.captureLog(log); + + // os.name and os.version are set by defaultAttributes() from Dart-level + // OS detection, not from native loadContexts(), so we only check device.* + expect(log.attributes['device.brand'], isNull); + expect(log.attributes['device.model'], isNull); + expect(log.attributes['device.family'], isNull); + }); }); group('metrics', () { From a786f9f48fd2dc5c0e4bf06f4737d23094ea0b4b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 11:56:40 +0100 Subject: [PATCH 57/79] Update tests --- .../load_contexts_integration_test.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index 2a8c0719d8..ce53247c45 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -738,6 +738,22 @@ void main() { expect(log.attributes['device.model'], isNull); expect(log.attributes['device.family'], isNull); }); + + test('handles throw during loadContexts', () async { + fixture.options.enableLogs = true; + await fixture.registerIntegration(); + + when(fixture.binding.loadContexts()).thenThrow(Exception('test')); + + final log = givenLog(); + await fixture.hub.captureLog(log); + + // os.name and os.version are set by defaultAttributes() from Dart-level + // OS detection, not from native loadContexts(), so we only check device.* + expect(log.attributes['device.brand'], isNull); + expect(log.attributes['device.model'], isNull); + expect(log.attributes['device.family'], isNull); + }); }); group('metrics', () { From e25645ba4454da8ed211c2aa7455423134311764 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 14:30:42 +0100 Subject: [PATCH 58/79] Update --- packages/dart/lib/src/protocol/sentry_log.dart | 7 ++++--- .../lib/src/telemetry/log/default_logger.dart | 15 +++------------ .../telemetry/log/log_capture_pipeline.dart | 2 -- .../src/telemetry/processing/processor.dart | 3 +++ .../dart/test/protocol/sentry_log_test.dart | 18 ------------------ 5 files changed, 10 insertions(+), 35 deletions(-) diff --git a/packages/dart/lib/src/protocol/sentry_log.dart b/packages/dart/lib/src/protocol/sentry_log.dart index 9c1b479ff3..9a2f7e2782 100644 --- a/packages/dart/lib/src/protocol/sentry_log.dart +++ b/packages/dart/lib/src/protocol/sentry_log.dart @@ -5,7 +5,7 @@ import 'span_id.dart'; class SentryLog { DateTime timestamp; - SentryId traceId; + SentryId? traceId; SpanId? spanId; SentryLogLevel level; String body; @@ -16,13 +16,14 @@ class SentryLog { /// by the time processing completes, it is guaranteed to be a valid non-empty trace id. SentryLog({ required this.timestamp, - required this.traceId, + // TODO(major-v10): this should be required non-null + SentryId? traceId, required this.level, required this.body, required this.attributes, this.spanId, this.severityNumber, - }); + }) : traceId = traceId ?? SentryId.empty(); Map toJson() { return { diff --git a/packages/dart/lib/src/telemetry/log/default_logger.dart b/packages/dart/lib/src/telemetry/log/default_logger.dart index e729c74c02..798ac3ba64 100644 --- a/packages/dart/lib/src/telemetry/log/default_logger.dart +++ b/packages/dart/lib/src/telemetry/log/default_logger.dart @@ -29,8 +29,6 @@ final class DefaultSentryLogger implements SentryLogger { Map? attributes, Scope? scope, }) { - internalLogger.debug(() => - 'Sentry.logger.trace("$body") called with attributes ${_formatAttributes(attributes)}'); return _captureLog(SentryLogLevel.trace, body, attributes: attributes, scope: scope); } @@ -41,8 +39,6 @@ final class DefaultSentryLogger implements SentryLogger { Map? attributes, Scope? scope, }) { - internalLogger.debug(() => - 'Sentry.logger.debug("$body") called with attributes ${_formatAttributes(attributes)}'); return _captureLog(SentryLogLevel.debug, body, attributes: attributes, scope: scope); } @@ -53,8 +49,6 @@ final class DefaultSentryLogger implements SentryLogger { Map? attributes, Scope? scope, }) { - internalLogger.debug(() => - 'Sentry.logger.info("$body") called with attributes ${_formatAttributes(attributes)}'); return _captureLog(SentryLogLevel.info, body, attributes: attributes, scope: scope); } @@ -65,8 +59,6 @@ final class DefaultSentryLogger implements SentryLogger { Map? attributes, Scope? scope, }) { - internalLogger.debug(() => - 'Sentry.logger.warn("$body") called with attributes ${_formatAttributes(attributes)}'); return _captureLog(SentryLogLevel.warn, body, attributes: attributes, scope: scope); } @@ -77,8 +69,6 @@ final class DefaultSentryLogger implements SentryLogger { Map? attributes, Scope? scope, }) { - internalLogger.debug(() => - 'Sentry.logger.error("$body") called with attributes ${_formatAttributes(attributes)}'); return _captureLog(SentryLogLevel.error, body, attributes: attributes, scope: scope); } @@ -89,8 +79,6 @@ final class DefaultSentryLogger implements SentryLogger { Map? attributes, Scope? scope, }) { - internalLogger.debug(() => - 'Sentry.logger.fatal("$body") called with attributes ${_formatAttributes(attributes)}'); return _captureLog(SentryLogLevel.fatal, body, attributes: attributes, scope: scope); } @@ -106,6 +94,9 @@ final class DefaultSentryLogger implements SentryLogger { Map? attributes, Scope? scope, }) { + internalLogger.debug(() => + 'Sentry.logger.${level.value}("$body") called with attributes ${_formatAttributes(attributes)}'); + final log = SentryLog( timestamp: _clockProvider(), level: level, diff --git a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart index 46b4af7ea5..841e50e40b 100644 --- a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart +++ b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart @@ -63,8 +63,6 @@ class LogCapturePipeline { } _options.telemetryProcessor.addLog(processedLog); - internalLogger.debug( - '$LogCapturePipeline: Log "${processedLog.body}" (${processedLog.level.name}) captured'); } catch (exception, stackTrace) { internalLogger.error( 'Error capturing log "${log.body}"', diff --git a/packages/dart/lib/src/telemetry/processing/processor.dart b/packages/dart/lib/src/telemetry/processing/processor.dart index 928dd029fa..267ebed1a3 100644 --- a/packages/dart/lib/src/telemetry/processing/processor.dart +++ b/packages/dart/lib/src/telemetry/processing/processor.dart @@ -58,6 +58,9 @@ class DefaultTelemetryProcessor implements TelemetryProcessor { return; } + internalLogger.debug(() => + '$DefaultTelemetryProcessor: Log "${log.body}" (${log.level.name}) added to buffer'); + _logBuffer.add(log); } diff --git a/packages/dart/test/protocol/sentry_log_test.dart b/packages/dart/test/protocol/sentry_log_test.dart index 4095dd7424..b3f08521b5 100644 --- a/packages/dart/test/protocol/sentry_log_test.dart +++ b/packages/dart/test/protocol/sentry_log_test.dart @@ -52,24 +52,6 @@ void main() { }); }); - test('$SentryLog to json without spanId', () { - final timestamp = DateTime.now(); - final traceId = SentryId.newId(); - - final logItem = SentryLog( - timestamp: timestamp, - traceId: traceId, - level: SentryLogLevel.info, - body: 'fixture-body', - attributes: {}, - severityNumber: 1, - ); - - final json = logItem.toJson(); - - expect(json.containsKey('span_id'), isFalse); - }); - test('$SentryLevel without severity number infers from level in toJson', () { final logItem = SentryLog( timestamp: DateTime.now(), From b70eee873ed2f3313d30cbebe04cd2edcfcced0a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 14:40:50 +0100 Subject: [PATCH 59/79] Update --- packages/dart/lib/src/protocol.dart | 4 ++-- .../{protocol/sentry_log.dart => telemetry/log/log.dart} | 8 ++++---- .../log/log_level.dart} | 2 +- .../log/log_level_test.dart} | 2 +- .../sentry_log_test.dart => telemetry/log/log_test.dart} | 0 5 files changed, 8 insertions(+), 8 deletions(-) rename packages/dart/lib/src/{protocol/sentry_log.dart => telemetry/log/log.dart} (87%) rename packages/dart/lib/src/{protocol/sentry_log_level.dart => telemetry/log/log_level.dart} (96%) rename packages/dart/test/{protocol/sentry_log_level_test.dart => telemetry/log/log_level_test.dart} (94%) rename packages/dart/test/{protocol/sentry_log_test.dart => telemetry/log/log_test.dart} (100%) diff --git a/packages/dart/lib/src/protocol.dart b/packages/dart/lib/src/protocol.dart index cb94d143b7..7347c0e88a 100644 --- a/packages/dart/lib/src/protocol.dart +++ b/packages/dart/lib/src/protocol.dart @@ -41,6 +41,6 @@ export 'protocol/span_status.dart'; export 'sentry_event_like.dart'; export 'protocol/sentry_feature_flag.dart'; export 'protocol/sentry_feature_flags.dart'; -export 'protocol/sentry_log.dart'; -export 'protocol/sentry_log_level.dart'; +export 'telemetry/log/log.dart'; +export 'telemetry/log/log_level.dart'; export 'protocol/sentry_attribute.dart'; diff --git a/packages/dart/lib/src/protocol/sentry_log.dart b/packages/dart/lib/src/telemetry/log/log.dart similarity index 87% rename from packages/dart/lib/src/protocol/sentry_log.dart rename to packages/dart/lib/src/telemetry/log/log.dart index 9a2f7e2782..ac0a211544 100644 --- a/packages/dart/lib/src/protocol/sentry_log.dart +++ b/packages/dart/lib/src/telemetry/log/log.dart @@ -1,7 +1,7 @@ -import 'sentry_attribute.dart'; -import 'sentry_id.dart'; -import 'sentry_log_level.dart'; -import 'span_id.dart'; +import '../../protocol/sentry_attribute.dart'; +import '../../protocol/sentry_id.dart'; +import '../../protocol/span_id.dart'; +import 'log_level.dart'; class SentryLog { DateTime timestamp; diff --git a/packages/dart/lib/src/protocol/sentry_log_level.dart b/packages/dart/lib/src/telemetry/log/log_level.dart similarity index 96% rename from packages/dart/lib/src/protocol/sentry_log_level.dart rename to packages/dart/lib/src/telemetry/log/log_level.dart index 5fd441e8c9..aa90878613 100644 --- a/packages/dart/lib/src/protocol/sentry_log_level.dart +++ b/packages/dart/lib/src/telemetry/log/log_level.dart @@ -1,4 +1,4 @@ -import 'sentry_level.dart'; +import '../../protocol/sentry_level.dart'; enum SentryLogLevel { trace('trace'), diff --git a/packages/dart/test/protocol/sentry_log_level_test.dart b/packages/dart/test/telemetry/log/log_level_test.dart similarity index 94% rename from packages/dart/test/protocol/sentry_log_level_test.dart rename to packages/dart/test/telemetry/log/log_level_test.dart index 8139859c0e..4ee72df62d 100644 --- a/packages/dart/test/protocol/sentry_log_level_test.dart +++ b/packages/dart/test/telemetry/log/log_level_test.dart @@ -1,5 +1,5 @@ import 'package:test/test.dart'; -import 'package:sentry/src/protocol/sentry_log_level.dart'; +import 'package:sentry/src/telemetry/log/log_level.dart'; import 'package:sentry/src/protocol/sentry_level.dart'; void main() { diff --git a/packages/dart/test/protocol/sentry_log_test.dart b/packages/dart/test/telemetry/log/log_test.dart similarity index 100% rename from packages/dart/test/protocol/sentry_log_test.dart rename to packages/dart/test/telemetry/log/log_test.dart From 7af466a3fdf946af321bc928bef0c9310eddced9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 15:08:32 +0100 Subject: [PATCH 60/79] Update --- packages/dart/lib/src/hub.dart | 12 +++--------- packages/dart/lib/src/hub_adapter.dart | 3 ++- packages/dart/lib/src/noop_hub.dart | 2 +- .../dart/lib/src/telemetry/log/default_logger.dart | 5 +++-- packages/dart/test/mocks/mock_hub.dart | 4 ++-- .../test/telemetry/log/logger_formatter_test.dart | 2 +- packages/dart/test/telemetry/log/logger_test.dart | 2 +- 7 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index c980419d68..320dc36f60 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -286,7 +286,7 @@ class Hub { return sentryId; } - FutureOr captureLog(SentryLog log) async { + FutureOr captureLog(SentryLog log, {Scope? scope}) async { if (!_isEnabled) { _options.log( SentryLevel.warning, @@ -294,18 +294,12 @@ class Hub { ); } else { final item = _peek(); - late Scope scope; - final s = _cloneAndRunWithScope(item.scope, null); - if (s is Future) { - scope = await s; - } else { - scope = s; - } + final effectiveScope = scope ?? item.scope; try { await item.client.captureLog( log, - scope: scope, + scope: effectiveScope, ); } catch (exception, stacktrace) { _options.log( diff --git a/packages/dart/lib/src/hub_adapter.dart b/packages/dart/lib/src/hub_adapter.dart index 73ee33c4cf..cbfb01dacb 100644 --- a/packages/dart/lib/src/hub_adapter.dart +++ b/packages/dart/lib/src/hub_adapter.dart @@ -199,7 +199,8 @@ class HubAdapter implements Hub { ); @override - FutureOr captureLog(SentryLog log) => Sentry.currentHub.captureLog(log); + FutureOr captureLog(SentryLog log, {Scope? scope}) => + Sentry.currentHub.captureLog(log, scope: scope); @override Future captureMetric(SentryMetric metric) => diff --git a/packages/dart/lib/src/noop_hub.dart b/packages/dart/lib/src/noop_hub.dart index 81855f0bef..cd5835c839 100644 --- a/packages/dart/lib/src/noop_hub.dart +++ b/packages/dart/lib/src/noop_hub.dart @@ -96,7 +96,7 @@ class NoOpHub implements Hub { SentryId.empty(); @override - FutureOr captureLog(SentryLog log) async {} + FutureOr captureLog(SentryLog log, {Scope? scope}) async {} @override Future captureMetric(SentryMetric metric) async {} diff --git a/packages/dart/lib/src/telemetry/log/default_logger.dart b/packages/dart/lib/src/telemetry/log/default_logger.dart index 798ac3ba64..a796d81817 100644 --- a/packages/dart/lib/src/telemetry/log/default_logger.dart +++ b/packages/dart/lib/src/telemetry/log/default_logger.dart @@ -4,7 +4,8 @@ import '../../../sentry.dart'; import '../../sentry_template_string.dart'; import '../../utils/internal_logger.dart'; -typedef CaptureLogCallback = FutureOr Function(SentryLog log); +typedef CaptureLogCallback = FutureOr Function(SentryLog log, + {Scope? scope}); typedef ScopeProvider = Scope Function(); final class DefaultSentryLogger implements SentryLogger { @@ -106,7 +107,7 @@ final class DefaultSentryLogger implements SentryLogger { attributes: attributes ?? {}, ); - return _captureLogCallback(log); + return _captureLogCallback(log, scope: scope); } SentryId _traceIdFor(Scope? scope) => diff --git a/packages/dart/test/mocks/mock_hub.dart b/packages/dart/test/mocks/mock_hub.dart index 6b8d530cae..2499af1a29 100644 --- a/packages/dart/test/mocks/mock_hub.dart +++ b/packages/dart/test/mocks/mock_hub.dart @@ -111,8 +111,8 @@ class MockHub with NoSuchMethodProvider implements Hub { } @override - FutureOr captureLog(SentryLog log) async { - captureLogCalls.add(CaptureLogCall(log, null)); + FutureOr captureLog(SentryLog log, {Scope? scope}) async { + captureLogCalls.add(CaptureLogCall(log, scope)); } @override diff --git a/packages/dart/test/telemetry/log/logger_formatter_test.dart b/packages/dart/test/telemetry/log/logger_formatter_test.dart index 8190bdb0e5..1c553db07c 100644 --- a/packages/dart/test/telemetry/log/logger_formatter_test.dart +++ b/packages/dart/test/telemetry/log/logger_formatter_test.dart @@ -285,7 +285,7 @@ class Fixture { Fixture() { scope = Scope(options); logger = DefaultSentryLogger( - captureLogCallback: (log) { + captureLogCallback: (log, {scope}) { capturedLogs.add(log); }, clockProvider: () => DateTime.now(), diff --git a/packages/dart/test/telemetry/log/logger_test.dart b/packages/dart/test/telemetry/log/logger_test.dart index 4a256cc534..7b1f93ee04 100644 --- a/packages/dart/test/telemetry/log/logger_test.dart +++ b/packages/dart/test/telemetry/log/logger_test.dart @@ -173,7 +173,7 @@ class Fixture { SentryLogger getSut() { return DefaultSentryLogger( - captureLogCallback: (log) { + captureLogCallback: (log, {scope}) { capturedLogs.add(log); }, clockProvider: () => timestamp, From 005a60128cc0d954a2b7e0ec15906c808e9da13f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 15:36:07 +0100 Subject: [PATCH 61/79] Update mocks --- packages/flutter/test/mocks.mocks.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/flutter/test/mocks.mocks.dart b/packages/flutter/test/mocks.mocks.dart index db45428c78..91b037a28e 100644 --- a/packages/flutter/test/mocks.mocks.dart +++ b/packages/flutter/test/mocks.mocks.dart @@ -4153,10 +4153,14 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i12.Future<_i2.SentryId>); @override - _i12.FutureOr captureLog(_i2.SentryLog? log) => + _i12.FutureOr captureLog( + _i2.SentryLog? log, { + _i2.Scope? scope, + }) => (super.noSuchMethod(Invocation.method( #captureLog, [log], + {#scope: scope}, )) as _i12.FutureOr); @override From 647b7929d3880ee441f0cf67a1c129c79e7cda8d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 18:04:08 +0100 Subject: [PATCH 62/79] Fix cursor review --- packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart index 841e50e40b..1cfa8d0a83 100644 --- a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart +++ b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart @@ -23,6 +23,10 @@ class LogCapturePipeline { try { if (scope != null) { + // Populate traceId from scope if not already set + if (log.traceId == null || log.traceId == SentryId.empty()) { + log.traceId = scope.propagationContext.traceId; + } log.attributes.addAllIfAbsent(scope.attributes); } From 4c73da9744ebc3a37c9aaf1cc302296fdc75ea38 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 22:58:28 +0100 Subject: [PATCH 63/79] Update --- packages/dart/lib/src/hub.dart | 12 +++++++++--- packages/dart/lib/src/sentry.dart | 4 ++-- ...p_integration.dart => log_setup_integration.dart} | 4 ++-- .../telemetry/log/logs_setup_integration_test.dart | 12 ++++++------ 4 files changed, 19 insertions(+), 13 deletions(-) rename packages/dart/lib/src/telemetry/log/{logs_setup_integration.dart => log_setup_integration.dart} (87%) diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index 320dc36f60..c980419d68 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -286,7 +286,7 @@ class Hub { return sentryId; } - FutureOr captureLog(SentryLog log, {Scope? scope}) async { + FutureOr captureLog(SentryLog log) async { if (!_isEnabled) { _options.log( SentryLevel.warning, @@ -294,12 +294,18 @@ class Hub { ); } else { final item = _peek(); - final effectiveScope = scope ?? item.scope; + late Scope scope; + final s = _cloneAndRunWithScope(item.scope, null); + if (s is Future) { + scope = await s; + } else { + scope = s; + } try { await item.client.captureLog( log, - scope: effectiveScope, + scope: scope, ); } catch (exception, stacktrace) { _options.log( diff --git a/packages/dart/lib/src/sentry.dart b/packages/dart/lib/src/sentry.dart index f4baa17f8d..54da879312 100644 --- a/packages/dart/lib/src/sentry.dart +++ b/packages/dart/lib/src/sentry.dart @@ -31,7 +31,7 @@ import 'transport/data_category.dart'; import 'transport/task_queue.dart'; import 'feature_flags_integration.dart'; import 'telemetry/log/logger.dart'; -import 'telemetry/log/logs_setup_integration.dart'; +import 'telemetry/log/log_setup_integration.dart'; /// Configuration options callback typedef OptionsConfiguration = FutureOr Function(SentryOptions); @@ -113,7 +113,7 @@ class Sentry { } options.addIntegration(MetricsSetupIntegration()); - options.addIntegration(LogsSetupIntegration()); + options.addIntegration(LogSetupIntegration()); options.addIntegration(FeatureFlagsIntegration()); options.addIntegration(InMemoryTelemetryProcessorIntegration()); diff --git a/packages/dart/lib/src/telemetry/log/logs_setup_integration.dart b/packages/dart/lib/src/telemetry/log/log_setup_integration.dart similarity index 87% rename from packages/dart/lib/src/telemetry/log/logs_setup_integration.dart rename to packages/dart/lib/src/telemetry/log/log_setup_integration.dart index ef5780d51b..39beba81a8 100644 --- a/packages/dart/lib/src/telemetry/log/logs_setup_integration.dart +++ b/packages/dart/lib/src/telemetry/log/log_setup_integration.dart @@ -3,8 +3,8 @@ import '../../utils/internal_logger.dart'; import 'default_logger.dart'; import 'noop_logger.dart'; -class LogsSetupIntegration extends Integration { - static const integrationName = 'LogsSetup'; +class LogSetupIntegration extends Integration { + static const integrationName = 'LogSetup'; @override void call(Hub hub, SentryOptions options) { diff --git a/packages/dart/test/telemetry/log/logs_setup_integration_test.dart b/packages/dart/test/telemetry/log/logs_setup_integration_test.dart index ffa022a45b..8475bc6d22 100644 --- a/packages/dart/test/telemetry/log/logs_setup_integration_test.dart +++ b/packages/dart/test/telemetry/log/logs_setup_integration_test.dart @@ -2,14 +2,14 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/telemetry/log/default_logger.dart'; -import 'package:sentry/src/telemetry/log/logs_setup_integration.dart'; +import 'package:sentry/src/telemetry/log/log_setup_integration.dart'; import 'package:sentry/src/telemetry/log/noop_logger.dart'; import 'package:test/test.dart'; import '../../test_utils.dart'; void main() { - group('$LogsSetupIntegration', () { + group('$LogSetupIntegration', () { late Fixture fixture; setUp(() { @@ -32,7 +32,7 @@ void main() { expect( fixture.options.sdk.integrations, - contains(LogsSetupIntegration.integrationName), + contains(LogSetupIntegration.integrationName), ); }); @@ -63,7 +63,7 @@ void main() { expect( fixture.options.sdk.integrations, - isNot(contains(LogsSetupIntegration.integrationName)), + isNot(contains(LogSetupIntegration.integrationName)), ); }); }); @@ -74,11 +74,11 @@ class Fixture { final options = defaultTestOptions(); late final Hub hub; - late final LogsSetupIntegration sut; + late final LogSetupIntegration sut; Fixture() { hub = Hub(options); - sut = LogsSetupIntegration(); + sut = LogSetupIntegration(); } } From 5a6123a7462b852c540750446fd5f1d3c72332b0 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 23:06:32 +0100 Subject: [PATCH 64/79] Update --- .../lib/src/telemetry/log/default_logger.dart | 70 +++++-------------- .../telemetry/log/log_setup_integration.dart | 2 +- .../dart/lib/src/telemetry/log/logger.dart | 12 ---- 3 files changed, 20 insertions(+), 64 deletions(-) diff --git a/packages/dart/lib/src/telemetry/log/default_logger.dart b/packages/dart/lib/src/telemetry/log/default_logger.dart index a796d81817..40ee263589 100644 --- a/packages/dart/lib/src/telemetry/log/default_logger.dart +++ b/packages/dart/lib/src/telemetry/log/default_logger.dart @@ -4,14 +4,13 @@ import '../../../sentry.dart'; import '../../sentry_template_string.dart'; import '../../utils/internal_logger.dart'; -typedef CaptureLogCallback = FutureOr Function(SentryLog log, - {Scope? scope}); +typedef CaptureLogCallback = FutureOr Function(SentryLog log); typedef ScopeProvider = Scope Function(); final class DefaultSentryLogger implements SentryLogger { final CaptureLogCallback _captureLogCallback; final ClockProvider _clockProvider; - final ScopeProvider _defaultScopeProvider; + final ScopeProvider _scopeProvider; late final SentryLoggerFormatter _formatter = _DefaultSentryLoggerFormatter(this); @@ -19,69 +18,57 @@ final class DefaultSentryLogger implements SentryLogger { DefaultSentryLogger({ required CaptureLogCallback captureLogCallback, required ClockProvider clockProvider, - required ScopeProvider defaultScopeProvider, + required ScopeProvider scopeProvider, }) : _captureLogCallback = captureLogCallback, _clockProvider = clockProvider, - _defaultScopeProvider = defaultScopeProvider; + _scopeProvider = scopeProvider; @override FutureOr trace( String body, { Map? attributes, - Scope? scope, }) { - return _captureLog(SentryLogLevel.trace, body, - attributes: attributes, scope: scope); + return _captureLog(SentryLogLevel.trace, body, attributes: attributes); } @override FutureOr debug( String body, { Map? attributes, - Scope? scope, }) { - return _captureLog(SentryLogLevel.debug, body, - attributes: attributes, scope: scope); + return _captureLog(SentryLogLevel.debug, body, attributes: attributes); } @override FutureOr info( String body, { Map? attributes, - Scope? scope, }) { - return _captureLog(SentryLogLevel.info, body, - attributes: attributes, scope: scope); + return _captureLog(SentryLogLevel.info, body, attributes: attributes); } @override FutureOr warn( String body, { Map? attributes, - Scope? scope, }) { - return _captureLog(SentryLogLevel.warn, body, - attributes: attributes, scope: scope); + return _captureLog(SentryLogLevel.warn, body, attributes: attributes); } @override FutureOr error( String body, { Map? attributes, - Scope? scope, }) { - return _captureLog(SentryLogLevel.error, body, - attributes: attributes, scope: scope); + return _captureLog(SentryLogLevel.error, body, attributes: attributes); } @override FutureOr fatal( String body, { Map? attributes, - Scope? scope, }) { - return _captureLog(SentryLogLevel.fatal, body, - attributes: attributes, scope: scope); + return _captureLog(SentryLogLevel.fatal, body, attributes: attributes); } @override @@ -93,7 +80,6 @@ final class DefaultSentryLogger implements SentryLogger { SentryLogLevel level, String body, { Map? attributes, - Scope? scope, }) { internalLogger.debug(() => 'Sentry.logger.${level.value}("$body") called with attributes ${_formatAttributes(attributes)}'); @@ -102,20 +88,14 @@ final class DefaultSentryLogger implements SentryLogger { timestamp: _clockProvider(), level: level, body: body, - traceId: _traceIdFor(scope), - spanId: _activeSpanIdFor(scope), + traceId: _scopeProvider().propagationContext.traceId, + spanId: _scopeProvider().span?.context.spanId, attributes: attributes ?? {}, ); - return _captureLogCallback(log, scope: scope); + return _captureLogCallback(log); } - SentryId _traceIdFor(Scope? scope) => - (scope ?? _defaultScopeProvider()).propagationContext.traceId; - - SpanId? _activeSpanIdFor(Scope? scope) => - (scope ?? _defaultScopeProvider()).span?.context.spanId; - String _formatAttributes(Map? attributes) { final formatted = attributes?.toFormattedString() ?? ''; return formatted.isEmpty ? '' : ' $formatted'; @@ -132,15 +112,13 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.trace(formattedBody, - attributes: allAttributes, scope: scope); + return _logger.trace(formattedBody, attributes: allAttributes); }, ); } @@ -150,15 +128,13 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.debug(formattedBody, - attributes: allAttributes, scope: scope); + return _logger.debug(formattedBody, attributes: allAttributes); }, ); } @@ -168,15 +144,13 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.info(formattedBody, - attributes: allAttributes, scope: scope); + return _logger.info(formattedBody, attributes: allAttributes); }, ); } @@ -186,15 +160,13 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.warn(formattedBody, - attributes: allAttributes, scope: scope); + return _logger.warn(formattedBody, attributes: allAttributes); }, ); } @@ -204,15 +176,13 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.error(formattedBody, - attributes: allAttributes, scope: scope); + return _logger.error(formattedBody, attributes: allAttributes); }, ); } @@ -222,15 +192,13 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.fatal(formattedBody, - attributes: allAttributes, scope: scope); + return _logger.fatal(formattedBody, attributes: allAttributes); }, ); } diff --git a/packages/dart/lib/src/telemetry/log/log_setup_integration.dart b/packages/dart/lib/src/telemetry/log/log_setup_integration.dart index 39beba81a8..7ee113437e 100644 --- a/packages/dart/lib/src/telemetry/log/log_setup_integration.dart +++ b/packages/dart/lib/src/telemetry/log/log_setup_integration.dart @@ -22,7 +22,7 @@ class LogSetupIntegration extends Integration { options.logger = DefaultSentryLogger( captureLogCallback: hub.captureLog, clockProvider: options.clock, - defaultScopeProvider: () => hub.scope, + scopeProvider: () => hub.scope, ); options.sdk.addIntegration(integrationName); diff --git a/packages/dart/lib/src/telemetry/log/logger.dart b/packages/dart/lib/src/telemetry/log/logger.dart index 40d330c744..0b8f253832 100644 --- a/packages/dart/lib/src/telemetry/log/logger.dart +++ b/packages/dart/lib/src/telemetry/log/logger.dart @@ -12,42 +12,36 @@ abstract interface class SentryLogger { FutureOr trace( String body, { Map? attributes, - Scope? scope, }); /// Logs a message at DEBUG level. FutureOr debug( String body, { Map? attributes, - Scope? scope, }); /// Logs a message at INFO level. FutureOr info( String body, { Map? attributes, - Scope? scope, }); /// Logs a message at WARN level. FutureOr warn( String body, { Map? attributes, - Scope? scope, }); /// Logs a message at ERROR level. FutureOr error( String body, { Map? attributes, - Scope? scope, }); /// Logs a message at FATAL level. FutureOr fatal( String body, { Map? attributes, - Scope? scope, }); /// Provides formatted logging with template strings. @@ -63,7 +57,6 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }); /// Logs a formatted message at DEBUG level. @@ -71,7 +64,6 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }); /// Logs a formatted message at INFO level. @@ -79,7 +71,6 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }); /// Logs a formatted message at WARN level. @@ -87,7 +78,6 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }); /// Logs a formatted message at ERROR level. @@ -95,7 +85,6 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }); /// Logs a formatted message at FATAL level. @@ -103,6 +92,5 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, - Scope? scope, }); } From 86220bdb22eed1fec66ba1e81336c70b7803e504 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 23:09:07 +0100 Subject: [PATCH 65/79] Update --- .../src/telemetry/metric/default_metrics.dart | 27 +++++++------------ .../lib/src/telemetry/metric/metrics.dart | 23 +++++++++++----- .../metric/metrics_setup_integration.dart | 1 + .../src/telemetry/metric/noop_metrics.dart | 23 +++++++++++----- 4 files changed, 44 insertions(+), 30 deletions(-) diff --git a/packages/dart/lib/src/telemetry/metric/default_metrics.dart b/packages/dart/lib/src/telemetry/metric/default_metrics.dart index 0b6c3ca9bf..a6380369c9 100644 --- a/packages/dart/lib/src/telemetry/metric/default_metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/default_metrics.dart @@ -10,22 +10,21 @@ typedef ScopeProvider = Scope Function(); final class DefaultSentryMetrics implements SentryMetrics { final CaptureMetricCallback _captureMetricCallback; final ClockProvider _clockProvider; - final ScopeProvider _defaultScopeProvider; + final ScopeProvider _scopeProvider; DefaultSentryMetrics( {required CaptureMetricCallback captureMetricCallback, required ClockProvider clockProvider, - required ScopeProvider defaultScopeProvider}) + required ScopeProvider scopeProvider}) : _captureMetricCallback = captureMetricCallback, _clockProvider = clockProvider, - _defaultScopeProvider = defaultScopeProvider; + _scopeProvider = scopeProvider; @override void count( String name, int value, { Map? attributes, - Scope? scope, }) { internalLogger.debug(() => 'Sentry.metrics.count("$name", $value) called with attributes ${_formatAttributes(attributes)}'); @@ -34,8 +33,8 @@ final class DefaultSentryMetrics implements SentryMetrics { timestamp: _clockProvider(), name: name, value: value, - spanId: _activeSpanIdFor(scope), - traceId: _traceIdFor(scope), + spanId: _scopeProvider().span?.context.spanId, + traceId: _scopeProvider().propagationContext.traceId, attributes: attributes ?? {}); unawaited(_captureMetricCallback(metric)); @@ -47,7 +46,6 @@ final class DefaultSentryMetrics implements SentryMetrics { num value, { String? unit, Map? attributes, - Scope? scope, }) { internalLogger.debug(() => 'Sentry.metrics.gauge("$name", $value${_formatUnit(unit)}) called with attributes ${_formatAttributes(attributes)}'); @@ -57,8 +55,8 @@ final class DefaultSentryMetrics implements SentryMetrics { name: name, value: value, unit: unit, - spanId: _activeSpanIdFor(scope), - traceId: _traceIdFor(scope), + spanId: _scopeProvider().span?.context.spanId, + traceId: _scopeProvider().propagationContext.traceId, attributes: attributes ?? {}); unawaited(_captureMetricCallback(metric)); @@ -70,7 +68,6 @@ final class DefaultSentryMetrics implements SentryMetrics { num value, { String? unit, Map? attributes, - Scope? scope, }) { internalLogger.debug(() => 'Sentry.metrics.distribution("$name", $value${_formatUnit(unit)}) called with attributes ${_formatAttributes(attributes)}'); @@ -80,19 +77,13 @@ final class DefaultSentryMetrics implements SentryMetrics { name: name, value: value, unit: unit, - spanId: _activeSpanIdFor(scope), - traceId: _traceIdFor(scope), + spanId: _scopeProvider().span?.context.spanId, + traceId: _scopeProvider().propagationContext.traceId, attributes: attributes ?? {}); unawaited(_captureMetricCallback(metric)); } - SentryId _traceIdFor(Scope? scope) => - (scope ?? _defaultScopeProvider()).propagationContext.traceId; - - SpanId? _activeSpanIdFor(Scope? scope) => - (scope ?? _defaultScopeProvider()).span?.context.spanId; - String _formatUnit(String? unit) => unit != null ? ', unit: $unit' : ''; String _formatAttributes(Map? attributes) { diff --git a/packages/dart/lib/src/telemetry/metric/metrics.dart b/packages/dart/lib/src/telemetry/metric/metrics.dart index 82f0d8f644..d91e1e5a0f 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics.dart @@ -8,8 +8,11 @@ abstract interface class SentryMetrics { /// Increments a counter metric by the given [value]. /// /// Use counters to track the number of times an event occurs. - void count(String name, int value, - {Map? attributes, Scope? scope}); + void count( + String name, + int value, { + Map? attributes, + }); /// Records a value in a distribution metric. /// @@ -17,8 +20,12 @@ abstract interface class SentryMetrics { /// such as response times or file sizes. /// /// See [SentryMetricUnit] for predefined unit constants. - void distribution(String name, num value, - {String? unit, Map? attributes, Scope? scope}); + void distribution( + String name, + num value, { + String? unit, + Map? attributes, + }); /// Sets the current value of a gauge metric. /// @@ -26,6 +33,10 @@ abstract interface class SentryMetrics { /// such as memory usage or queue depth. /// /// See [SentryMetricUnit] for predefined unit constants. - void gauge(String name, num value, - {String? unit, Map? attributes, Scope? scope}); + void gauge( + String name, + num value, { + String? unit, + Map? attributes, + }); } diff --git a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart index 5c0cfd4742..f947db7bc6 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart @@ -3,6 +3,7 @@ import '../../utils/internal_logger.dart'; import 'default_metrics.dart'; import 'noop_metrics.dart'; +/// Integration that sets up the default Sentry metrics implementation. class MetricsSetupIntegration extends Integration { static const integrationName = 'MetricsSetup'; diff --git a/packages/dart/lib/src/telemetry/metric/noop_metrics.dart b/packages/dart/lib/src/telemetry/metric/noop_metrics.dart index de71339af8..aa53620b42 100644 --- a/packages/dart/lib/src/telemetry/metric/noop_metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/noop_metrics.dart @@ -4,14 +4,25 @@ final class NoOpSentryMetrics implements SentryMetrics { const NoOpSentryMetrics(); @override - void count(String name, int value, - {Map? attributes, Scope? scope}) {} + void count( + String name, + int value, { + Map? attributes, + }) {} @override - void distribution(String name, num value, - {String? unit, Map? attributes, Scope? scope}) {} + void distribution( + String name, + num value, { + String? unit, + Map? attributes, + }) {} @override - void gauge(String name, num value, - {String? unit, Map? attributes, Scope? scope}) {} + void gauge( + String name, + num value, { + String? unit, + Map? attributes, + }) {} } From 7b61969fc603155f14c7a32aa9237d25b5d5319f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 23:14:51 +0100 Subject: [PATCH 66/79] Update --- packages/dart/lib/src/sentry.dart | 2 +- .../telemetry/log/log_capture_pipeline.dart | 1 + ...ion.dart => logger_setup_integration.dart} | 5 +- .../lib/src/telemetry/log/noop_logger.dart | 79 +++++++++++++------ .../log/logs_setup_integration_test.dart | 10 +-- 5 files changed, 64 insertions(+), 33 deletions(-) rename packages/dart/lib/src/telemetry/log/{log_setup_integration.dart => logger_setup_integration.dart} (81%) diff --git a/packages/dart/lib/src/sentry.dart b/packages/dart/lib/src/sentry.dart index 54da879312..4b7dc6b118 100644 --- a/packages/dart/lib/src/sentry.dart +++ b/packages/dart/lib/src/sentry.dart @@ -113,7 +113,7 @@ class Sentry { } options.addIntegration(MetricsSetupIntegration()); - options.addIntegration(LogSetupIntegration()); + options.addIntegration(LoggerSetupIntegration()); options.addIntegration(FeatureFlagsIntegration()); options.addIntegration(InMemoryTelemetryProcessorIntegration()); diff --git a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart index 1cfa8d0a83..4abf24b6df 100644 --- a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart +++ b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart @@ -24,6 +24,7 @@ class LogCapturePipeline { try { if (scope != null) { // Populate traceId from scope if not already set + // TODO(major-v10): this can be removed once we make the traceId required on the log if (log.traceId == null || log.traceId == SentryId.empty()) { log.traceId = scope.propagationContext.traceId; } diff --git a/packages/dart/lib/src/telemetry/log/log_setup_integration.dart b/packages/dart/lib/src/telemetry/log/logger_setup_integration.dart similarity index 81% rename from packages/dart/lib/src/telemetry/log/log_setup_integration.dart rename to packages/dart/lib/src/telemetry/log/logger_setup_integration.dart index 7ee113437e..9eb48cffae 100644 --- a/packages/dart/lib/src/telemetry/log/log_setup_integration.dart +++ b/packages/dart/lib/src/telemetry/log/logger_setup_integration.dart @@ -3,8 +3,9 @@ import '../../utils/internal_logger.dart'; import 'default_logger.dart'; import 'noop_logger.dart'; -class LogSetupIntegration extends Integration { - static const integrationName = 'LogSetup'; +/// Integration that sets up the default Sentry logger implementation. +class LoggerSetupIntegration extends Integration { + static const integrationName = 'LoggerSetup'; @override void call(Hub hub, SentryOptions options) { diff --git a/packages/dart/lib/src/telemetry/log/noop_logger.dart b/packages/dart/lib/src/telemetry/log/noop_logger.dart index 784f48054d..0381bdcb88 100644 --- a/packages/dart/lib/src/telemetry/log/noop_logger.dart +++ b/packages/dart/lib/src/telemetry/log/noop_logger.dart @@ -1,7 +1,6 @@ import 'dart:async'; import '../../protocol/sentry_attribute.dart'; -import '../../scope.dart'; import 'logger.dart'; final class NoOpSentryLogger implements SentryLogger { @@ -10,28 +9,40 @@ final class NoOpSentryLogger implements SentryLogger { static const _formatter = _NoOpSentryLoggerFormatter(); @override - FutureOr trace(String body, - {Map? attributes, Scope? scope}) {} + FutureOr trace( + String body, { + Map? attributes, + }) {} @override - FutureOr debug(String body, - {Map? attributes, Scope? scope}) {} + FutureOr debug( + String body, { + Map? attributes, + }) {} @override - FutureOr info(String body, - {Map? attributes, Scope? scope}) {} + FutureOr info( + String body, { + Map? attributes, + }) {} @override - FutureOr warn(String body, - {Map? attributes, Scope? scope}) {} + FutureOr warn( + String body, { + Map? attributes, + }) {} @override - FutureOr error(String body, - {Map? attributes, Scope? scope}) {} + FutureOr error( + String body, { + Map? attributes, + }) {} @override - FutureOr fatal(String body, - {Map? attributes, Scope? scope}) {} + FutureOr fatal( + String body, { + Map? attributes, + }) {} @override SentryLoggerFormatter get fmt => _formatter; @@ -41,26 +52,44 @@ final class _NoOpSentryLoggerFormatter implements SentryLoggerFormatter { const _NoOpSentryLoggerFormatter(); @override - FutureOr trace(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr trace( + String templateBody, + List arguments, { + Map? attributes, + }) {} @override - FutureOr debug(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr debug( + String templateBody, + List arguments, { + Map? attributes, + }) {} @override - FutureOr info(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr info( + String templateBody, + List arguments, { + Map? attributes, + }) {} @override - FutureOr warn(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr warn( + String templateBody, + List arguments, { + Map? attributes, + }) {} @override - FutureOr error(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr error( + String templateBody, + List arguments, { + Map? attributes, + }) {} @override - FutureOr fatal(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr fatal( + String templateBody, + List arguments, { + Map? attributes, + }) {} } diff --git a/packages/dart/test/telemetry/log/logs_setup_integration_test.dart b/packages/dart/test/telemetry/log/logs_setup_integration_test.dart index 8475bc6d22..f660621c93 100644 --- a/packages/dart/test/telemetry/log/logs_setup_integration_test.dart +++ b/packages/dart/test/telemetry/log/logs_setup_integration_test.dart @@ -9,7 +9,7 @@ import 'package:test/test.dart'; import '../../test_utils.dart'; void main() { - group('$LogSetupIntegration', () { + group('$LoggerSetupIntegration', () { late Fixture fixture; setUp(() { @@ -32,7 +32,7 @@ void main() { expect( fixture.options.sdk.integrations, - contains(LogSetupIntegration.integrationName), + contains(LoggerSetupIntegration.integrationName), ); }); @@ -63,7 +63,7 @@ void main() { expect( fixture.options.sdk.integrations, - isNot(contains(LogSetupIntegration.integrationName)), + isNot(contains(LoggerSetupIntegration.integrationName)), ); }); }); @@ -74,11 +74,11 @@ class Fixture { final options = defaultTestOptions(); late final Hub hub; - late final LogSetupIntegration sut; + late final LoggerSetupIntegration sut; Fixture() { hub = Hub(options); - sut = LogSetupIntegration(); + sut = LoggerSetupIntegration(); } } From 8c29471bf7c023b86519dfa4b4d1b5ce8127a657 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 23:16:32 +0100 Subject: [PATCH 67/79] Update --- packages/dart/lib/src/hub_adapter.dart | 3 +-- packages/dart/lib/src/noop_hub.dart | 2 +- packages/dart/lib/src/sentry.dart | 2 +- .../dart/test/telemetry/log/logs_setup_integration_test.dart | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/dart/lib/src/hub_adapter.dart b/packages/dart/lib/src/hub_adapter.dart index cbfb01dacb..73ee33c4cf 100644 --- a/packages/dart/lib/src/hub_adapter.dart +++ b/packages/dart/lib/src/hub_adapter.dart @@ -199,8 +199,7 @@ class HubAdapter implements Hub { ); @override - FutureOr captureLog(SentryLog log, {Scope? scope}) => - Sentry.currentHub.captureLog(log, scope: scope); + FutureOr captureLog(SentryLog log) => Sentry.currentHub.captureLog(log); @override Future captureMetric(SentryMetric metric) => diff --git a/packages/dart/lib/src/noop_hub.dart b/packages/dart/lib/src/noop_hub.dart index cd5835c839..81855f0bef 100644 --- a/packages/dart/lib/src/noop_hub.dart +++ b/packages/dart/lib/src/noop_hub.dart @@ -96,7 +96,7 @@ class NoOpHub implements Hub { SentryId.empty(); @override - FutureOr captureLog(SentryLog log, {Scope? scope}) async {} + FutureOr captureLog(SentryLog log) async {} @override Future captureMetric(SentryMetric metric) async {} diff --git a/packages/dart/lib/src/sentry.dart b/packages/dart/lib/src/sentry.dart index 4b7dc6b118..9c658e5643 100644 --- a/packages/dart/lib/src/sentry.dart +++ b/packages/dart/lib/src/sentry.dart @@ -31,7 +31,7 @@ import 'transport/data_category.dart'; import 'transport/task_queue.dart'; import 'feature_flags_integration.dart'; import 'telemetry/log/logger.dart'; -import 'telemetry/log/log_setup_integration.dart'; +import 'telemetry/log/logger_setup_integration.dart'; /// Configuration options callback typedef OptionsConfiguration = FutureOr Function(SentryOptions); diff --git a/packages/dart/test/telemetry/log/logs_setup_integration_test.dart b/packages/dart/test/telemetry/log/logs_setup_integration_test.dart index f660621c93..6988e0bb6d 100644 --- a/packages/dart/test/telemetry/log/logs_setup_integration_test.dart +++ b/packages/dart/test/telemetry/log/logs_setup_integration_test.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/telemetry/log/default_logger.dart'; -import 'package:sentry/src/telemetry/log/log_setup_integration.dart'; +import 'package:sentry/src/telemetry/log/logger_setup_integration.dart'; import 'package:sentry/src/telemetry/log/noop_logger.dart'; import 'package:test/test.dart'; From 58a9bf1487de9db35af9392010c1b17ac6147877 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 23:17:17 +0100 Subject: [PATCH 68/79] Update --- .../lib/src/telemetry/metric/metrics_setup_integration.dart | 2 +- packages/dart/test/telemetry/metric/metrics_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart index f947db7bc6..2052904db1 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics_setup_integration.dart @@ -24,7 +24,7 @@ class MetricsSetupIntegration extends Integration { options.metrics = DefaultSentryMetrics( captureMetricCallback: hub.captureMetric, clockProvider: options.clock, - defaultScopeProvider: () => hub.scope); + scopeProvider: () => hub.scope); options.sdk.addIntegration(integrationName); internalLogger.debug('$integrationName: Metrics configured successfully'); diff --git a/packages/dart/test/telemetry/metric/metrics_test.dart b/packages/dart/test/telemetry/metric/metrics_test.dart index d1785d921b..40d937b8a3 100644 --- a/packages/dart/test/telemetry/metric/metrics_test.dart +++ b/packages/dart/test/telemetry/metric/metrics_test.dart @@ -146,7 +146,7 @@ class Fixture { sut = DefaultSentryMetrics( captureMetricCallback: (metric) async => capturedMetrics.add(metric), clockProvider: () => fixedTimestamp, - defaultScopeProvider: () => scope, + scopeProvider: () => scope, ); } } From 6bb8e65243130abb5c3f7c8bf334de63b981c871 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 23:17:38 +0100 Subject: [PATCH 69/79] Update --- packages/dart/test/telemetry/log/logger_formatter_test.dart | 2 +- packages/dart/test/telemetry/log/logger_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dart/test/telemetry/log/logger_formatter_test.dart b/packages/dart/test/telemetry/log/logger_formatter_test.dart index 1c553db07c..3c74f2385d 100644 --- a/packages/dart/test/telemetry/log/logger_formatter_test.dart +++ b/packages/dart/test/telemetry/log/logger_formatter_test.dart @@ -289,7 +289,7 @@ class Fixture { capturedLogs.add(log); }, clockProvider: () => DateTime.now(), - defaultScopeProvider: () => scope, + scopeProvider: () => scope, ); } } diff --git a/packages/dart/test/telemetry/log/logger_test.dart b/packages/dart/test/telemetry/log/logger_test.dart index 7b1f93ee04..421166e725 100644 --- a/packages/dart/test/telemetry/log/logger_test.dart +++ b/packages/dart/test/telemetry/log/logger_test.dart @@ -177,7 +177,7 @@ class Fixture { capturedLogs.add(log); }, clockProvider: () => timestamp, - defaultScopeProvider: () => scope, + scopeProvider: () => scope, ); } } From 7dc6a2bb430d75ad344b4f3da121f3ccee3dbc35 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 21 Jan 2026 23:21:54 +0100 Subject: [PATCH 70/79] Update --- packages/dart/test/telemetry/log/logger_test.dart | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/dart/test/telemetry/log/logger_test.dart b/packages/dart/test/telemetry/log/logger_test.dart index 421166e725..ffb37a0530 100644 --- a/packages/dart/test/telemetry/log/logger_test.dart +++ b/packages/dart/test/telemetry/log/logger_test.dart @@ -112,21 +112,6 @@ void main() { expect(capturedLog.spanId, span.context.spanId); }); - test('uses provided scope for trace id and span id', () async { - final customScope = Scope(fixture.options); - final span = _MockSpan(); - customScope.span = span; - - final logger = fixture.getSut(); - - await logger.info('test', scope: customScope); - - expect(fixture.capturedLogs.length, 1); - final capturedLog = fixture.capturedLogs[0]; - expect(capturedLog.traceId, customScope.propagationContext.traceId); - expect(capturedLog.spanId, span.context.spanId); - }); - test('sets timestamp from clock provider', () async { final logger = fixture.getSut(); From 813c90261963896e95fc6c7fc4458f89b6d62441 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 22 Jan 2026 12:31:26 +0100 Subject: [PATCH 71/79] Update --- packages/dart/lib/sentry.dart | 2 +- .../lib/src/telemetry/metric/metrics.dart | 63 +++++++++++++++---- .../dart/lib/src/telemetry/telemetry.dart | 2 + packages/flutter/example/lib/main.dart | 7 +++ 4 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 packages/dart/lib/src/telemetry/telemetry.dart diff --git a/packages/dart/lib/sentry.dart b/packages/dart/lib/sentry.dart index 46186a1949..c809b5e2af 100644 --- a/packages/dart/lib/sentry.dart +++ b/packages/dart/lib/sentry.dart @@ -61,6 +61,6 @@ export 'src/utils/url_details.dart'; // ignore: invalid_export_of_internal_element export 'src/utils/breadcrumb_log_level.dart'; export 'src/sentry_logger.dart'; -export 'src/telemetry/metric/metrics.dart'; +export 'src/telemetry/telemetry.dart'; // ignore: invalid_export_of_internal_element export 'src/utils/internal_logger.dart' show SentryInternalLogger; diff --git a/packages/dart/lib/src/telemetry/metric/metrics.dart b/packages/dart/lib/src/telemetry/metric/metrics.dart index d91e1e5a0f..e36d453086 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics.dart @@ -5,35 +5,74 @@ import 'metric.dart'; /// /// Access via [Sentry.metrics]. abstract interface class SentryMetrics { - /// Increments a counter metric by the given [value]. + /// Increments a cumulative counter by [value]. /// - /// Use counters to track the number of times an event occurs. + /// Use counters for values that only increase, like request counts or error + /// totals. The [name] identifies the metric (e.g., `'api.requests'`). + /// Optionally attach [attributes] to add dimensions for filtering. + /// + /// ```dart + /// Sentry.metrics.count( + /// 'api.requests', + /// 1, + /// attributes: { + /// 'endpoint': SentryAttribute.string('/api/users'), + /// 'method': SentryAttribute.string('POST'), + /// }, + /// ); + /// ``` void count( String name, int value, { Map? attributes, }); - /// Records a value in a distribution metric. + /// Records a point-in-time [value] that can increase or decrease. /// - /// Use distributions to track the statistical distribution of values, - /// such as response times or file sizes. + /// Use gauges for values that fluctuate, like memory usage, queue depth, or + /// active connections. The [name] identifies the metric. Specify [unit] to + /// describe the measurement— [SentryMetricUnit] provides officially supported + /// units (e.g., [SentryMetricUnit.byte]), but custom strings are also + /// accepted. Optionally attach [attributes] to add dimensions for filtering. /// - /// See [SentryMetricUnit] for predefined unit constants. - void distribution( + /// ```dart + /// Sentry.metrics.gauge( + /// 'memory.heap_used', + /// 1, + /// unit: SentryMetricUnit.megabyte, + /// attributes: { + /// 'process': SentryAttribute.string('main'), + /// }, + /// ); + /// ``` + void gauge( String name, num value, { String? unit, Map? attributes, }); - /// Sets the current value of a gauge metric. + /// Records a [value] for statistical distribution analysis. /// - /// Use gauges to track values that can increase or decrease over time, - /// such as memory usage or queue depth. + /// Use distributions to track values where you need percentiles, averages, + /// and histograms—like response times or payload sizes. The [name] identifies + /// the metric. Specify [unit] to describe the measurement — [SentryMetricUnit] + /// provides officially supported units (e.g., [SentryMetricUnit.millisecond]), + /// but custom strings are also accepted. Optionally attach [attributes] to + /// add dimensions for filtering. /// - /// See [SentryMetricUnit] for predefined unit constants. - void gauge( + /// ```dart + /// Sentry.metrics.distribution( + /// 'http.request.duration', + /// 245.3, + /// unit: SentryMetricUnit.millisecond, + /// attributes: { + /// 'endpoint': SentryAttribute.string('/api/users'), + /// 'status_code': SentryAttribute.int(200), + /// }, + /// ); + /// ``` + void distribution( String name, num value, { String? unit, diff --git a/packages/dart/lib/src/telemetry/telemetry.dart b/packages/dart/lib/src/telemetry/telemetry.dart new file mode 100644 index 0000000000..f1bbe2e574 --- /dev/null +++ b/packages/dart/lib/src/telemetry/telemetry.dart @@ -0,0 +1,2 @@ +export 'metric/metrics.dart'; +export 'metric/metric.dart'; diff --git a/packages/flutter/example/lib/main.dart b/packages/flutter/example/lib/main.dart index d528c46659..fc73988b02 100644 --- a/packages/flutter/example/lib/main.dart +++ b/packages/flutter/example/lib/main.dart @@ -97,6 +97,13 @@ Future setupSentry( options.enableLogs = true; + options.beforeSendMetric = (metric) { + if (metric.name == 'drop-metric') { + return null; + } + return metric; + }; + _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { options.dist = '1'; From e55495787a8e33318e78c5583eca267433c95044 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 22 Jan 2026 12:50:01 +0100 Subject: [PATCH 72/79] Use export barrel file --- packages/dart/lib/src/telemetry/telemetry.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/dart/lib/src/telemetry/telemetry.dart b/packages/dart/lib/src/telemetry/telemetry.dart index f1bbe2e574..98c0f23ba0 100644 --- a/packages/dart/lib/src/telemetry/telemetry.dart +++ b/packages/dart/lib/src/telemetry/telemetry.dart @@ -1,2 +1,4 @@ export 'metric/metrics.dart'; export 'metric/metric.dart'; +export 'log/log.dart'; +export 'log/logger.dart'; From e3b882b14f3da26e3640bbc0d12290ea189f635a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 22 Jan 2026 13:01:57 +0100 Subject: [PATCH 73/79] Fix analyzer --- packages/dart/lib/src/hub.dart | 1 - packages/dart/lib/src/sdk_lifecycle_hooks.dart | 1 - packages/dart/lib/src/sentry_options.dart | 1 - packages/dart/lib/src/telemetry/metric/default_metrics.dart | 1 - .../dart/lib/src/telemetry/metric/metric_capture_pipeline.dart | 1 - packages/dart/lib/src/telemetry/metric/metrics.dart | 1 - packages/dart/lib/src/telemetry/processing/processor.dart | 1 - .../dart/lib/src/telemetry/processing/processor_integration.dart | 1 - packages/dart/test/hub_test.dart | 1 - packages/dart/test/mocks/mock_metric_capture_pipeline.dart | 1 - packages/dart/test/mocks/mock_sentry_client.dart | 1 - packages/dart/test/mocks/mock_telemetry_processor.dart | 1 - packages/dart/test/sentry_client_test.dart | 1 - .../dart/test/telemetry/metric/metric_capture_pipeline_test.dart | 1 - packages/dart/test/telemetry/metric/metric_test.dart | 1 - packages/dart/test/telemetry/metric/metrics_test.dart | 1 - packages/dart/test/telemetry/processing/processor_test.dart | 1 - .../test/integrations/load_contexts_integration_test.dart | 1 - .../test/integrations/replay_telemetry_integration_test.dart | 1 - packages/flutter/test/mocks.dart | 1 - 20 files changed, 20 deletions(-) diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index c980419d68..8ffc57d15b 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -9,7 +9,6 @@ import 'client_reports/discard_reason.dart'; import 'profiling.dart'; import 'sentry_tracer.dart'; import 'sentry_traces_sampler.dart'; -import 'telemetry/metric/metric.dart'; import 'transport/data_category.dart'; /// Configures the scope through the callback. diff --git a/packages/dart/lib/src/sdk_lifecycle_hooks.dart b/packages/dart/lib/src/sdk_lifecycle_hooks.dart index b0df1b19b9..c0cbcaf343 100644 --- a/packages/dart/lib/src/sdk_lifecycle_hooks.dart +++ b/packages/dart/lib/src/sdk_lifecycle_hooks.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:meta/meta.dart'; import '../sentry.dart'; -import 'telemetry/metric/metric.dart'; @internal typedef SdkLifecycleCallback = FutureOr diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 0db9f3b527..d36e29d2c9 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -12,7 +12,6 @@ import 'noop_client.dart'; import 'platform/platform.dart'; import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.dart'; -import 'telemetry/metric/metric.dart'; import 'telemetry/metric/noop_metrics.dart'; import 'telemetry/processing/processor.dart'; import 'transport/noop_transport.dart'; diff --git a/packages/dart/lib/src/telemetry/metric/default_metrics.dart b/packages/dart/lib/src/telemetry/metric/default_metrics.dart index a6380369c9..b2aeda5f3e 100644 --- a/packages/dart/lib/src/telemetry/metric/default_metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/default_metrics.dart @@ -2,7 +2,6 @@ import 'dart:async'; import '../../../sentry.dart'; import '../../utils/internal_logger.dart'; -import 'metric.dart'; typedef CaptureMetricCallback = Future Function(SentryMetric metric); typedef ScopeProvider = Scope Function(); diff --git a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart index e8ace638cf..9bfa7fc23e 100644 --- a/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart +++ b/packages/dart/lib/src/telemetry/metric/metric_capture_pipeline.dart @@ -5,7 +5,6 @@ import '../../client_reports/discard_reason.dart'; import '../../transport/data_category.dart'; import '../../utils/internal_logger.dart'; import '../default_attributes.dart'; -import 'metric.dart'; @internal class MetricCapturePipeline { diff --git a/packages/dart/lib/src/telemetry/metric/metrics.dart b/packages/dart/lib/src/telemetry/metric/metrics.dart index e36d453086..81a689cec9 100644 --- a/packages/dart/lib/src/telemetry/metric/metrics.dart +++ b/packages/dart/lib/src/telemetry/metric/metrics.dart @@ -1,5 +1,4 @@ import '../../../sentry.dart'; -import 'metric.dart'; /// Interface for emitting custom metrics to Sentry. /// diff --git a/packages/dart/lib/src/telemetry/processing/processor.dart b/packages/dart/lib/src/telemetry/processing/processor.dart index 928dd029fa..778d7accb7 100644 --- a/packages/dart/lib/src/telemetry/processing/processor.dart +++ b/packages/dart/lib/src/telemetry/processing/processor.dart @@ -4,7 +4,6 @@ import 'package:meta/meta.dart'; import '../../../sentry.dart'; import '../../utils/internal_logger.dart'; -import '../metric/metric.dart'; import 'buffer.dart'; /// Interface for processing and buffering telemetry data before sending. diff --git a/packages/dart/lib/src/telemetry/processing/processor_integration.dart b/packages/dart/lib/src/telemetry/processing/processor_integration.dart index d1ffd3c961..bbf8be767f 100644 --- a/packages/dart/lib/src/telemetry/processing/processor_integration.dart +++ b/packages/dart/lib/src/telemetry/processing/processor_integration.dart @@ -1,6 +1,5 @@ import '../../../sentry.dart'; import '../../utils/internal_logger.dart'; -import '../metric/metric.dart'; import 'in_memory_buffer.dart'; import 'processor.dart'; diff --git a/packages/dart/test/hub_test.dart b/packages/dart/test/hub_test.dart index 9c97993ffb..a5dcc6e1c5 100644 --- a/packages/dart/test/hub_test.dart +++ b/packages/dart/test/hub_test.dart @@ -4,7 +4,6 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/client_reports/discard_reason.dart'; import 'package:sentry/src/propagation_context.dart'; import 'package:sentry/src/sentry_tracer.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; diff --git a/packages/dart/test/mocks/mock_metric_capture_pipeline.dart b/packages/dart/test/mocks/mock_metric_capture_pipeline.dart index 79de9b5d8a..d714b512f6 100644 --- a/packages/dart/test/mocks/mock_metric_capture_pipeline.dart +++ b/packages/dart/test/mocks/mock_metric_capture_pipeline.dart @@ -1,5 +1,4 @@ import 'package:sentry/sentry.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/telemetry/metric/metric_capture_pipeline.dart'; import 'mock_sentry_client.dart'; diff --git a/packages/dart/test/mocks/mock_sentry_client.dart b/packages/dart/test/mocks/mock_sentry_client.dart index 0286e3e6b3..e6550a6ba7 100644 --- a/packages/dart/test/mocks/mock_sentry_client.dart +++ b/packages/dart/test/mocks/mock_sentry_client.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'no_such_method_provider.dart'; diff --git a/packages/dart/test/mocks/mock_telemetry_processor.dart b/packages/dart/test/mocks/mock_telemetry_processor.dart index 450f03d0f3..34dd0ba744 100644 --- a/packages/dart/test/mocks/mock_telemetry_processor.dart +++ b/packages/dart/test/mocks/mock_telemetry_processor.dart @@ -1,5 +1,4 @@ import 'package:sentry/sentry.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/telemetry/processing/processor.dart'; class MockTelemetryProcessor implements TelemetryProcessor { diff --git a/packages/dart/test/sentry_client_test.dart b/packages/dart/test/sentry_client_test.dart index 7016e59be0..c2c7eb1e6f 100644 --- a/packages/dart/test/sentry_client_test.dart +++ b/packages/dart/test/sentry_client_test.dart @@ -14,7 +14,6 @@ import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry/src/transport/client_report_transport.dart'; import 'package:sentry/src/transport/data_category.dart'; import 'package:sentry/src/transport/noop_transport.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/transport/spotlight_http_transport.dart'; import 'package:sentry/src/utils/iterable_utils.dart'; import 'package:test/test.dart'; diff --git a/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart b/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart index 5467756b8c..e60f63d0e6 100644 --- a/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart +++ b/packages/dart/test/telemetry/metric/metric_capture_pipeline_test.dart @@ -1,6 +1,5 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/client_reports/discard_reason.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/telemetry/metric/metric_capture_pipeline.dart'; import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; diff --git a/packages/dart/test/telemetry/metric/metric_test.dart b/packages/dart/test/telemetry/metric/metric_test.dart index 08cf6e80e2..b4558fc188 100644 --- a/packages/dart/test/telemetry/metric/metric_test.dart +++ b/packages/dart/test/telemetry/metric/metric_test.dart @@ -1,5 +1,4 @@ import 'package:sentry/sentry.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/dart/test/telemetry/metric/metrics_test.dart b/packages/dart/test/telemetry/metric/metrics_test.dart index 40d937b8a3..68ab5a9b5e 100644 --- a/packages/dart/test/telemetry/metric/metrics_test.dart +++ b/packages/dart/test/telemetry/metric/metrics_test.dart @@ -1,6 +1,5 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/telemetry/metric/default_metrics.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:test/test.dart'; import '../../test_utils.dart'; diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart index 1f81288c43..46fefd0299 100644 --- a/packages/dart/test/telemetry/processing/processor_test.dart +++ b/packages/dart/test/telemetry/processing/processor_test.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/telemetry/processing/processor.dart'; import 'package:test/test.dart'; diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index ce53247c45..5c07b1648a 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -7,7 +7,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry/src/logs_enricher_integration.dart'; import 'package:sentry/src/sentry_tracer.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/integrations/load_contexts_integration.dart'; diff --git a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart index 190625966d..460c0c0c0a 100644 --- a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart +++ b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart @@ -2,7 +2,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/integrations/replay_telemetry_integration.dart'; diff --git a/packages/flutter/test/mocks.dart b/packages/flutter/test/mocks.dart index 2230a545c9..7c59da2b7f 100644 --- a/packages/flutter/test/mocks.dart +++ b/packages/flutter/test/mocks.dart @@ -8,7 +8,6 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry/src/platform/platform.dart'; import 'package:sentry/src/sentry_tracer.dart'; -import 'package:sentry/src/telemetry/metric/metric.dart'; import 'package:sentry/src/telemetry/processing/processor.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/frames_tracking/sentry_delayed_frames_tracker.dart'; From 0d6b55d45a6354ce86d0006e4633a35aae642512 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 22 Jan 2026 13:05:15 +0100 Subject: [PATCH 74/79] Fix review --- .../dart/lib/src/telemetry/metric/metric.dart | 4 ++-- packages/dart/test/sentry_test.dart | 22 +------------------ 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/packages/dart/lib/src/telemetry/metric/metric.dart b/packages/dart/lib/src/telemetry/metric/metric.dart index da3ed4c90c..35cf843f50 100644 --- a/packages/dart/lib/src/telemetry/metric/metric.dart +++ b/packages/dart/lib/src/telemetry/metric/metric.dart @@ -51,8 +51,8 @@ abstract class SentryMetric { 'type': type, 'name': name, 'value': value, - 'trace_id': traceId, - if (spanId != null) 'span_id': spanId, + 'trace_id': traceId.toString(), + if (spanId != null) 'span_id': spanId.toString(), if (unit != null) 'unit': unit, if (attributes.isNotEmpty) 'attributes': attributes.map((k, v) => MapEntry(k, v.toJson())), diff --git a/packages/dart/test/sentry_test.dart b/packages/dart/test/sentry_test.dart index 55cf4bc3b4..ab688fdfd7 100644 --- a/packages/dart/test/sentry_test.dart +++ b/packages/dart/test/sentry_test.dart @@ -377,27 +377,7 @@ void main() { ); }, onPlatform: {'vm': Skip()}); - test('should add feature flag FeatureFlagsIntegration', () async { - await Sentry.init( - options: defaultTestOptions(), - (options) => options.dsn = fakeDsn, - ); - - await Sentry.addFeatureFlag('foo', true); - - expect( - Sentry.currentHub.scope.contexts[SentryFeatureFlags.type]?.values.first - .flag, - equals('foo'), - ); - expect( - Sentry.currentHub.scope.contexts[SentryFeatureFlags.type]?.values.first - .result, - equals(true), - ); - }); - - test('should add feature flag $MetricsSetupIntegration', () async { + test('should add feature flag $FeatureFlagsIntegration', () async { await Sentry.init( options: defaultTestOptions(), (options) => options.dsn = fakeDsn, From b1a869025295b9b05159a39691d8c4ccc8b70845 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 22 Jan 2026 13:08:17 +0100 Subject: [PATCH 75/79] Fix asserts --- packages/dart/test/telemetry/metric/metric_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dart/test/telemetry/metric/metric_test.dart b/packages/dart/test/telemetry/metric/metric_test.dart index b4558fc188..472c5772c1 100644 --- a/packages/dart/test/telemetry/metric/metric_test.dart +++ b/packages/dart/test/telemetry/metric/metric_test.dart @@ -24,8 +24,8 @@ void main() { expect(json['type'], 'counter'); expect(json['name'], 'button_clicks'); expect(json['value'], 5); - expect(json['trace_id'], traceId); - expect(json['span_id'], spanId); + expect(json['trace_id'], traceId.toString()); + expect(json['span_id'], spanId.toString()); expect(json['unit'], 'click'); expect(json['attributes']['key'], {'type': 'string', 'value': 'value'}); }); From d2290ea8d7d717ef0f2fcb80eb1e0e85f42800c2 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 22 Jan 2026 14:00:13 +0100 Subject: [PATCH 76/79] Fix analyze --- packages/logging/lib/src/logging_integration.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/logging/lib/src/logging_integration.dart b/packages/logging/lib/src/logging_integration.dart index 948faf1df4..061e699c69 100644 --- a/packages/logging/lib/src/logging_integration.dart +++ b/packages/logging/lib/src/logging_integration.dart @@ -1,3 +1,5 @@ +// ignore_for_file: invalid_use_of_internal_member + import 'dart:async'; import 'package:meta/meta.dart'; From 4fbe49329e1f7b71acb0d4bad30a4b1a08eabc79 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 22 Jan 2026 14:32:19 +0100 Subject: [PATCH 77/79] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b46c7884b..66e8db206d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - `Sentry.metrics.count(...)` - `Sentry.metrics.distribution(...)` +### Enhancements + +- Refactor Logging API to be consistent with Metrics ([#3463](https://github.com/getsentry/sentry-dart/pull/3463)) + ### Dependencies - Bump Android SDK from v8.28.0 to v8.30.0 ([#3451](https://github.com/getsentry/sentry-dart/pull/3451)) From 36e26c0fd16b2138cca9d356184f1dea91ade18f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 22 Jan 2026 14:34:22 +0100 Subject: [PATCH 78/79] Update test --- packages/logging/test/logging_integration_test.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/logging/test/logging_integration_test.dart b/packages/logging/test/logging_integration_test.dart index fb96a6cecd..1147f6099c 100644 --- a/packages/logging/test/logging_integration_test.dart +++ b/packages/logging/test/logging_integration_test.dart @@ -492,7 +492,6 @@ class MockSentryLogger implements SentryLogger { Future trace( String body, { Map? attributes, - Scope? scope, }) async { traceCalls.add(MockLogCall(body, attributes)); } @@ -501,7 +500,6 @@ class MockSentryLogger implements SentryLogger { Future debug( String body, { Map? attributes, - Scope? scope, }) async { debugCalls.add(MockLogCall(body, attributes)); } @@ -510,7 +508,6 @@ class MockSentryLogger implements SentryLogger { Future info( String body, { Map? attributes, - Scope? scope, }) async { infoCalls.add(MockLogCall(body, attributes)); } @@ -519,7 +516,6 @@ class MockSentryLogger implements SentryLogger { Future warn( String body, { Map? attributes, - Scope? scope, }) async { warnCalls.add(MockLogCall(body, attributes)); } @@ -528,7 +524,6 @@ class MockSentryLogger implements SentryLogger { Future error( String body, { Map? attributes, - Scope? scope, }) async { errorCalls.add(MockLogCall(body, attributes)); } @@ -537,7 +532,6 @@ class MockSentryLogger implements SentryLogger { Future fatal( String body, { Map? attributes, - Scope? scope, }) async { fatalCalls.add(MockLogCall(body, attributes)); } From 4e9efadb8f2a4cae273d123637e3e6405eeef191 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 22 Jan 2026 14:35:20 +0100 Subject: [PATCH 79/79] Update test --- ...art => logger_setup_integration_test.dart} | 78 +++++++++++++------ 1 file changed, 54 insertions(+), 24 deletions(-) rename packages/dart/test/telemetry/log/{logs_setup_integration_test.dart => logger_setup_integration_test.dart} (63%) diff --git a/packages/dart/test/telemetry/log/logs_setup_integration_test.dart b/packages/dart/test/telemetry/log/logger_setup_integration_test.dart similarity index 63% rename from packages/dart/test/telemetry/log/logs_setup_integration_test.dart rename to packages/dart/test/telemetry/log/logger_setup_integration_test.dart index 6988e0bb6d..24ef14af52 100644 --- a/packages/dart/test/telemetry/log/logs_setup_integration_test.dart +++ b/packages/dart/test/telemetry/log/logger_setup_integration_test.dart @@ -84,28 +84,40 @@ class Fixture { class _CustomSentryLogger implements SentryLogger { @override - FutureOr trace(String body, - {Map? attributes, Scope? scope}) {} + FutureOr trace( + String body, { + Map? attributes, + }) {} @override - FutureOr debug(String body, - {Map? attributes, Scope? scope}) {} + FutureOr debug( + String body, { + Map? attributes, + }) {} @override - FutureOr info(String body, - {Map? attributes, Scope? scope}) {} + FutureOr info( + String body, { + Map? attributes, + }) {} @override - FutureOr warn(String body, - {Map? attributes, Scope? scope}) {} + FutureOr warn( + String body, { + Map? attributes, + }) {} @override - FutureOr error(String body, - {Map? attributes, Scope? scope}) {} + FutureOr error( + String body, { + Map? attributes, + }) {} @override - FutureOr fatal(String body, - {Map? attributes, Scope? scope}) {} + FutureOr fatal( + String body, { + Map? attributes, + }) {} @override SentryLoggerFormatter get fmt => _CustomSentryLoggerFormatter(); @@ -113,26 +125,44 @@ class _CustomSentryLogger implements SentryLogger { class _CustomSentryLoggerFormatter implements SentryLoggerFormatter { @override - FutureOr trace(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr trace( + String templateBody, + List arguments, { + Map? attributes, + }) {} @override - FutureOr debug(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr debug( + String templateBody, + List arguments, { + Map? attributes, + }) {} @override - FutureOr info(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr info( + String templateBody, + List arguments, { + Map? attributes, + }) {} @override - FutureOr warn(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr warn( + String templateBody, + List arguments, { + Map? attributes, + }) {} @override - FutureOr error(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr error( + String templateBody, + List arguments, { + Map? attributes, + }) {} @override - FutureOr fatal(String templateBody, List arguments, - {Map? attributes, Scope? scope}) {} + FutureOr fatal( + String templateBody, + List arguments, { + Map? attributes, + }) {} }