Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
12aee4d
Add support
buenaflor Jan 14, 2026
5c3b095
Add test
buenaflor Jan 14, 2026
023c89e
Merge branch 'feat/span-first' into feat/span/frames-tracking-support
buenaflor Jan 16, 2026
e9f9b74
Merge branch 'feat/span-first' into feat/span/frames-tracking-support
buenaflor Feb 3, 2026
4133f4f
Update test
buenaflor Jan 14, 2026
8ea21f8
Update doc
buenaflor Feb 3, 2026
1a33052
Update to OnProcessSpan
buenaflor Feb 3, 2026
74dc1f9
Update
buenaflor Feb 3, 2026
49eae68
Update
buenaflor Feb 3, 2026
3c9ad4a
Set noop datetime to 0
buenaflor Feb 3, 2026
6d5613b
Use switch for traceLifecycle
buenaflor Feb 3, 2026
9aa55a4
Merge branch 'feat/span-first' into feat/span/frames-tracking-support
buenaflor Feb 4, 2026
e97ec07
Deprecate collectors
buenaflor Feb 4, 2026
8c954bd
Review
buenaflor Feb 4, 2026
3e9b6c4
Review
buenaflor Feb 4, 2026
b499167
Review
buenaflor Feb 4, 2026
1c14c1c
Review
buenaflor Feb 4, 2026
bb0fe39
Review
buenaflor Feb 4, 2026
4f088cb
Review
buenaflor Feb 4, 2026
4a6eac3
Review
buenaflor Feb 4, 2026
084fabd
Analyze
buenaflor Feb 4, 2026
7444114
Update to unified collector
buenaflor Feb 5, 2026
1d5083e
Analyze
buenaflor Feb 5, 2026
2c27e38
Update
buenaflor Feb 5, 2026
62e81d8
Update
buenaflor Feb 5, 2026
1ea10a9
Update
buenaflor Feb 5, 2026
703a66f
Fix frames tracking
buenaflor Feb 5, 2026
1b084cd
Update finished bool
buenaflor Feb 5, 2026
f80fd0d
Update
buenaflor Feb 5, 2026
9413892
Update
buenaflor Feb 5, 2026
514e7b5
Fix race condition for finish
buenaflor Feb 5, 2026
b66742e
Update
buenaflor Feb 5, 2026
6d6b381
Update finish condition
buenaflor Feb 5, 2026
ced147d
Fix analyze
buenaflor Feb 5, 2026
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
1 change: 1 addition & 0 deletions packages/dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export 'src/sentry_envelope.dart';
export 'src/sentry_envelope_item.dart';
export 'src/sentry_options.dart';
export 'src/telemetry/sentry_trace_lifecycle.dart';
export 'src/telemetry/span/sentry_span_v2.dart';
// ignore: invalid_export_of_internal_element
export 'src/sentry_trace_origins.dart';
export 'src/span_data_convention.dart';
Expand Down
13 changes: 13 additions & 0 deletions packages/dart/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ abstract class SemanticAttributesConstants {
/// The device family (e.g., "iOS", "Android").
static const deviceFamily = 'device.family';

/// The number of total frames rendered during the lifetime of the span.
static const framesTotal = 'frames.total';

/// The number of slow frames rendered during the lifetime of the span.
static const framesSlow = 'frames.slow';

/// The number of frozen frames rendered during the lifetime of the span.
static const framesFrozen = 'frames.frozen';

/// The sum of all delayed frame durations in seconds during the lifetime of the span.
/// For more information see [frames delay](https://develop.sentry.dev/sdk/performance/frames-delay/).
static const framesDelay = 'frames.delay';

/// The HTTP request method (e.g., "GET", "POST").
static const httpRequestMethod = 'http.request.method';

Expand Down
2 changes: 2 additions & 0 deletions packages/dart/lib/src/hub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,8 @@ class Hub {
scope.setActiveSpan(span);
}

_options.lifecycleRegistry.dispatchCallback(OnSpanStartV2(span));

return span;
}

