Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions packages/dart/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,18 @@ class SentrySpanDescriptions {
static String dbOpen({required String dbName}) => 'Open database $dbName';
static String dbClose({required String dbName}) => 'Close database $dbName';
}

abstract final class SemanticAttributesConstants {
/// 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';
}
2 changes: 2 additions & 0 deletions packages/dart/lib/src/hub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import 'sentry_tracer.dart';
import 'sentry_traces_sampler.dart';
import 'telemetry/span/sentry_span_sampling_context.dart';
import 'telemetry/span/sentry_span_v2.dart';

Check notice on line 13 in packages/dart/lib/src/hub.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

The import of 'telemetry/span/sentry_span_v2.dart' is unnecessary because all of the used elements are also provided by the import of '../sentry.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unnecessary_import to learn more about this problem.
import 'transport/data_category.dart';
import 'utils/internal_logger.dart';

Expand Down Expand Up @@ -682,6 +682,8 @@
scope.setActiveSpan(span);
}

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

return span;
}

Expand Down
11 changes: 11 additions & 0 deletions packages/dart/lib/src/performance_collector.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '../sentry.dart';
import 'telemetry/span/sentry_span_v2.dart';

Check notice on line 2 in packages/dart/lib/src/performance_collector.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

The import of 'telemetry/span/sentry_span_v2.dart' is unnecessary because all of the used elements are also provided by the import of '../sentry.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unnecessary_import to learn more about this problem.

abstract class PerformanceCollector {}

Expand All @@ -11,3 +12,13 @@

void clear();
}

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

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

void clear();
}
18 changes: 18 additions & 0 deletions packages/dart/lib/src/sdk_lifecycle_hooks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,21 @@ class OnSpanFinish extends SdkLifecycleEvent {

final ISentrySpan span;
}

/// Dispatched when a sampled span is started.
@internal
class OnSpanStartV2 extends SdkLifecycleEvent {
OnSpanStartV2(this.span);

final SentrySpanV2 span;
}

/// Dispatched when a sampled span is ended.
///
/// Trigger directly after span.end() and before SDK enrichment is applied.
@internal
class OnSpanEndV2 extends SdkLifecycleEvent {
OnSpanEndV2(this.span);

final SentrySpanV2 span;
}
1 change: 1 addition & 0 deletions packages/dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ class SentryClient {
return;
case RecordingSentrySpanV2 span:
// TODO(next-pr): add common attributes, merge scope attributes
_options.lifecycleRegistry.dispatchCallback(OnSpanEndV2(span));

_options.telemetryProcessor.addSpan(span);
}
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.now();

@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 @@ -31,7 +31,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,28 @@ 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;
if (options.traceLifecycle == SentryTraceLifecycle.streaming) {
final collector = SpanFrameMetricsCollectorV2(framesTracker,
resumeFrameTracking: () => widgetsBinding.resumeTrackingFrames(),
pauseFrameTracking: () => widgetsBinding.pauseTrackingFrames());
_collector = collector;

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

options.lifecycleRegistry.registerCallback<OnSpanEndV2>((event) {
if (event.span.endTimestamp != null) {
collector.onSpanFinished(event.span, event.span.endTimestamp!);
}
});
} else {
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