Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### Features

- Synchronize `traceId` to native SDKs ([#3507](https://github.com/getsentry/sentry-dart/pull/3507))
- Native events (e.g. from Android or iOS) such as errors, logs, and spans now share the same trace as Dart events, enabling unified trace views across layers

### Dependencies

- Bump Android SDK from v8.31.0 to v8.32.0 ([#3506](https://github.com/getsentry/sentry-dart/pull/3506))
Expand Down
7 changes: 6 additions & 1 deletion packages/dart/lib/src/hub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,12 @@ class Hub {
void generateNewTrace() {
// Create a brand-new trace and reset the sampling flag and sampleRand so
// that the next root transaction can set it again.
scope.propagationContext.resetTrace();
scope.propagationContext.generateNewTrace();
// Fire-and-forget the callback
// Native SDK synchronization over async method channels may be slightly delayed, but this is not problematic in practice.
_options.lifecycleRegistry.dispatchCallback(OnGenerateNewTrace(
scope.propagationContext.traceId,
getSpan()?.context.spanId ?? SpanId.newId()));
}

/// Gets the current active transaction or span.
Expand Down
2 changes: 1 addition & 1 deletion packages/dart/lib/src/propagation_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class PropagationContext {
double? sampleRand;

/// Starts a brand-new trace (new ID, new sampling value & sampled state).
void resetTrace() {
void generateNewTrace() {
traceId = SentryId.newId();
sampleRand = null;
_sampled = null;
Expand Down
9 changes: 9 additions & 0 deletions packages/dart/lib/src/sdk_lifecycle_hooks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,12 @@ class OnProcessMetric extends SdkLifecycleEvent {

OnProcessMetric(this.metric);
}

/// Dispatched when a new trace is generated via [Hub.generateNewTrace].
@internal
class OnGenerateNewTrace extends SdkLifecycleEvent {
final SentryId traceId;
final SpanId spanId;

OnGenerateNewTrace(this.traceId, this.spanId);
}
62 changes: 62 additions & 0 deletions packages/dart/test/hub_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,68 @@ void main() {
final newSampleRand = hub.scope.propagationContext.sampleRand;
expect(newSampleRand, isNull);
});

test('generateNewTrace dispatches OnTraceReset with traceId', () {
SentryId? receivedTraceId;
hub.options.lifecycleRegistry
.registerCallback<OnGenerateNewTrace>((event) {
receivedTraceId = event.traceId;
});

hub.generateNewTrace();

expect(receivedTraceId, isNotNull);
expect(receivedTraceId, hub.scope.propagationContext.traceId);
});

test(
'generateNewTrace dispatches OnTraceReset with spanId from active span',
() {
SpanId? receivedSpanId;
hub.options.tracesSampleRate = 1.0;
hub.options.lifecycleRegistry
.registerCallback<OnGenerateNewTrace>((event) {
receivedSpanId = event.spanId;
});

hub.startTransaction('name', 'op', bindToScope: true);
hub.generateNewTrace();

expect(receivedSpanId, isNotNull);
expect(receivedSpanId, hub.getSpan()?.context.spanId);
});

test(
'generateNewTrace dispatches OnTraceReset with new spanId if no active span',
() {
SpanId? receivedSpanId;
hub.options.tracesSampleRate = 1.0;
hub.options.lifecycleRegistry
.registerCallback<OnGenerateNewTrace>((event) {
receivedSpanId = event.spanId;
});

hub.generateNewTrace();

expect(hub.getSpan(), isNull);
expect(receivedSpanId, isNotNull);
});

test(
'generateNewTrace dispatches OnTraceReset with new spanId if tracing is disabled',
() {
SpanId? receivedSpanId;
hub.options.tracesSampleRate = null;
hub.options.lifecycleRegistry
.registerCallback<OnGenerateNewTrace>((event) {
receivedSpanId = event.spanId;
});

hub.generateNewTrace();

expect(hub.getSpan(), isNull);
expect(receivedSpanId, isNotNull);
});
});

group('Hub scope callback', () {
Expand Down
2 changes: 1 addition & 1 deletion packages/dart/test/propagation_context_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ void main() {
sut.sampleRand = 1.0;
sut.applySamplingDecision(true);

sut.resetTrace();
sut.generateNewTrace();

expect(sut.traceId, isNot(traceId));
expect(sut.sampleRand, isNull);
Expand Down
38 changes: 38 additions & 0 deletions packages/flutter/example/integration_test/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ void main() {
(p) => p.name == package.name && p.version == package.version);
expect(findMatchingPackage, isNotNull);
}
expect(androidOptions.isEnableAutoTraceIdGeneration(), isFalse);
final androidProxy = androidOptions.getProxy();
expect(androidProxy, isNotNull);
expect(androidProxy!.getHost()?.toDartString(), 'proxy.local');
Expand Down Expand Up @@ -1065,6 +1066,43 @@ void main() {
}
});

// We currently only test this on Android
// Setting up iOS for testing this is a big time effort so we rely on manually testing there for now
testWidgets('setTrace syncs Dart traceId to native Android scope',
(tester) async {
await restoreFlutterOnErrorAfter(() async {
await setupSentryAndApp(tester);
});

final dartTraceId =
Sentry.currentHub.scope.propagationContext.traceId.toString();

final traceParent = jni.Sentry.getTraceparent();
expect(traceParent, isNotNull,
reason: 'Native traceparent should not be null');
final traceHeader = traceParent!.getValue().toDartString();

final nativeTraceId = traceHeader.split('-').first;
expect(nativeTraceId, dartTraceId,
reason: 'Native traceId should match Dart traceId after initial sync');

Sentry.currentHub.generateNewTrace();
final newDartTraceId =
Sentry.currentHub.scope.propagationContext.traceId.toString();

// Allow the fire-and-forget dispatch to complete
await Future<void>.delayed(const Duration(milliseconds: 100));

final newTraceParent =
jni.Sentry.getTraceparent()?.getValue().toDartString();
final newTraceHeader = newTraceParent!.toString();

final newNativeTraceId = newTraceHeader.split('-').first;
expect(newNativeTraceId, newDartTraceId,
reason:
'Native traceId should match new Dart traceId after generateNewTrace');
}, skip: !Platform.isAndroid);

group('e2e', () {
var output = find.byKey(const Key('output'));
late Fixture fixture;
Expand Down
1 change: 1 addition & 0 deletions packages/flutter/ffi-jni.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ classes:
- io.sentry.protocol.SentryPackage
- io.sentry.rrweb.RRWebOptionsEvent
- io.sentry.rrweb.RRWebEvent
- io.sentry.SentryTraceHeader
- java.net.Proxy
- android.graphics.Bitmap
- android.content.Context
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
result(nil)
#endif

case "setTrace":
let arguments = call.arguments as? [String: Any?]
let traceId = arguments?["traceId"] as? String
let spanId = arguments?["spanId"] as? String
setTrace(traceId: traceId, spanId: spanId, result: result)

default:
result(FlutterMethodNotImplemented)
}
Expand Down Expand Up @@ -696,6 +702,18 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin {
result("")
}

private func setTrace(traceId: String?, spanId: String?, result: @escaping FlutterResult) {
guard let traceId = traceId, let spanId = spanId else {
print("Cannot set trace: traceId or spanId is null")
result(FlutterError(code: "10", message: "Cannot set trace: traceId or spanId is null", details: nil))
return
}
let sentryTraceId = SentryId(uuidString: traceId)
let sentrySpanId = SpanId(value: spanId)
PrivateSentrySDKOnly.setTrace(sentryTraceId, spanId: sentrySpanId)
result("")
}

private func crash() {
SentrySDK.crash()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// ignore_for_file: invalid_use_of_internal_member

import 'dart:async';

import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
import '../native/sentry_native_binding.dart';
import '../utils/internal_logger.dart';

@internal
class NativeTraceSyncIntegration implements Integration<SentryFlutterOptions> {
static const integrationName = 'NativeTraceSync';
final SentryNativeBinding _native;
SentryOptions? _options;

NativeTraceSyncIntegration(this._native);

@override
void call(Hub hub, SentryFlutterOptions options) {
if (!options.enableNativeTraceSync) {
internalLogger.info('$integrationName: disabled, skipping setup');
return;
}

_options = options;
options.lifecycleRegistry
.registerCallback<OnGenerateNewTrace>(_syncTraceToNative);
options.sdk.addIntegration(integrationName);

final traceId = hub.scope.propagationContext.traceId;
final spanId = hub.getSpan()?.context.spanId ?? SpanId.newId();

// Sync the initial PropagationContext created at Hub construction.
_syncTraceToNative(OnGenerateNewTrace(traceId, spanId));
}

@override
void close() {
_options?.lifecycleRegistry
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do other integrations nil options here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yup, would you set it as late instead?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

no preference really, just to be consistent in cleanup like we do elsewhere

.removeCallback<OnGenerateNewTrace>(_syncTraceToNative);
}

FutureOr<void> _syncTraceToNative(OnGenerateNewTrace event) =>
_native.setTrace(event.traceId, event.spanId);
}
8 changes: 8 additions & 0 deletions packages/flutter/lib/src/native/c/sentry_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,14 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding {
FutureOr<void> updateSession({int? errors, String? status}) {
_logNotSupported('updating session');
}

@override
bool get supportsTraceSync => false;

@override
FutureOr<void> setTrace(SentryId traceId, SpanId spanId) {
_logNotSupported('setting trace');
}
}

extension on binding.sentry_value_u {
Expand Down
Loading
Loading