Expand Down
81 changes: 46 additions & 35 deletions packages/dart/lib/src/protocol/sentry_span.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class SentrySpan extends ISentrySpan {
late final DateTime _startTimestamp;
final Hub _hub;

bool _isFinished = false;
bool _isFinishing = false;
bool _isRootSpan = false;

bool get isRootSpan => _isRootSpan;
Expand Down Expand Up @@ -51,50 +53,59 @@ class SentrySpan extends ISentrySpan {
@override
Future<void> finish(
{SpanStatus? status, DateTime? endTimestamp, Hint? hint}) async {
if (finished) {
// Prevent concurrent or duplicate finish() calls
if (_isFinished || _isFinishing) {
return;
}

if (status != null) {
_status = status;
}
_isFinishing = true;

if (endTimestamp == null) {
endTimestamp = _hub.options.clock();
} else if (endTimestamp.isBefore(_startTimestamp)) {
_hub.options.log(
SentryLevel.warning,
'End timestamp ($endTimestamp) cannot be before start timestamp ($_startTimestamp)',
);
endTimestamp = _hub.options.clock();
} else {
endTimestamp = endTimestamp.toUtc();
}
try {
if (status != null) {
_status = status;
}

for (final collector in _hub.options.performanceCollectors) {
if (collector is PerformanceContinuousCollector) {
await collector.onSpanFinished(this, endTimestamp);
if (endTimestamp == null) {
endTimestamp = _hub.options.clock();
} else if (endTimestamp.isBefore(_startTimestamp)) {
_hub.options.log(
SentryLevel.warning,
'End timestamp ($endTimestamp) cannot be before start timestamp ($_startTimestamp)',
);
endTimestamp = _hub.options.clock();
} else {
endTimestamp = endTimestamp.toUtc();
}
}

// Dispatch OnSpanFinish lifecycle event
final callback =
_hub.options.lifecycleRegistry.dispatchCallback(OnSpanFinish(this));
if (callback is Future) {
await callback;
}
_endTimestamp = endTimestamp;

// ignore: deprecated_member_use_from_same_package
for (final collector in _hub.options.performanceCollectors) {
if (collector is PerformanceContinuousCollector) {
await collector.onSpanFinished(this, endTimestamp);
}
}

// Dispatch OnSpanFinish lifecycle event
final callback =
_hub.options.lifecycleRegistry.dispatchCallback(OnSpanFinish(this));
if (callback is Future) {
await callback;
}

// associate error
if (_throwable != null) {
_hub.setSpanContext(_throwable, this, _tracer.name);
}

// The finished flag depends on the _endTimestamp
// If we set this earlier then finished is true and then we cannot use setData etc...
_endTimestamp = endTimestamp;
_isFinished = true;

// associate error
if (_throwable != null) {
_hub.setSpanContext(_throwable, this, _tracer.name);
await _finishedCallback?.call(endTimestamp: _endTimestamp, hint: hint);
return super
.finish(status: status, endTimestamp: _endTimestamp, hint: hint);
} finally {
_isFinishing = false;
}
await _finishedCallback?.call(endTimestamp: _endTimestamp, hint: hint);
return super
.finish(status: status, endTimestamp: _endTimestamp, hint: hint);
}

@override
Expand Down Expand Up @@ -207,7 +218,7 @@ class SentrySpan extends ISentrySpan {
}

@override
bool get finished => _endTimestamp != null;
Copy link
Contributor Author

@buenaflor buenaflor Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change is necessary for the lifecycle callbacks to function correctly. we were triggering the callbacks before the endTimestamp was set (which means we never had access to the endTimestamp) but we could not trigger it after because then the span would be immutable to change in setData for example. so this check alone is not enough

bool get finished => _isFinished && _endTimestamp != null;

@override
dynamic get throwable => _throwable;
Expand Down
13 changes: 12 additions & 1 deletion packages/dart/lib/src/sdk_lifecycle_hooks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,16 @@ class OnSpanFinish extends SdkLifecycleEvent {
final ISentrySpan span;
}

/// Dispatched when span is ready for processing (before default enrichment).
/// Dispatched when a sampled span is started.
@internal
class OnSpanStartV2 extends SdkLifecycleEvent {
OnSpanStartV2(this.span);

final SentrySpanV2 span;
}

/// Dispatched when a span has been captured and is ready for processing (before default enrichment)
/// and before it's being added to the telemetry processor.
///
/// This is useful for integrations to hook into e.g for enriching with attributes.
@internal
Expand All @@ -107,6 +116,8 @@ class OnProcessSpan extends SdkLifecycleEvent {
OnProcessSpan(this.span);
}

/// Dispatched when a metric has been captured and is ready for processing (before default enrichment)
/// and before it's being added to the telemetry processor.
@internal
class OnProcessMetric extends SdkLifecycleEvent {
final SentryMetric metric;
Expand Down
2 changes: 2 additions & 0 deletions packages/dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -657,10 +657,12 @@ class SentryOptions {
return tracesSampleRate != null || tracesSampler != null;
}

@Deprecated('Will be removed in the next major v10')
List<PerformanceCollector> get performanceCollectors =>
_performanceCollectors;
final List<PerformanceCollector> _performanceCollectors = [];

@Deprecated('Will be removed in the next major v10')
void addPerformanceCollector(PerformanceCollector collector) {
_performanceCollectors.add(collector);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/dart/lib/src/sentry_tracer.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// ignore_for_file: deprecated_member_use_from_same_package

import 'dart:async';

import 'package:meta/meta.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ final class NoOpSentrySpanV2 implements SentrySpanV2 {
@override
SentrySpanV2? get parentSpan => null;

@override
DateTime get startTimestamp => DateTime.fromMillisecondsSinceEpoch(0);

@override
DateTime? get endTimestamp => null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ final class RecordingSentrySpanV2 implements SentrySpanV2 {
@override
set status(SentrySpanStatusV2 value) => _status = value;

@override
DateTime get startTimestamp => _startTimestamp;

@override
DateTime? get endTimestamp => _endTimestamp;

Expand Down
5 changes: 5 additions & 0 deletions packages/dart/lib/src/telemetry/span/sentry_span_v2.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ sealed class SentrySpanV2 {
/// Sets the status of this span.
set status(SentrySpanStatusV2 status);

/// The start timestamp of this span.
DateTime get startTimestamp;

/// The end timestamp of this span.
///
/// Returns null if the span has not ended yet.
DateTime? get endTimestamp;

/// Whether this span has ended.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ final class UnsetSentrySpanV2 implements SentrySpanV2 {
@override
SentrySpanV2? get parentSpan => _throw();

@override
DateTime get startTimestamp => _throw();

@override
DateTime? get endTimestamp => _throw();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ abstract class InstrumentationSpan {
Future<void> finish({SpanStatus? status, DateTime? endTimestamp});
SentryTraceHeader toSentryTrace();
SentryBaggageHeader? toBaggageHeader();

/// Returns true if this span is a no-op span that doesn't record data.
bool get isNoop;

/// The start timestamp of this span.
DateTime get startTimestamp;
}

/// [InstrumentationSpan] implementation wrapping [ISentrySpan].
Expand Down Expand Up @@ -65,6 +71,22 @@ class LegacyInstrumentationSpan implements InstrumentationSpan {

@override
SentryBaggageHeader? toBaggageHeader() => _span.toBaggageHeader();

@override
bool get isNoop => _span is NoOpSentrySpan;

@override
DateTime get startTimestamp => _span.startTimestamp;

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LegacyInstrumentationSpan &&
runtimeType == other.runtimeType &&
identical(_span, other._span);

@override
int get hashCode => _span.hashCode;
}

@internal
Expand Down Expand Up @@ -180,4 +202,20 @@ class StreamingInstrumentationSpan implements InstrumentationSpan {
}
return SentrySpanStatusV2.error;
}

@override
bool get isNoop => _span is! RecordingSentrySpanV2;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this check suffices


@override
DateTime get startTimestamp => _span.startTimestamp;

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is StreamingInstrumentationSpan &&
runtimeType == other.runtimeType &&
identical(_span, other._span);

@override
int get hashCode => _span.hashCode;
}
Loading
Loading