From ebad35e3f82f2a4bb1aff74e5ce842259111d6ad Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 10 Mar 2026 12:33:46 +0100 Subject: [PATCH 1/4] feat: Add Hint support to beforeSendLog and pass LogRecord from sentry_logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional Hint parameter to BeforeSendLogCallback and threads it through the entire log capture pipeline. The LoggingIntegration now passes the full LogRecord as a hint, allowing users to access the original object in beforeSendLog and derive custom attributes from it — without needing Sentry-specific code at log call sites. --- packages/dart/lib/src/hub.dart | 3 +- packages/dart/lib/src/hub_adapter.dart | 3 +- packages/dart/lib/src/noop_hub.dart | 2 +- packages/dart/lib/src/noop_sentry_client.dart | 2 +- packages/dart/lib/src/sentry_client.dart | 4 +- packages/dart/lib/src/sentry_options.dart | 3 +- .../lib/src/telemetry/log/default_logger.dart | 30 +++++++++---- .../telemetry/log/log_capture_pipeline.dart | 4 +- .../dart/lib/src/telemetry/log/logger.dart | 6 +++ .../lib/src/telemetry/log/noop_logger.dart | 7 +++ packages/dart/test/mocks/mock_hub.dart | 4 +- .../test/mocks/mock_log_capture_pipeline.dart | 4 +- .../dart/test/mocks/mock_sentry_client.dart | 7 +-- .../log/log_capture_pipeline_test.dart | 12 ++--- .../telemetry/log/logger_formatter_test.dart | 2 +- .../log/logger_setup_integration_test.dart | 6 +++ .../dart/test/telemetry/log/logger_test.dart | 2 +- .../logging/lib/src/logging_integration.dart | 17 ++++--- .../test/logging_integration_test.dart | 44 ++++++++++++++++--- 19 files changed, 118 insertions(+), 44 deletions(-) diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index 77a1c8581e..54f2bed2fc 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -285,7 +285,7 @@ class Hub { return sentryId; } - FutureOr captureLog(SentryLog log) async { + FutureOr captureLog(SentryLog log, {Hint? hint}) async { if (!_isEnabled) { _options.log( SentryLevel.warning, @@ -305,6 +305,7 @@ class Hub { await item.client.captureLog( log, scope: scope, + hint: hint, ); } catch (exception, stacktrace) { _options.log( diff --git a/packages/dart/lib/src/hub_adapter.dart b/packages/dart/lib/src/hub_adapter.dart index 73ee33c4cf..3926fa54c9 100644 --- a/packages/dart/lib/src/hub_adapter.dart +++ b/packages/dart/lib/src/hub_adapter.dart @@ -199,7 +199,8 @@ class HubAdapter implements Hub { ); @override - FutureOr captureLog(SentryLog log) => Sentry.currentHub.captureLog(log); + FutureOr captureLog(SentryLog log, {Hint? hint}) => + Sentry.currentHub.captureLog(log, hint: hint); @override Future captureMetric(SentryMetric metric) => diff --git a/packages/dart/lib/src/noop_hub.dart b/packages/dart/lib/src/noop_hub.dart index 81855f0bef..5716bc387f 100644 --- a/packages/dart/lib/src/noop_hub.dart +++ b/packages/dart/lib/src/noop_hub.dart @@ -96,7 +96,7 @@ class NoOpHub implements Hub { SentryId.empty(); @override - FutureOr captureLog(SentryLog log) async {} + FutureOr captureLog(SentryLog log, {Hint? hint}) async {} @override Future captureMetric(SentryMetric metric) async {} diff --git a/packages/dart/lib/src/noop_sentry_client.dart b/packages/dart/lib/src/noop_sentry_client.dart index a889df5d20..57bb9d7c2b 100644 --- a/packages/dart/lib/src/noop_sentry_client.dart +++ b/packages/dart/lib/src/noop_sentry_client.dart @@ -69,7 +69,7 @@ class NoOpSentryClient implements SentryClient { SentryId.empty(); @override - FutureOr captureLog(SentryLog log, {Scope? scope}) async {} + FutureOr captureLog(SentryLog log, {Scope? scope, Hint? hint}) async {} @override Future captureMetric(SentryMetric metric, {Scope? scope}) async {} diff --git a/packages/dart/lib/src/sentry_client.dart b/packages/dart/lib/src/sentry_client.dart index 38c686ba9d..d4592a2827 100644 --- a/packages/dart/lib/src/sentry_client.dart +++ b/packages/dart/lib/src/sentry_client.dart @@ -501,8 +501,8 @@ class SentryClient { } @internal - FutureOr captureLog(SentryLog log, {Scope? scope}) => - _logCapturePipeline.captureLog(log, scope: scope); + FutureOr captureLog(SentryLog log, {Scope? scope, Hint? hint}) => + _logCapturePipeline.captureLog(log, scope: scope, hint: hint); Future captureMetric(SentryMetric metric, {Scope? scope}) => _metricCapturePipeline.captureMetric(metric, scope: scope); diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 6a40127734..2332bffa6f 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -710,7 +710,8 @@ typedef BeforeBreadcrumbCallback = Breadcrumb? Function( /// 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); +typedef BeforeSendLogCallback = FutureOr Function(SentryLog log, + {Hint? hint}); /// This function is called right before a metric is about to be emitted. /// Can return true to emit the metric, or false to drop it. diff --git a/packages/dart/lib/src/telemetry/log/default_logger.dart b/packages/dart/lib/src/telemetry/log/default_logger.dart index 40ee263589..4209b3043a 100644 --- a/packages/dart/lib/src/telemetry/log/default_logger.dart +++ b/packages/dart/lib/src/telemetry/log/default_logger.dart @@ -4,7 +4,8 @@ import '../../../sentry.dart'; import '../../sentry_template_string.dart'; import '../../utils/internal_logger.dart'; -typedef CaptureLogCallback = FutureOr Function(SentryLog log); +typedef CaptureLogCallback = FutureOr Function(SentryLog log, + {Hint? hint}); typedef ScopeProvider = Scope Function(); final class DefaultSentryLogger implements SentryLogger { @@ -27,48 +28,60 @@ final class DefaultSentryLogger implements SentryLogger { FutureOr trace( String body, { Map? attributes, + Hint? hint, }) { - return _captureLog(SentryLogLevel.trace, body, attributes: attributes); + return _captureLog(SentryLogLevel.trace, body, + attributes: attributes, hint: hint); } @override FutureOr debug( String body, { Map? attributes, + Hint? hint, }) { - return _captureLog(SentryLogLevel.debug, body, attributes: attributes); + return _captureLog(SentryLogLevel.debug, body, + attributes: attributes, hint: hint); } @override FutureOr info( String body, { Map? attributes, + Hint? hint, }) { - return _captureLog(SentryLogLevel.info, body, attributes: attributes); + return _captureLog(SentryLogLevel.info, body, + attributes: attributes, hint: hint); } @override FutureOr warn( String body, { Map? attributes, + Hint? hint, }) { - return _captureLog(SentryLogLevel.warn, body, attributes: attributes); + return _captureLog(SentryLogLevel.warn, body, + attributes: attributes, hint: hint); } @override FutureOr error( String body, { Map? attributes, + Hint? hint, }) { - return _captureLog(SentryLogLevel.error, body, attributes: attributes); + return _captureLog(SentryLogLevel.error, body, + attributes: attributes, hint: hint); } @override FutureOr fatal( String body, { Map? attributes, + Hint? hint, }) { - return _captureLog(SentryLogLevel.fatal, body, attributes: attributes); + return _captureLog(SentryLogLevel.fatal, body, + attributes: attributes, hint: hint); } @override @@ -80,6 +93,7 @@ final class DefaultSentryLogger implements SentryLogger { SentryLogLevel level, String body, { Map? attributes, + Hint? hint, }) { internalLogger.debug(() => 'Sentry.logger.${level.value}("$body") called with attributes ${_formatAttributes(attributes)}'); @@ -93,7 +107,7 @@ final class DefaultSentryLogger implements SentryLogger { attributes: attributes ?? {}, ); - return _captureLogCallback(log); + return _captureLogCallback(log, hint: hint); } String _formatAttributes(Map? attributes) { diff --git a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart index 4abf24b6df..25dda9e2f5 100644 --- a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart +++ b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart @@ -14,7 +14,7 @@ class LogCapturePipeline { LogCapturePipeline(this._options); - FutureOr captureLog(SentryLog log, {Scope? scope}) async { + FutureOr captureLog(SentryLog log, {Scope? scope, Hint? hint}) async { if (!_options.enableLogs) { internalLogger .debug('$LogCapturePipeline: Logs disabled, dropping ${log.body}'); @@ -40,7 +40,7 @@ class LogCapturePipeline { SentryLog? processedLog = log; if (beforeSendLog != null) { try { - final callbackResult = beforeSendLog(log); + final callbackResult = beforeSendLog(log, hint: hint); if (callbackResult is Future) { processedLog = await callbackResult; diff --git a/packages/dart/lib/src/telemetry/log/logger.dart b/packages/dart/lib/src/telemetry/log/logger.dart index 0b8f253832..7d7bce6bb4 100644 --- a/packages/dart/lib/src/telemetry/log/logger.dart +++ b/packages/dart/lib/src/telemetry/log/logger.dart @@ -12,36 +12,42 @@ abstract interface class SentryLogger { FutureOr trace( String body, { Map? attributes, + Hint? hint, }); /// Logs a message at DEBUG level. FutureOr debug( String body, { Map? attributes, + Hint? hint, }); /// Logs a message at INFO level. FutureOr info( String body, { Map? attributes, + Hint? hint, }); /// Logs a message at WARN level. FutureOr warn( String body, { Map? attributes, + Hint? hint, }); /// Logs a message at ERROR level. FutureOr error( String body, { Map? attributes, + Hint? hint, }); /// Logs a message at FATAL level. FutureOr fatal( String body, { Map? attributes, + Hint? hint, }); /// Provides formatted logging with template strings. diff --git a/packages/dart/lib/src/telemetry/log/noop_logger.dart b/packages/dart/lib/src/telemetry/log/noop_logger.dart index 0381bdcb88..1ec1752b34 100644 --- a/packages/dart/lib/src/telemetry/log/noop_logger.dart +++ b/packages/dart/lib/src/telemetry/log/noop_logger.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import '../../hint.dart'; import '../../protocol/sentry_attribute.dart'; import 'logger.dart'; @@ -12,36 +13,42 @@ final class NoOpSentryLogger implements SentryLogger { FutureOr trace( String body, { Map? attributes, + Hint? hint, }) {} @override FutureOr debug( String body, { Map? attributes, + Hint? hint, }) {} @override FutureOr info( String body, { Map? attributes, + Hint? hint, }) {} @override FutureOr warn( String body, { Map? attributes, + Hint? hint, }) {} @override FutureOr error( String body, { Map? attributes, + Hint? hint, }) {} @override FutureOr fatal( String body, { Map? attributes, + Hint? hint, }) {} @override diff --git a/packages/dart/test/mocks/mock_hub.dart b/packages/dart/test/mocks/mock_hub.dart index 2499af1a29..a0b9c33a49 100644 --- a/packages/dart/test/mocks/mock_hub.dart +++ b/packages/dart/test/mocks/mock_hub.dart @@ -111,8 +111,8 @@ class MockHub with NoSuchMethodProvider implements Hub { } @override - FutureOr captureLog(SentryLog log, {Scope? scope}) async { - captureLogCalls.add(CaptureLogCall(log, scope)); + FutureOr captureLog(SentryLog log, {Hint? hint}) async { + captureLogCalls.add(CaptureLogCall(log, null, hint)); } @override diff --git a/packages/dart/test/mocks/mock_log_capture_pipeline.dart b/packages/dart/test/mocks/mock_log_capture_pipeline.dart index 0e88e6b4da..00f683118f 100644 --- a/packages/dart/test/mocks/mock_log_capture_pipeline.dart +++ b/packages/dart/test/mocks/mock_log_capture_pipeline.dart @@ -13,7 +13,7 @@ class MockLogCapturePipeline extends LogCapturePipeline { int get callCount => captureLogCalls.length; @override - FutureOr captureLog(SentryLog log, {Scope? scope}) async { - captureLogCalls.add(CaptureLogCall(log, scope)); + FutureOr captureLog(SentryLog log, {Scope? scope, Hint? hint}) async { + captureLogCalls.add(CaptureLogCall(log, scope, hint)); } } diff --git a/packages/dart/test/mocks/mock_sentry_client.dart b/packages/dart/test/mocks/mock_sentry_client.dart index e6550a6ba7..8d227fd31f 100644 --- a/packages/dart/test/mocks/mock_sentry_client.dart +++ b/packages/dart/test/mocks/mock_sentry_client.dart @@ -87,8 +87,8 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { } @override - FutureOr captureLog(SentryLog log, {Scope? scope}) async { - captureLogCalls.add(CaptureLogCall(log, scope)); + FutureOr captureLog(SentryLog log, {Scope? scope, Hint? hint}) async { + captureLogCalls.add(CaptureLogCall(log, scope, hint)); } @override @@ -189,8 +189,9 @@ class CaptureTransactionCall { class CaptureLogCall { final SentryLog log; final Scope? scope; + final Hint? hint; - CaptureLogCall(this.log, this.scope); + CaptureLogCall(this.log, this.scope, this.hint); } class CaptureMetricCall { diff --git a/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart b/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart index ffbe119712..cd02b6b094 100644 --- a/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart +++ b/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart @@ -98,7 +98,7 @@ void main() { event.log.attributes.containsKey('scope-attr'); }); - fixture.options.beforeSendLog = (log) { + fixture.options.beforeSendLog = (log, {Hint? hint}) { operations.add('beforeSendLog'); return log; }; @@ -145,7 +145,7 @@ void main() { group('when beforeSendLog is configured', () { test('returning null drops the log', () async { - fixture.options.beforeSendLog = (_) => null; + fixture.options.beforeSendLog = (_, {Hint? hint}) => null; final log = givenLog(); @@ -155,7 +155,7 @@ void main() { }); test('returning null records lost event in client report', () async { - fixture.options.beforeSendLog = (_) => null; + fixture.options.beforeSendLog = (_, {Hint? hint}) => null; final log = givenLog(); @@ -169,7 +169,7 @@ void main() { }); test('can mutate the log', () async { - fixture.options.beforeSendLog = (log) { + fixture.options.beforeSendLog = (log, {Hint? hint}) { log.body = 'modified-body'; log.attributes['added-key'] = SentryAttribute.string('added'); return log; @@ -186,7 +186,7 @@ void main() { }); test('async callback is awaited', () async { - fixture.options.beforeSendLog = (log) async { + fixture.options.beforeSendLog = (log, {Hint? hint}) async { await Future.delayed(Duration(milliseconds: 10)); log.body = 'async-modified'; return log; @@ -204,7 +204,7 @@ void main() { test('exception in callback is caught and log is still captured', () async { fixture.options.automatedTestMode = false; - fixture.options.beforeSendLog = (log) { + fixture.options.beforeSendLog = (log, {Hint? hint}) { throw Exception('test'); }; diff --git a/packages/dart/test/telemetry/log/logger_formatter_test.dart b/packages/dart/test/telemetry/log/logger_formatter_test.dart index 3c74f2385d..74f24afa4a 100644 --- a/packages/dart/test/telemetry/log/logger_formatter_test.dart +++ b/packages/dart/test/telemetry/log/logger_formatter_test.dart @@ -285,7 +285,7 @@ class Fixture { Fixture() { scope = Scope(options); logger = DefaultSentryLogger( - captureLogCallback: (log, {scope}) { + captureLogCallback: (log, {Hint? hint}) { capturedLogs.add(log); }, clockProvider: () => DateTime.now(), diff --git a/packages/dart/test/telemetry/log/logger_setup_integration_test.dart b/packages/dart/test/telemetry/log/logger_setup_integration_test.dart index 24ef14af52..0e555d2acb 100644 --- a/packages/dart/test/telemetry/log/logger_setup_integration_test.dart +++ b/packages/dart/test/telemetry/log/logger_setup_integration_test.dart @@ -87,36 +87,42 @@ class _CustomSentryLogger implements SentryLogger { FutureOr trace( String body, { Map? attributes, + Hint? hint, }) {} @override FutureOr debug( String body, { Map? attributes, + Hint? hint, }) {} @override FutureOr info( String body, { Map? attributes, + Hint? hint, }) {} @override FutureOr warn( String body, { Map? attributes, + Hint? hint, }) {} @override FutureOr error( String body, { Map? attributes, + Hint? hint, }) {} @override FutureOr fatal( String body, { Map? attributes, + Hint? hint, }) {} @override diff --git a/packages/dart/test/telemetry/log/logger_test.dart b/packages/dart/test/telemetry/log/logger_test.dart index ffb37a0530..7a786313f1 100644 --- a/packages/dart/test/telemetry/log/logger_test.dart +++ b/packages/dart/test/telemetry/log/logger_test.dart @@ -158,7 +158,7 @@ class Fixture { SentryLogger getSut() { return DefaultSentryLogger( - captureLogCallback: (log, {scope}) { + captureLogCallback: (log, {Hint? hint}) { capturedLogs.add(log); }, clockProvider: () => timestamp, diff --git a/packages/logging/lib/src/logging_integration.dart b/packages/logging/lib/src/logging_integration.dart index 061e699c69..83fff5ad97 100644 --- a/packages/logging/lib/src/logging_integration.dart +++ b/packages/logging/lib/src/logging_integration.dart @@ -108,25 +108,32 @@ class LoggingIntegration implements Integration { 'sentry.origin': SentryAttribute.string(origin), }; + final hint = Hint.withMap({TypeCheckHint.record: record}); + // Map log levels based on value ranges final levelValue = record.level.value; if (levelValue >= Level.SEVERE.value) { // >= 1000 → error - await _options.logger.error(record.message, attributes: attributes); + await _options.logger + .error(record.message, attributes: attributes, hint: hint); } else if (levelValue >= Level.WARNING.value) { // >= 900 → warn - await _options.logger.warn(record.message, attributes: attributes); + await _options.logger + .warn(record.message, attributes: attributes, hint: hint); } else if (levelValue >= Level.INFO.value) { // >= 800 → info - await _options.logger.info(record.message, attributes: attributes); + await _options.logger + .info(record.message, attributes: attributes, hint: hint); } else if (levelValue >= Level.CONFIG.value || levelValue == Level.FINE.value || levelValue == Level.ALL.value) { // >= 700 || 500 || 0 → debug - await _options.logger.debug(record.message, attributes: attributes); + await _options.logger + .debug(record.message, attributes: attributes, hint: hint); } else { // < 700 → trace - await _options.logger.trace(record.message, attributes: attributes); + await _options.logger + .trace(record.message, attributes: attributes, hint: hint); } } } diff --git a/packages/logging/test/logging_integration_test.dart b/packages/logging/test/logging_integration_test.dart index 1147f6099c..d94c065582 100644 --- a/packages/logging/test/logging_integration_test.dart +++ b/packages/logging/test/logging_integration_test.dart @@ -344,6 +344,29 @@ void main() { expect(fullAttributes.containsKey('stackTrace'), false); }); + test('passes LogRecord hint to sentry logger calls', () async { + final mockLogger = MockSentryLogger(); + final options = TestSentryOptions(mockLogger)..enableLogs = true; + + final sut = fixture.createSut(minSentryLogLevel: Level.INFO); + sut.call(fixture.hub, options); + + final log = Logger('TestLogger'); + final customObject = {'key': 'value'}; + log.info(customObject); + + await Future.delayed(Duration(milliseconds: 10)); + + expect(mockLogger.infoCalls.length, 1); + final hint = mockLogger.infoCalls.first.hint; + expect(hint, isNotNull); + + final record = hint!.get(TypeCheckHint.record) as LogRecord; + expect(record.loggerName, 'TestLogger'); + expect(record.level, Level.INFO); + expect(record.object, same(customObject)); + }); + test('Level.OFF is never sent to sentry logger', () async { final mockLogger = MockSentryLogger(); final options = TestSentryOptions(mockLogger)..enableLogs = true; @@ -492,48 +515,54 @@ class MockSentryLogger implements SentryLogger { Future trace( String body, { Map? attributes, + Hint? hint, }) async { - traceCalls.add(MockLogCall(body, attributes)); + traceCalls.add(MockLogCall(body, attributes, hint)); } @override Future debug( String body, { Map? attributes, + Hint? hint, }) async { - debugCalls.add(MockLogCall(body, attributes)); + debugCalls.add(MockLogCall(body, attributes, hint)); } @override Future info( String body, { Map? attributes, + Hint? hint, }) async { - infoCalls.add(MockLogCall(body, attributes)); + infoCalls.add(MockLogCall(body, attributes, hint)); } @override Future warn( String body, { Map? attributes, + Hint? hint, }) async { - warnCalls.add(MockLogCall(body, attributes)); + warnCalls.add(MockLogCall(body, attributes, hint)); } @override Future error( String body, { Map? attributes, + Hint? hint, }) async { - errorCalls.add(MockLogCall(body, attributes)); + errorCalls.add(MockLogCall(body, attributes, hint)); } @override Future fatal( String body, { Map? attributes, + Hint? hint, }) async { - fatalCalls.add(MockLogCall(body, attributes)); + fatalCalls.add(MockLogCall(body, attributes, hint)); } @override @@ -543,8 +572,9 @@ class MockSentryLogger implements SentryLogger { class MockLogCall { final String message; final Map? attributes; + final Hint? hint; - MockLogCall(this.message, this.attributes); + MockLogCall(this.message, this.attributes, this.hint); } class TestSentryOptions extends SentryOptions { From dc7f3f0347ed73ac2be145b547b1e8f72300121a Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Fri, 27 Mar 2026 14:08:18 +0100 Subject: [PATCH 2/4] add cl entry --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77268030e6..869f77eecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add Hint support to `beforeSendLog` and pass LogRecord from sentry_logging ([#3549](https://github.com/getsentry/sentry-dart/pull/3549)) + ## 9.16.0 ### Dependencies From 2dbeef5f20f5d57dfd320bde68194a81f4720a04 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Fri, 27 Mar 2026 14:21:05 +0100 Subject: [PATCH 3/4] update callback signature, always pass hint --- CHANGELOG.md | 4 ++ packages/dart/lib/src/sentry_options.dart | 6 ++- .../lib/src/telemetry/log/default_logger.dart | 24 ++++++++--- .../telemetry/log/log_capture_pipeline.dart | 2 +- .../dart/lib/src/telemetry/log/logger.dart | 6 +++ .../lib/src/telemetry/log/noop_logger.dart | 6 +++ .../log/log_capture_pipeline_test.dart | 43 ++++++++++++++++--- .../log/logger_setup_integration_test.dart | 6 +++ 8 files changed, 82 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 869f77eecc..a3f0c9a9b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Add Hint support to `beforeSendLog` and pass LogRecord from sentry_logging ([#3549](https://github.com/getsentry/sentry-dart/pull/3549)) +### Breaking Changes + +- `BeforeSendLogCallback` now takes `Hint` as a required positional parameter (matching `BeforeSendCallback` and `BeforeSendTransactionCallback`) ([#3549](https://github.com/getsentry/sentry-dart/pull/3549)) + ## 9.16.0 ### Dependencies diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index 2332bffa6f..a49732770c 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -710,8 +710,10 @@ typedef BeforeBreadcrumbCallback = Breadcrumb? Function( /// 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, - {Hint? hint}); +typedef BeforeSendLogCallback = FutureOr Function( + SentryLog log, + Hint hint, +); /// This function is called right before a metric is about to be emitted. /// Can return true to emit the metric, or false to drop it. diff --git a/packages/dart/lib/src/telemetry/log/default_logger.dart b/packages/dart/lib/src/telemetry/log/default_logger.dart index 4209b3043a..093c5ea5eb 100644 --- a/packages/dart/lib/src/telemetry/log/default_logger.dart +++ b/packages/dart/lib/src/telemetry/log/default_logger.dart @@ -126,13 +126,15 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.trace(formattedBody, attributes: allAttributes); + return _logger.trace(formattedBody, + attributes: allAttributes, hint: hint); }, ); } @@ -142,13 +144,15 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.debug(formattedBody, attributes: allAttributes); + return _logger.debug(formattedBody, + attributes: allAttributes, hint: hint); }, ); } @@ -158,13 +162,15 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.info(formattedBody, attributes: allAttributes); + return _logger.info(formattedBody, + attributes: allAttributes, hint: hint); }, ); } @@ -174,13 +180,15 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.warn(formattedBody, attributes: allAttributes); + return _logger.warn(formattedBody, + attributes: allAttributes, hint: hint); }, ); } @@ -190,13 +198,15 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.error(formattedBody, attributes: allAttributes); + return _logger.error(formattedBody, + attributes: allAttributes, hint: hint); }, ); } @@ -206,13 +216,15 @@ final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) { return _format( templateBody, arguments, attributes, (formattedBody, allAttributes) { - return _logger.fatal(formattedBody, attributes: allAttributes); + return _logger.fatal(formattedBody, + attributes: allAttributes, hint: hint); }, ); } diff --git a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart index 25dda9e2f5..9c00931c9b 100644 --- a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart +++ b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart @@ -40,7 +40,7 @@ class LogCapturePipeline { SentryLog? processedLog = log; if (beforeSendLog != null) { try { - final callbackResult = beforeSendLog(log, hint: hint); + final callbackResult = beforeSendLog(log, hint ?? Hint()); if (callbackResult is Future) { processedLog = await callbackResult; diff --git a/packages/dart/lib/src/telemetry/log/logger.dart b/packages/dart/lib/src/telemetry/log/logger.dart index 7d7bce6bb4..d5883ed83f 100644 --- a/packages/dart/lib/src/telemetry/log/logger.dart +++ b/packages/dart/lib/src/telemetry/log/logger.dart @@ -63,6 +63,7 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }); /// Logs a formatted message at DEBUG level. @@ -70,6 +71,7 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }); /// Logs a formatted message at INFO level. @@ -77,6 +79,7 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }); /// Logs a formatted message at WARN level. @@ -84,6 +87,7 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }); /// Logs a formatted message at ERROR level. @@ -91,6 +95,7 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }); /// Logs a formatted message at FATAL level. @@ -98,5 +103,6 @@ abstract interface class SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }); } diff --git a/packages/dart/lib/src/telemetry/log/noop_logger.dart b/packages/dart/lib/src/telemetry/log/noop_logger.dart index 1ec1752b34..232676a39b 100644 --- a/packages/dart/lib/src/telemetry/log/noop_logger.dart +++ b/packages/dart/lib/src/telemetry/log/noop_logger.dart @@ -63,6 +63,7 @@ final class _NoOpSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} @override @@ -70,6 +71,7 @@ final class _NoOpSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} @override @@ -77,6 +79,7 @@ final class _NoOpSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} @override @@ -84,6 +87,7 @@ final class _NoOpSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} @override @@ -91,6 +95,7 @@ final class _NoOpSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} @override @@ -98,5 +103,6 @@ final class _NoOpSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} } diff --git a/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart b/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart index cd02b6b094..41ebfe596a 100644 --- a/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart +++ b/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart @@ -98,7 +98,7 @@ void main() { event.log.attributes.containsKey('scope-attr'); }); - fixture.options.beforeSendLog = (log, {Hint? hint}) { + fixture.options.beforeSendLog = (log, hint) { operations.add('beforeSendLog'); return log; }; @@ -145,7 +145,7 @@ void main() { group('when beforeSendLog is configured', () { test('returning null drops the log', () async { - fixture.options.beforeSendLog = (_, {Hint? hint}) => null; + fixture.options.beforeSendLog = (_, hint) => null; final log = givenLog(); @@ -155,7 +155,7 @@ void main() { }); test('returning null records lost event in client report', () async { - fixture.options.beforeSendLog = (_, {Hint? hint}) => null; + fixture.options.beforeSendLog = (_, hint) => null; final log = givenLog(); @@ -169,7 +169,7 @@ void main() { }); test('can mutate the log', () async { - fixture.options.beforeSendLog = (log, {Hint? hint}) { + fixture.options.beforeSendLog = (log, hint) { log.body = 'modified-body'; log.attributes['added-key'] = SentryAttribute.string('added'); return log; @@ -186,7 +186,7 @@ void main() { }); test('async callback is awaited', () async { - fixture.options.beforeSendLog = (log, {Hint? hint}) async { + fixture.options.beforeSendLog = (log, hint) async { await Future.delayed(Duration(milliseconds: 10)); log.body = 'async-modified'; return log; @@ -201,10 +201,41 @@ void main() { expect(captured.body, 'async-modified'); }); + test('forwards hint to beforeSendLog callback', () async { + Hint? receivedHint; + fixture.options.beforeSendLog = (log, hint) { + receivedHint = hint; + return log; + }; + + final log = givenLog(); + final hint = Hint.withMap({'custom-key': 'custom-value'}); + + await fixture.pipeline + .captureLog(log, scope: fixture.scope, hint: hint); + + expect(receivedHint, isNotNull); + expect(receivedHint!.get('custom-key'), 'custom-value'); + }); + + test('provides empty Hint when no hint is passed', () async { + Hint? receivedHint; + fixture.options.beforeSendLog = (log, hint) { + receivedHint = hint; + return log; + }; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(receivedHint, isNotNull); + }); + test('exception in callback is caught and log is still captured', () async { fixture.options.automatedTestMode = false; - fixture.options.beforeSendLog = (log, {Hint? hint}) { + fixture.options.beforeSendLog = (log, hint) { throw Exception('test'); }; diff --git a/packages/dart/test/telemetry/log/logger_setup_integration_test.dart b/packages/dart/test/telemetry/log/logger_setup_integration_test.dart index 0e555d2acb..ad52d31229 100644 --- a/packages/dart/test/telemetry/log/logger_setup_integration_test.dart +++ b/packages/dart/test/telemetry/log/logger_setup_integration_test.dart @@ -135,6 +135,7 @@ class _CustomSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} @override @@ -142,6 +143,7 @@ class _CustomSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} @override @@ -149,6 +151,7 @@ class _CustomSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} @override @@ -156,6 +159,7 @@ class _CustomSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} @override @@ -163,6 +167,7 @@ class _CustomSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} @override @@ -170,5 +175,6 @@ class _CustomSentryLoggerFormatter implements SentryLoggerFormatter { String templateBody, List arguments, { Map? attributes, + Hint? hint, }) {} } From 393c3e9d00d6adfe1fb6d48944a6531a65129eaf Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Fri, 27 Mar 2026 14:25:51 +0100 Subject: [PATCH 4/4] add extension helper --- packages/logging/example/sentry_logging_example.dart | 10 ++++++++++ packages/logging/lib/sentry_logging.dart | 1 + packages/logging/lib/src/extension.dart | 6 ++++++ packages/logging/test/logging_integration_test.dart | 5 +++-- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/logging/example/sentry_logging_example.dart b/packages/logging/example/sentry_logging_example.dart index d7b1713e44..f391fcf30a 100644 --- a/packages/logging/example/sentry_logging_example.dart +++ b/packages/logging/example/sentry_logging_example.dart @@ -12,6 +12,16 @@ Future main() async { (options) { options.dsn = dsn; options.addIntegration(LoggingIntegration()); + options.enableLogs = true; + options.beforeSendLog = (log, hint) { + // Access the original LogRecord from the logging package via the hint. + final record = hint.logRecord; + if (record != null) { + print('LogRecord logger: ${record.loggerName}'); + print('LogRecord object: ${record.object}'); + } + return log; + }; }, appRunner: runApp, ); diff --git a/packages/logging/lib/sentry_logging.dart b/packages/logging/lib/sentry_logging.dart index 236f075583..641989c486 100644 --- a/packages/logging/lib/sentry_logging.dart +++ b/packages/logging/lib/sentry_logging.dart @@ -1,3 +1,4 @@ library; +export 'src/extension.dart' show SentryLoggingHint; export 'src/logging_integration.dart'; diff --git a/packages/logging/lib/src/extension.dart b/packages/logging/lib/src/extension.dart index 8ea4a9e19d..370b958054 100644 --- a/packages/logging/lib/src/extension.dart +++ b/packages/logging/lib/src/extension.dart @@ -37,6 +37,12 @@ extension LogRecordX on LogRecord { } } +extension SentryLoggingHint on Hint { + /// The original [LogRecord] from the `logging` package, if this + /// log originated from [LoggingIntegration]. + LogRecord? get logRecord => get(TypeCheckHint.record) as LogRecord?; +} + extension LogLevelX on Level { SentryLevel? toSentryLevel() { return { diff --git a/packages/logging/test/logging_integration_test.dart b/packages/logging/test/logging_integration_test.dart index d94c065582..7f21c7c16a 100644 --- a/packages/logging/test/logging_integration_test.dart +++ b/packages/logging/test/logging_integration_test.dart @@ -361,8 +361,9 @@ void main() { final hint = mockLogger.infoCalls.first.hint; expect(hint, isNotNull); - final record = hint!.get(TypeCheckHint.record) as LogRecord; - expect(record.loggerName, 'TestLogger'); + final record = hint!.logRecord; + expect(record, isNotNull); + expect(record!.loggerName, 'TestLogger'); expect(record.level, Level.INFO); expect(record.object, same(customObject)); });