Skip to content

Commit 59a47db

Browse files
committed
feat(tracing): introduce SpanWrapper and LegacySpanWrapper for enhanced tracing support
- Added SpanWrapper interface for abstraction of span operations. - Implemented LegacySpanWrapper for legacy transaction-based tracing. - Updated SentryOptions to include spanWrapper initialization. - Enhanced SentryQueryInterceptor to utilize the new spanWrapper for database operations. - Removed SentrySpanHelper as it is no longer needed. - Added tests for the new tracing functionality and status mapping. This update improves the flexibility and maintainability of tracing operations within the Sentry Dart SDK.
1 parent 7f626a2 commit 59a47db

File tree

11 files changed

+963
-200
lines changed

11 files changed

+963
-200
lines changed

packages/dart/lib/sentry.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export 'src/sentry_trace_origins.dart';
4646
export 'src/span_data_convention.dart';
4747
export 'src/spotlight.dart';
4848
export 'src/throwable_mechanism.dart';
49+
// ignore: invalid_export_of_internal_element
4950
export 'src/tracing.dart';
5051
export 'src/transport/transport.dart';
5152
export 'src/type_check_hint.dart';

packages/dart/lib/src/sentry_options.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,10 @@ class SentryOptions {
555555
@internal
556556
TelemetryProcessor telemetryProcessor = NoOpTelemetryProcessor();
557557

558+
// TODO(next-pr): in span-first this should be using the span v2 wrapper
559+
@internal
560+
late final SpanWrapper spanWrapper = LegacySpanWrapper(hub: HubAdapter());
561+
558562
SentryOptions({String? dsn, Platform? platform, RuntimeChecker? checker}) {
559563
this.dsn = dsn;
560564
if (platform != null) {

packages/dart/lib/src/tracing.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ export 'sentry_measurement.dart';
88
export 'sentry_measurement_unit.dart';
99
export 'sentry_trace_context_header.dart';
1010
export 'sentry_traces_sampling_decision.dart';
11+
export 'tracing/span_wrapper.dart';
12+
// ignore: invalid_export_of_internal_element
13+
export 'tracing/legacy_span_wrapper.dart';
14+
export 'tracing/tracing_status.dart';
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// ignore_for_file: invalid_use_of_internal_member
2+
3+
import 'package:meta/meta.dart';
4+
5+
import '../../sentry.dart';
6+
7+
/// [SpanWrapper] implementation using Sentry's legacy transaction-based tracing.
8+
///
9+
/// This implementation uses [ISentrySpan] and [SentryTracer] for span operations.
10+
/// It creates child spans under the current active span from the hub.
11+
///
12+
/// Integration packages should not instantiate this directly. Instead, access
13+
/// it via [SentryOptions.spanWrapper].
14+
@internal
15+
class LegacySpanWrapper implements SpanWrapper {
16+
final Hub _hub;
17+
18+
LegacySpanWrapper({required Hub hub}) : _hub = hub;
19+
20+
/// Resolves the parent span from the provided [parentSpan] or falls back to hub.
21+
///
22+
/// If [parentSpan] is provided and is an [ISentrySpan], it's used directly.
23+
/// Otherwise, falls back to the hub's active span.
24+
ISentrySpan? _resolveParent(Object? parentSpan) {
25+
if (parentSpan is ISentrySpan) {
26+
return parentSpan;
27+
}
28+
return _hub.getSpan();
29+
}
30+
31+
@override
32+
Future<T> wrapAsync<T>({
33+
required String operation,
34+
required String description,
35+
required Future<T> Function() execute,
36+
String? origin,
37+
Map<String, Object>? attributes,
38+
TracingStatus Function(T result)? deriveStatus,
39+
Object? parentSpan,
40+
}) async {
41+
final parent = _resolveParent(parentSpan);
42+
if (parent == null) {
43+
return execute();
44+
}
45+
46+
final span = parent.startChild(operation, description: description);
47+
_configureSpan(span, origin: origin, attributes: attributes);
48+
49+
try {
50+
final result = await execute();
51+
span.status = _toSpanStatus(deriveStatus?.call(result) ?? TracingStatus.ok);
52+
return result;
53+
} catch (exception) {
54+
span.throwable = exception;
55+
span.status = SpanStatus.internalError();
56+
rethrow;
57+
} finally {
58+
await span.finish();
59+
}
60+
}
61+
62+
@override
63+
T wrapSync<T>({
64+
required String operation,
65+
required String description,
66+
required T Function() execute,
67+
String? origin,
68+
Map<String, Object>? attributes,
69+
TracingStatus Function(T result)? deriveStatus,
70+
Object? parentSpan,
71+
}) {
72+
final parent = _resolveParent(parentSpan);
73+
if (parent == null) {
74+
return execute();
75+
}
76+
77+
final span = parent.startChild(operation, description: description);
78+
_configureSpan(span, origin: origin, attributes: attributes);
79+
80+
try {
81+
final result = execute();
82+
span.status = _toSpanStatus(deriveStatus?.call(result) ?? TracingStatus.ok);
83+
return result;
84+
} catch (exception) {
85+
span.throwable = exception;
86+
span.status = SpanStatus.internalError();
87+
rethrow;
88+
} finally {
89+
span.finish();
90+
}
91+
}
92+
93+
@override
94+
Future<T> wrapAsyncOrStartTransaction<T>({
95+
required String operation,
96+
required String description,
97+
required Future<T> Function() execute,
98+
String? origin,
99+
Map<String, Object>? attributes,
100+
TracingStatus Function(T result)? deriveStatus,
101+
}) async {
102+
final existingSpan = _hub.getSpan();
103+
104+
if (existingSpan != null) {
105+
return wrapAsync(
106+
operation: operation,
107+
description: description,
108+
execute: execute,
109+
origin: origin,
110+
attributes: attributes,
111+
deriveStatus: deriveStatus,
112+
);
113+
}
114+
115+
// Start a new transaction
116+
final transaction = _hub.startTransaction(
117+
operation,
118+
description,
119+
bindToScope: true,
120+
);
121+
_configureSpan(transaction, origin: origin, attributes: attributes);
122+
123+
try {
124+
final result = await execute();
125+
transaction.status =
126+
_toSpanStatus(deriveStatus?.call(result) ?? TracingStatus.ok);
127+
return result;
128+
} catch (exception) {
129+
transaction.throwable = exception;
130+
transaction.status = SpanStatus.internalError();
131+
rethrow;
132+
} finally {
133+
await transaction.finish();
134+
}
135+
}
136+
137+
void _configureSpan(
138+
ISentrySpan span, {
139+
String? origin,
140+
Map<String, Object>? attributes,
141+
}) {
142+
if (origin != null) {
143+
span.origin = origin;
144+
}
145+
attributes?.forEach((key, value) => span.setData(key, value));
146+
}
147+
148+
/// Converts [TracingStatus] to legacy [SpanStatus].
149+
SpanStatus _toSpanStatus(TracingStatus status) {
150+
return switch (status) {
151+
TracingStatus.ok => SpanStatus.ok(),
152+
TracingStatus.cancelled => SpanStatus.cancelled(),
153+
TracingStatus.unknown => SpanStatus.unknownError(),
154+
TracingStatus.invalidArgument => SpanStatus.invalidArgument(),
155+
TracingStatus.deadlineExceeded => SpanStatus.deadlineExceeded(),
156+
TracingStatus.notFound => SpanStatus.notFound(),
157+
TracingStatus.alreadyExists => SpanStatus.alreadyExists(),
158+
TracingStatus.permissionDenied => SpanStatus.permissionDenied(),
159+
TracingStatus.resourceExhausted => SpanStatus.resourceExhausted(),
160+
TracingStatus.failedPrecondition => SpanStatus.failedPrecondition(),
161+
TracingStatus.aborted => SpanStatus.aborted(),
162+
TracingStatus.outOfRange => SpanStatus.outOfRange(),
163+
TracingStatus.unimplemented => SpanStatus.unimplemented(),
164+
TracingStatus.internalError => SpanStatus.internalError(),
165+
TracingStatus.unavailable => SpanStatus.unavailable(),
166+
TracingStatus.dataLoss => SpanStatus.dataLoss(),
167+
TracingStatus.unauthenticated => SpanStatus.unauthenticated(),
168+
};
169+
}
170+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import 'package:meta/meta.dart';
2+
3+
import 'tracing_status.dart';
4+
5+
/// Abstraction for span operations that enables swapping tracing implementations.
6+
///
7+
/// This interface provides a unified, implementation-agnostic way to wrap
8+
/// operations with spans. It does not expose any span types, allowing
9+
/// integration packages to instrument code without coupling to a specific
10+
/// tracing implementation.
11+
///
12+
/// The implementation (e.g., [SentrySpanWrapper]) handles the actual span
13+
/// creation using the configured tracing backend.
14+
///
15+
/// Example usage:
16+
/// ```dart
17+
/// final wrapper = hub.options.spanWrapper;
18+
/// final result = await wrapper.wrapAsync(
19+
/// operation: 'db.sql.query',
20+
/// description: 'SELECT * FROM users',
21+
/// execute: () => database.query('SELECT * FROM users'),
22+
/// origin: 'auto.db.my_integration',
23+
/// attributes: {'db.system': 'sqlite'},
24+
/// );
25+
/// ```
26+
@internal
27+
abstract class SpanWrapper {
28+
/// Wraps an asynchronous operation with a span.
29+
///
30+
/// Creates a child span under the current active span, executes [execute],
31+
/// and finishes the span when complete. If no parent span is available,
32+
/// [execute] is called directly without creating a span.
33+
///
34+
/// Parameters:
35+
/// - [operation]: The span operation name (e.g., 'db.sql.query', 'http.client').
36+
/// - [description]: A description of the operation (e.g., the SQL query, URL).
37+
/// - [execute]: The async function to execute within the span.
38+
/// - [origin]: Optional origin identifier for the span.
39+
/// - [attributes]: Optional attributes to attach to the span.
40+
/// - [deriveStatus]: Optional function to derive span status from the result.
41+
/// If not provided, defaults to [TracingStatus.ok] on success.
42+
/// - [parentSpan]: Optional parent span to use instead of the hub's active span.
43+
/// This is useful for integrations that manage their own span hierarchy
44+
/// (e.g., nested database transactions).
45+
///
46+
/// Returns the result of [execute].
47+
///
48+
/// On exception, the span is marked with [TracingStatus.internalError] and
49+
/// the exception is recorded before being rethrown.
50+
Future<T> wrapAsync<T>({
51+
required String operation,
52+
required String description,
53+
required Future<T> Function() execute,
54+
String? origin,
55+
Map<String, Object>? attributes,
56+
TracingStatus Function(T result)? deriveStatus,
57+
Object? parentSpan,
58+
});
59+
60+
/// Wraps a synchronous operation with a span.
61+
///
62+
/// Creates a child span under the current active span, executes [execute],
63+
/// and finishes the span when complete. If no parent span is available,
64+
/// [execute] is called directly without creating a span.
65+
///
66+
/// Parameters:
67+
/// - [operation]: The span operation name (e.g., 'db.sql.query', 'file.read').
68+
/// - [description]: A description of the operation (e.g., the SQL query, filename).
69+
/// - [execute]: The sync function to execute within the span.
70+
/// - [origin]: Optional origin identifier for the span.
71+
/// - [attributes]: Optional attributes to attach to the span.
72+
/// - [deriveStatus]: Optional function to derive span status from the result.
73+
/// If not provided, defaults to [TracingStatus.ok] on success.
74+
/// - [parentSpan]: Optional parent span to use instead of the hub's active span.
75+
/// This is useful for integrations that manage their own span hierarchy
76+
/// (e.g., nested database transactions).
77+
///
78+
/// Returns the result of [execute].
79+
///
80+
/// On exception, the span is marked with [TracingStatus.internalError] and
81+
/// the exception is recorded before being rethrown.
82+
T wrapSync<T>({
83+
required String operation,
84+
required String description,
85+
required T Function() execute,
86+
String? origin,
87+
Map<String, Object>? attributes,
88+
TracingStatus Function(T result)? deriveStatus,
89+
Object? parentSpan,
90+
});
91+
92+
/// Wraps an async operation, starting a transaction if no parent span exists.
93+
///
94+
/// This is useful for entry points (like GraphQL operations) where we want
95+
/// to create a root transaction if there's no active span, but create a
96+
/// child span if one exists.
97+
///
98+
/// Parameters are the same as [wrapAsync].
99+
Future<T> wrapAsyncOrStartTransaction<T>({
100+
required String operation,
101+
required String description,
102+
required Future<T> Function() execute,
103+
String? origin,
104+
Map<String, Object>? attributes,
105+
TracingStatus Function(T result)? deriveStatus,
106+
});
107+
}

0 commit comments

Comments
 (0)