Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
63e3107
Update
buenaflor Nov 28, 2025
eb0cdef
Updateg
buenaflor Dec 2, 2025
19a8c19
Update
buenaflor Dec 2, 2025
8713bfc
Update
buenaflor Dec 2, 2025
1adb9a3
Update
buenaflor Dec 2, 2025
5e74caa
Remove unnecessary import
buenaflor Dec 2, 2025
b1a3832
Remove unnecessary import
buenaflor Dec 2, 2025
c7cbeea
Update
buenaflor Dec 2, 2025
144405e
Update
buenaflor Dec 2, 2025
ab66ec9
Update
buenaflor Dec 2, 2025
7d685be
Update
buenaflor Dec 2, 2025
33d8892
Update
buenaflor Dec 2, 2025
d74fe5a
Update
buenaflor Dec 2, 2025
2b9c8b2
Update
buenaflor Dec 2, 2025
c5a24b4
Update
buenaflor Dec 2, 2025
540851d
Update
buenaflor Dec 2, 2025
a446d24
Merge branch 'feat/span-first' into feat/spans/active-spans-impl
buenaflor Dec 9, 2025
3a8a2b8
Refactor Span implementations to standardize spanId handling and impr…
buenaflor Dec 10, 2025
2f9e3ca
Add tests for finished spans as parents and handling of disabled tracing
buenaflor Dec 10, 2025
0c2ef8b
Implement idempotent behavior for ending spans and enhance tests for …
buenaflor Dec 10, 2025
1746c3a
Fix end method in SimpleSpan to ensure captureSpan is called after se…
buenaflor Dec 10, 2025
5ef63d2
Update scope_test to use getActiveSpan method for retrieving the last…
buenaflor Dec 10, 2025
787b43c
Refactor Span documentation and clean up tests
buenaflor Dec 10, 2025
8f5cdf8
Update TODO comment for clarity
buenaflor Dec 10, 2025
be67e2e
Update TODO comment for span processing
buenaflor Dec 10, 2025
c5ed99a
Rename test for setting span status to improve clarity
buenaflor Dec 11, 2025
7f3bc3d
Fix end method in SimpleSpan to ensure endTimestamp is set as UTC
buenaflor Dec 11, 2025
fa9af69
Update span_test to ensure endTimestamp is set as UTC in tests
buenaflor Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions packages/dart/lib/src/hub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -586,12 +587,48 @@ 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)
final Span? resolvedParentSpan;
if (parentSpan is UnsetSpan) {
resolvedParentSpan = scope.getActiveSpan();
} else {
resolvedParentSpan = parentSpan;
}

final span =
SimpleSpan(name: name, parentSpan: resolvedParentSpan, hub: this);
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;
Comment thread
buenaflor marked this conversation as resolved.
}

scope.removeActiveSpan(span);

// TODO(next-pr): run this span through span specific pipeline and then forward to span buffer
}

@internal
Expand Down
3 changes: 3 additions & 0 deletions packages/dart/lib/src/hub_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
denrase marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions packages/dart/lib/src/noop_hub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,7 @@ class NoOpHub implements Hub {
Map<String, SentryAttribute>? attributes,
}) =>
NoOpSpan();

