Skip to content
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;
}

scope.removeActiveSpan(span);

// TODO: 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);
}
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) {}
}
24 changes: 21 additions & 3 deletions packages/dart/lib/src/protocol/noop_span.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,39 @@ import '../../sentry.dart';
class NoOpSpan implements Span {
const NoOpSpan();

@override
final String name = 'NoOpSpan';

@override
final SpanV2Status status = SpanV2Status.ok;

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

@override
Span? get parentSpan => null;

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

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

@override
void setName(String name) {}
Map<String, dynamic> toJson() => {};

@override
set name(String name) {}

@override
void setStatus(SpanV2Status status) {}
set status(SpanV2Status status) {}

@override
Map<String, dynamic> toJson() => {};
Map<String, SentryAttribute> get attributes => {};

@override
DateTime? get endTimestamp => null;

@override
bool get isFinished => false;
}
55 changes: 45 additions & 10 deletions packages/dart/lib/src/protocol/simple_span.dart
Original file line number Diff line number Diff line change
@@ -1,31 +1,66 @@
import '../../sentry.dart';

class SimpleSpan implements Span {
final Hub hub;
final Map<String, SentryAttribute> _attributes = {};

@override
void end({DateTime? endTimestamp}) {
// TODO: implement end
final Span? parentSpan;

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

SimpleSpan({
required String name,
this.parentSpan,
Hub? hub,
}) : hub = hub ?? HubAdapter(),
_name = name;

@override
DateTime? get endTimestamp => _endTimestamp;

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

@override
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<String, SentryAttribute> attributes) {
// TODO: implement setAttributes
void end({DateTime? endTimestamp}) {
_endTimestamp = endTimestamp ?? DateTime.now().toUtc();
hub.captureSpan(this);
_isFinished = true;
}

@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<String, SentryAttribute> attributes) {
_attributes.addAll(attributes);
}

@override
bool get isFinished => _isFinished;

@override
Map<String, dynamic> toJson() {
// TODO: implement toJson
Expand Down
37 changes: 30 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,41 @@ 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 name of the span.
String get name;

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

/// Gets the parent span.
/// 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<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 +49,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
30 changes: 25 additions & 5 deletions packages/dart/lib/src/protocol/unset_span.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -24,17 +32,29 @@ class UnsetSpan extends Span {
}

@override
void setName(String name) {
Map<String, dynamic> 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<String, dynamic> toJson() {
throw UnimplementedError('$UnsetSpan methods should not be used');
set status(SpanV2Status status) {
throw UnimplementedError();
}

@override
SpanV2Status get status => throw UnimplementedError();

@override
Map<String, SentryAttribute> get attributes => throw UnimplementedError();

@override
DateTime? get endTimestamp => throw UnimplementedError();

@override
bool get isFinished => throw UnimplementedError();
}
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