From 63e3107a767f140dbe9ffa4a16137b98288f13f6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 28 Nov 2025 16:13:28 +0100 Subject: [PATCH 01/16] Update --- packages/dart/lib/src/hub.dart | 47 +++++++++++++++++-- .../dart/lib/src/protocol/simple_span.dart | 5 ++ packages/dart/lib/src/scope.dart | 34 ++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index be2d4ddd5f..88e8cb2420 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -11,6 +11,7 @@ import 'protocol/unset_span.dart'; import 'sentry_tracer.dart'; import 'sentry_traces_sampler.dart'; import 'protocol/noop_span.dart'; +import 'protocol/simple_span.dart'; import 'transport/data_category.dart'; /// Configures the scope through the callback. @@ -586,12 +587,52 @@ class Hub { SentryLevel.warning, "Instance is disabled and this 'startSpan' call is a no-op.", ); - } else if (_options.isTracingEnabled()) { - // TODO: implementation of span api behaviour according to https://develop.sentry.dev/sdk/telemetry/spans/span-api/ return NoOpSpan(); } - return NoOpSpan(); + if (!_options.isTracingEnabled()) { + return NoOpSpan(); + } + + // Determine the parent span based on the parentSpan parameter: + // - If parentSpan is UnsetSpan (default), use the currently active span + // - If parentSpan is a specific Span, use that as the parent + // - If parentSpan is null, create a root/segment span (no parent) + // ignore: unused_local_variable + final Span? resolvedParentSpan; + if (parentSpan is UnsetSpan) { + // Use the currently active span from scope as parent + resolvedParentSpan = scope.getActiveSpan(); + } else { + // Use the explicitly provided parent (can be null for root/segment span) + resolvedParentSpan = parentSpan; + } + + final span = SimpleSpan(parentSpan: resolvedParentSpan, hub: this); + + span.setName(name); + if (attributes != null) { + span.setAttributes(attributes); + } + if (active) { + scope.setActiveSpan(span); + } + + return span; + } + + void captureSpan(Span span) { + if (!_isEnabled) { + _options.log( + SentryLevel.warning, + "Instance is disabled and this 'captureSpan' call is a no-op.", + ); + return; + } + + scope.removeActiveSpan(span); + + // TODO: implement span buffer and add the span to the buffer } @internal diff --git a/packages/dart/lib/src/protocol/simple_span.dart b/packages/dart/lib/src/protocol/simple_span.dart index dc31156916..c56f65b9b9 100644 --- a/packages/dart/lib/src/protocol/simple_span.dart +++ b/packages/dart/lib/src/protocol/simple_span.dart @@ -1,6 +1,11 @@ import '../../sentry.dart'; class SimpleSpan implements Span { + final Hub hub; + final Span? parentSpan; + + SimpleSpan({required this.parentSpan, required this.hub}); + @override void end({DateTime? endTimestamp}) { // TODO: implement end diff --git a/packages/dart/lib/src/scope.dart b/packages/dart/lib/src/scope.dart index 6024022210..051c6dedf4 100644 --- a/packages/dart/lib/src/scope.dart +++ b/packages/dart/lib/src/scope.dart @@ -43,6 +43,40 @@ class Scope { /// Returns active transaction or null if there is no active transaction. ISentrySpan? span; + final List _activeSpans = []; + + /// Returns the currently active span, or `null` if no span is active. + /// + /// The active span is the most recently set span via [setActiveSpan]. + /// When starting a new span with `active: true` (the default), the new span + /// becomes a child of this active span. + @internal + Span? getActiveSpan() { + return _activeSpans.isNotEmpty ? _activeSpans.last : null; + } + + /// Sets the given [span] as the currently active span. + /// + /// Active spans are used to automatically parent new spans. + /// When a new span is started with `active: true` (the default), it becomes + /// a child of the currently active span. + /// + /// The active spans are maintained as a stack - the most recently set span + /// is the current active span. + @internal + void setActiveSpan(Span span) { + _activeSpans.add(span); + } + + /// Removes the given [span] from the active spans list. + /// + /// This should be called when a span ends to remove it from the active + /// span hierarchy. + @internal + void removeActiveSpan(Span span) { + _activeSpans.remove(span); + } + /// The propagation context for connecting errors and spans to traces. /// There should always be a propagation context available at all times. /// From eb0cdefae564b12c5b24cdccbb72b4baca538f2e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 11:18:34 +0100 Subject: [PATCH 02/16] Updateg --- packages/dart/lib/src/hub_adapter.dart | 3 ++ packages/dart/lib/src/noop_hub.dart | 3 ++ .../dart/lib/src/protocol/simple_span.dart | 2 +- packages/dart/lib/src/scope.dart | 3 ++ packages/dart/test/scope_test.dart | 41 +++++++++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/dart/lib/src/hub_adapter.dart b/packages/dart/lib/src/hub_adapter.dart index 1203bcc1f3..1e9ddf4425 100644 --- a/packages/dart/lib/src/hub_adapter.dart +++ b/packages/dart/lib/src/hub_adapter.dart @@ -221,4 +221,7 @@ class HubAdapter implements Hub { @override void removeAttribute(String key) => Sentry.currentHub.removeAttribute(key); + + @override + void captureSpan(Span span) => Sentry.currentHub.captureSpan(span); } diff --git a/packages/dart/lib/src/noop_hub.dart b/packages/dart/lib/src/noop_hub.dart index 6e8845a84f..44b1c82f87 100644 --- a/packages/dart/lib/src/noop_hub.dart +++ b/packages/dart/lib/src/noop_hub.dart @@ -161,4 +161,7 @@ class NoOpHub implements Hub { Map? attributes, }) => NoOpSpan(); + + @override + void captureSpan(Span span) {} } diff --git a/packages/dart/lib/src/protocol/simple_span.dart b/packages/dart/lib/src/protocol/simple_span.dart index c56f65b9b9..6ba0d70490 100644 --- a/packages/dart/lib/src/protocol/simple_span.dart +++ b/packages/dart/lib/src/protocol/simple_span.dart @@ -4,7 +4,7 @@ class SimpleSpan implements Span { final Hub hub; final Span? parentSpan; - SimpleSpan({required this.parentSpan, required this.hub}); + SimpleSpan({required this.parentSpan, Hub? hub}) : hub = hub ?? HubAdapter(); @override void end({DateTime? endTimestamp}) { diff --git a/packages/dart/lib/src/scope.dart b/packages/dart/lib/src/scope.dart index 051c6dedf4..c6679e4d6d 100644 --- a/packages/dart/lib/src/scope.dart +++ b/packages/dart/lib/src/scope.dart @@ -45,6 +45,9 @@ class Scope { final List _activeSpans = []; + @visibleForTesting + List get activeSpans => List.unmodifiable(_activeSpans); + /// Returns the currently active span, or `null` if no span is active. /// /// The active span is the most recently set span via [setActiveSpan]. diff --git a/packages/dart/test/scope_test.dart b/packages/dart/test/scope_test.dart index 44683f1c50..78853d3853 100644 --- a/packages/dart/test/scope_test.dart +++ b/packages/dart/test/scope_test.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; +import 'package:sentry/src/protocol/simple_span.dart'; import 'package:sentry/src/sentry_tracer.dart'; import 'package:test/test.dart'; @@ -352,6 +353,46 @@ void main() { expect(sut.extra['test'], null); }); + test('setActiveSpan adds span to active span list', () { + final sut = fixture.getSut(); + + final span = SimpleSpan(parentSpan: null); + final span2 = SimpleSpan(parentSpan: null); + sut.setActiveSpan(span); + sut.setActiveSpan(span2); + + expect(sut.activeSpans.length, 2); + expect(sut.activeSpans.last, span2); + }); + + test('getActiveSpan returns the last active span in the list', () { + final sut = fixture.getSut(); + + final span = SimpleSpan(parentSpan: null); + final span2 = SimpleSpan(parentSpan: null); + sut.setActiveSpan(span); + sut.setActiveSpan(span2); + + final activeSpan = sut.getActiveSpan(); + expect(activeSpan, span2); + }); + + test( + 'removeActiveSpan removes the active span in the list regardless of order', + () { + final sut = fixture.getSut(); + + final span = SimpleSpan(parentSpan: null); + final span2 = SimpleSpan(parentSpan: null); + sut.setActiveSpan(span); + sut.setActiveSpan(span2); + + sut.removeActiveSpan(span); + + expect(sut.activeSpans.length, 1); + expect(sut.activeSpans.first, span2); + }); + test('clears $Scope', () { final sut = fixture.getSut(); From 19a8c19fa8d19fca6ee8ce64ec1343016460e04c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 15:12:12 +0100 Subject: [PATCH 03/16] Update --- packages/dart/lib/src/hub.dart | 5 +- packages/dart/lib/src/protocol/noop_span.dart | 6 + .../dart/lib/src/protocol/simple_span.dart | 8 +- packages/dart/lib/src/protocol/span.dart | 7 + .../dart/lib/src/protocol/unset_span.dart | 8 + packages/dart/test/hub_span_test.dart | 193 ++++++++++++++++++ packages/dart/test/scope_test.dart | 12 +- packages/dart/test/span_test.dart | 138 +++++++++++++ 8 files changed, 367 insertions(+), 10 deletions(-) create mode 100644 packages/dart/test/hub_span_test.dart create mode 100644 packages/dart/test/span_test.dart diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index 88e8cb2420..f8bfbc14f2 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -608,9 +608,8 @@ class Hub { resolvedParentSpan = parentSpan; } - final span = SimpleSpan(parentSpan: resolvedParentSpan, hub: this); - - span.setName(name); + final span = + SimpleSpan(name: name, parentSpan: resolvedParentSpan, hub: this); if (attributes != null) { span.setAttributes(attributes); } diff --git a/packages/dart/lib/src/protocol/noop_span.dart b/packages/dart/lib/src/protocol/noop_span.dart index 80b8b611a1..ef55a4410c 100644 --- a/packages/dart/lib/src/protocol/noop_span.dart +++ b/packages/dart/lib/src/protocol/noop_span.dart @@ -3,6 +3,12 @@ import '../../sentry.dart'; class NoOpSpan implements Span { const NoOpSpan(); + @override + String get name => ''; + + @override + Span? get parentSpan => null; + @override void end({DateTime? endTimestamp}) {} diff --git a/packages/dart/lib/src/protocol/simple_span.dart b/packages/dart/lib/src/protocol/simple_span.dart index 6ba0d70490..c43555fe29 100644 --- a/packages/dart/lib/src/protocol/simple_span.dart +++ b/packages/dart/lib/src/protocol/simple_span.dart @@ -2,9 +2,15 @@ import '../../sentry.dart'; class SimpleSpan implements Span { final Hub hub; + + @override final Span? parentSpan; - SimpleSpan({required this.parentSpan, Hub? hub}) : hub = hub ?? HubAdapter(); + @override + final String name; + + SimpleSpan({required this.name, required this.parentSpan, Hub? hub}) + : hub = hub ?? HubAdapter(); @override void end({DateTime? endTimestamp}) { diff --git a/packages/dart/lib/src/protocol/span.dart b/packages/dart/lib/src/protocol/span.dart index 6759ada6ea..d800b12b16 100644 --- a/packages/dart/lib/src/protocol/span.dart +++ b/packages/dart/lib/src/protocol/span.dart @@ -7,6 +7,13 @@ abstract class Span { @internal const Span(); + /// Gets the name of the span. + String get name; + + /// Gets the parentSpan. + /// If null this span has no parent. + Span? get parentSpan; + /// Ends the span. /// /// [endTimestamp] can be used to override the end time. diff --git a/packages/dart/lib/src/protocol/unset_span.dart b/packages/dart/lib/src/protocol/unset_span.dart index 2dfbae74ce..164c868ed4 100644 --- a/packages/dart/lib/src/protocol/unset_span.dart +++ b/packages/dart/lib/src/protocol/unset_span.dart @@ -8,6 +8,14 @@ import '../../sentry.dart'; class UnsetSpan extends Span { const UnsetSpan(); + @override + String get name => + throw UnimplementedError('$UnsetSpan methods should not be used'); + + @override + Span? get parentSpan => + throw UnimplementedError('$UnsetSpan methods should not be used'); + @override void end({DateTime? endTimestamp}) { throw UnimplementedError('$UnsetSpan methods should not be used'); diff --git a/packages/dart/test/hub_span_test.dart b/packages/dart/test/hub_span_test.dart new file mode 100644 index 0000000000..2f487393b0 --- /dev/null +++ b/packages/dart/test/hub_span_test.dart @@ -0,0 +1,193 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/protocol/noop_span.dart'; +import 'package:sentry/src/protocol/simple_span.dart'; +import 'package:test/test.dart'; + +import 'mocks/mock_client_report_recorder.dart'; +import 'mocks/mock_sentry_client.dart'; +import 'test_utils.dart'; + +void main() { + group('Hub startSpan', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('startSpan returns SimpleSpan when tracing is enabled', () { + final hub = fixture.getSut(); + + final span = hub.startSpan('test-span'); + + expect(span, isA()); + }); + + test('startSpan returns NoOpSpan when tracing is disabled', () { + final hub = fixture.getSut(tracesSampleRate: null); + + final span = hub.startSpan('test-span'); + + expect(span, isA()); + }); + + test('startSpan returns NoOpSpan when hub is disabled', () async { + final hub = fixture.getSut(); + await hub.close(); + + final span = hub.startSpan('test-span'); + + expect(span, isA()); + }); + + test('startSpan sets span name', () { + final hub = fixture.getSut(); + + final span = hub.startSpan('my-span-name'); + + expect(span, isA()); + // TODO: verify span name once SimpleSpan implements it + }); + + test('startSpan with active=true sets span as active on scope', () { + final hub = fixture.getSut(); + + final span = hub.startSpan('test-span', active: true); + + expect(hub.scope.getActiveSpan(), equals(span)); + }); + + test('startSpan with active=false does not set span as active on scope', + () { + final hub = fixture.getSut(); + + hub.startSpan('test-span', active: false); + + expect(hub.scope.getActiveSpan(), isNull); + }); + + test('startSpan uses active span as parent when parentSpan is not provided', + () { + final hub = fixture.getSut(); + + // Start first span which becomes active + final parentSpan = hub.startSpan('parent-span'); + expect(hub.scope.getActiveSpan(), equals(parentSpan)); + + // Start second span - should use active span as parent + final childSpan = hub.startSpan('child-span'); + expect(childSpan, isA()); + expect(childSpan.parentSpan, equals(parentSpan)); + }); + + test('startSpan with explicit parentSpan uses that as parent', () { + final hub = fixture.getSut(); + + final explicitParent = hub.startSpan('explicit-parent'); + // Start another span to change active span + hub.startSpan('other-span'); + + // Start span with explicit parent + final childSpan = hub.startSpan('child-span', + parentSpan: explicitParent, active: false); + + expect(childSpan, isA()); + expect(childSpan.parentSpan, equals(explicitParent)); + }); + + test('startSpan with parentSpan=null creates root/segment span', () { + final hub = fixture.getSut(); + + // Start active span first + hub.startSpan('active-span'); + + // Start span with null parent - should be root span + final rootSpan = hub.startSpan('root-span', parentSpan: null); + + expect(rootSpan, isA()); + expect(rootSpan.parentSpan, isNull); + }); + + test('startSpan with attributes sets attributes on span', () { + final hub = fixture.getSut(); + final attributes = { + 'attr1': SentryAttribute.string('value1'), + 'attr2': SentryAttribute.int(42), + }; + + final span = hub.startSpan('test-span', attributes: attributes); + + expect(span, isA()); + // TODO: verify attributes once SimpleSpan implements it + }); + }); + + group('Hub captureSpan', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('captureSpan removes span from active spans', () { + final hub = fixture.getSut(); + + final span = hub.startSpan('test-span'); + expect(hub.scope.activeSpans, contains(span)); + + hub.captureSpan(span); + + expect(hub.scope.activeSpans, isNot(contains(span))); + }); + + test('captureSpan does nothing when hub is disabled', () async { + final hub = fixture.getSut(); + final span = hub.startSpan('test-span'); + await hub.close(); + + // Should not throw + hub.captureSpan(span); + }); + }); +} + +class Fixture { + final client = MockSentryClient(); + final recorder = MockClientReportRecorder(); + + final options = defaultTestOptions(); + + SentryLevel? loggedLevel; + String? loggedMessage; + Object? loggedException; + + Hub getSut({ + double? tracesSampleRate = 1.0, + TracesSamplerCallback? tracesSampler, + bool debug = false, + }) { + options.tracesSampleRate = tracesSampleRate; + options.tracesSampler = tracesSampler; + options.debug = debug; + options.log = mockLogger; + + final hub = Hub(options); + + hub.bindClient(client); + options.recorder = recorder; + + return hub; + } + + void mockLogger( + SentryLevel level, + String message, { + String? logger, + Object? exception, + StackTrace? stackTrace, + }) { + loggedLevel = level; + loggedMessage = message; + loggedException = exception; + } +} diff --git a/packages/dart/test/scope_test.dart b/packages/dart/test/scope_test.dart index 78853d3853..98b7892d19 100644 --- a/packages/dart/test/scope_test.dart +++ b/packages/dart/test/scope_test.dart @@ -356,8 +356,8 @@ void main() { test('setActiveSpan adds span to active span list', () { final sut = fixture.getSut(); - final span = SimpleSpan(parentSpan: null); - final span2 = SimpleSpan(parentSpan: null); + final span = SimpleSpan(name: 'span1', parentSpan: null); + final span2 = SimpleSpan(name: 'span2', parentSpan: null); sut.setActiveSpan(span); sut.setActiveSpan(span2); @@ -368,8 +368,8 @@ void main() { test('getActiveSpan returns the last active span in the list', () { final sut = fixture.getSut(); - final span = SimpleSpan(parentSpan: null); - final span2 = SimpleSpan(parentSpan: null); + final span = SimpleSpan(name: 'span1', parentSpan: null); + final span2 = SimpleSpan(name: 'span2', parentSpan: null); sut.setActiveSpan(span); sut.setActiveSpan(span2); @@ -382,8 +382,8 @@ void main() { () { final sut = fixture.getSut(); - final span = SimpleSpan(parentSpan: null); - final span2 = SimpleSpan(parentSpan: null); + final span = SimpleSpan(name: 'span1', parentSpan: null); + final span2 = SimpleSpan(name: 'span2', parentSpan: null); sut.setActiveSpan(span); sut.setActiveSpan(span2); diff --git a/packages/dart/test/span_test.dart b/packages/dart/test/span_test.dart new file mode 100644 index 0000000000..8222977ab0 --- /dev/null +++ b/packages/dart/test/span_test.dart @@ -0,0 +1,138 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/protocol/noop_span.dart'; +import 'package:sentry/src/protocol/simple_span.dart'; +import 'package:test/test.dart'; + +import 'mocks/mock_client_report_recorder.dart'; +import 'mocks/mock_sentry_client.dart'; +import 'test_utils.dart'; + +void main() { + group('SimpleSpan', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('end finishes the span', () { + final hub = fixture.getSut(); + final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); + + // Should not throw + span.end(); + // TODO: verify span is finished once SimpleSpan implements it + }); + + test('end with custom timestamp sets end time', () { + final hub = fixture.getSut(); + final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); + final endTime = DateTime.now().add(Duration(seconds: 5)); + + span.end(endTimestamp: endTime); + // TODO: verify end timestamp once SimpleSpan implements it + }); + + test('setAttribute sets single attribute', () { + final hub = fixture.getSut(); + final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); + + span.setAttribute('key', SentryAttribute.string('value')); + // TODO: verify attribute once SimpleSpan implements it + }); + + test('setAttributes sets multiple attributes', () { + final hub = fixture.getSut(); + final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); + + span.setAttributes({ + 'key1': SentryAttribute.string('value1'), + 'key2': SentryAttribute.int(42), + }); + // TODO: verify attributes once SimpleSpan implements it + }); + + test('setName sets span name', () { + final hub = fixture.getSut(); + final span = SimpleSpan(name: 'initial-name', parentSpan: null, hub: hub); + + span.setName('updated-name'); + // TODO: verify name once SimpleSpan implements it + }); + + test('setStatus sets span status to ok', () { + final hub = fixture.getSut(); + final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); + + span.setStatus(SpanV2Status.ok); + // TODO: verify status once SimpleSpan implements it + }); + + test('setStatus sets span status to error', () { + final hub = fixture.getSut(); + final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); + + span.setStatus(SpanV2Status.error); + // TODO: verify status once SimpleSpan implements it + }); + + test('parentSpan returns the parent span', () { + final hub = fixture.getSut(); + final parent = SimpleSpan(name: 'parent', parentSpan: null, hub: hub); + final child = SimpleSpan(name: 'child', parentSpan: parent, hub: hub); + + expect(child.parentSpan, equals(parent)); + }); + + test('parentSpan returns null for root span', () { + final hub = fixture.getSut(); + final span = SimpleSpan(name: 'root', parentSpan: null, hub: hub); + + expect(span.parentSpan, isNull); + }); + + test('name returns the span name', () { + final hub = fixture.getSut(); + final span = SimpleSpan(name: 'my-span-name', parentSpan: null, hub: hub); + + expect(span.name, equals('my-span-name')); + }); + }); + + group('NoOpSpan', () { + test('NoOpSpan operations do not throw', () { + const span = NoOpSpan(); + + // All operations should be no-ops and not throw + span.end(); + span.end(endTimestamp: DateTime.now()); + span.setAttribute('key', SentryAttribute.string('value')); + span.setAttributes({'key': SentryAttribute.string('value')}); + span.setName('name'); + span.setStatus(SpanV2Status.ok); + span.setStatus(SpanV2Status.error); + expect(span.toJson(), isEmpty); + }); + }); +} + +class Fixture { + final client = MockSentryClient(); + final recorder = MockClientReportRecorder(); + + final options = defaultTestOptions(); + + Hub getSut({ + double? tracesSampleRate = 1.0, + }) { + options.tracesSampleRate = tracesSampleRate; + + final hub = Hub(options); + + hub.bindClient(client); + options.recorder = recorder; + + return hub; + } +} + From 8713bfc03f5cf1e63fd4c6e250cae5919d730ebc Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 22:29:29 +0100 Subject: [PATCH 04/16] Update --- packages/dart/lib/src/protocol/noop_span.dart | 19 +- .../dart/lib/src/protocol/simple_span.dart | 45 ++-- packages/dart/lib/src/protocol/span.dart | 31 ++- .../dart/lib/src/protocol/unset_span.dart | 19 +- packages/dart/test/hub_span_test.dart | 204 ++++++++---------- packages/dart/test/span_test.dart | 55 +++-- 6 files changed, 208 insertions(+), 165 deletions(-) diff --git a/packages/dart/lib/src/protocol/noop_span.dart b/packages/dart/lib/src/protocol/noop_span.dart index ef55a4410c..bd673bcd96 100644 --- a/packages/dart/lib/src/protocol/noop_span.dart +++ b/packages/dart/lib/src/protocol/noop_span.dart @@ -4,14 +4,17 @@ class NoOpSpan implements Span { const NoOpSpan(); @override - String get name => ''; + final String name = 'NoOpSpan'; @override - Span? get parentSpan => null; + final SpanV2Status status = SpanV2Status.ok; @override void end({DateTime? endTimestamp}) {} + @override + Span? get parentSpan => NoOpSpan(); + @override void setAttribute(String key, SentryAttribute value) {} @@ -19,11 +22,17 @@ class NoOpSpan implements Span { void setAttributes(Map attributes) {} @override - void setName(String name) {} + Map toJson() => {}; @override - void setStatus(SpanV2Status status) {} + set name(String name) {} @override - Map toJson() => {}; + set status(SpanV2Status status) {} + + @override + Map get attributes => {}; + + @override + DateTime? get endTimestamp => null; } diff --git a/packages/dart/lib/src/protocol/simple_span.dart b/packages/dart/lib/src/protocol/simple_span.dart index c43555fe29..dbaae046ab 100644 --- a/packages/dart/lib/src/protocol/simple_span.dart +++ b/packages/dart/lib/src/protocol/simple_span.dart @@ -2,39 +2,58 @@ import '../../sentry.dart'; class SimpleSpan implements Span { final Hub hub; + final Map _attributes = {}; @override final Span? parentSpan; + String _name; + SpanV2Status _status = SpanV2Status.ok; + DateTime? _endTimestamp; + + SimpleSpan({ + required String name, + this.parentSpan, + Hub? hub, + }) : hub = hub ?? HubAdapter(), + _name = name; + @override - final String name; + DateTime? get endTimestamp => _endTimestamp; - SimpleSpan({required this.name, required this.parentSpan, Hub? hub}) - : hub = hub ?? HubAdapter(); + @override + Map get attributes => Map.unmodifiable(_attributes); @override - void end({DateTime? endTimestamp}) { - // TODO: implement end + String get name => _name; + + @override + set name(String value) { + _name = value; } @override - void setAttribute(String key, SentryAttribute value) { - // TODO: implement setAttribute + SpanV2Status get status => _status; + + @override + set status(SpanV2Status value) { + _status = value; } @override - void setAttributes(Map attributes) { - // TODO: implement setAttributes + void end({DateTime? endTimestamp}) { + _endTimestamp = endTimestamp ?? DateTime.now().toUtc(); + // TODO: add this span to buffer through hub.captureSpan } @override - void setName(String name) { - // TODO: implement setName + void setAttribute(String key, SentryAttribute value) { + _attributes[key] = value; } @override - void setStatus(SpanV2Status status) { - // TODO: implement setStatus + void setAttributes(Map attributes) { + _attributes.addAll(attributes); } @override diff --git a/packages/dart/lib/src/protocol/span.dart b/packages/dart/lib/src/protocol/span.dart index d800b12b16..15509c67fb 100644 --- a/packages/dart/lib/src/protocol/span.dart +++ b/packages/dart/lib/src/protocol/span.dart @@ -1,8 +1,12 @@ +import 'dart:collection'; + import 'package:meta/meta.dart'; import '../../sentry.dart'; -/// Represents the Span model based on https://develop.sentry.dev/sdk/telemetry/spans/span-api/ +// Span specs: https://develop.sentry.dev/sdk/telemetry/spans/span-api/ + +/// Represents a basic telemetry span. abstract class Span { @internal const Span(); @@ -10,14 +14,31 @@ abstract class Span { /// Gets the name of the span. String get name; + /// Sets the name of the span. + set name(String name); + /// Gets the parentSpan. /// If null this span has no parent. Span? get parentSpan; + /// Gets the status of the span. + SpanV2Status get status; + + /// Sets the status of the span. + set status(SpanV2Status status); + + /// Gets the end timestamp of the span. + DateTime? get endTimestamp; + + /// Gets a read-only view of the attributes of the span. + /// + /// The returned map must not be mutated by callers. + Map get attributes; + /// Ends the span. /// /// [endTimestamp] can be used to override the end time. - /// If omitted, the span ends using the current time. + /// If omitted, the span ends using the current time when end is executed. void end({DateTime? endTimestamp}); /// Sets a single attribute. @@ -30,12 +51,6 @@ abstract class Span { /// Overrides if the attributes already exist. void setAttributes(Map attributes); - /// Sets the status of the span. - void setStatus(SpanV2Status status); - - /// Sets the name of the span. - void setName(String name); - @internal Map toJson(); } diff --git a/packages/dart/lib/src/protocol/unset_span.dart b/packages/dart/lib/src/protocol/unset_span.dart index 164c868ed4..6411bcae9f 100644 --- a/packages/dart/lib/src/protocol/unset_span.dart +++ b/packages/dart/lib/src/protocol/unset_span.dart @@ -32,17 +32,26 @@ class UnsetSpan extends Span { } @override - void setName(String name) { + Map toJson() { throw UnimplementedError('$UnsetSpan methods should not be used'); } @override - void setStatus(SpanV2Status status) { - throw UnimplementedError('$UnsetSpan methods should not be used'); + set name(String name) { + throw UnimplementedError(); } @override - Map toJson() { - throw UnimplementedError('$UnsetSpan methods should not be used'); + set status(SpanV2Status status) { + throw UnimplementedError(); } + + @override + SpanV2Status get status => throw UnimplementedError(); + + @override + Map get attributes => throw UnimplementedError(); + + @override + DateTime? get endTimestamp => throw UnimplementedError(); } diff --git a/packages/dart/test/hub_span_test.dart b/packages/dart/test/hub_span_test.dart index 2f487393b0..b1e110d60d 100644 --- a/packages/dart/test/hub_span_test.dart +++ b/packages/dart/test/hub_span_test.dart @@ -8,145 +8,144 @@ import 'mocks/mock_sentry_client.dart'; import 'test_utils.dart'; void main() { - group('Hub startSpan', () { + group('Hub', () { late Fixture fixture; setUp(() { fixture = Fixture(); }); - test('startSpan returns SimpleSpan when tracing is enabled', () { - final hub = fixture.getSut(); + group('startSpan', () { + group('span creation', () { + test('returns SimpleSpan when tracing is enabled', () { + final hub = fixture.getSut(); - final span = hub.startSpan('test-span'); + final span = hub.startSpan('test-span'); - expect(span, isA()); - }); + expect(span, isA()); + }); - test('startSpan returns NoOpSpan when tracing is disabled', () { - final hub = fixture.getSut(tracesSampleRate: null); + test('returns NoOpSpan when tracing is disabled', () { + final hub = fixture.getSut(tracesSampleRate: null); - final span = hub.startSpan('test-span'); + final span = hub.startSpan('test-span'); - expect(span, isA()); - }); + expect(span, isA()); + }); - test('startSpan returns NoOpSpan when hub is disabled', () async { - final hub = fixture.getSut(); - await hub.close(); + test('returns NoOpSpan when hub is closed', () async { + final hub = fixture.getSut(); + await hub.close(); - final span = hub.startSpan('test-span'); + final span = hub.startSpan('test-span'); - expect(span, isA()); - }); + expect(span, isA()); + }); - test('startSpan sets span name', () { - final hub = fixture.getSut(); + test('sets span name from parameter', () { + final hub = fixture.getSut(); - final span = hub.startSpan('my-span-name'); - - expect(span, isA()); - // TODO: verify span name once SimpleSpan implements it - }); + final span = hub.startSpan('my-span-name'); - test('startSpan with active=true sets span as active on scope', () { - final hub = fixture.getSut(); + expect(span.name, equals('my-span-name')); + }); - final span = hub.startSpan('test-span', active: true); + test('sets attributes on span when provided', () { + final hub = fixture.getSut(); + final attributes = { + 'attr1': SentryAttribute.string('value1'), + 'attr2': SentryAttribute.int(42), + }; - expect(hub.scope.getActiveSpan(), equals(span)); - }); + final span = hub.startSpan('test-span', attributes: attributes); - test('startSpan with active=false does not set span as active on scope', - () { - final hub = fixture.getSut(); + expect(span.attributes, equals(attributes)); + }); + }); - hub.startSpan('test-span', active: false); + group('active span handling', () { + test('sets span as active on scope when active is true', () { + final hub = fixture.getSut(); - expect(hub.scope.getActiveSpan(), isNull); - }); + final span = hub.startSpan('test-span', active: true); - test('startSpan uses active span as parent when parentSpan is not provided', - () { - final hub = fixture.getSut(); + expect(hub.scope.getActiveSpan(), equals(span)); + }); - // Start first span which becomes active - final parentSpan = hub.startSpan('parent-span'); - expect(hub.scope.getActiveSpan(), equals(parentSpan)); + test('does not set span as active on scope when active is false', () { + final hub = fixture.getSut(); - // Start second span - should use active span as parent - final childSpan = hub.startSpan('child-span'); - expect(childSpan, isA()); - expect(childSpan.parentSpan, equals(parentSpan)); - }); + hub.startSpan('test-span', active: false); - test('startSpan with explicit parentSpan uses that as parent', () { - final hub = fixture.getSut(); + expect(hub.scope.getActiveSpan(), isNull); + }); + }); - final explicitParent = hub.startSpan('explicit-parent'); - // Start another span to change active span - hub.startSpan('other-span'); + group('parent span resolution', () { + test('creates root span when no active span exists', () { + final hub = fixture.getSut(); - // Start span with explicit parent - final childSpan = hub.startSpan('child-span', - parentSpan: explicitParent, active: false); + final span = hub.startSpan('test-span'); - expect(childSpan, isA()); - expect(childSpan.parentSpan, equals(explicitParent)); - }); + expect(span.parentSpan, isNull); + }); - test('startSpan with parentSpan=null creates root/segment span', () { - final hub = fixture.getSut(); + test('uses active span as parent when parentSpan is not provided', () { + final hub = fixture.getSut(); + final parentSpan = hub.startSpan('parent-span'); - // Start active span first - hub.startSpan('active-span'); + final childSpan = hub.startSpan('child-span'); - // Start span with null parent - should be root span - final rootSpan = hub.startSpan('root-span', parentSpan: null); + expect(childSpan.parentSpan, equals(parentSpan)); + }); - expect(rootSpan, isA()); - expect(rootSpan.parentSpan, isNull); - }); + test('uses explicit parentSpan instead of active span when provided', + () { + final hub = fixture.getSut(); + final explicitParent = hub.startSpan('explicit-parent'); + hub.startSpan('other-span'); // Changes active span - test('startSpan with attributes sets attributes on span', () { - final hub = fixture.getSut(); - final attributes = { - 'attr1': SentryAttribute.string('value1'), - 'attr2': SentryAttribute.int(42), - }; + final childSpan = hub.startSpan( + 'child-span', + parentSpan: explicitParent, + active: false, + ); - final span = hub.startSpan('test-span', attributes: attributes); + expect(childSpan.parentSpan, equals(explicitParent)); + }); - expect(span, isA()); - // TODO: verify attributes once SimpleSpan implements it - }); - }); + test('creates root span when parentSpan is explicitly set to null', () { + final hub = fixture.getSut(); + hub.startSpan('active-span'); - group('Hub captureSpan', () { - late Fixture fixture; + final rootSpan = hub.startSpan('root-span', parentSpan: null); - setUp(() { - fixture = Fixture(); + expect(rootSpan.parentSpan, isNull); + }); + }); }); - test('captureSpan removes span from active spans', () { - final hub = fixture.getSut(); + group('captureSpan', () { + // TODO: add test that it was added to buffer - final span = hub.startSpan('test-span'); - expect(hub.scope.activeSpans, contains(span)); + test('removes span from active spans on scope', () { + final hub = fixture.getSut(); + final span = hub.startSpan('test-span'); + expect(hub.scope.activeSpans, contains(span)); - hub.captureSpan(span); + hub.captureSpan(span); - expect(hub.scope.activeSpans, isNot(contains(span))); - }); + expect(hub.scope.activeSpans, isNot(contains(span))); + }); - test('captureSpan does nothing when hub is disabled', () async { - final hub = fixture.getSut(); - final span = hub.startSpan('test-span'); - await hub.close(); + test('does nothing when hub is closed', () async { + final hub = fixture.getSut(); + final span = hub.startSpan('test-span'); + await hub.close(); - // Should not throw - hub.captureSpan(span); + // Should not throw + hub.captureSpan(span); + }); }); }); } @@ -157,10 +156,6 @@ class Fixture { final options = defaultTestOptions(); - SentryLevel? loggedLevel; - String? loggedMessage; - Object? loggedException; - Hub getSut({ double? tracesSampleRate = 1.0, TracesSamplerCallback? tracesSampler, @@ -169,7 +164,6 @@ class Fixture { options.tracesSampleRate = tracesSampleRate; options.tracesSampler = tracesSampler; options.debug = debug; - options.log = mockLogger; final hub = Hub(options); @@ -178,16 +172,4 @@ class Fixture { return hub; } - - void mockLogger( - SentryLevel level, - String message, { - String? logger, - Object? exception, - StackTrace? stackTrace, - }) { - loggedLevel = level; - loggedMessage = message; - loggedException = exception; - } } diff --git a/packages/dart/test/span_test.dart b/packages/dart/test/span_test.dart index 8222977ab0..54fb3a15df 100644 --- a/packages/dart/test/span_test.dart +++ b/packages/dart/test/span_test.dart @@ -19,61 +19,71 @@ void main() { final hub = fixture.getSut(); final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); - // Should not throw span.end(); // TODO: verify span is finished once SimpleSpan implements it }); + test('end sets current time by default', () { + final hub = fixture.getSut(); + final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); + + span.end(); + + final now = DateTime.now(); + expect(span.endTimestamp?.microsecondsSinceEpoch, + lessThan(now.microsecondsSinceEpoch)); + }); + test('end with custom timestamp sets end time', () { final hub = fixture.getSut(); final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); final endTime = DateTime.now().add(Duration(seconds: 5)); span.end(endTimestamp: endTime); - // TODO: verify end timestamp once SimpleSpan implements it + + expect(span.endTimestamp, equals(endTime)); }); test('setAttribute sets single attribute', () { final hub = fixture.getSut(); final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); - span.setAttribute('key', SentryAttribute.string('value')); - // TODO: verify attribute once SimpleSpan implements it + final attributeValue = SentryAttribute.string('value'); + span.setAttribute('key', attributeValue); + + expect(span.attributes, equals({'key': attributeValue})); }); test('setAttributes sets multiple attributes', () { final hub = fixture.getSut(); final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); - span.setAttributes({ + final attributes = { 'key1': SentryAttribute.string('value1'), 'key2': SentryAttribute.int(42), - }); - // TODO: verify attributes once SimpleSpan implements it + }; + span.setAttributes(attributes); + + expect(span.attributes, equals(attributes)); }); test('setName sets span name', () { final hub = fixture.getSut(); final span = SimpleSpan(name: 'initial-name', parentSpan: null, hub: hub); - span.setName('updated-name'); - // TODO: verify name once SimpleSpan implements it + span.name = 'updated-name'; + expect(span.status, equals('updated-name')); }); - test('setStatus sets span status to ok', () { + test('can set span status', () { final hub = fixture.getSut(); final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); - span.setStatus(SpanV2Status.ok); - // TODO: verify status once SimpleSpan implements it - }); - - test('setStatus sets span status to error', () { - final hub = fixture.getSut(); - final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); + span.status = SpanV2Status.ok; + expect(span.status, equals(SpanV2Status.ok)); - span.setStatus(SpanV2Status.error); - // TODO: verify status once SimpleSpan implements it + span.status = SpanV2Status.error; + expect(span.status, equals(SpanV2Status.error)); }); test('parentSpan returns the parent span', () { @@ -108,9 +118,9 @@ void main() { span.end(endTimestamp: DateTime.now()); span.setAttribute('key', SentryAttribute.string('value')); span.setAttributes({'key': SentryAttribute.string('value')}); - span.setName('name'); - span.setStatus(SpanV2Status.ok); - span.setStatus(SpanV2Status.error); + span.name = 'name'; + span.status = SpanV2Status.ok; + span.status = SpanV2Status.error; expect(span.toJson(), isEmpty); }); }); @@ -135,4 +145,3 @@ class Fixture { return hub; } } - From 1adb9a3600bd76948dac4e5c29948c26d15979ae Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 22:30:26 +0100 Subject: [PATCH 05/16] Update --- packages/dart/test/span_test.dart | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/dart/test/span_test.dart b/packages/dart/test/span_test.dart index 54fb3a15df..84739595d5 100644 --- a/packages/dart/test/span_test.dart +++ b/packages/dart/test/span_test.dart @@ -3,7 +3,6 @@ import 'package:sentry/src/protocol/noop_span.dart'; import 'package:sentry/src/protocol/simple_span.dart'; import 'package:test/test.dart'; -import 'mocks/mock_client_report_recorder.dart'; import 'mocks/mock_sentry_client.dart'; import 'test_utils.dart'; @@ -128,7 +127,6 @@ void main() { class Fixture { final client = MockSentryClient(); - final recorder = MockClientReportRecorder(); final options = defaultTestOptions(); @@ -136,12 +134,7 @@ class Fixture { double? tracesSampleRate = 1.0, }) { options.tracesSampleRate = tracesSampleRate; - - final hub = Hub(options); - - hub.bindClient(client); - options.recorder = recorder; - + final hub = Hub(options)..bindClient(client); return hub; } } From 5e74caa35898a968f0b378eaaaab457e62e78938 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 22:32:27 +0100 Subject: [PATCH 06/16] Remove unnecessary import --- packages/dart/lib/src/protocol/span.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/dart/lib/src/protocol/span.dart b/packages/dart/lib/src/protocol/span.dart index 15509c67fb..0d73355517 100644 --- a/packages/dart/lib/src/protocol/span.dart +++ b/packages/dart/lib/src/protocol/span.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:meta/meta.dart'; import '../../sentry.dart'; From b1a38325b971007ef042ddaff980b5b3763dc372 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 22:32:59 +0100 Subject: [PATCH 07/16] Remove unnecessary import --- packages/dart/lib/src/protocol/span.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dart/lib/src/protocol/span.dart b/packages/dart/lib/src/protocol/span.dart index 0d73355517..b014cc9296 100644 --- a/packages/dart/lib/src/protocol/span.dart +++ b/packages/dart/lib/src/protocol/span.dart @@ -15,7 +15,7 @@ abstract class Span { /// Sets the name of the span. set name(String name); - /// Gets the parentSpan. + /// Gets the parent span. /// If null this span has no parent. Span? get parentSpan; From c7cbeeae2884b416062cf86aaa7d874beeb9658f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 22:34:09 +0100 Subject: [PATCH 08/16] Update --- packages/dart/lib/src/scope.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dart/lib/src/scope.dart b/packages/dart/lib/src/scope.dart index c6679e4d6d..c522293140 100644 --- a/packages/dart/lib/src/scope.dart +++ b/packages/dart/lib/src/scope.dart @@ -55,7 +55,7 @@ class Scope { /// becomes a child of this active span. @internal Span? getActiveSpan() { - return _activeSpans.isNotEmpty ? _activeSpans.last : null; + return _activeSpans.lastOrNull; } /// Sets the given [span] as the currently active span. From 144405e4aff0e5d583c394abf155352fbdc52b86 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 22:34:54 +0100 Subject: [PATCH 09/16] Update --- packages/dart/lib/src/scope.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/dart/lib/src/scope.dart b/packages/dart/lib/src/scope.dart index c522293140..f1e16892e5 100644 --- a/packages/dart/lib/src/scope.dart +++ b/packages/dart/lib/src/scope.dart @@ -63,9 +63,6 @@ class Scope { /// Active spans are used to automatically parent new spans. /// When a new span is started with `active: true` (the default), it becomes /// a child of the currently active span. - /// - /// The active spans are maintained as a stack - the most recently set span - /// is the current active span. @internal void setActiveSpan(Span span) { _activeSpans.add(span); From ab66ec99a65b2b74aed3b6ba234f830eaf7aa886 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 22:40:37 +0100 Subject: [PATCH 10/16] Update --- packages/dart/test/span_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dart/test/span_test.dart b/packages/dart/test/span_test.dart index 84739595d5..e6dfe44208 100644 --- a/packages/dart/test/span_test.dart +++ b/packages/dart/test/span_test.dart @@ -71,7 +71,7 @@ void main() { final span = SimpleSpan(name: 'initial-name', parentSpan: null, hub: hub); span.name = 'updated-name'; - expect(span.status, equals('updated-name')); + expect(span.name, equals('updated-name')); }); test('can set span status', () { From 7d685bef4cab42e8ce750c714340759503f6f334 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 22:41:32 +0100 Subject: [PATCH 11/16] Update --- packages/dart/lib/src/scope.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dart/lib/src/scope.dart b/packages/dart/lib/src/scope.dart index f1e16892e5..f8e1d41229 100644 --- a/packages/dart/lib/src/scope.dart +++ b/packages/dart/lib/src/scope.dart @@ -71,7 +71,7 @@ class Scope { /// Removes the given [span] from the active spans list. /// /// This should be called when a span ends to remove it from the active - /// span hierarchy. + /// span list. @internal void removeActiveSpan(Span span) { _activeSpans.remove(span); From 33d88922390a36a108f9109df694f2d0173deefa Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 23:34:07 +0100 Subject: [PATCH 12/16] Update --- packages/dart/lib/src/hub.dart | 2 +- packages/dart/lib/src/protocol/noop_span.dart | 2 +- packages/dart/lib/src/protocol/simple_span.dart | 4 +++- packages/dart/test/hub_span_test.dart | 4 ++++ packages/dart/test/span_test.dart | 11 +++++++++++ 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index f8bfbc14f2..d12cff1ca8 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -631,7 +631,7 @@ class Hub { scope.removeActiveSpan(span); - // TODO: implement span buffer and add the span to the buffer + // TODO: run this span through span specific pipeline and then forward to span buffer } @internal diff --git a/packages/dart/lib/src/protocol/noop_span.dart b/packages/dart/lib/src/protocol/noop_span.dart index bd673bcd96..326c8d36be 100644 --- a/packages/dart/lib/src/protocol/noop_span.dart +++ b/packages/dart/lib/src/protocol/noop_span.dart @@ -13,7 +13,7 @@ class NoOpSpan implements Span { void end({DateTime? endTimestamp}) {} @override - Span? get parentSpan => NoOpSpan(); + Span? get parentSpan => null; @override void setAttribute(String key, SentryAttribute value) {} diff --git a/packages/dart/lib/src/protocol/simple_span.dart b/packages/dart/lib/src/protocol/simple_span.dart index dbaae046ab..84d9d61976 100644 --- a/packages/dart/lib/src/protocol/simple_span.dart +++ b/packages/dart/lib/src/protocol/simple_span.dart @@ -1,3 +1,5 @@ +import 'package:meta/meta.dart'; + import '../../sentry.dart'; class SimpleSpan implements Span { @@ -43,7 +45,7 @@ class SimpleSpan implements Span { @override void end({DateTime? endTimestamp}) { _endTimestamp = endTimestamp ?? DateTime.now().toUtc(); - // TODO: add this span to buffer through hub.captureSpan + hub.captureSpan(this); } @override diff --git a/packages/dart/test/hub_span_test.dart b/packages/dart/test/hub_span_test.dart index b1e110d60d..0126fe993f 100644 --- a/packages/dart/test/hub_span_test.dart +++ b/packages/dart/test/hub_span_test.dart @@ -122,6 +122,10 @@ void main() { expect(rootSpan.parentSpan, isNull); }); + + test('should not allow finished span to be use as parent', () { + // TODO: this test case needs more clarification + }); }); }); diff --git a/packages/dart/test/span_test.dart b/packages/dart/test/span_test.dart index e6dfe44208..484595341a 100644 --- a/packages/dart/test/span_test.dart +++ b/packages/dart/test/span_test.dart @@ -43,6 +43,17 @@ void main() { expect(span.endTimestamp, equals(endTime)); }); + test('end removes active span from scope', () { + final hub = fixture.getSut(); + final span = hub.startSpan('test-span'); + expect(hub.scope.activeSpans.length, 1); + expect(hub.scope.activeSpans.first, equals(span)); + + span.end(); + + expect(hub.scope.activeSpans, isEmpty); + }); + test('setAttribute sets single attribute', () { final hub = fixture.getSut(); final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); From d74fe5a38d071ff799143f30d6a33478ee6d3925 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 23:39:42 +0100 Subject: [PATCH 13/16] Update --- packages/dart/lib/src/protocol/noop_span.dart | 3 +++ packages/dart/lib/src/protocol/simple_span.dart | 7 +++++-- packages/dart/lib/src/protocol/span.dart | 3 +++ packages/dart/lib/src/protocol/unset_span.dart | 3 +++ packages/dart/lib/src/scope.dart | 7 +++++++ packages/dart/test/scope_test.dart | 1 + packages/dart/test/span_test.dart | 4 +++- 7 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/dart/lib/src/protocol/noop_span.dart b/packages/dart/lib/src/protocol/noop_span.dart index 326c8d36be..c0980fe4e8 100644 --- a/packages/dart/lib/src/protocol/noop_span.dart +++ b/packages/dart/lib/src/protocol/noop_span.dart @@ -35,4 +35,7 @@ class NoOpSpan implements Span { @override DateTime? get endTimestamp => null; + + @override + bool get isFinished => false; } diff --git a/packages/dart/lib/src/protocol/simple_span.dart b/packages/dart/lib/src/protocol/simple_span.dart index 84d9d61976..1e00257975 100644 --- a/packages/dart/lib/src/protocol/simple_span.dart +++ b/packages/dart/lib/src/protocol/simple_span.dart @@ -1,5 +1,3 @@ -import 'package:meta/meta.dart'; - import '../../sentry.dart'; class SimpleSpan implements Span { @@ -12,6 +10,7 @@ class SimpleSpan implements Span { String _name; SpanV2Status _status = SpanV2Status.ok; DateTime? _endTimestamp; + bool _isFinished = false; SimpleSpan({ required String name, @@ -46,6 +45,7 @@ class SimpleSpan implements Span { void end({DateTime? endTimestamp}) { _endTimestamp = endTimestamp ?? DateTime.now().toUtc(); hub.captureSpan(this); + _isFinished = true; } @override @@ -58,6 +58,9 @@ class SimpleSpan implements Span { _attributes.addAll(attributes); } + @override + bool get isFinished => _isFinished; + @override Map toJson() { // TODO: implement toJson diff --git a/packages/dart/lib/src/protocol/span.dart b/packages/dart/lib/src/protocol/span.dart index b014cc9296..28efff7ef9 100644 --- a/packages/dart/lib/src/protocol/span.dart +++ b/packages/dart/lib/src/protocol/span.dart @@ -49,6 +49,9 @@ abstract class Span { /// Overrides if the attributes already exist. void setAttributes(Map attributes); + @internal + bool get isFinished; + @internal Map toJson(); } diff --git a/packages/dart/lib/src/protocol/unset_span.dart b/packages/dart/lib/src/protocol/unset_span.dart index 6411bcae9f..ed7303e6c0 100644 --- a/packages/dart/lib/src/protocol/unset_span.dart +++ b/packages/dart/lib/src/protocol/unset_span.dart @@ -54,4 +54,7 @@ class UnsetSpan extends Span { @override DateTime? get endTimestamp => throw UnimplementedError(); + + @override + bool get isFinished => throw UnimplementedError(); } diff --git a/packages/dart/lib/src/scope.dart b/packages/dart/lib/src/scope.dart index f8e1d41229..f1382d9ede 100644 --- a/packages/dart/lib/src/scope.dart +++ b/packages/dart/lib/src/scope.dart @@ -43,6 +43,8 @@ class Scope { /// Returns active transaction or null if there is no active transaction. ISentrySpan? span; + /// List of active spans. + /// The last span in the list is the current active span. final List _activeSpans = []; @visibleForTesting @@ -307,6 +309,7 @@ class Scope { _replayId = null; propagationContext = PropagationContext(); _attributes.clear(); + _activeSpans.clear(); _clearBreadcrumbsSync(); _setUserSync(null); @@ -514,6 +517,10 @@ class Scope { clone.setAttributes(Map.from(_attributes)); } + if (_activeSpans.isNotEmpty) { + clone._activeSpans.addAll(_activeSpans); + } + return clone; } diff --git a/packages/dart/test/scope_test.dart b/packages/dart/test/scope_test.dart index 98b7892d19..a57bde0f84 100644 --- a/packages/dart/test/scope_test.dart +++ b/packages/dart/test/scope_test.dart @@ -431,6 +431,7 @@ void main() { expect(sut.eventProcessors.length, 0); expect(sut.replayId, isNull); expect(sut.attributes, isEmpty); + expect(sut.activeSpans, isEmpty); }); test('clones', () async { diff --git a/packages/dart/test/span_test.dart b/packages/dart/test/span_test.dart index 484595341a..64fed76482 100644 --- a/packages/dart/test/span_test.dart +++ b/packages/dart/test/span_test.dart @@ -19,7 +19,9 @@ void main() { final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); span.end(); - // TODO: verify span is finished once SimpleSpan implements it + + expect(span.endTimestamp, isNotNull); + expect(span.isFinished, isTrue); }); test('end sets current time by default', () { From 2b9c8b26e826beb64e9dfb08aee23fa6b799fb00 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 23:48:33 +0100 Subject: [PATCH 14/16] Update --- packages/dart/test/span_test.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/dart/test/span_test.dart b/packages/dart/test/span_test.dart index 64fed76482..073bd00229 100644 --- a/packages/dart/test/span_test.dart +++ b/packages/dart/test/span_test.dart @@ -28,11 +28,17 @@ void main() { final hub = fixture.getSut(); final span = SimpleSpan(name: 'test-span', parentSpan: null, hub: hub); + final before = DateTime.now().toUtc(); span.end(); + final after = DateTime.now().toUtc(); - final now = DateTime.now(); - expect(span.endTimestamp?.microsecondsSinceEpoch, - lessThan(now.microsecondsSinceEpoch)); + expect(span.endTimestamp, isNotNull); + expect(span.endTimestamp!.isAfter(before) || span.endTimestamp == before, + isTrue, + reason: 'endTimestamp should be >= time before end() was called'); + expect(span.endTimestamp!.isBefore(after) || span.endTimestamp == after, + isTrue, + reason: 'endTimestamp should be <= time after end() was called'); }); test('end with custom timestamp sets end time', () { From c5a24b443bbae42f6e36a46169e0a13fead70879 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 2 Dec 2025 23:54:45 +0100 Subject: [PATCH 15/16] Update --- packages/dart/lib/src/hub.dart | 3 - packages/dart/test/hub_span_test.dart | 87 +++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/packages/dart/lib/src/hub.dart b/packages/dart/lib/src/hub.dart index d12cff1ca8..b8a6eecd67 100644 --- a/packages/dart/lib/src/hub.dart +++ b/packages/dart/lib/src/hub.dart @@ -598,13 +598,10 @@ class Hub { // - If parentSpan is UnsetSpan (default), use the currently active span // - If parentSpan is a specific Span, use that as the parent // - If parentSpan is null, create a root/segment span (no parent) - // ignore: unused_local_variable final Span? resolvedParentSpan; if (parentSpan is UnsetSpan) { - // Use the currently active span from scope as parent resolvedParentSpan = scope.getActiveSpan(); } else { - // Use the explicitly provided parent (can be null for root/segment span) resolvedParentSpan = parentSpan; } diff --git a/packages/dart/test/hub_span_test.dart b/packages/dart/test/hub_span_test.dart index 0126fe993f..d9c3e3eba4 100644 --- a/packages/dart/test/hub_span_test.dart +++ b/packages/dart/test/hub_span_test.dart @@ -127,6 +127,93 @@ void main() { // TODO: this test case needs more clarification }); }); + + group('span hierarchy', () { + test('builds 3-level hierarchy with automatic parenting', () { + final hub = fixture.getSut(); + + final grandparent = hub.startSpan('grandparent'); + final parent = hub.startSpan('parent'); + final child = hub.startSpan('child'); + + expect(grandparent.parentSpan, isNull); + expect(parent.parentSpan, equals(grandparent)); + expect(child.parentSpan, equals(parent)); + }); + + test('ending middle span allows new span to parent to grandparent', () { + final hub = fixture.getSut(); + + final grandparent = hub.startSpan('grandparent'); + final parent = hub.startSpan('parent'); + parent.end(); + + final sibling = hub.startSpan('sibling'); + + expect(sibling.parentSpan, equals(grandparent)); + }); + + test('sibling spans share same parent when created as inactive', () { + final hub = fixture.getSut(); + + final parent = hub.startSpan('parent'); + final child1 = hub.startSpan('child1', active: false); + final child2 = hub.startSpan('child2', active: false); + + expect(child1.parentSpan, equals(parent)); + expect(child2.parentSpan, equals(parent)); + }); + + test('ending spans in reverse order cleans up active spans correctly', + () { + final hub = fixture.getSut(); + + final span1 = hub.startSpan('span1'); + final span2 = hub.startSpan('span2'); + final span3 = hub.startSpan('span3'); + + expect(hub.scope.activeSpans.length, 3); + + span3.end(); + expect(hub.scope.activeSpans.length, 2); + expect(hub.scope.getActiveSpan(), equals(span2)); + + span2.end(); + expect(hub.scope.getActiveSpan(), equals(span1)); + + span1.end(); + expect(hub.scope.getActiveSpan(), isNull); + }); + + test('ending spans out of order removes them from active spans', () { + final hub = fixture.getSut(); + + final parent = hub.startSpan('parent'); + final child = hub.startSpan('child'); + + parent.end(); + expect(hub.scope.activeSpans, contains(child)); + expect(hub.scope.activeSpans, isNot(contains(parent))); + + child.end(); + expect(hub.scope.activeSpans, isEmpty); + }); + + test('deep hierarchy maintains correct parent chain', () { + final hub = fixture.getSut(); + + final spans = []; + for (var i = 0; i < 5; i++) { + spans.add(hub.startSpan('span-$i')); + } + + // Verify chain: span0 <- span1 <- span2 <- span3 <- span4 + expect(spans[0].parentSpan, isNull); + for (var i = 1; i < spans.length; i++) { + expect(spans[i].parentSpan, equals(spans[i - 1])); + } + }); + }); }); group('captureSpan', () { From 540851decb0a4ab5fd5900b851abd79e059e776f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 3 Dec 2025 00:05:19 +0100 Subject: [PATCH 16/16] Update --- packages/dart/test/hub_span_test.dart | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/dart/test/hub_span_test.dart b/packages/dart/test/hub_span_test.dart index d9c3e3eba4..5f78608aff 100644 --- a/packages/dart/test/hub_span_test.dart +++ b/packages/dart/test/hub_span_test.dart @@ -199,6 +199,26 @@ void main() { expect(hub.scope.activeSpans, isEmpty); }); + test( + 'new span parents to active root span when multiple root spans exist', + () { + final hub = fixture.getSut(); + + final activeRoot = + hub.startSpan('active-root', parentSpan: null, active: true); + final inactiveRoot = + hub.startSpan('inactive-root', parentSpan: null, active: false); + + final childToActiveSpan = hub.startSpan('child'); + expect(childToActiveSpan.parentSpan, equals(activeRoot)); + expect(childToActiveSpan.parentSpan, isNot(equals(inactiveRoot))); + + final childToInactiveSpan = + hub.startSpan('child', parentSpan: inactiveRoot); + expect(childToInactiveSpan.parentSpan, equals(inactiveRoot)); + expect(childToInactiveSpan.parentSpan, isNot(equals(activeRoot))); + }); + test('deep hierarchy maintains correct parent chain', () { final hub = fixture.getSut();