@override
void captureSpan(Span span) {}
}
31 changes: 26 additions & 5 deletions packages/dart/lib/src/protocol/noop_span.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,40 @@ class NoOpSpan implements Span {
const NoOpSpan();

@override
void end({DateTime? endTimestamp}) {}
SpanId get spanId => SpanId.empty();

@override
void setAttribute(String key, SentryAttribute value) {}
final String name = 'NoOpSpan';

@override
void setAttributes(Map<String, SentryAttribute> attributes) {}
set name(String name) {}

@override
final SpanV2Status status = SpanV2Status.ok;

@override
set status(SpanV2Status status) {}

@override
Span? get parentSpan => null;

@override
DateTime? get endTimestamp => null;

@override
Map<String, SentryAttribute> get attributes => {};

@override
bool get isFinished => false;

@override
void setName(String name) {}
void setAttribute(String key, SentryAttribute value) {}

@override
void setStatus(SpanV2Status status) {}
void setAttributes(Map<String, SentryAttribute> attributes) {}

@override
void end({DateTime? endTimestamp}) {}

@override
Map<String, dynamic> toJson() => {};
Expand Down
62 changes: 50 additions & 12 deletions packages/dart/lib/src/protocol/simple_span.dart
Original file line number Diff line number Diff line change
@@ -1,29 +1,67 @@
import '../../sentry.dart';

class SimpleSpan implements Span {
final SpanId _spanId;
final Hub _hub;
@override
void end({DateTime? endTimestamp}) {
// TODO: implement end
}
final Span? parentSpan;
final Map<String, SentryAttribute> _attributes = {};

String _name;
SpanV2Status _status = SpanV2Status.ok;
DateTime? _endTimestamp;
bool _isFinished = false;

SimpleSpan({
required String name,
this.parentSpan,
Hub? hub,
}) : _spanId = SpanId.newId(),
_hub = hub ?? HubAdapter(),
_name = name;
Comment thread
buenaflor marked this conversation as resolved.

@override
void setAttribute(String key, SentryAttribute value) {
// TODO: implement setAttribute
}
SpanId get spanId => _spanId;
Comment thread
denrase marked this conversation as resolved.

@override
void setAttributes(Map<String, SentryAttribute> attributes) {
// TODO: implement setAttributes
String get name => _name;

@override
set name(String value) => _name = value;

@override
SpanV2Status get status => _status;

@override
set status(SpanV2Status value) => _status = value;

@override
DateTime? get endTimestamp => _endTimestamp;

@override
Map<String, SentryAttribute> get attributes => Map.unmodifiable(_attributes);

@override
bool get isFinished => _isFinished;

@override
void setAttribute(String key, SentryAttribute value) {
_attributes[key] = value;
}

@override
void setName(String name) {
// TODO: implement setName
void setAttributes(Map<String, SentryAttribute> attributes) {
Comment thread
denrase marked this conversation as resolved.
_attributes.addAll(attributes);
}

@override
void setStatus(SpanV2Status status) {
// TODO: implement setStatus
void end({DateTime? endTimestamp}) {
if (_isFinished) {
return;
}
_endTimestamp = (endTimestamp ?? DateTime.now()).toUtc();
_isFinished = true;
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
_hub.captureSpan(this);
}

@override
Expand Down
39 changes: 32 additions & 7 deletions packages/dart/lib/src/protocol/span.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,43 @@ 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();

/// Gets the id of the span.
SpanId get spanId;

/// Gets the name of the span.
String get name;

/// Sets the name of the span.
set name(String name);

/// Gets the parent span.
Span? get parentSpan;

/// Gets the status of the span.
SpanV2Status get status;

/// Sets the status of the span.
set status(SpanV2Status status);
Comment thread
buenaflor marked this conversation as resolved.

/// Gets the end timestamp of the span.
DateTime? get endTimestamp;

/// Gets a read-only view of the attributes of the span using [Map.unmodifiable](https://api.flutter.dev/flutter/dart-core/Map/Map.unmodifiable.html).
///
/// The returned map must not be mutated by callers.
Comment thread
buenaflor marked this conversation as resolved.
Map<String, SentryAttribute> 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.
Expand All @@ -23,11 +51,8 @@ abstract class Span {
/// Overrides if the attributes already exist.
void setAttributes(Map<String, SentryAttribute> attributes);

/// Sets the status of the span.
void setStatus(SpanV2Status status);

/// Sets the name of the span.
void setName(String name);
@internal
bool get isFinished;

@internal
Map<String, dynamic> toJson();
Expand Down
52 changes: 39 additions & 13 deletions packages/dart/lib/src/protocol/unset_span.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,58 @@ class UnsetSpan extends Span {
const UnsetSpan();

@override
void end({DateTime? endTimestamp}) {
throw UnimplementedError('$UnsetSpan methods should not be used');
}
SpanId get spanId =>
throw UnimplementedError('$UnsetSpan apis should not be used');

@override
void setAttribute(String key, SentryAttribute value) {
throw UnimplementedError('$UnsetSpan methods should not be used');
}
String get name =>
throw UnimplementedError('$UnsetSpan apis should not be used');

@override
void setAttributes(Map<String, SentryAttribute> attributes) {
throw UnimplementedError('$UnsetSpan methods should not be used');
set name(String name) =>
throw UnimplementedError('$UnsetSpan apis should not be used');

@override
SpanV2Status get status =>
throw UnimplementedError('$UnsetSpan apis should not be used');

@override
set status(SpanV2Status status) =>
throw UnimplementedError('$UnsetSpan apis should not be used');

@override
Span? get parentSpan =>
throw UnimplementedError('$UnsetSpan apis should not be used');

@override
DateTime? get endTimestamp =>
throw UnimplementedError('$UnsetSpan apis should not be used');

@override
Map<String, SentryAttribute> get attributes =>
throw UnimplementedError('$UnsetSpan apis should not be used');

@override
bool get isFinished =>
throw UnimplementedError('$UnsetSpan apis should not be used');

@override
void setAttribute(String key, SentryAttribute value) {
throw UnimplementedError('$UnsetSpan apis should not be used');
}

@override
void setName(String name) {
throw UnimplementedError('$UnsetSpan methods should not be used');
void setAttributes(Map<String, SentryAttribute> attributes) {
throw UnimplementedError('$UnsetSpan apis should not be used');
}

@override
void setStatus(SpanV2Status status) {
throw UnimplementedError('$UnsetSpan methods should not be used');
void end({DateTime? endTimestamp}) {
throw UnimplementedError('$UnsetSpan apis should not be used');
}

@override
Map<String, dynamic> toJson() {
throw UnimplementedError('$UnsetSpan methods should not be used');
throw UnimplementedError('$UnsetSpan apis should not be used');
}
}
41 changes: 41 additions & 0 deletions packages/dart/lib/src/scope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,42 @@ 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<Span> _activeSpans = [];

@visibleForTesting
List<Span> 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].
/// 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.lastOrNull;
}

/// 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.
@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 list.
@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.
///
Expand Down Expand Up @@ -273,6 +309,7 @@ class Scope {
_replayId = null;
propagationContext = PropagationContext();
_attributes.clear();
_activeSpans.clear();

_clearBreadcrumbsSync();
_setUserSync(null);
Expand Down Expand Up @@ -480,6 +517,10 @@ class Scope {
clone.setAttributes(Map.from(_attributes));
}

if (_activeSpans.isNotEmpty) {
clone._activeSpans.addAll(_activeSpans);
}

return clone;
}

Expand Down
Loading
Loading