diff --git a/CHANGELOG.md b/CHANGELOG.md index 12be5b4fdb..191e95bda3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +### Features + +- Logs: Models & Envelopes ([#2916](https://github.com/getsentry/sentry-dart/pull/2916)) +- Logs: Integrate in Sentry Client ([#2920](https://github.com/getsentry/sentry-dart/pull/2920)) +- [Structured Logs]: Buffering and Flushing of Logs ([#2930](https://github.com/getsentry/sentry-dart/pull/2930)) +- [Structured Logs]: Expose Public API ([#2940](https://github.com/getsentry/sentry-dart/pull/2940)) + - The old `SentryLogger` has been renamed to `SdkLogCallback` and can be accessed through `options.log` now. +- [Structured Logs]: Send client reports for dropped logs ([#2942](https://github.com/getsentry/sentry-dart/pull/2942)) ## 9.0.0-RC.1 ### Fixes diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index eea87f2ec6..63b39a7c6b 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -60,3 +60,4 @@ 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'; diff --git a/dart/lib/src/client_reports/discarded_event.dart b/dart/lib/src/client_reports/discarded_event.dart index df0244c63f..f3ab3d932a 100644 --- a/dart/lib/src/client_reports/discarded_event.dart +++ b/dart/lib/src/client_reports/discarded_event.dart @@ -64,6 +64,8 @@ extension _DataCategoryExtension on DataCategory { return 'security'; case DataCategory.unknown: return 'unknown'; + case DataCategory.logItem: + return 'log_item'; case DataCategory.metricBucket: return 'metric_bucket'; } diff --git a/dart/lib/src/diagnostic_logger.dart b/dart/lib/src/diagnostic_log.dart similarity index 80% rename from dart/lib/src/diagnostic_logger.dart rename to dart/lib/src/diagnostic_log.dart index 39d1d5f295..2379dafbc1 100644 --- a/dart/lib/src/diagnostic_logger.dart +++ b/dart/lib/src/diagnostic_log.dart @@ -1,12 +1,12 @@ import 'protocol.dart'; import 'sentry_options.dart'; -class DiagnosticLogger { +class DiagnosticLog { final SentryOptions _options; - final SentryLogger _logger; - SentryLogger get logger => _logger; + final SdkLogCallback _logger; + SdkLogCallback get logger => _logger; - DiagnosticLogger(this._logger, this._options); + DiagnosticLog(this._logger, this._options); void log( SentryLevel level, diff --git a/dart/lib/src/event_processor/deduplication_event_processor.dart b/dart/lib/src/event_processor/deduplication_event_processor.dart index 8706082cf5..48661257a6 100644 --- a/dart/lib/src/event_processor/deduplication_event_processor.dart +++ b/dart/lib/src/event_processor/deduplication_event_processor.dart @@ -32,7 +32,7 @@ class DeduplicationEventProcessor implements EventProcessor { } if (!_options.enableDeduplication) { - _options.logger(SentryLevel.debug, 'Deduplication is disabled'); + _options.log(SentryLevel.debug, 'Deduplication is disabled'); return event; } return _deduplicate(event); @@ -52,7 +52,7 @@ class DeduplicationEventProcessor implements EventProcessor { final exceptionHashCode = exception.hashCode; if (_exceptionToDeduplicate.contains(exceptionHashCode)) { - _options.logger( + _options.log( SentryLevel.info, 'Duplicated exception detected. ' 'Event ${event.eventId} will be discarded.', diff --git a/dart/lib/src/event_processor/enricher/io_platform_memory.dart b/dart/lib/src/event_processor/enricher/io_platform_memory.dart index 6fc0fb66b6..89a0a31ee7 100644 --- a/dart/lib/src/event_processor/enricher/io_platform_memory.dart +++ b/dart/lib/src/event_processor/enricher/io_platform_memory.dart @@ -87,7 +87,7 @@ class PlatformMemory { return result.stdout.toString(); } } catch (e) { - options.logger(SentryLevel.warning, "Failed to run process: $e"); + options.log(SentryLevel.warning, "Failed to run process: $e"); if (options.automatedTestMode) { rethrow; } diff --git a/dart/lib/src/event_processor/exception/io_exception_event_processor.dart b/dart/lib/src/event_processor/exception/io_exception_event_processor.dart index 40aa5fa9a9..1b698d5c7a 100644 --- a/dart/lib/src/event_processor/exception/io_exception_event_processor.dart +++ b/dart/lib/src/event_processor/exception/io_exception_event_processor.dart @@ -71,7 +71,7 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor { var uri = Uri.parse(address.host); request = SentryRequest.fromUri(uri: uri); } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'Could not parse ${address.host} to Uri', exception: exception, diff --git a/dart/lib/src/event_processor/run_event_processors.dart b/dart/lib/src/event_processor/run_event_processors.dart index 422288c1f0..98af52cde3 100644 --- a/dart/lib/src/event_processor/run_event_processors.dart +++ b/dart/lib/src/event_processor/run_event_processors.dart @@ -25,7 +25,7 @@ Future runEventProcessors( final e = processor.apply(processedEvent!, hint); processedEvent = e is Future ? await e : e; } catch (exception, stackTrace) { - options.logger( + options.log( SentryLevel.error, 'An exception occurred while processing event by a processor', exception: exception, @@ -47,7 +47,7 @@ Future runEventProcessors( count: spanCountBeforeEventProcessors + 1, ); } - options.logger(SentryLevel.debug, 'Event was dropped by a processor'); + options.log(SentryLevel.debug, 'Event was dropped by a processor'); break; } else if (event is SentryTransaction && processedEvent is SentryTransaction) { diff --git a/dart/lib/src/http_client/io_client_provider.dart b/dart/lib/src/http_client/io_client_provider.dart index b8ec366d06..6e611cbb97 100644 --- a/dart/lib/src/http_client/io_client_provider.dart +++ b/dart/lib/src/http_client/io_client_provider.dart @@ -36,13 +36,13 @@ class IoClientProvider implements ClientProvider { } final pac = proxy.toPacString(); if (proxy.type == SentryProxyType.socks) { - options.logger( + options.log( SentryLevel.warning, "Setting proxy '$pac' is not supported.", ); return Client(); } - options.logger( + options.log( SentryLevel.info, "Setting proxy '$pac'", ); diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 2b9590206f..0c2f958742 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -79,7 +79,7 @@ class Hub { var sentryId = SentryId.empty(); if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'captureEvent' call is a no-op.", ); @@ -105,7 +105,7 @@ class Hub { hint: hint, ); } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'Error while capturing event with id: ${event.eventId}', exception: exception, @@ -131,12 +131,12 @@ class Hub { var sentryId = SentryId.empty(); if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'captureException' call is a no-op.", ); } else if (throwable == null) { - _options.logger( + _options.log( SentryLevel.warning, 'captureException called with null parameter.', ); @@ -167,7 +167,7 @@ class Hub { hint: hint, ); } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'Error while capturing exception', exception: exception, @@ -196,12 +196,12 @@ class Hub { var sentryId = SentryId.empty(); if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'captureMessage' call is a no-op.", ); } else if (message == null) { - _options.logger( + _options.log( SentryLevel.warning, 'captureMessage called with null parameter.', ); @@ -225,7 +225,7 @@ class Hub { hint: hint, ); } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'Error while capturing message with id: $message', exception: exception, @@ -250,7 +250,7 @@ class Hub { var sentryId = SentryId.empty(); if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'captureFeedback' call is a no-op.", ); @@ -271,7 +271,7 @@ class Hub { scope: scope, ); } catch (exception, stacktrace) { - _options.logger( + _options.log( SentryLevel.error, 'Error while capturing feedback', exception: exception, @@ -282,6 +282,38 @@ class Hub { return sentryId; } + FutureOr captureLog(SentryLog log) async { + if (!_isEnabled) { + _options.log( + SentryLevel.warning, + "Instance is disabled and this 'captureFeedback' 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.captureLog( + log, + scope: scope, + ); + } catch (exception, stacktrace) { + _options.log( + SentryLevel.error, + 'Error while capturing log', + exception: exception, + stackTrace: stacktrace, + ); + } + } + } + FutureOr _cloneAndRunWithScope( Scope scope, ScopeCallback? withScope) async { if (withScope != null) { @@ -292,7 +324,7 @@ class Hub { await s; } } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'Exception in withScope callback.', exception: exception, @@ -309,7 +341,7 @@ class Hub { /// Adds a breacrumb to the current Scope Future addBreadcrumb(Breadcrumb crumb, {Hint? hint}) async { if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'addBreadcrumb' call is a no-op.", ); @@ -322,13 +354,13 @@ class Hub { /// Binds a different client to the hub void bindClient(SentryClient client) { if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'bindClient' call is a no-op.", ); } else { final item = _peek(); - _options.logger(SentryLevel.debug, 'New client bound to scope.'); + _options.log(SentryLevel.debug, 'New client bound to scope.'); item.client = client; } } @@ -336,7 +368,7 @@ class Hub { /// Clones the Hub Hub clone() { if (!_isEnabled) { - _options.logger(SentryLevel.warning, 'Disabled Hub cloned.'); + _options.log(SentryLevel.warning, 'Disabled Hub cloned.'); } final clone = Hub(_options); for (final item in _stack) { @@ -348,7 +380,7 @@ class Hub { /// Flushes out the queue for up to timeout seconds and disable the Hub. Future close() async { if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'close' call is a no-op.", ); @@ -366,7 +398,7 @@ class Hub { try { item.client.close(); } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'Error while closing the Hub', exception: exception, @@ -384,7 +416,7 @@ class Hub { /// Configures the scope through the callback. FutureOr configureScope(ScopeCallback callback) async { if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'configureScope' call is a no-op.", ); @@ -397,7 +429,7 @@ class Hub { await result; } } catch (err) { - _options.logger( + _options.log( SentryLevel.error, "Error in the 'configureScope' callback, error: $err", ); @@ -449,7 +481,7 @@ class Hub { OnTransactionFinish? onFinish, }) { if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'startTransaction' call is a no-op.", ); @@ -503,7 +535,7 @@ class Hub { ISentrySpan? getSpan() { ISentrySpan? span; if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'getSpan' call is a no-op.", ); @@ -525,17 +557,17 @@ class Hub { var sentryId = SentryId.empty(); if (!_isEnabled) { - _options.logger( + _options.log( SentryLevel.warning, "Instance is disabled and this 'captureTransaction' call is a no-op.", ); } else if (!_options.isTracingEnabled()) { - _options.logger( + _options.log( SentryLevel.info, "Tracing is disabled and this 'captureTransaction' call is a no-op.", ); } else if (!transaction.finished) { - _options.logger( + _options.log( SentryLevel.warning, 'Capturing unfinished transaction: ${transaction.eventId}', ); @@ -552,7 +584,7 @@ class Hub { DataCategory.span, count: transaction.spans.length + 1, ); - _options.logger( + _options.log( SentryLevel.warning, 'Transaction ${transaction.eventId} was dropped due to sampling decision.', ); @@ -565,7 +597,7 @@ class Hub { hint: hint, ); } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'Error while capturing transaction with id: ${transaction.eventId}', exception: exception, @@ -647,7 +679,7 @@ class _WeakMap { _expando[throwable] = MapEntry(span, transaction); } } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.info, 'Throwable type: ${throwable.runtimeType} is not supported for associating errors to a transaction.', exception: exception, @@ -667,7 +699,7 @@ class _WeakMap { try { return _expando[throwable] as MapEntry?; } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.info, 'Throwable type: ${throwable.runtimeType} is not supported for associating errors to a transaction.', exception: exception, diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 2446d967b2..964356ce17 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -194,4 +194,7 @@ class HubAdapter implements Hub { hint: hint, withScope: withScope, ); + + @override + FutureOr captureLog(SentryLog log) => Sentry.currentHub.captureLog(log); } diff --git a/dart/lib/src/load_dart_debug_images_integration.dart b/dart/lib/src/load_dart_debug_images_integration.dart index 8043764dba..8cba0f5456 100644 --- a/dart/lib/src/load_dart_debug_images_integration.dart +++ b/dart/lib/src/load_dart_debug_images_integration.dart @@ -61,7 +61,7 @@ class LoadImageIntegrationEventProcessor implements EventProcessor { try { _debugImage ??= createDebugImage(stackTrace); } catch (e, stack) { - _options.logger( + _options.log( SentryLevel.info, "Couldn't add Dart debug image to event. The event will still be reported.", exception: e, @@ -77,7 +77,7 @@ class LoadImageIntegrationEventProcessor implements EventProcessor { @visibleForTesting DebugImage? createDebugImage(SentryStackTrace stackTrace) { if (stackTrace.buildId == null || stackTrace.baseAddr == null) { - _options.logger(SentryLevel.warning, + _options.log(SentryLevel.warning, 'Cannot create DebugImage without a build ID and image base address.'); return null; } @@ -105,7 +105,7 @@ class LoadImageIntegrationEventProcessor implements EventProcessor { debugId = _formatHexToUuid(stackTrace.buildId!); codeFile = 'App.Framework/App'; } else { - _options.logger( + _options.log( SentryLevel.warning, 'Unsupported platform for creating Dart debug images.', ); diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 1252f9f4c0..07e020b086 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -93,6 +93,9 @@ class NoOpHub implements Hub { }) async => SentryId.empty(); + @override + FutureOr captureLog(SentryLog log) async {} + @override ISentrySpan startTransaction( String name, diff --git a/dart/lib/src/noop_log_batcher.dart b/dart/lib/src/noop_log_batcher.dart new file mode 100644 index 0000000000..7d5c94523d --- /dev/null +++ b/dart/lib/src/noop_log_batcher.dart @@ -0,0 +1,12 @@ +import 'dart:async'; + +import 'sentry_log_batcher.dart'; +import 'protocol/sentry_log.dart'; + +class NoopLogBatcher implements SentryLogBatcher { + @override + FutureOr addLog(SentryLog log) {} + + @override + Future flush() async {} +} diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 39bd728dd9..d071edbf16 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -66,4 +66,7 @@ class NoOpSentryClient implements SentryClient { Future captureFeedback(SentryFeedback feedback, {Scope? scope, Hint? hint}) async => SentryId.empty(); + + @override + FutureOr captureLog(SentryLog log, {Scope? scope}) async {} } diff --git a/dart/lib/src/protocol.dart b/dart/lib/src/protocol.dart index 4127259d77..07b7e0b30b 100644 --- a/dart/lib/src/protocol.dart +++ b/dart/lib/src/protocol.dart @@ -41,3 +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 'protocol/sentry_log_attribute.dart'; diff --git a/dart/lib/src/protocol/sentry_log.dart b/dart/lib/src/protocol/sentry_log.dart new file mode 100644 index 0000000000..55c0174d90 --- /dev/null +++ b/dart/lib/src/protocol/sentry_log.dart @@ -0,0 +1,35 @@ +import 'sentry_id.dart'; +import 'sentry_log_level.dart'; +import 'sentry_log_attribute.dart'; + +class SentryLog { + DateTime timestamp; + SentryId traceId; + SentryLogLevel level; + String body; + Map attributes; + int? severityNumber; + + /// The traceId is initially an empty default value and is populated during event processing; + /// by the time processing completes, it is guaranteed to be a valid non-empty trace id. + SentryLog({ + required this.timestamp, + SentryId? traceId, + required this.level, + required this.body, + required this.attributes, + this.severityNumber, + }) : traceId = traceId ?? SentryId.empty(); + + Map toJson() { + return { + 'timestamp': timestamp.toIso8601String(), + 'trace_id': traceId.toString(), + 'level': level.value, + 'body': body, + 'attributes': + attributes.map((key, value) => MapEntry(key, value.toJson())), + 'severity_number': severityNumber ?? level.toSeverityNumber(), + }; + } +} diff --git a/dart/lib/src/protocol/sentry_log_attribute.dart b/dart/lib/src/protocol/sentry_log_attribute.dart new file mode 100644 index 0000000000..63ac85eb87 --- /dev/null +++ b/dart/lib/src/protocol/sentry_log_attribute.dart @@ -0,0 +1,30 @@ +class SentryLogAttribute { + final dynamic value; + final String type; + + const SentryLogAttribute._(this.value, this.type); + + factory SentryLogAttribute.string(String value) { + return SentryLogAttribute._(value, 'string'); + } + + factory SentryLogAttribute.bool(bool value) { + return SentryLogAttribute._(value, 'boolean'); + } + + factory SentryLogAttribute.int(int value) { + return SentryLogAttribute._(value, 'integer'); + } + + factory SentryLogAttribute.double(double value) { + return SentryLogAttribute._(value, 'double'); + } + + // In the future the SDK will also support List, List, List, List values. + Map toJson() { + return { + 'value': value, + 'type': type, + }; + } +} diff --git a/dart/lib/src/protocol/sentry_log_level.dart b/dart/lib/src/protocol/sentry_log_level.dart new file mode 100644 index 0000000000..aac0b386bc --- /dev/null +++ b/dart/lib/src/protocol/sentry_log_level.dart @@ -0,0 +1,28 @@ +enum SentryLogLevel { + trace('trace'), + debug('debug'), + info('info'), + warn('warn'), + error('error'), + fatal('fatal'); + + final String value; + const SentryLogLevel(this.value); + + int toSeverityNumber() { + switch (this) { + case SentryLogLevel.trace: + return 1; + case SentryLogLevel.debug: + return 5; + case SentryLogLevel.info: + return 9; + case SentryLogLevel.warn: + return 13; + case SentryLogLevel.error: + return 17; + case SentryLogLevel.fatal: + return 21; + } + } +} diff --git a/dart/lib/src/protocol/sentry_span.dart b/dart/lib/src/protocol/sentry_span.dart index fb650d0f4a..8c1bec2b9d 100644 --- a/dart/lib/src/protocol/sentry_span.dart +++ b/dart/lib/src/protocol/sentry_span.dart @@ -62,7 +62,7 @@ class SentrySpan extends ISentrySpan { if (endTimestamp == null) { endTimestamp = _hub.options.clock(); } else if (endTimestamp.isBefore(_startTimestamp)) { - _hub.options.logger( + _hub.options.log( SentryLevel.warning, 'End timestamp ($endTimestamp) cannot be before start timestamp ($_startTimestamp)', ); @@ -137,7 +137,7 @@ class SentrySpan extends ISentrySpan { } if (startTimestamp?.isBefore(_startTimestamp) ?? false) { - _hub.options.logger( + _hub.options.log( SentryLevel.warning, "Start timestamp ($startTimestamp) cannot be before parent span's start timestamp ($_startTimestamp). Returning NoOpSpan.", ); @@ -226,7 +226,7 @@ class SentrySpan extends ISentrySpan { SentryMeasurementUnit? unit, }) { if (finished) { - _hub.options.logger(SentryLevel.debug, + _hub.options.log(SentryLevel.debug, "The span is already finished. Measurement $name cannot be set"); return; } diff --git a/dart/lib/src/recursive_exception_cause_extractor.dart b/dart/lib/src/recursive_exception_cause_extractor.dart index 360044f0bc..b292f01a56 100644 --- a/dart/lib/src/recursive_exception_cause_extractor.dart +++ b/dart/lib/src/recursive_exception_cause_extractor.dart @@ -37,7 +37,7 @@ class RecursiveExceptionCauseExtractor { currentExceptionCause = extractor?.cause(extractionSourceSource); currentException = currentExceptionCause?.exception; } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'An exception occurred while extracting exception cause', exception: exception, diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 485dbe50d0..81e65176eb 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -184,14 +184,14 @@ class Scope { hint, ); if (processedBreadcrumb == null) { - _options.logger( + _options.log( SentryLevel.info, 'Breadcrumb was dropped by beforeBreadcrumb', ); return null; } } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'The BeforeBreadcrumb callback threw an exception', exception: exception, diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 593f86c959..72cfb0f24b 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -27,6 +27,7 @@ import 'tracing.dart'; import 'transport/data_category.dart'; import 'transport/task_queue.dart'; import 'feature_flags_integration.dart'; +import 'sentry_logger.dart'; /// Configuration options callback typedef OptionsConfiguration = FutureOr Function(SentryOptions); @@ -62,11 +63,11 @@ class Sentry { } _taskQueue = DefaultTaskQueue( sentryOptions.maxQueueSize, - sentryOptions.logger, + sentryOptions.log, sentryOptions.recorder, ); } catch (exception, stackTrace) { - sentryOptions.logger( + sentryOptions.log( SentryLevel.error, 'Error in options configuration.', exception: exception, @@ -97,7 +98,7 @@ class Sentry { if (options.runtimeChecker.isDebugMode()) { options.debug = true; - options.logger( + options.log( SentryLevel.debug, 'Debug mode is enabled: Application is running in a debug environment.', ); @@ -146,7 +147,7 @@ class Sentry { RunZonedGuardedOnError? runZonedGuardedOnError, ) async { if (isEnabled) { - options.logger( + options.log( SentryLevel.warning, 'Sentry has been already initialized. Previous configuration will be overwritten.', ); @@ -375,7 +376,7 @@ class Sentry { .firstOrNull; if (featureFlagsIntegration == null) { - currentHub.options.logger( + currentHub.options.log( SentryLevel.warning, '$FeatureFlagsIntegration not found. Make sure Sentry is initialized before accessing the addFeatureFlag API.', ); @@ -428,4 +429,6 @@ class Sentry { zoneValues: zoneValues, zoneSpecification: zoneSpecification, ); + + static SentryLogger get logger => currentHub.options.logger; } diff --git a/dart/lib/src/sentry_baggage.dart b/dart/lib/src/sentry_baggage.dart index 8eedee0dc9..710725eff3 100644 --- a/dart/lib/src/sentry_baggage.dart +++ b/dart/lib/src/sentry_baggage.dart @@ -13,11 +13,11 @@ class SentryBaggage { SentryBaggage( this._keyValues, { - this.logger, + this.log, }); final Map _keyValues; - final SentryLogger? logger; + final SdkLogCallback? log; String toHeaderString() { final buffer = StringBuffer(); @@ -26,7 +26,7 @@ class SentryBaggage { for (final entry in _keyValues.entries) { if (listMemberCount >= _maxListMember) { - logger?.call( + log?.call( SentryLevel.info, 'Baggage key ${entry.key} dropped because of max list member.', ); @@ -40,7 +40,7 @@ class SentryBaggage { final totalLengthIfValueAdded = buffer.length + encodedKeyValue.length; if (totalLengthIfValueAdded >= _maxChars) { - logger?.call( + log?.call( SentryLevel.info, 'Baggage key ${entry.key} dropped because of max baggage chars.', ); @@ -51,7 +51,7 @@ class SentryBaggage { buffer.write(encodedKeyValue); separator = ','; } catch (exception, stackTrace) { - logger?.call( + log?.call( SentryLevel.error, 'Failed to parse the baggage key ${entry.key}.', exception: exception, @@ -66,31 +66,31 @@ class SentryBaggage { factory SentryBaggage.fromHeaderList( List headerValues, { - SentryLogger? logger, + SdkLogCallback? log, }) { final keyValues = {}; for (final headerValue in headerValues) { final keyValuesToAdd = _extractKeyValuesFromBaggageString( headerValue, - logger: logger, + log: log, ); keyValues.addAll(keyValuesToAdd); } - return SentryBaggage(keyValues, logger: logger); + return SentryBaggage(keyValues, log: log); } factory SentryBaggage.fromHeader( String headerValue, { - SentryLogger? logger, + SdkLogCallback? log, }) { final keyValues = _extractKeyValuesFromBaggageString( headerValue, - logger: logger, + log: log, ); - return SentryBaggage(keyValues, logger: logger); + return SentryBaggage(keyValues, log: log); } @internal @@ -114,7 +114,7 @@ class SentryBaggage { static Map _extractKeyValuesFromBaggageString( String headerValue, { - SentryLogger? logger, + SdkLogCallback? log, }) { final keyValues = {}; @@ -130,7 +130,7 @@ class SentryBaggage { final value = _urlDecode(keyAndValue.last.trim()); keyValues[key] = value; } catch (exception, stackTrace) { - logger?.call( + log?.call( SentryLevel.error, 'Failed to parse the baggage entry $keyAndValue.', exception: exception, diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index a3198e7524..3b1b1de2db 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -27,6 +27,7 @@ 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,6 +76,9 @@ class SentryClient { if (enableFlutterSpotlight) { options.transport = SpotlightHttpTransport(options, options.transport); } + if (options.enableLogs) { + options.logBatcher = SentryLogBatcher(options); + } return SentryClient._(options); } @@ -90,7 +94,7 @@ class SentryClient { Hint? hint, }) async { if (_isIgnoredError(event)) { - _options.logger( + _options.log( SentryLevel.debug, 'Error was ignored as specified in the ignoredErrors options.', ); @@ -100,7 +104,7 @@ class SentryClient { } if (_options.containsIgnoredExceptionForType(event.throwable)) { - _options.logger( + _options.log( SentryLevel.debug, 'Event was dropped as the exception ${event.throwable.runtimeType.toString()} is ignored.', ); @@ -112,7 +116,7 @@ class SentryClient { if (_sampleRate() && event.type != 'feedback') { _options.recorder .recordLostEvent(DiscardReason.sampleRate, _getCategory(event)); - _options.logger( + _options.log( SentryLevel.debug, 'Event ${event.eventId.toString()} was dropped due to sampling decision.', ); @@ -127,7 +131,7 @@ class SentryClient { if (scope != null) { preparedEvent = await scope.applyToEvent(preparedEvent, hint); } else { - _options.logger( + _options.log( SentryLevel.debug, 'No scope to apply on event was provided'); } @@ -179,7 +183,7 @@ class SentryClient { if (traceContext == null) { if (scope != null) { scope.propagationContext.baggage ??= - SentryBaggage({}, logger: _options.logger) + SentryBaggage({}, log: _options.log) ..setValuesFromScope(scope, _options); traceContext = SentryTraceContextHeader.fromBaggage( scope.propagationContext.baggage!); @@ -389,7 +393,7 @@ class SentryClient { preparedTransaction = await scope.applyToEvent(preparedTransaction, hint) as SentryTransaction?; } else { - _options.logger( + _options.log( SentryLevel.debug, 'No scope to apply on transaction was provided'); } @@ -411,7 +415,7 @@ class SentryClient { } if (_isIgnoredTransaction(preparedTransaction)) { - _options.logger( + _options.log( SentryLevel.debug, 'Transaction was ignored as specified in the ignoredTransactions options.', ); @@ -485,6 +489,78 @@ class SentryClient { ); } + @internal + FutureOr captureLog( + SentryLog log, { + Scope? scope, + }) async { + if (!_options.enableLogs) { + return; + } + + log.attributes['sentry.sdk.name'] = SentryLogAttribute.string( + _options.sdk.name, + ); + log.attributes['sentry.sdk.version'] = SentryLogAttribute.string( + _options.sdk.version, + ); + final environment = _options.environment; + if (environment != null) { + log.attributes['sentry.environment'] = SentryLogAttribute.string( + environment, + ); + } + final release = _options.release; + if (release != null) { + log.attributes['sentry.release'] = SentryLogAttribute.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'] = SentryLogAttribute.string( + span.context.spanId.toString(), + ); + } + + 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) { + _options.logBatcher.addLog(processedLog); + } else { + _options.recorder.recordLostEvent( + DiscardReason.beforeSend, + DataCategory.logItem, + ); + } + } + void close() { _options.httpClient.close(); } @@ -527,7 +603,7 @@ class SentryClient { } } } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'The $beforeSendName callback threw an exception', exception: exception, @@ -546,7 +622,7 @@ class SentryClient { _options.recorder.recordLostEvent(discardReason, DataCategory.span, count: spanCountBeforeCallback + 1); } - _options.logger( + _options.log( SentryLevel.debug, '${event.runtimeType} was dropped by $beforeSendName callback', ); @@ -587,7 +663,7 @@ class SentryClient { await result; } } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'Error while running beforeSendEvent observer', exception: exception, diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index 10d4b3a085..83cccb14b7 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -81,6 +81,21 @@ class SentryEnvelope { ); } + factory SentryEnvelope.fromLogs( + List items, + SdkVersion sdkVersion, + ) { + return SentryEnvelope( + SentryEnvelopeHeader( + null, + sdkVersion, + ), + [ + SentryEnvelopeItem.fromLogs(items), + ], + ); + } + /// Stream binary data representation of `Envelope` file encoded. Stream> envelopeStream(SentryOptions options) async* { yield utf8JsonEncoder.convert(header.toJson()); diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index bfe4d818eb..06261380db 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -63,6 +63,21 @@ class SentryEnvelopeItem { ); } + factory SentryEnvelopeItem.fromLogs(List items) { + final payload = { + 'items': items.map((e) => e.toJson()).toList(), + }; + return SentryEnvelopeItem( + SentryEnvelopeItemHeader( + SentryItemType.log, + itemCount: items.length, + contentType: 'application/vnd.sentry.items.log+json', + ), + () => utf8JsonEncoder.convert(payload), + originalObject: payload, + ); + } + /// Header with info about type and length of data in bytes. final SentryEnvelopeItemHeader header; diff --git a/dart/lib/src/sentry_envelope_item_header.dart b/dart/lib/src/sentry_envelope_item_header.dart index c1e742cfd1..31ff3d8602 100644 --- a/dart/lib/src/sentry_envelope_item_header.dart +++ b/dart/lib/src/sentry_envelope_item_header.dart @@ -2,6 +2,7 @@ class SentryEnvelopeItemHeader { SentryEnvelopeItemHeader( this.type, { + this.itemCount, this.contentType, this.fileName, this.attachmentType, @@ -10,6 +11,8 @@ class SentryEnvelopeItemHeader { /// Type of encoded data. final String type; + final int? itemCount; + final String? contentType; final String? fileName; @@ -19,6 +22,7 @@ class SentryEnvelopeItemHeader { /// Item header encoded as JSON Future> toJson(int length) async { return { + if (itemCount != null) 'item_count': itemCount, if (contentType != null) 'content_type': contentType, if (fileName != null) 'filename': fileName, if (attachmentType != null) 'attachment_type': attachmentType, diff --git a/dart/lib/src/sentry_isolate.dart b/dart/lib/src/sentry_isolate.dart index 9998544d5b..89b3e38ed2 100644 --- a/dart/lib/src/sentry_isolate.dart +++ b/dart/lib/src/sentry_isolate.dart @@ -46,7 +46,7 @@ class SentryIsolate { Hub hub, dynamic error, ) async { - hub.options.logger(SentryLevel.debug, 'Capture from IsolateError $error'); + hub.options.log(SentryLevel.debug, 'Capture from IsolateError $error'); // https://api.dartlang.org/stable/2.7.0/dart-isolate/Isolate/addErrorListener.html // error is a list of 2 elements @@ -60,7 +60,7 @@ class SentryIsolate { final String throwable = error.first; final String? stackTrace = error.last; - hub.options.logger( + hub.options.log( SentryLevel.error, 'Uncaught isolate error', logger: 'sentry.isolateError', diff --git a/dart/lib/src/sentry_item_type.dart b/dart/lib/src/sentry_item_type.dart index d6bf2a31de..c712ad8793 100644 --- a/dart/lib/src/sentry_item_type.dart +++ b/dart/lib/src/sentry_item_type.dart @@ -5,5 +5,6 @@ class SentryItemType { static const String clientReport = 'client_report'; static const String profile = 'profile'; static const String statsd = 'statsd'; + static const String log = 'log'; static const String unknown = '__unknown__'; } diff --git a/dart/lib/src/sentry_log_batcher.dart b/dart/lib/src/sentry_log_batcher.dart new file mode 100644 index 0000000000..d5f023f979 --- /dev/null +++ b/dart/lib/src/sentry_log_batcher.dart @@ -0,0 +1,52 @@ +import 'dart:async'; +import 'sentry_envelope.dart'; +import 'sentry_options.dart'; +import 'protocol/sentry_log.dart'; +import 'package:meta/meta.dart'; + +@internal +class SentryLogBatcher { + SentryLogBatcher(this._options, {Duration? flushTimeout, int? maxBufferSize}) + : _flushTimeout = flushTimeout ?? Duration(seconds: 5), + _maxBufferSize = maxBufferSize ?? 100; + + final SentryOptions _options; + final Duration _flushTimeout; + final int _maxBufferSize; + + final _logBuffer = []; + + Timer? _flushTimer; + + void addLog(SentryLog log) { + _logBuffer.add(log); + + _flushTimer?.cancel(); + + if (_logBuffer.length >= _maxBufferSize) { + return flush(); + } else { + _flushTimer = Timer(_flushTimeout, flush); + } + } + + void flush() { + _flushTimer?.cancel(); + _flushTimer = null; + + final logs = List.from(_logBuffer); + _logBuffer.clear(); + + if (logs.isEmpty) { + return; + } + + final envelope = SentryEnvelope.fromLogs( + logs, + _options.sdk, + ); + + // TODO: Make sure the Android SDK understands the log envelope type. + _options.transport.send(envelope); + } +} diff --git a/dart/lib/src/sentry_logger.dart b/dart/lib/src/sentry_logger.dart new file mode 100644 index 0000000000..28cbf6864b --- /dev/null +++ b/dart/lib/src/sentry_logger.dart @@ -0,0 +1,72 @@ +import 'dart:async'; +import 'hub.dart'; +import 'hub_adapter.dart'; +import 'protocol/sentry_log.dart'; +import 'protocol/sentry_log_level.dart'; +import 'protocol/sentry_log_attribute.dart'; +import 'sentry_options.dart'; + +class SentryLogger { + SentryLogger(this._clock, {Hub? hub}) : _hub = hub ?? HubAdapter(); + + final ClockProvider _clock; + final Hub _hub; + + 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 ?? {}, + ); + return _hub.captureLog(log); + } +} diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 90abb7947d..a8db6ee8fd 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'package:http/http.dart'; import 'package:meta/meta.dart'; @@ -7,7 +6,7 @@ import 'package:meta/meta.dart'; import '../sentry.dart'; import 'client_reports/client_report_recorder.dart'; import 'client_reports/noop_client_report_recorder.dart'; -import 'diagnostic_logger.dart'; +import 'diagnostic_log.dart'; import 'environment/environment_variables.dart'; import 'noop_client.dart'; import 'platform/platform.dart'; @@ -15,6 +14,9 @@ import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.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 // 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 @@ -125,18 +127,19 @@ class SentryOptions { /// This does not change whether an event is captured. MaxRequestBodySize maxRequestBodySize = MaxRequestBodySize.never; - SentryLogger _logger = noOpLogger; + SdkLogCallback _log = noOpLog; - /// Logger interface to log useful debugging information if debug is enabled - SentryLogger get logger => _logger; + /// Log callback to log useful debugging information if debug is enabled + SdkLogCallback get log => _log; - set logger(SentryLogger logger) { - diagnosticLogger = DiagnosticLogger(logger, this); - _logger = diagnosticLogger!.log; + @internal + set log(SdkLogCallback value) { + diagnosticLog = DiagnosticLog(value, this); + _log = diagnosticLog!.log; } @visibleForTesting - DiagnosticLogger? diagnosticLogger; + DiagnosticLog? diagnosticLog; final List _eventProcessors = []; @@ -160,12 +163,12 @@ class SentryOptions { set debug(bool newValue) { _debug = newValue; if (_debug == true && - (logger == noOpLogger || diagnosticLogger?.logger == noOpLogger)) { - logger = debugLogger; + (log == noOpLog || diagnosticLog?.logger == noOpLog)) { + log = debugLog; } if (_debug == false && - (logger == debugLogger || diagnosticLogger?.logger == debugLogger)) { - logger = noOpLogger; + (log == debugLog || diagnosticLog?.logger == debugLog)) { + log = noOpLog; } } @@ -198,6 +201,10 @@ class SentryOptions { /// Can return true to emit the metric, or false to drop it. BeforeMetricCallback? beforeMetricCallback; + /// This function is called right before a log is about to be sent. + /// Can return a modified log or null to drop the log. + BeforeSendLogCallback? beforeSendLog; + /// 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; @@ -531,6 +538,16 @@ class SentryOptions { /// This is opt-in, as it can lead to existing exception beeing grouped as new ones. bool groupExceptions = false; + /// Enable to capture and send logs to Sentry. + /// + /// Disabled by default. + bool enableLogs = false; + + late final SentryLogger logger = SentryLogger(clock); + + @internal + SentryLogBatcher logBatcher = NoopLogBatcher(); + SentryOptions({String? dsn, Platform? platform, RuntimeChecker? checker}) { this.dsn = dsn; if (platform != null) { @@ -605,14 +622,14 @@ class SentryOptions { SentryStackTraceFactory(this); @visibleForTesting - void debugLogger( + void debugLog( SentryLevel level, String message, { String? logger, Object? exception, StackTrace? stackTrace, }) { - log( + developer.log( '[${level.name}] $message', level: level.toDartLogLevel(), name: logger ?? 'sentry', @@ -624,7 +641,7 @@ class SentryOptions { } @visibleForTesting -void noOpLogger( +void noOpLog( SentryLevel level, String message, { String? logger, @@ -660,11 +677,15 @@ typedef BeforeMetricCallback = bool Function( Map? tags, }); +/// This function is called right before a log is about to be sent. +/// Can return a modified log or null to drop the log. +typedef BeforeSendLogCallback = FutureOr Function(SentryLog log); + /// Used to provide timestamp for logging. typedef ClockProvider = DateTime Function(); -/// Logger interface to log useful debugging information if debug is enabled -typedef SentryLogger = void Function( +/// Logger callback to log useful debugging information if debug is enabled +typedef SdkLogCallback = void Function( SentryLevel level, String message, { String? logger, diff --git a/dart/lib/src/sentry_run_zoned_guarded.dart b/dart/lib/src/sentry_run_zoned_guarded.dart index d71d688c22..d2f47fbef5 100644 --- a/dart/lib/src/sentry_run_zoned_guarded.dart +++ b/dart/lib/src/sentry_run_zoned_guarded.dart @@ -87,7 +87,7 @@ class SentryRunZonedGuarded { Object exception, StackTrace stackTrace, ) async { - options.logger( + options.log( SentryLevel.error, 'Uncaught zone error', logger: 'sentry.runZonedGuarded', diff --git a/dart/lib/src/sentry_stack_trace_factory.dart b/dart/lib/src/sentry_stack_trace_factory.dart index e6b742fb0d..8ef6f9e717 100644 --- a/dart/lib/src/sentry_stack_trace_factory.dart +++ b/dart/lib/src/sentry_stack_trace_factory.dart @@ -123,8 +123,7 @@ class SentryStackTraceFactory { // We shouldn't get here. If we do, it means there's likely an issue in // the parsing so let's fall back and post a stack trace as is, so that at // least we get an indication something's wrong and are able to fix it. - _options.logger( - SentryLevel.debug, "Failed to parse stack frame: $member"); + _options.log(SentryLevel.debug, "Failed to parse stack frame: $member"); } final platform = _options.platform.isWeb ? 'javascript' : 'dart'; diff --git a/dart/lib/src/sentry_trace_context_header.dart b/dart/lib/src/sentry_trace_context_header.dart index 370dd0a94b..f94f772dc7 100644 --- a/dart/lib/src/sentry_trace_context_header.dart +++ b/dart/lib/src/sentry_trace_context_header.dart @@ -71,9 +71,9 @@ class SentryTraceContextHeader { } SentryBaggage toBaggage({ - SentryLogger? logger, + SdkLogCallback? log, }) { - final baggage = SentryBaggage({}, logger: logger); + final baggage = SentryBaggage({}, log: log); baggage.setTraceId(traceId.toString()); baggage.setPublicKey(publicKey); diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index d998c53697..1cc618a716 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -214,7 +214,7 @@ class SentryTracer extends ISentrySpan { } if (children.length >= _hub.options.maxSpans) { - _hub.options.logger( + _hub.options.log( SentryLevel.warning, 'Span operation: $operation, description: $description dropped due to limit reached. Returning NoOpSpan.', ); @@ -242,7 +242,7 @@ class SentryTracer extends ISentrySpan { _scheduleTimer(); if (children.length >= _hub.options.maxSpans) { - _hub.options.logger( + _hub.options.log( SentryLevel.warning, 'Span operation: $operation, description: $description dropped due to limit reached. Returning NoOpSpan.', ); @@ -345,7 +345,7 @@ class SentryTracer extends ISentrySpan { @override void setMeasurement(String name, num value, {SentryMeasurementUnit? unit}) { if (finished) { - _hub.options.logger(SentryLevel.debug, + _hub.options.log(SentryLevel.debug, "The tracer is already finished. Measurement $name cannot be set"); return; } @@ -365,7 +365,7 @@ class SentryTracer extends ISentrySpan { final context = traceContext(); if (context != null) { - final baggage = context.toBaggage(logger: _hub.options.logger); + final baggage = context.toBaggage(log: _hub.options.log); return SentryBaggageHeader.fromBaggage(baggage); } return null; diff --git a/dart/lib/src/sentry_traces_sampler.dart b/dart/lib/src/sentry_traces_sampler.dart index 1b4634aee5..338d518302 100644 --- a/dart/lib/src/sentry_traces_sampler.dart +++ b/dart/lib/src/sentry_traces_sampler.dart @@ -14,7 +14,7 @@ class SentryTracesSampler { Random? random, }) : _random = random ?? Random() { if (_options.tracesSampler != null && _options.tracesSampleRate != null) { - _options.logger(SentryLevel.warning, + _options.log(SentryLevel.warning, 'Both tracesSampler and traceSampleRate are set. tracesSampler will take precedence and fallback to traceSampleRate if it returns null.'); } } @@ -34,7 +34,7 @@ class SentryTracesSampler { return _makeSampleDecision(sampleRate); } } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'The tracesSampler callback threw an exception', exception: exception, diff --git a/dart/lib/src/transport/data_category.dart b/dart/lib/src/transport/data_category.dart index cbfb26ea58..7da020e0db 100644 --- a/dart/lib/src/transport/data_category.dart +++ b/dart/lib/src/transport/data_category.dart @@ -9,6 +9,7 @@ enum DataCategory { attachment, security, metricBucket, + logItem, unknown; static DataCategory fromItemType(String itemType) { @@ -25,6 +26,8 @@ enum DataCategory { // whereas the client report category is metric_bucket case 'statsd': return DataCategory.metricBucket; + case 'log': + return DataCategory.logItem; default: return DataCategory.unknown; } diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 1b26c9a705..73c8e41c69 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -51,7 +51,7 @@ class HttpTransport implements Transport { return _parseEventId(response); } if (response.statusCode == 429) { - _options.logger( + _options.log( SentryLevel.warning, 'Rate limit reached, failed to send envelope'); } return SentryId.empty(); @@ -62,7 +62,7 @@ class HttpTransport implements Transport { final eventId = json.decode(response.body)['id']; return eventId != null ? SentryId.fromId(eventId) : null; } catch (e) { - _options.logger(SentryLevel.error, 'Error parsing response: $e'); + _options.log(SentryLevel.error, 'Error parsing response: $e'); if (_options.automatedTestMode) { rethrow; } diff --git a/dart/lib/src/transport/rate_limit_parser.dart b/dart/lib/src/transport/rate_limit_parser.dart index 1cf1a7e3e1..f0fea1dfde 100644 --- a/dart/lib/src/transport/rate_limit_parser.dart +++ b/dart/lib/src/transport/rate_limit_parser.dart @@ -87,6 +87,8 @@ extension _DataCategoryExtension on DataCategory { return DataCategory.security; case 'metric_bucket': return DataCategory.metricBucket; + case 'log_item': + return DataCategory.logItem; } return DataCategory.unknown; } diff --git a/dart/lib/src/transport/spotlight_http_transport.dart b/dart/lib/src/transport/spotlight_http_transport.dart index 79de9e5f6e..e2c98fda2a 100644 --- a/dart/lib/src/transport/spotlight_http_transport.dart +++ b/dart/lib/src/transport/spotlight_http_transport.dart @@ -31,7 +31,7 @@ class SpotlightHttpTransport extends Transport { try { await _sendToSpotlight(envelope); } catch (e) { - _options.logger( + _options.log( SentryLevel.warning, 'Failed to send envelope to Spotlight: $e'); if (_options.automatedTestMode) { rethrow; diff --git a/dart/lib/src/transport/task_queue.dart b/dart/lib/src/transport/task_queue.dart index 34daa81241..4c99393caa 100644 --- a/dart/lib/src/transport/task_queue.dart +++ b/dart/lib/src/transport/task_queue.dart @@ -19,7 +19,7 @@ class DefaultTaskQueue implements TaskQueue { DefaultTaskQueue(this._maxQueueSize, this._logger, this._recorder); final int _maxQueueSize; - final SentryLogger _logger; + final SdkLogCallback _logger; final ClientReportRecorder _recorder; int _queueCount = 0; diff --git a/dart/lib/src/utils/add_tracing_headers_to_http_request.dart b/dart/lib/src/utils/add_tracing_headers_to_http_request.dart index fc4e21fe04..08cc41d1b4 100644 --- a/dart/lib/src/utils/add_tracing_headers_to_http_request.dart +++ b/dart/lib/src/utils/add_tracing_headers_to_http_request.dart @@ -10,7 +10,7 @@ void addTracingHeadersToHttpHeader(Map headers, Hub hub, addBaggageHeaderFromSpan( span, headers, - logger: hub.options.logger, + log: hub.options.log, ); } else { final scope = hub.scope; @@ -21,7 +21,7 @@ void addTracingHeadersToHttpHeader(Map headers, Hub hub, final baggageHeader = propagationContext.toBaggageHeader(); if (baggageHeader != null) { - addBaggageHeader(baggageHeader, headers, logger: hub.options.logger); + addBaggageHeader(baggageHeader, headers, log: hub.options.log); } } } diff --git a/dart/lib/src/utils/tracing_utils.dart b/dart/lib/src/utils/tracing_utils.dart index 5a14311d1c..dfec43623b 100644 --- a/dart/lib/src/utils/tracing_utils.dart +++ b/dart/lib/src/utils/tracing_utils.dart @@ -14,28 +14,28 @@ void addSentryTraceHeader( void addBaggageHeaderFromSpan( ISentrySpan span, Map headers, { - SentryLogger? logger, + SdkLogCallback? log, }) { final baggage = span.toBaggageHeader(); if (baggage != null) { - addBaggageHeader(baggage, headers, logger: logger); + addBaggageHeader(baggage, headers, log: log); } } void addBaggageHeader( SentryBaggageHeader baggage, Map headers, { - SentryLogger? logger, + SdkLogCallback? log, }) { final currentValue = headers[baggage.name] as String? ?? ''; final currentBaggage = SentryBaggage.fromHeader( currentValue, - logger: logger, + log: log, ); final sentryBaggage = SentryBaggage.fromHeader( baggage.value, - logger: logger, + log: log, ); // overwrite sentry's keys https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#baggage @@ -47,7 +47,7 @@ void addBaggageHeader( ...sentryBaggage.keyValues, }; - final newBaggage = SentryBaggage(mergedBaggage, logger: logger); + final newBaggage = SentryBaggage(mergedBaggage, log: log); headers[baggage.name] = newBaggage.toHeaderString(); } diff --git a/dart/lib/src/utils/transport_utils.dart b/dart/lib/src/utils/transport_utils.dart index 399809b179..3dae120d58 100644 --- a/dart/lib/src/utils/transport_utils.dart +++ b/dart/lib/src/utils/transport_utils.dart @@ -12,7 +12,7 @@ class TransportUtils { {required String target}) { if (response.statusCode != 200) { if (options.debug) { - options.logger( + options.log( SentryLevel.error, 'Error, statusCode = ${response.statusCode}, body = ${response.body}', ); @@ -36,7 +36,7 @@ class TransportUtils { } } } else { - options.logger( + options.log( SentryLevel.debug, 'Envelope ${envelope.header.eventId ?? "--"} was sent successfully to $target.', ); diff --git a/dart/test/diagnostic_logger_test.dart b/dart/test/diagnostic_logger_test.dart index 0c85a8264e..3bd5c46fe1 100644 --- a/dart/test/diagnostic_logger_test.dart +++ b/dart/test/diagnostic_logger_test.dart @@ -1,5 +1,5 @@ import 'package:sentry/sentry.dart'; -import 'package:sentry/src/diagnostic_logger.dart'; +import 'package:sentry/src/diagnostic_log.dart'; import 'package:test/test.dart'; import 'test_utils.dart'; @@ -11,7 +11,7 @@ void main() { fixture = Fixture(); }); - group(DiagnosticLogger, () { + group(DiagnosticLog, () { test('does not log if debug is disabled', () { fixture.options.debug = false; @@ -52,8 +52,8 @@ class Fixture { Object? loggedMessage; - DiagnosticLogger getSut() { - return DiagnosticLogger(mockLogger, options); + DiagnosticLog getSut() { + return DiagnosticLog(mockLogger, options); } void mockLogger( diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 9db9296a1e..410bca58a1 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -823,7 +823,7 @@ class Fixture { options.tracesSampleRate = tracesSampleRate; options.tracesSampler = tracesSampler; options.debug = debug; - options.logger = mockLogger; // Enable logging in DiagnosticsLogger + options.log = mockLogger; // Enable logging in DiagnosticsLogger final hub = Hub(options); diff --git a/dart/test/mocks/mock_hub.dart b/dart/test/mocks/mock_hub.dart index 6b5b280830..0a6484eab6 100644 --- a/dart/test/mocks/mock_hub.dart +++ b/dart/test/mocks/mock_hub.dart @@ -12,6 +12,7 @@ class MockHub with NoSuchMethodProvider implements Hub { List captureExceptionCalls = []; List captureMessageCalls = []; List addBreadcrumbCalls = []; + List captureLogCalls = []; List bindClientCalls = []; // ignore: deprecated_member_use_from_same_package @@ -107,6 +108,11 @@ class MockHub with NoSuchMethodProvider implements Hub { return SentryId.newId(); } + @override + FutureOr captureLog(SentryLog log) async { + captureLogCalls.add(CaptureLogCall(log, null)); + } + @override Future close() async { closeCalls = closeCalls + 1; diff --git a/dart/test/mocks/mock_log_batcher.dart b/dart/test/mocks/mock_log_batcher.dart new file mode 100644 index 0000000000..9de9a8ae5d --- /dev/null +++ b/dart/test/mocks/mock_log_batcher.dart @@ -0,0 +1,19 @@ +import 'dart:async'; + +import 'package:sentry/src/protocol/sentry_log.dart'; +import 'package:sentry/src/sentry_log_batcher.dart'; + +class MockLogBatcher implements SentryLogBatcher { + final addLogCalls = []; + final flushCalls = []; + + @override + FutureOr addLog(SentryLog log) { + addLogCalls.add(log); + } + + @override + Future flush() async { + flushCalls.add(null); + } +} diff --git a/dart/test/mocks/mock_sentry_client.dart b/dart/test/mocks/mock_sentry_client.dart index 6138eb5295..00a61cc203 100644 --- a/dart/test/mocks/mock_sentry_client.dart +++ b/dart/test/mocks/mock_sentry_client.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:sentry/sentry.dart'; import 'no_such_method_provider.dart'; @@ -8,8 +9,8 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { List captureMessageCalls = []; List captureEnvelopeCalls = []; List captureTransactionCalls = []; - List captureFeedbackCalls = []; + List captureLogCalls = []; int closeCalls = 0; @override @@ -84,6 +85,11 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { return SentryId.newId(); } + @override + FutureOr captureLog(SentryLog log, {Scope? scope}) async { + captureLogCalls.add(CaptureLogCall(log, scope)); + } + @override void close() { closeCalls = closeCalls + 1; @@ -173,3 +179,10 @@ class CaptureTransactionCall { CaptureTransactionCall(this.transaction, this.traceContext, this.hint); } + +class CaptureLogCall { + final SentryLog log; + final Scope? scope; + + CaptureLogCall(this.log, this.scope); +} diff --git a/dart/test/mocks/mock_transport.dart b/dart/test/mocks/mock_transport.dart index 4f4c117dde..f9ba5b4829 100644 --- a/dart/test/mocks/mock_transport.dart +++ b/dart/test/mocks/mock_transport.dart @@ -7,6 +7,7 @@ class MockTransport implements Transport { List envelopes = []; List events = []; List statsdItems = []; + List> logs = []; int _calls = 0; String _exceptions = ''; @@ -31,7 +32,7 @@ class MockTransport implements Transport { try { envelopes.add(envelope); if (parseFromEnvelope) { - await _eventFromEnvelope(envelope); + await _parseEnvelope(envelope); } } catch (e, stack) { _exceptions += '$e\n$stack\n\n'; @@ -41,7 +42,7 @@ class MockTransport implements Transport { return envelope.header.eventId ?? SentryId.empty(); } - Future _eventFromEnvelope(SentryEnvelope envelope) async { + Future _parseEnvelope(SentryEnvelope envelope) async { final RegExp statSdRegex = RegExp('^(?!{).+@.+:.+\\|.+', multiLine: true); final envelopeItemData = await envelope.items.first.dataFactory(); @@ -49,6 +50,13 @@ class MockTransport implements Transport { if (statSdRegex.hasMatch(envelopeItem)) { statsdItems.add(envelopeItem); + } else if (envelopeItem.contains('items') && + envelopeItem.contains('timestamp') && + envelopeItem.contains('trace_id') && + envelopeItem.contains('level') && + envelopeItem.contains('body')) { + final envelopeItemJson = jsonDecode(envelopeItem) as Map; + logs.add(envelopeItemJson); } else { final envelopeItemJson = jsonDecode(envelopeItem) as Map; events.add(SentryEvent.fromJson(envelopeItemJson)); diff --git a/dart/test/protocol/rate_limiter_test.dart b/dart/test/protocol/rate_limiter_test.dart index c90c689e01..9a6079810b 100644 --- a/dart/test/protocol/rate_limiter_test.dart +++ b/dart/test/protocol/rate_limiter_test.dart @@ -281,6 +281,43 @@ void main() { final result = rateLimiter.filter(envelope); expect(result, isNull); }); + + test('log', () { + final rateLimiter = fixture.getSut(); + fixture.dateTimeToReturn = 0; + + final log = SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.info, + body: 'test', + attributes: { + 'test': SentryLogAttribute.string('test'), + }, + ); + + final sdkVersion = SdkVersion(name: 'test', version: 'test'); + final envelope = SentryEnvelope.fromLogs([log], sdkVersion); + + rateLimiter.updateRetryAfterLimits( + '1:log_item:key, 5:log_item:organization', null, 1); + + final result = rateLimiter.filter(envelope); + expect(result, isNull); + }); + }); + + group('$DataCategory', () { + test('fromItemType', () { + expect(DataCategory.fromItemType('event'), DataCategory.error); + expect(DataCategory.fromItemType('session'), DataCategory.session); + expect(DataCategory.fromItemType('attachment'), DataCategory.attachment); + expect( + DataCategory.fromItemType('transaction'), DataCategory.transaction); + expect(DataCategory.fromItemType('statsd'), DataCategory.metricBucket); + expect(DataCategory.fromItemType('log'), DataCategory.logItem); + expect(DataCategory.fromItemType('unknown'), DataCategory.unknown); + }); }); } diff --git a/dart/test/protocol/sentry_log_attribute_test.dart b/dart/test/protocol/sentry_log_attribute_test.dart new file mode 100644 index 0000000000..2c0fb7ce31 --- /dev/null +++ b/dart/test/protocol/sentry_log_attribute_test.dart @@ -0,0 +1,42 @@ +import 'package:test/test.dart'; +import 'package:sentry/sentry.dart'; + +void main() { + test('$SentryLogAttribute string to json', () { + final attribute = SentryLogAttribute.string('test'); + final json = attribute.toJson(); + expect(json, { + 'value': 'test', + 'type': 'string', + }); + }); + + test('$SentryLogAttribute bool to json', () { + final attribute = SentryLogAttribute.bool(true); + final json = attribute.toJson(); + expect(json, { + 'value': true, + 'type': 'boolean', + }); + }); + + test('$SentryLogAttribute int to json', () { + final attribute = SentryLogAttribute.int(1); + final json = attribute.toJson(); + + expect(json, { + 'value': 1, + 'type': 'integer', + }); + }); + + test('$SentryLogAttribute double to json', () { + final attribute = SentryLogAttribute.double(1.0); + final json = attribute.toJson(); + + expect(json, { + 'value': 1.0, + 'type': 'double', + }); + }); +} diff --git a/dart/test/protocol/sentry_log_test.dart b/dart/test/protocol/sentry_log_test.dart new file mode 100644 index 0000000000..0921df7a32 --- /dev/null +++ b/dart/test/protocol/sentry_log_test.dart @@ -0,0 +1,86 @@ +import 'package:test/test.dart'; +import 'package:sentry/sentry.dart'; + +void main() { + test('$SentryLog to json', () { + final timestamp = DateTime.now(); + final traceId = SentryId.newId(); + + final logItem = SentryLog( + timestamp: timestamp, + traceId: traceId, + level: SentryLogLevel.info, + body: 'fixture-body', + attributes: { + 'test': SentryLogAttribute.string('fixture-test'), + 'test2': SentryLogAttribute.bool(true), + 'test3': SentryLogAttribute.int(9001), + 'test4': SentryLogAttribute.double(9000.1), + }, + severityNumber: 1, + ); + + final json = logItem.toJson(); + + expect(json, { + 'timestamp': timestamp.toIso8601String(), + 'trace_id': traceId.toString(), + 'level': 'info', + 'body': 'fixture-body', + 'attributes': { + 'test': { + 'value': 'fixture-test', + 'type': 'string', + }, + 'test2': { + 'value': true, + 'type': 'boolean', + }, + 'test3': { + 'value': 9001, + 'type': 'integer', + }, + 'test4': { + 'value': 9000.1, + 'type': 'double', + }, + }, + 'severity_number': 1, + }); + }); + + test('$SentryLevel without severity number infers from level in toJson', () { + final logItem = SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.trace, + body: 'fixture-body', + attributes: { + 'test': SentryLogAttribute.string('fixture-test'), + }, + ); + + var json = logItem.toJson(); + expect(json['severity_number'], 1); + + logItem.level = SentryLogLevel.debug; + json = logItem.toJson(); + expect(json['severity_number'], 5); + + logItem.level = SentryLogLevel.info; + json = logItem.toJson(); + expect(json['severity_number'], 9); + + logItem.level = SentryLogLevel.warn; + json = logItem.toJson(); + expect(json['severity_number'], 13); + + logItem.level = SentryLogLevel.error; + json = logItem.toJson(); + expect(json['severity_number'], 17); + + logItem.level = SentryLogLevel.fatal; + json = logItem.toJson(); + expect(json['severity_number'], 21); + }); +} diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index 6895faa579..975b641934 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -810,7 +810,7 @@ class Fixture { options.maxBreadcrumbs = maxBreadcrumbs; options.beforeBreadcrumb = beforeBreadcrumbCallback; options.debug = debug; - options.logger = mockLogger; + options.log = mockLogger; if (scopeObserver != null) { options.addScopeObserver(scopeObserver); diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 0d44050d89..b0fb8bb53c 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -17,12 +17,15 @@ 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:test/test.dart'; +import 'package:sentry/src/noop_log_batcher.dart'; import 'mocks.dart'; import 'mocks/mock_client_report_recorder.dart'; import 'mocks/mock_hub.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', () { @@ -1702,6 +1705,240 @@ void main() { }); }); + group('SentryClient captureLog', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + SentryLog givenLog() { + return SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.info, + body: 'test', + attributes: { + 'attribute': SentryLogAttribute.string('value'), + }, + ); + } + + 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 log = givenLog(); + + await client.captureLog(log); + + final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; + expect(mockLogBatcher.addLogCalls, isEmpty); + }); + + test('should capture logs as envelope', () async { + fixture.options.enableLogs = true; + + final client = fixture.getSut(); + fixture.options.logBatcher = MockLogBatcher(); + + 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(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(); + fixture.options.logBatcher = MockLogBatcher(); + + await client.captureLog(log, scope: scope); + + final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; + expect(mockLogBatcher.addLogCalls.length, 1); + final capturedLog = mockLogBatcher.addLogCalls.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 set trace id from propagation context', () async { + fixture.options.enableLogs = true; + + final client = fixture.getSut(); + fixture.options.logBatcher = MockLogBatcher(); + + 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(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(); + fixture.options.logBatcher = MockLogBatcher(); + + final log = givenLog(); + + await client.captureLog(log); + + final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher; + expect(mockLogBatcher.addLogCalls.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(); + fixture.options.logBatcher = MockLogBatcher(); + + 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(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(); + fixture.options.logBatcher = MockLogBatcher(); + + 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(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(); + fixture.options.logBatcher = MockLogBatcher(); + + 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(capturedLog.body, 'test'); + }); + }); + group('SentryClient captures envelope', () { late Fixture fixture; final fakeEnvelope = getFakeEnvelope(); @@ -2424,7 +2661,7 @@ class Fixture { options.beforeSendTransaction = beforeSendTransaction; options.beforeSendFeedback = beforeSendFeedback; options.debug = debug; - options.logger = mockLogger; + options.log = mockLogger; if (eventProcessor != null) { options.addEventProcessor(eventProcessor); diff --git a/dart/test/sentry_envelope_item_header_test.dart b/dart/test/sentry_envelope_item_header_test.dart index 3ffccaf0b4..b948568ed9 100644 --- a/dart/test/sentry_envelope_item_header_test.dart +++ b/dart/test/sentry_envelope_item_header_test.dart @@ -6,8 +6,9 @@ void main() { group('SentryEnvelopeItemHeader', () { test('serialize', () async { final sut = SentryEnvelopeItemHeader(SentryItemType.event, - contentType: 'application/json'); + itemCount: 3, contentType: 'application/json'); final expected = { + 'item_count': 3, 'content_type': 'application/json', 'type': 'event', 'length': 3 diff --git a/dart/test/sentry_envelope_item_test.dart b/dart/test/sentry_envelope_item_test.dart index 9e63104a31..741773a66a 100644 --- a/dart/test/sentry_envelope_item_test.dart +++ b/dart/test/sentry_envelope_item_test.dart @@ -95,5 +95,42 @@ void main() { expect(sut.header.type, SentryItemType.clientReport); expect(actualData, expectedData); }); + + test('fromLog', () async { + final logs = [ + SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.info, + body: 'test', + attributes: { + 'test': SentryLogAttribute.string('test'), + }, + ), + SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.info, + body: 'test2', + attributes: { + 'test2': SentryLogAttribute.int(9001), + }, + ), + ]; + + final sut = SentryEnvelopeItem.fromLogs(logs); + + final expectedData = utf8.encode(jsonEncode( + { + 'items': logs.map((e) => e.toJson()).toList(), + }, + toEncodable: jsonSerializationFallback, + )); + final actualData = await sut.dataFactory(); + + expect(sut.header.contentType, 'application/vnd.sentry.items.log+json'); + expect(sut.header.type, SentryItemType.log); + expect(actualData, expectedData); + }); }); } diff --git a/dart/test/sentry_envelope_test.dart b/dart/test/sentry_envelope_test.dart index 9f60ab4bc7..9a21339aaa 100644 --- a/dart/test/sentry_envelope_test.dart +++ b/dart/test/sentry_envelope_test.dart @@ -135,6 +135,44 @@ void main() { expect(actualItem, expectedItem); }); + test('fromLogs', () async { + final logs = [ + SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.info, + body: 'test', + attributes: { + 'test': SentryLogAttribute.string('test'), + }, + ), + SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.info, + body: 'test2', + attributes: { + 'test2': SentryLogAttribute.int(9001), + }, + ), + ]; + + final sdkVersion = SdkVersion( + name: 'fixture-name', + version: 'fixture-version', + ); + + final sut = SentryEnvelope.fromLogs(logs, sdkVersion); + + expect(sut.header.sdkVersion, sdkVersion); + + final expectedItem = SentryEnvelopeItem.fromLogs(logs); + final expectedItemData = await expectedItem.dataFactory(); + final actualItemData = await sut.items[0].dataFactory(); + + expect(actualItemData, expectedItemData); + }); + test('max attachment size', () async { final attachment = SentryAttachment.fromLoader( loader: () => Uint8List.fromList([1, 2, 3, 4]), diff --git a/dart/test/sentry_log_batcher_test.dart b/dart/test/sentry_log_batcher_test.dart new file mode 100644 index 0000000000..5e9f7cf5f3 --- /dev/null +++ b/dart/test/sentry_log_batcher_test.dart @@ -0,0 +1,148 @@ +import 'package:test/test.dart'; +import 'package:sentry/src/sentry_log_batcher.dart'; +import 'package:sentry/src/sentry_options.dart'; +import 'package:sentry/src/protocol/sentry_log.dart'; +import 'package:sentry/src/protocol/sentry_log_level.dart'; + +import 'mocks/mock_transport.dart'; + +void main() { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('added logs are flushed after timeout', () async { + final flushTimeout = Duration(milliseconds: 1); + + final batcher = fixture.getSut(flushTimeout: flushTimeout); + + final log = SentryLog( + timestamp: DateTime.now(), + level: SentryLogLevel.info, + body: 'test', + attributes: {}, + ); + final log2 = SentryLog( + timestamp: DateTime.now(), + level: SentryLogLevel.info, + body: 'test2', + attributes: {}, + ); + + batcher.addLog(log); + batcher.addLog(log2); + + expect(fixture.mockTransport.envelopes.length, 0); + + await Future.delayed(flushTimeout); + + expect(fixture.mockTransport.envelopes.length, 1); + + final envelopePayloadJson = (fixture.mockTransport).logs.first; + + expect(envelopePayloadJson, isNotNull); + expect(envelopePayloadJson['items'].length, 2); + expect(envelopePayloadJson['items'].first['body'], log.body); + expect(envelopePayloadJson['items'].last['body'], log2.body); + }); + + test('max logs are flushed without timeout', () async { + final batcher = fixture.getSut(maxBufferSize: 10); + + final log = SentryLog( + timestamp: DateTime.now(), + level: SentryLogLevel.info, + body: 'test', + attributes: {}, + ); + + for (var i = 0; i < 10; i++) { + batcher.addLog(log); + } + + // Just wait a little bit, as we call capture without awaiting internally. + await Future.delayed(Duration(milliseconds: 1)); + + expect(fixture.mockTransport.envelopes.length, 1); + final envelopePayloadJson = (fixture.mockTransport).logs.first; + + expect(envelopePayloadJson, isNotNull); + expect(envelopePayloadJson['items'].length, 10); + }); + + test('more than max logs are flushed eventuelly', () async { + final flushTimeout = Duration(milliseconds: 100); + final batcher = fixture.getSut( + maxBufferSize: 10, + flushTimeout: flushTimeout, + ); + + final log = SentryLog( + timestamp: DateTime.now(), + level: SentryLogLevel.info, + body: 'test', + attributes: {}, + ); + + for (var i = 0; i < 15; i++) { + batcher.addLog(log); + } + + await Future.delayed(flushTimeout); + + expect(fixture.mockTransport.envelopes.length, 2); + + final firstEnvelopePayloadJson = (fixture.mockTransport).logs.first; + + expect(firstEnvelopePayloadJson, isNotNull); + expect(firstEnvelopePayloadJson['items'].length, 10); + + final secondEnvelopePayloadJson = (fixture.mockTransport).logs.last; + + expect(secondEnvelopePayloadJson, isNotNull); + expect(secondEnvelopePayloadJson['items'].length, 5); + }); + + test('calling flush directly flushes logs', () async { + final batcher = fixture.getSut(); + + final log = SentryLog( + timestamp: DateTime.now(), + level: SentryLogLevel.info, + body: 'test', + attributes: {}, + ); + + batcher.addLog(log); + batcher.addLog(log); + batcher.flush(); + + // Just wait a little bit, as we call capture without awaiting internally. + await Future.delayed(Duration(milliseconds: 1)); + + expect(fixture.mockTransport.envelopes.length, 1); + final envelopePayloadJson = (fixture.mockTransport).logs.first; + + expect(envelopePayloadJson, isNotNull); + expect(envelopePayloadJson['items'].length, 2); + }); +} + +class Fixture { + final options = SentryOptions(); + final mockTransport = MockTransport(); + + Fixture() { + options.transport = mockTransport; + } + + SentryLogBatcher getSut({Duration? flushTimeout, int? maxBufferSize}) { + return SentryLogBatcher( + options, + flushTimeout: flushTimeout, + maxBufferSize: maxBufferSize, + ); + } +} diff --git a/dart/test/sentry_logger_test.dart b/dart/test/sentry_logger_test.dart new file mode 100644 index 0000000000..96e4aa7743 --- /dev/null +++ b/dart/test/sentry_logger_test.dart @@ -0,0 +1,95 @@ +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); + }); +} + +class Fixture { + final options = defaultTestOptions(); + final hub = MockHub(); + final timestamp = DateTime.fromMicrosecondsSinceEpoch(0); + + final attributes = { + 'string': SentryLogAttribute.string('string'), + 'int': SentryLogAttribute.int(1), + 'double': SentryLogAttribute.double(1.0), + 'bool': SentryLogAttribute.bool(true), + }; + + SentryLogger getSut() { + return SentryLogger(() => timestamp, hub: hub); + } +} diff --git a/dart/test/sentry_options_test.dart b/dart/test/sentry_options_test.dart index 2132315924..25402e2f74 100644 --- a/dart/test/sentry_options_test.dart +++ b/dart/test/sentry_options_test.dart @@ -33,32 +33,32 @@ void main() { expect(200, options.maxBreadcrumbs); }); - test('SentryLogger sets a diagnostic logger', () { + test('SdkLogger sets a diagnostic logger', () { final options = defaultTestOptions(); - expect(options.logger, noOpLogger); + expect(options.log, noOpLog); options.debug = true; - expect(options.logger, isNot(noOpLogger)); + expect(options.log, isNot(noOpLog)); }); test('setting debug correctly sets logger', () { final options = defaultTestOptions(); - expect(options.logger, noOpLogger); - expect(options.diagnosticLogger, isNull); + expect(options.log, noOpLog); + expect(options.diagnosticLog, isNull); options.debug = true; - expect(options.logger, isNot(options.debugLogger)); - expect(options.diagnosticLogger!.logger, options.debugLogger); - expect(options.logger, options.diagnosticLogger!.log); + expect(options.log, isNot(options.debugLog)); + expect(options.diagnosticLog!.logger, options.debugLog); + expect(options.log, options.diagnosticLog!.log); options.debug = false; - expect(options.logger, isNot(noOpLogger)); - expect(options.diagnosticLogger!.logger, noOpLogger); - expect(options.logger, options.diagnosticLogger!.log); + expect(options.log, isNot(noOpLog)); + expect(options.diagnosticLog!.logger, noOpLog); + expect(options.log, options.diagnosticLog!.log); options.debug = true; - expect(options.logger, isNot(options.debugLogger)); - expect(options.diagnosticLogger!.logger, options.debugLogger); - expect(options.logger, options.diagnosticLogger!.log); + expect(options.log, isNot(options.debugLog)); + expect(options.diagnosticLog!.logger, options.debugLog); + expect(options.log, options.diagnosticLog!.log); }); test('tracesSampler is null by default', () { diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 5ea7789b3e..be2c6fb426 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -567,18 +567,18 @@ void main() { (options) { options.dsn = fakeDsn; options.debug = true; - expect(options.diagnosticLogger?.logger, isNot(noOpLogger)); + expect(options.diagnosticLog?.logger, isNot(noOpLog)); options.debug = false; - expect(options.diagnosticLogger?.logger, noOpLogger); + expect(options.diagnosticLog?.logger, noOpLog); options.debug = true; - expect(options.diagnosticLogger?.logger, isNot(noOpLogger)); + expect(options.diagnosticLog?.logger, isNot(noOpLog)); }, options: sentryOptions, ); - expect(sentryOptions.diagnosticLogger?.logger, isNot(noOpLogger)); + expect(sentryOptions.diagnosticLog?.logger, isNot(noOpLog)); }); group('Sentry init optionsConfiguration', () { @@ -594,7 +594,7 @@ void main() { defaultTestOptions(checker: MockRuntimeChecker(isRelease: true)) ..automatedTestMode = false ..debug = true - ..logger = fixture.mockLogger; + ..log = fixture.mockLogger; final exception = Exception("Exception in options callback"); await Sentry.init( diff --git a/dart/test/sentry_traces_sampler_test.dart b/dart/test/sentry_traces_sampler_test.dart index 8d0ca97f02..1b057c6a1f 100644 --- a/dart/test/sentry_traces_sampler_test.dart +++ b/dart/test/sentry_traces_sampler_test.dart @@ -132,7 +132,7 @@ class Fixture { options.tracesSampleRate = tracesSampleRate; options.tracesSampler = tracesSampler; options.debug = debug; - options.logger = mockLogger; + options.log = mockLogger; return SentryTracesSampler(options); } diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index a497536d90..55f7d62e57 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -246,7 +246,7 @@ class Fixture { HttpTransport getSut(http.Client client, RateLimiter rateLimiter) { options.debug = true; - options.logger = mockLogger; + options.log = mockLogger; options.httpClient = client; options.recorder = clientReportRecorder; options.clock = () { diff --git a/dart/test/transport/tesk_queue_test.dart b/dart/test/transport/tesk_queue_test.dart index e61299173b..dca3243f01 100644 --- a/dart/test/transport/tesk_queue_test.dart +++ b/dart/test/transport/tesk_queue_test.dart @@ -140,6 +140,6 @@ class Fixture { late var clientReportRecorder = MockClientReportRecorder(); TaskQueue getSut({required int maxQueueSize}) { - return DefaultTaskQueue(maxQueueSize, options.logger, clientReportRecorder); + return DefaultTaskQueue(maxQueueSize, options.log, clientReportRecorder); } } diff --git a/dart/test/utils/url_details_test.dart b/dart/test/utils/url_details_test.dart index 828335aee2..673d4452da 100644 --- a/dart/test/utils/url_details_test.dart +++ b/dart/test/utils/url_details_test.dart @@ -86,4 +86,8 @@ void main() { }); } -class MockSpan extends Mock implements SentrySpan {} +class MockSpan extends Mock implements SentrySpan { + final SentrySpanContext _context = SentrySpanContext(operation: 'test'); + @override + SentrySpanContext get context => _context; +} diff --git a/drift/lib/src/sentry_span_helper.dart b/drift/lib/src/sentry_span_helper.dart index f9a04e0ed7..f56932ee17 100644 --- a/drift/lib/src/sentry_span_helper.dart +++ b/drift/lib/src/sentry_span_helper.dart @@ -31,7 +31,7 @@ class SentrySpanHelper { }) async { final parentSpan = _transactionStack.lastOrNull ?? _hub.getSpan(); if (parentSpan == null) { - _hub.options.logger( + _hub.options.log( SentryLevel.warning, 'Active Sentry transaction does not exist, could not start span for the Drift operation: $description', logger: loggerName, @@ -76,7 +76,7 @@ class SentrySpanHelper { }) { final parentSpan = _transactionStack.lastOrNull ?? _hub.getSpan(); if (parentSpan == null) { - _hub.options.logger( + _hub.options.log( SentryLevel.warning, 'Active Sentry transaction does not exist, could not start span for Drift operation: Begin Transaction', logger: loggerName, @@ -119,7 +119,7 @@ class SentrySpanHelper { Future finishTransaction(Future Function() execute) async { final parentSpan = _transactionStack.lastOrNull; if (parentSpan == null) { - _hub.options.logger( + _hub.options.log( SentryLevel.warning, 'Active Sentry transaction does not exist, could not finish span for Drift operation: Finish Transaction', logger: loggerName, @@ -146,7 +146,7 @@ class SentrySpanHelper { Future abortTransaction(Future Function() execute) async { final parentSpan = _transactionStack.lastOrNull; if (parentSpan == null) { - _hub.options.logger( + _hub.options.log( SentryLevel.warning, 'Active Sentry transaction does not exist, could not finish span for Drift operation: Abort Transaction', logger: loggerName, diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 8f3b547368..92e249563a 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -90,6 +90,8 @@ Future setupSentry( options.replay.sessionSampleRate = 1.0; options.replay.onErrorSampleRate = 1.0; + options.enableLogs = true; + _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { options.dist = '1'; @@ -527,6 +529,16 @@ class MainScaffold extends StatelessWidget { text: 'Demonstrates the feature flags.', buttonTitle: 'Add "feature-one" flag', ), + TooltipButton( + onPressed: () { + Sentry.logger + .info('Sentry Log With Test Attribute', attributes: { + 'test-attribute': SentryLogAttribute.string('test-value'), + }); + }, + text: 'Demonstrates the logging with Sentry Log.', + buttonTitle: 'Sentry Log with Attribute', + ), if (UniversalPlatform.isIOS || UniversalPlatform.isMacOS) const CocoaExample(), if (UniversalPlatform.isAndroid) const AndroidExample(), diff --git a/flutter/lib/src/binding_wrapper.dart b/flutter/lib/src/binding_wrapper.dart index 4b3b9d8094..aef380e0e9 100644 --- a/flutter/lib/src/binding_wrapper.dart +++ b/flutter/lib/src/binding_wrapper.dart @@ -20,7 +20,7 @@ class BindingWrapper { try { return _ambiguate(WidgetsBinding.instance); } catch (e, s) { - _hub.options.logger( + _hub.options.log( SentryLevel.error, 'WidgetsBinding.instance was not yet initialized', exception: e, @@ -57,7 +57,7 @@ class SentryWidgetsFlutterBinding extends WidgetsFlutterBinding // Try to get the existing binding instance return WidgetsBinding.instance; } catch (_) { - Sentry.currentHub.options.logger( + Sentry.currentHub.options.log( SentryLevel.info, 'WidgetsFlutterBinding has not been initialized yet. ' 'Creating $SentryWidgetsFlutterBinding.'); diff --git a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart index 60dbadc378..d5dde78b3f 100644 --- a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart +++ b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart @@ -57,7 +57,7 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { detailsStackTrace, ); } catch (e, stackTrace) { - _options.logger( + _options.log( SentryLevel.info, "Couldn't prettify PlatformException. " 'The exception will still be reported.', diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 17de36b99a..47195ce748 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -68,7 +68,7 @@ class ScreenshotEventProcessor implements EventProcessor { takeScreenshot = result; } } else if (shouldDebounce) { - _options.logger( + _options.log( SentryLevel.debug, 'Skipping screenshot capture due to debouncing (too many captures within ${_debouncer.waitTime.inMilliseconds}ms)', ); @@ -79,7 +79,7 @@ class ScreenshotEventProcessor implements EventProcessor { return event; } } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'The beforeCaptureScreenshot/beforeScreenshot callback threw an exception', exception: exception, @@ -93,7 +93,7 @@ class ScreenshotEventProcessor implements EventProcessor { final renderer = _options.rendererWrapper.getRenderer(); if (_options.platform.isWeb && renderer != FlutterRenderer.canvasKit) { - _options.logger( + _options.log( SentryLevel.debug, 'Cannot take screenshot with ${renderer?.name} renderer.', ); diff --git a/flutter/lib/src/file_system_transport.dart b/flutter/lib/src/file_system_transport.dart index e28f81ae78..7a43b49af0 100644 --- a/flutter/lib/src/file_system_transport.dart +++ b/flutter/lib/src/file_system_transport.dart @@ -22,7 +22,7 @@ class FileSystemTransport implements Transport { await _native.captureEnvelope(Uint8List.fromList(envelopeData), envelope.containsUnhandledException); } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'Failed to save envelope', exception: exception, diff --git a/flutter/lib/src/frames_tracking/sentry_delayed_frames_tracker.dart b/flutter/lib/src/frames_tracking/sentry_delayed_frames_tracker.dart index 7c4f53cee5..5f2bafe603 100644 --- a/flutter/lib/src/frames_tracking/sentry_delayed_frames_tracker.dart +++ b/flutter/lib/src/frames_tracking/sentry_delayed_frames_tracker.dart @@ -85,7 +85,7 @@ class SentryDelayedFramesTracker { return; } if (_delayedFrames.length > maxDelayedFramesBuffer) { - _options.logger(SentryLevel.debug, + _options.log(SentryLevel.debug, 'Frame tracking buffer is full, stopping frame collection until all active spans have finished processing'); return; } diff --git a/flutter/lib/src/frames_tracking/span_frame_metrics_collector.dart b/flutter/lib/src/frames_tracking/span_frame_metrics_collector.dart index 95a879dce7..3069fbd374 100644 --- a/flutter/lib/src/frames_tracking/span_frame_metrics_collector.dart +++ b/flutter/lib/src/frames_tracking/span_frame_metrics_collector.dart @@ -66,7 +66,7 @@ class SpanFrameMetricsCollector implements PerformanceContinuousCollector { try { return fn(); } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'SpanFrameMetricsCollector $methodName failed', exception: exception, diff --git a/flutter/lib/src/integrations/debug_print_integration.dart b/flutter/lib/src/integrations/debug_print_integration.dart index faeb97eaad..a5d0e9e0f9 100644 --- a/flutter/lib/src/integrations/debug_print_integration.dart +++ b/flutter/lib/src/integrations/debug_print_integration.dart @@ -49,7 +49,7 @@ class DebugPrintIntegration implements Integration { void _debugPrint(String? message, {int? wrapWidth}) { if (message == null) { - _options.logger( + _options.log( SentryLevel.debug, 'debugPrint Integration received "null" as message. ' 'The message is dropped.', diff --git a/flutter/lib/src/integrations/flutter_error_integration.dart b/flutter/lib/src/integrations/flutter_error_integration.dart index cae17cc24b..4e5965c561 100644 --- a/flutter/lib/src/integrations/flutter_error_integration.dart +++ b/flutter/lib/src/integrations/flutter_error_integration.dart @@ -25,7 +25,7 @@ class FlutterErrorIntegration implements Integration { _integrationOnError = (FlutterErrorDetails errorDetails) async { final exception = errorDetails.exception; - options.logger( + options.log( SentryLevel.debug, 'Capture from onError $exception', ); @@ -49,7 +49,7 @@ class FlutterErrorIntegration implements Integration { if (library != null) 'library': library, }; - options.logger( + options.log( SentryLevel.error, errorDetails.toStringShort(), logger: 'sentry.flutterError', @@ -92,7 +92,7 @@ class FlutterErrorIntegration implements Integration { // we don't call Zone.current.handleUncaughtError because we'd like // to set a specific mechanism for FlutterError.onError. } else { - options.logger( + options.log( SentryLevel.debug, 'Error not captured due to [FlutterErrorDetails.silent], ' 'Enable [SentryFlutterOptions.reportSilentFlutterErrors] ' diff --git a/flutter/lib/src/integrations/frames_tracking_integration.dart b/flutter/lib/src/integrations/frames_tracking_integration.dart index 76782d2e44..103b679e4e 100644 --- a/flutter/lib/src/integrations/frames_tracking_integration.dart +++ b/flutter/lib/src/integrations/frames_tracking_integration.dart @@ -22,26 +22,26 @@ class FramesTrackingIntegration implements Integration { _options = options; if (!options.enableFramesTracking) { - return options.logger(SentryLevel.debug, + return options.log(SentryLevel.debug, '$FramesTrackingIntegration disabled: enableFramesTracking option is false'); } if (options.tracesSampleRate == null && options.tracesSampler == null) { - return options.logger(SentryLevel.debug, + return options.log(SentryLevel.debug, '$FramesTrackingIntegration disabled: tracesSampleRate and tracesSampler are disabled'); } final widgetsBinding = options.bindingUtils.instance; if (widgetsBinding == null || widgetsBinding is! SentryWidgetsBindingMixin) { - return options.logger(SentryLevel.warning, + return options.log(SentryLevel.warning, '$FramesTrackingIntegration disabled: incompatible binding, SentryWidgetsFlutterBinding has not been instantiated. Please, use SentryWidgetsFlutterBinding.ensureInitialized() instead of WidgetsFlutterBinding.ensureInitialized()'); } _widgetsBinding = widgetsBinding; final expectedFrameDuration = await _initializeExpectedFrameDuration(); if (expectedFrameDuration == null) { - return options.logger(SentryLevel.debug, + return options.log(SentryLevel.debug, '$FramesTrackingIntegration disabled: could not fetch valid display refresh rate'); } @@ -57,7 +57,7 @@ class FramesTrackingIntegration implements Integration { _collector = collector; options.sdk.addIntegration(integrationName); - options.logger(SentryLevel.debug, + options.log(SentryLevel.debug, '$FramesTrackingIntegration successfully initialized with an expected frame duration of ${expectedFrameDuration.inMilliseconds}ms'); } diff --git a/flutter/lib/src/integrations/load_contexts_integration.dart b/flutter/lib/src/integrations/load_contexts_integration.dart index 98521d275c..3b2c44e4f9 100644 --- a/flutter/lib/src/integrations/load_contexts_integration.dart +++ b/flutter/lib/src/integrations/load_contexts_integration.dart @@ -224,7 +224,7 @@ class _LoadContextsIntegrationEventProcessor implements EventProcessor { event.tags = tags; } } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'loadContextsIntegration failed', exception: exception, diff --git a/flutter/lib/src/integrations/load_release_integration.dart b/flutter/lib/src/integrations/load_release_integration.dart index ca8a29d11e..c1e2b3cd5e 100644 --- a/flutter/lib/src/integrations/load_release_integration.dart +++ b/flutter/lib/src/integrations/load_release_integration.dart @@ -32,7 +32,7 @@ class LoadReleaseIntegration extends Integration { release = '$release+$buildNumber'; } - options.logger(SentryLevel.debug, 'release: $release'); + options.log(SentryLevel.debug, 'release: $release'); options.release = options.release ?? release; if (buildNumber.isNotEmpty) { @@ -40,7 +40,7 @@ class LoadReleaseIntegration extends Integration { } } } catch (exception, stackTrace) { - options.logger( + options.log( SentryLevel.error, 'Failed to load release and dist', exception: exception, diff --git a/flutter/lib/src/integrations/native_app_start_handler.dart b/flutter/lib/src/integrations/native_app_start_handler.dart index 25de0c933f..7b112d185f 100644 --- a/flutter/lib/src/integrations/native_app_start_handler.dart +++ b/flutter/lib/src/integrations/native_app_start_handler.dart @@ -118,7 +118,7 @@ class NativeAppStartHandler { description: entry.key as String, )); } catch (e) { - _options.logger( + _options.log( SentryLevel.warning, 'Failed to parse native span times: $e'); continue; } @@ -213,7 +213,7 @@ class NativeAppStartHandler { span.data.putIfAbsent('native', () => true); transaction.children.add(span); } catch (e) { - _options.logger(SentryLevel.warning, + _options.log(SentryLevel.warning, 'Failed to attach native span to app start transaction: $e'); } }); diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index b2b55d2bfa..c27155137d 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -35,7 +35,7 @@ class NativeAppStartIntegration extends Integration { appStartEnd: appStartEnd, ); } catch (exception, stackTrace) { - options.logger( + options.log( SentryLevel.error, 'Error while capturing native app start', exception: exception, diff --git a/flutter/lib/src/integrations/native_sdk_integration.dart b/flutter/lib/src/integrations/native_sdk_integration.dart index c9f6ac39f2..76c91eda6e 100644 --- a/flutter/lib/src/integrations/native_sdk_integration.dart +++ b/flutter/lib/src/integrations/native_sdk_integration.dart @@ -29,7 +29,7 @@ class NativeSdkIntegration implements Integration { await _native.init(hub); options.sdk.addIntegration('nativeSdkIntegration'); } catch (exception, stackTrace) { - options.logger( + options.log( SentryLevel.fatal, 'nativeSdkIntegration failed to be installed', exception: exception, @@ -47,7 +47,7 @@ class NativeSdkIntegration implements Integration { try { await _native.close(); } catch (exception, stackTrace) { - _options?.logger( + _options?.log( SentryLevel.fatal, 'nativeSdkIntegration failed to be closed', exception: exception, diff --git a/flutter/lib/src/integrations/on_error_integration.dart b/flutter/lib/src/integrations/on_error_integration.dart index c3ca7e086d..66fdc3e184 100644 --- a/flutter/lib/src/integrations/on_error_integration.dart +++ b/flutter/lib/src/integrations/on_error_integration.dart @@ -45,7 +45,7 @@ class OnErrorIntegration implements Integration { // error printing. To make sure these exceptions are still visible // to developers (and to Sentry), we log them explicitly here. if (handled) { - options.logger( + options.log( SentryLevel.error, 'Uncaught Platform Error', logger: 'sentry.platformError', diff --git a/flutter/lib/src/integrations/screenshot_integration.dart b/flutter/lib/src/integrations/screenshot_integration.dart index 00782ea4aa..2812d82c69 100644 --- a/flutter/lib/src/integrations/screenshot_integration.dart +++ b/flutter/lib/src/integrations/screenshot_integration.dart @@ -12,7 +12,7 @@ class ScreenshotIntegration implements Integration { void call(Hub hub, SentryFlutterOptions options) { if (options.isMultiViewApp) { // ignore: invalid_use_of_internal_member - options.logger( + options.log( SentryLevel.debug, '`ScreenshotIntegration` is not available in multi-view applications.', ); diff --git a/flutter/lib/src/integrations/web_sdk_integration.dart b/flutter/lib/src/integrations/web_sdk_integration.dart index 98ca504a49..7b8856ea71 100644 --- a/flutter/lib/src/integrations/web_sdk_integration.dart +++ b/flutter/lib/src/integrations/web_sdk_integration.dart @@ -40,7 +40,7 @@ class WebSdkIntegration implements Integration { await _web.init(hub); options.sdk.addIntegration(name); } catch (exception, stackTrace) { - options.logger( + options.log( SentryLevel.fatal, '$name failed to be installed.', exception: exception, @@ -58,7 +58,7 @@ class WebSdkIntegration implements Integration { await _web.close(); await _scriptLoader.close(); } catch (error, stackTrace) { - _options?.logger(SentryLevel.warning, '$name failed to be closed.', + _options?.log(SentryLevel.warning, '$name failed to be closed.', exception: error, stackTrace: stackTrace); if (_options?.automatedTestMode == true) { rethrow; diff --git a/flutter/lib/src/integrations/web_session_integration.dart b/flutter/lib/src/integrations/web_session_integration.dart index 99115930d9..2667f86949 100644 --- a/flutter/lib/src/integrations/web_session_integration.dart +++ b/flutter/lib/src/integrations/web_session_integration.dart @@ -27,7 +27,7 @@ class WebSessionIntegration @override void call(Hub hub, SentryFlutterOptions options) { _options = options; - _options?.logger(SentryLevel.info, + _options?.log(SentryLevel.info, '$integrationName initialization started, waiting for SentryNavigatorObserver to be initialized.'); } @@ -42,8 +42,7 @@ class WebSessionIntegration /// so we need to wait until this function is called by the observer. void enable() { if (_isEnabled) { - _options?.logger( - SentryLevel.debug, '$integrationName is already enabled.'); + _options?.log(SentryLevel.debug, '$integrationName is already enabled.'); return; } if (!_shouldEnable()) { @@ -53,8 +52,7 @@ class WebSessionIntegration _webSessionHandler = WebSessionHandler(_native); _options?.addBeforeSendEventObserver(this); _options?.sdk.addIntegration(integrationName); - _options?.logger( - SentryLevel.info, '$integrationName successfully enabled.'); + _options?.log(SentryLevel.info, '$integrationName successfully enabled.'); } bool _shouldEnable() { @@ -62,7 +60,7 @@ class WebSessionIntegration return false; } if (!_options!.enableAutoSessionTracking) { - _options?.logger(SentryLevel.info, + _options?.log(SentryLevel.info, '$integrationName disabled: enableAutoSessionTracking is not enabled'); return false; } diff --git a/flutter/lib/src/integrations/widgets_binding_integration.dart b/flutter/lib/src/integrations/widgets_binding_integration.dart index 64a4fd3717..43477017be 100644 --- a/flutter/lib/src/integrations/widgets_binding_integration.dart +++ b/flutter/lib/src/integrations/widgets_binding_integration.dart @@ -14,7 +14,7 @@ class WidgetsBindingIntegration implements Integration { void call(Hub hub, SentryFlutterOptions options) { if (options.isMultiViewApp) { // ignore: invalid_use_of_internal_member - options.logger( + options.log( SentryLevel.debug, '`WidgetsBindingIntegration` is not available in multi-view applications.', ); diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index af74114fd3..677096543c 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -37,14 +37,13 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { SentryNative(this.options); - void _logNotSupported(String operation) => options.logger( + void _logNotSupported(String operation) => options.log( SentryLevel.debug, 'SentryNative: $operation is not supported'); @override FutureOr init(Hub hub) { if (!options.enableNativeCrashHandling) { - options.logger( - SentryLevel.info, 'SentryNative crash handling is disabled'); + options.log(SentryLevel.info, 'SentryNative crash handling is disabled'); } else { tryCatchSync("init", () { final cOptions = createOptions(options); @@ -73,7 +72,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { native.options_set_max_breadcrumbs(cOptions, options.maxBreadcrumbs); if (options.proxy != null) { // sentry-native expects a single string and it doesn't support different types or authentication - options.logger(SentryLevel.warning, + options.log(SentryLevel.warning, 'SentryNative: setting a proxy is currently not supported'); } @@ -121,7 +120,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { tryCatchSync('remove_user', native.remove_user); } else { tryCatchSync('set_user', () { - var cUser = user.toJson().toNativeValue(options.logger); + var cUser = user.toJson().toNativeValue(options.log); native.set_user(cUser); }); } @@ -130,7 +129,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { @override FutureOr addBreadcrumb(Breadcrumb breadcrumb) { tryCatchSync('add_breadcrumb', () { - var cBreadcrumb = breadcrumb.toJson().toNativeValue(options.logger); + var cBreadcrumb = breadcrumb.toJson().toNativeValue(options.log); native.add_breadcrumb(cBreadcrumb); }); } @@ -152,13 +151,13 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { @override FutureOr setContexts(String key, dynamic value) { tryCatchSync('set_context', () { - final cValue = dynamicToNativeValue(value, options.logger); + final cValue = dynamicToNativeValue(value, options.log); if (cValue != null) { final cKey = key.toNativeUtf8(); native.set_context(cKey.cast(), cValue); malloc.free(cKey); } else { - options.logger(SentryLevel.warning, + options.log(SentryLevel.warning, 'SentryNative: failed to set context $key - value couldn\'t be converted to native'); } }); @@ -176,13 +175,13 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { @override FutureOr setExtra(String key, dynamic value) { tryCatchSync('set_extra', () { - final cValue = dynamicToNativeValue(value, options.logger); + final cValue = dynamicToNativeValue(value, options.log); if (cValue != null) { final cKey = key.toNativeUtf8(); native.set_extra(cKey.cast(), cValue); malloc.free(cKey); } else { - options.logger(SentryLevel.warning, + options.log(SentryLevel.warning, 'SentryNative: failed to set extra $key - value couldn\'t be converted to native'); } }); @@ -248,13 +247,13 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { native.value_get_length(cImages), (index) { final cImage = native.value_get_by_index(cImages, index); return DebugImage( - type: cImage.get('type').castPrimitive(options.logger) ?? '', - imageAddr: cImage.get('image_addr').castPrimitive(options.logger), - imageSize: cImage.get('image_size').castPrimitive(options.logger), - codeFile: cImage.get('code_file').castPrimitive(options.logger), - debugId: cImage.get('debug_id').castPrimitive(options.logger), - debugFile: cImage.get('debug_file').castPrimitive(options.logger), - codeId: cImage.get('code_id').castPrimitive(options.logger), + type: cImage.get('type').castPrimitive(options.log) ?? '', + imageAddr: cImage.get('image_addr').castPrimitive(options.log), + imageSize: cImage.get('image_size').castPrimitive(options.log), + codeFile: cImage.get('code_file').castPrimitive(options.log), + debugId: cImage.get('debug_id').castPrimitive(options.log), + debugFile: cImage.get('debug_file').castPrimitive(options.log), + codeId: cImage.get('code_id').castPrimitive(options.log), ); }); return images; @@ -330,7 +329,7 @@ extension on binding.sentry_value_u { } } - T? castPrimitive(SentryLogger logger) { + T? castPrimitive(SdkLogCallback logger) { if (SentryNative.native.value_is_null(this) == 1) { return null; } @@ -358,7 +357,7 @@ extension on binding.sentry_value_u { } binding.sentry_value_u? dynamicToNativeValue( - dynamic value, SentryLogger logger) { + dynamic value, SdkLogCallback logger) { if (value is String) { return value.toNativeValue(); } else if (value is int) { @@ -410,7 +409,7 @@ extension on bool { } extension on Map { - binding.sentry_value_u toNativeValue(SentryLogger logger) { + binding.sentry_value_u toNativeValue(SdkLogCallback logger) { final cObject = SentryNative.native.value_new_object(); for (final entry in entries) { final cValue = dynamicToNativeValue(entry.value, logger); @@ -421,7 +420,7 @@ extension on Map { } extension on List { - binding.sentry_value_u toNativeValue(SentryLogger logger) { + binding.sentry_value_u toNativeValue(SdkLogCallback logger) { final cObject = SentryNative.native.value_new_list(); for (final value in this) { final cValue = dynamicToNativeValue(value, logger); diff --git a/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart b/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart index 531baf3b65..d822f6beb5 100644 --- a/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart +++ b/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart @@ -24,7 +24,7 @@ class CocoaReplayRecorder { Future?> captureScreenshot() async { return _recorder.capture((screenshot) async { final data = await screenshot.rawRgbaData; - _options.logger( + _options.log( SentryLevel.debug, 'Replay: captured screenshot (' '${screenshot.width}x${screenshot.height} pixels, ' diff --git a/flutter/lib/src/native/java/android_replay_recorder.dart b/flutter/lib/src/native/java/android_replay_recorder.dart index f0af91d004..b85ee9a00e 100644 --- a/flutter/lib/src/native/java/android_replay_recorder.dart +++ b/flutter/lib/src/native/java/android_replay_recorder.dart @@ -46,7 +46,7 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder { try { final data = await screenshot.rawRgbaData; - options.logger( + options.log( SentryLevel.debug, '$logName: captured screenshot (' '${screenshot.width}x${screenshot.height} pixels, ' @@ -59,7 +59,7 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder { height: screenshot.height, )); } catch (error, stackTrace) { - options.logger( + options.log( SentryLevel.error, '$logName: native call `addReplayScreenshot` failed', exception: error, diff --git a/flutter/lib/src/native/native_app_start.dart b/flutter/lib/src/native/native_app_start.dart index 16723400f9..71f0c1eb58 100644 --- a/flutter/lib/src/native/native_app_start.dart +++ b/flutter/lib/src/native/native_app_start.dart @@ -25,7 +25,7 @@ class NativeAppStart { isColdStart is! bool || nativeSpanTimes is! Map) { // ignore: invalid_use_of_internal_member - Sentry.currentHub.options.logger( + Sentry.currentHub.options.log( SentryLevel.warning, 'Failed to parse json when capturing App Start metrics. App Start wont be reported.', ); diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index 72437b7141..68242e49c3 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -29,7 +29,7 @@ class SentryNativeChannel SentryNativeChannel(this.options) : channel = SentrySafeMethodChannel(options); - void _logNotSupported(String operation) => options.logger( + void _logNotSupported(String operation) => options.log( SentryLevel.debug, 'SentryNativeChannel: $operation is not supported'); @override diff --git a/flutter/lib/src/native/sentry_native_invoker.dart b/flutter/lib/src/native/sentry_native_invoker.dart index 0c0a637601..6b20aff03d 100644 --- a/flutter/lib/src/native/sentry_native_invoker.dart +++ b/flutter/lib/src/native/sentry_native_invoker.dart @@ -35,7 +35,7 @@ mixin SentryNativeSafeInvoker { } void _logError(String nativeMethodName, Object error, StackTrace stackTrace) { - options.logger( + options.log( SentryLevel.error, 'Native call `$nativeMethodName` failed', exception: error, diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 9d4a32df42..798d7a3bac 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -353,7 +353,7 @@ class SentryNavigatorObserver extends RouteObserver> { } } } catch (exception, stacktrace) { - _hub.options.logger( + _hub.options.log( SentryLevel.error, 'Error while finishing time to display tracking', exception: exception, @@ -393,7 +393,7 @@ class SentryNavigatorObserver extends RouteObserver> { ); } } catch (exception, stacktrace) { - _hub.options.logger( + _hub.options.log( SentryLevel.error, 'Error while tracking time to display', exception: exception, diff --git a/flutter/lib/src/navigation/time_to_full_display_tracker.dart b/flutter/lib/src/navigation/time_to_full_display_tracker.dart index b8af54eeca..b0cd8619b4 100644 --- a/flutter/lib/src/navigation/time_to_full_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_full_display_tracker.dart @@ -76,7 +76,7 @@ class TimeToFullDisplayTracker { ttfdSpan.finished || startTimestamp == null || endTimestamp == null) { - options.logger( + options.log( SentryLevel.warning, 'TTFD tracker not started or already completed. Dropping TTFD measurement.', ); @@ -98,7 +98,7 @@ class TimeToFullDisplayTracker { endTimestamp: endTimestamp, ); } catch (e, stackTrace) { - options.logger( + options.log( SentryLevel.error, 'Failed to finish TTFD span', exception: e, diff --git a/flutter/lib/src/replay/scheduled_recorder.dart b/flutter/lib/src/replay/scheduled_recorder.dart index 4a79dd832d..4aeccf7d07 100644 --- a/flutter/lib/src/replay/scheduled_recorder.dart +++ b/flutter/lib/src/replay/scheduled_recorder.dart @@ -63,7 +63,7 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { return true; }()); - options.logger(SentryLevel.debug, + options.log(SentryLevel.debug, "$logName: starting capture (${config.width}x${config.height} @ ${config.frameRate} Hz)."); _status = _Status.running; _startScheduler(); @@ -84,11 +84,11 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { } Future stop() async { - options.logger(SentryLevel.debug, "$logName: stopping capture."); + options.log(SentryLevel.debug, "$logName: stopping capture."); _status = _Status.stopped; await _stopScheduler(); // await Future.wait([_stopScheduler(), _idleFrameFiller.stop()]); - options.logger(SentryLevel.debug, "$logName: capture stopped."); + options.log(SentryLevel.debug, "$logName: capture stopped."); } Future pause() async { @@ -113,7 +113,7 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { // _idleFrameFiller.actualFrameReceived(screenshot); } else { // drop any screenshots from callbacks if the replay has already been stopped/paused. - options.logger(SentryLevel.debug, + options.log(SentryLevel.debug, '$logName: screenshot dropped because status=${_status.name}.'); } } @@ -124,7 +124,7 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { await _callback(screenshot, isNewlyCaptured); } else { // drop any screenshots from callbacks if the replay has already been stopped/paused. - options.logger(SentryLevel.debug, + options.log(SentryLevel.debug, '$logName: screenshot dropped because status=${_status.name}.'); } } diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 01e5920eec..2ae1ae28aa 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -41,7 +41,7 @@ class ScreenshotRecorder { void _log(SentryLevel level, String message, {String? logger, Object? exception, StackTrace? stackTrace}) { - options.logger(level, '$logName: $message', + options.log(level, '$logName: $message', logger: logger, exception: exception, stackTrace: stackTrace); } @@ -114,7 +114,7 @@ class ScreenshotRecorder { List? _obscureSync(_Capture capture) { if (_maskingConfig != null) { - final filter = WidgetFilter(_maskingConfig, options.logger); + final filter = WidgetFilter(_maskingConfig, options.log); final colorScheme = capture.context.findColorScheme(); filter.obscure( root: capture.root, diff --git a/flutter/lib/src/screenshot/widget_filter.dart b/flutter/lib/src/screenshot/widget_filter.dart index f78c5f61f9..b7e19a3314 100644 --- a/flutter/lib/src/screenshot/widget_filter.dart +++ b/flutter/lib/src/screenshot/widget_filter.dart @@ -9,7 +9,7 @@ import 'masking_config.dart'; @internal class WidgetFilter { final items = []; - final SentryLogger logger; + final SdkLogCallback logger; final SentryMaskingConfig config; late WidgetFilterColorScheme _scheme; late RenderObject _root; diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 21a8e510d0..ca1dfbcbaf 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -252,7 +252,7 @@ mixin SentryFlutter { try { return options.timeToDisplayTracker.reportFullyDisplayed(); } catch (exception, stackTrace) { - options.logger( + options.log( SentryLevel.error, 'Error while reporting TTFD', exception: exception, @@ -289,7 +289,7 @@ mixin SentryFlutter { static Future captureScreenshot() async { final options = Sentry.currentHub.options; if (!SentryScreenshotWidget.isMounted) { - options.logger( + options.log( SentryLevel.debug, 'SentryScreenshotWidget could not be found in the widget tree.', ); @@ -298,7 +298,7 @@ mixin SentryFlutter { final processors = options.eventProcessors.whereType(); if (processors.isEmpty) { - options.logger( + options.log( SentryLevel.debug, 'ScreenshotEventProcessor could not be found.', ); @@ -333,7 +333,7 @@ mixin SentryFlutter { } static void _logNativeIntegrationNotAvailable(String methodName) { - Sentry.currentHub.options.logger( + Sentry.currentHub.options.log( SentryLevel.debug, 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the $methodName API.', ); diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index 138494571a..308f482078 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -28,7 +28,7 @@ class SentryPrivacyOptions { @internal SentryMaskingConfig buildMaskingConfig( - SentryLogger logger, RuntimeChecker runtimeChecker) { + SdkLogCallback logger, RuntimeChecker runtimeChecker) { // First, we collect rules defined by the user (so they're applied first). final rules = _userMaskingRules.toList(); diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 027d351d50..3c432dd313 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -37,7 +37,7 @@ class _SentryWidgetState extends State { Widget content = widget.child; if (widget._options?.isMultiViewApp ?? false) { // ignore: invalid_use_of_internal_member - Sentry.currentHub.options.logger( + Sentry.currentHub.options.log( SentryLevel.debug, '`SentryWidget` is not available in multi-view apps.', ); diff --git a/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart b/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart index a9fc134fcf..f16459d34d 100644 --- a/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart +++ b/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart @@ -301,7 +301,7 @@ class _SentryUserInteractionWidgetState _lastPointerId = event.pointer; _lastPointerDownLocation = event.localPosition; } catch (exception, stacktrace) { - _options?.logger( + _options?.log( SentryLevel.error, 'Error while handling pointer-down event $event in $SentryUserInteractionWidget', exception: exception, @@ -331,7 +331,7 @@ class _SentryUserInteractionWidgetState _onTappedAt(event.localPosition); } } catch (exception, stacktrace) { - _options?.logger( + _options?.log( SentryLevel.error, 'Error while handling pointer-up event $event in $SentryUserInteractionWidget', exception: exception, diff --git a/flutter/lib/src/utils/platform_dispatcher_wrapper.dart b/flutter/lib/src/utils/platform_dispatcher_wrapper.dart index 38933d6269..0e5823a843 100644 --- a/flutter/lib/src/utils/platform_dispatcher_wrapper.dart +++ b/flutter/lib/src/utils/platform_dispatcher_wrapper.dart @@ -24,7 +24,7 @@ class PlatformDispatcherWrapper { return false; } catch (exception, stacktrace) { // This error is neither expected on pre 3.10.0 nor on >= 3.10.0 Flutter versions - options.logger( + options.log( SentryLevel.debug, 'An unexpected exception was thrown, please create an issue at https://github.com/getsentry/sentry-dart/issues', exception: exception, @@ -54,7 +54,7 @@ class PlatformDispatcherWrapper { return false; } catch (exception, stacktrace) { // This error is neither expected on pre 3.1 nor on >= 3.1 Flutter versions - options.logger( + options.log( SentryLevel.debug, 'An unexpected exception was thrown, please create an issue at https://github.com/getsentry/sentry-dart/issues', exception: exception, diff --git a/flutter/lib/src/view_hierarchy/view_hierarchy_event_processor.dart b/flutter/lib/src/view_hierarchy/view_hierarchy_event_processor.dart index f595b3d456..aa3343389a 100644 --- a/flutter/lib/src/view_hierarchy/view_hierarchy_event_processor.dart +++ b/flutter/lib/src/view_hierarchy/view_hierarchy_event_processor.dart @@ -55,7 +55,7 @@ class SentryViewHierarchyEventProcessor implements EventProcessor { captureViewHierarchy = result; } } else if (shouldDebounce) { - _options.logger( + _options.log( SentryLevel.debug, 'Skipping view hierarchy capture due to debouncing (too many captures within ${_debouncer.waitTime.inMilliseconds}ms)', ); @@ -66,7 +66,7 @@ class SentryViewHierarchyEventProcessor implements EventProcessor { return event; } } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'The beforeCaptureViewHierarchy callback threw an exception', exception: exception, diff --git a/flutter/lib/src/web/javascript_transport.dart b/flutter/lib/src/web/javascript_transport.dart index 0ff17da5f8..2c5a93acb0 100644 --- a/flutter/lib/src/web/javascript_transport.dart +++ b/flutter/lib/src/web/javascript_transport.dart @@ -12,7 +12,7 @@ class JavascriptTransport implements Transport { try { await _binding.captureStructuredEnvelope(envelope); } catch (exception, stackTrace) { - _options.logger( + _options.log( SentryLevel.error, 'Failed to send envelope', exception: exception, diff --git a/flutter/lib/src/web/script_loader/sentry_script_loader.dart b/flutter/lib/src/web/script_loader/sentry_script_loader.dart index 621f84e51b..d25e7a2eba 100644 --- a/flutter/lib/src/web/script_loader/sentry_script_loader.dart +++ b/flutter/lib/src/web/script_loader/sentry_script_loader.dart @@ -45,10 +45,10 @@ class SentryScriptLoader { }); _scriptLoaded = true; - _options.logger(SentryLevel.debug, + _options.log(SentryLevel.debug, 'JS SDK integration: all Sentry scripts loaded successfully.'); } catch (e) { - _options.logger(SentryLevel.error, 'Failed to load Sentry scripts: $e'); + _options.log(SentryLevel.error, 'Failed to load Sentry scripts: $e'); // ignore: invalid_use_of_internal_member if (_options.automatedTestMode) { rethrow; diff --git a/flutter/lib/src/web/sentry_web.dart b/flutter/lib/src/web/sentry_web.dart index 5e209c0707..ac7a3cc1de 100644 --- a/flutter/lib/src/web/sentry_web.dart +++ b/flutter/lib/src/web/sentry_web.dart @@ -18,8 +18,8 @@ class SentryWeb with SentryNativeSafeInvoker implements SentryNativeBinding { final SentryJsBinding _binding; final SentryFlutterOptions _options; - void _logNotSupported(String operation) => options.logger( - SentryLevel.debug, 'SentryWeb: $operation is not supported'); + void _logNotSupported(String operation) => + options.log(SentryLevel.debug, 'SentryWeb: $operation is not supported'); @override FutureOr init(Hub hub) { diff --git a/flutter/lib/src/widgets_binding_observer.dart b/flutter/lib/src/widgets_binding_observer.dart index 63c63a560e..56a37b23e1 100644 --- a/flutter/lib/src/widgets_binding_observer.dart +++ b/flutter/lib/src/widgets_binding_observer.dart @@ -94,6 +94,7 @@ class SentryWidgetsBindingObserver with WidgetsBindingObserver { if (!_isNavigatorObserverCreated() && !_options.platform.isWeb) { if (state == AppLifecycleState.inactive) { _appInBackgroundStopwatch.start(); + _options.logBatcher.flush(); } else if (_appInBackgroundStopwatch.isRunning && state == AppLifecycleState.resumed) { _appInBackgroundStopwatch.stop(); diff --git a/flutter/test/widgets_binding_observer_test.dart b/flutter/test/widgets_binding_observer_test.dart index 122510f0a3..c470ac2ad8 100644 --- a/flutter/test/widgets_binding_observer_test.dart +++ b/flutter/test/widgets_binding_observer_test.dart @@ -9,6 +9,8 @@ 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'; @@ -561,5 +563,46 @@ void main() { instance.removeObserver(observer); }); + + testWidgets( + 'calls flush on logs batcher when transitioning to inactive state', + (WidgetTester tester) async { + final hub = MockHub(); + + final mockLogBatcher = MockLogBatcher(); + + final options = defaultTestOptions(); + options.platform = MockPlatform(isWeb: false); + options.bindingUtils = TestBindingWrapper(); + + options.logBatcher = mockLogBatcher; + options.enableLogs = true; + + final observer = SentryWidgetsBindingObserver( + hub: hub, + options: options, + isNavigatorObserverCreated: () => false, + ); + final instance = options.bindingUtils.instance!; + instance.addObserver(observer); + + await sendLifecycle('inactive'); + + expect(mockLogBatcher.flushCalled, true); + + instance.removeObserver(observer); + }); }); } + +class MockLogBatcher implements SentryLogBatcher { + var flushCalled = false; + + @override + void addLog(SentryLog log) {} + + @override + Future flush() async { + flushCalled = true; + } +}