Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
40 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
8741446
Update
buenaflor Feb 6, 2026
0cc7c26
Review
buenaflor Feb 6, 2026
c55cfa6
Fix tracer bug
buenaflor Feb 6, 2026
5799275
Review
buenaflor Feb 6, 2026
c6d4abe
Simplify
buenaflor Feb 6, 2026
a0fcebc
Fix test
buenaflor Feb 6, 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
10 changes: 10 additions & 0 deletions packages/dart/lib/src/performance_collector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ abstract class PerformanceContinuousCollector extends PerformanceCollector {

void clear();
}

/// Used for collecting continuous data about vitals (slow, frozen frames, etc.)
/// during a span (v2 API).
abstract class PerformanceContinuousCollectorV2 extends PerformanceCollector {
Future<void> onSpanStarted(SentrySpanV2 span);

Future<void> onSpanFinished(SentrySpanV2 span, DateTime endTimestamp);

void clear();
}
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
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
@@ -0,0 +1,95 @@
// ignore_for_file: invalid_use_of_internal_member

import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
import '../utils/internal_logger.dart';
import 'sentry_delayed_frames_tracker.dart';

/// Collects frames from [SentryDelayedFramesTracker], calculates the metrics
/// and attaches them to spans.
@internal
class SpanFrameMetricsCollectorV2 implements PerformanceContinuousCollectorV2 {
SpanFrameMetricsCollectorV2(
this._frameTracker, {
required void Function() resumeFrameTracking,
required void Function() pauseFrameTracking,
}) : _resumeFrameTracking = resumeFrameTracking,
_pauseFrameTracking = pauseFrameTracking;

final SentryDelayedFramesTracker _frameTracker;
final void Function() _resumeFrameTracking;
final void Function() _pauseFrameTracking;

/// Stores the spans that are actively being tracked.
/// After the frames are calculated and stored in the span the span is removed from this list.
@visibleForTesting
final List<SentrySpanV2> activeSpans = [];

@override
Future<void> onSpanStarted(SentrySpanV2 span) async {
return _tryCatch('onSpanStarted', () async {
if (span is NoOpSentrySpan) {
return;
}

activeSpans.add(span);
_resumeFrameTracking();
});
}

@override
Future<void> onSpanFinished(SentrySpanV2 span, DateTime endTimestamp) async {
return _tryCatch('onSpanFinished', () async {
if (span is NoOpSentrySpan) {
return;
}

final startTimestamp = span.startTimestamp;
final metrics = _frameTracker.getFrameMetrics(
spanStartTimestamp: startTimestamp, spanEndTimestamp: endTimestamp);

if (metrics != null) {
final attributes = Map<String, SentryAttribute>.from(span.attributes);
attributes.putIfAbsent(SemanticAttributesConstants.framesTotal,
() => SentryAttribute.int(metrics.totalFrameCount));
attributes.putIfAbsent(SemanticAttributesConstants.framesSlow,
() => SentryAttribute.int(metrics.slowFrameCount));
attributes.putIfAbsent(SemanticAttributesConstants.framesFrozen,
() => SentryAttribute.int(metrics.frozenFrameCount));
attributes.putIfAbsent(SemanticAttributesConstants.framesDelay,
() => SentryAttribute.int(metrics.framesDelay));
span.setAttributes(attributes);
}

activeSpans.remove(span);
if (activeSpans.isEmpty) {
clear();
} else {
_frameTracker.removeIrrelevantFrames(activeSpans.first.startTimestamp);
}
});
}

Future<void> _tryCatch(String methodName, Future<void> Function() fn) async {
try {
return fn();
} catch (exception, stackTrace) {
internalLogger.error(
'SpanV2FrameMetricsCollector $methodName failed',
error: exception,
stackTrace: stackTrace,
);
clear();
}
}

@override
void clear() {
_pauseFrameTracking();
_frameTracker.clear();
activeSpans.clear();
// we don't need to clear the expected frame duration as that realistically
// won't change throughout the application's lifecycle
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import '../../sentry_flutter.dart';
import '../binding_wrapper.dart';
import '../frames_tracking/sentry_delayed_frames_tracker.dart';
import '../frames_tracking/span_frame_metrics_collector.dart';
import '../frames_tracking/span_frame_metrics_collector_v2.dart';
import '../native/sentry_native_binding.dart';

class FramesTrackingIntegration implements Integration<SentryFlutterOptions> {
Expand Down Expand Up @@ -50,11 +51,29 @@ class FramesTrackingIntegration implements Integration<SentryFlutterOptions> {
SentryDelayedFramesTracker(options, expectedFrameDuration);
widgetsBinding.initializeFramesTracking(
framesTracker.addDelayedFrame, options, expectedFrameDuration);
final collector = SpanFrameMetricsCollector(options, framesTracker,
resumeFrameTracking: () => widgetsBinding.resumeTrackingFrames(),
pauseFrameTracking: () => widgetsBinding.pauseTrackingFrames());
options.addPerformanceCollector(collector);
_collector = collector;
switch (options.traceLifecycle) {
case SentryTraceLifecycle.streaming:
final collector = SpanFrameMetricsCollectorV2(framesTracker,
resumeFrameTracking: () => widgetsBinding.resumeTrackingFrames(),
pauseFrameTracking: () => widgetsBinding.pauseTrackingFrames());
_collector = collector;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We did implement a V2 perfomrce collector, but are not adding it to options, is this correct?

Copy link
Contributor Author

@buenaflor buenaflor Feb 3, 2026

Choose a reason for hiding this comment

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

it's a behaviour difference.

for transactions we add it to the options so we can manually trigger onSpanFinish and onSpanEnded via options.collector.onSpanFinished() but now that we use lifecycle callbacks we dont need to do that anymroe for spanv2 so we dont need to add it to the options

I'll refactor the static version to use lifeecycle callbacks as well


options.lifecycleRegistry.registerCallback<OnSpanStartV2>((event) {
collector.onSpanStarted(event.span);
});

options.lifecycleRegistry.registerCallback<OnProcessSpan>((event) {
if (event.span.endTimestamp != null) {
collector.onSpanFinished(event.span, event.span.endTimestamp!);
}
});
case SentryTraceLifecycle.static:
final collector = SpanFrameMetricsCollector(options, framesTracker,
resumeFrameTracking: () => widgetsBinding.resumeTrackingFrames(),
pauseFrameTracking: () => widgetsBinding.pauseTrackingFrames());
options.addPerformanceCollector(collector);
_collector = collector;
}

options.sdk.addIntegration(integrationName);
options.log(SentryLevel.debug,
Expand Down
Loading
Loading