diff --git a/dart/lib/src/protocol/sentry_trace_context.dart b/dart/lib/src/protocol/sentry_trace_context.dart index 25c4ca7ad8..2a9c3bb2fc 100644 --- a/dart/lib/src/protocol/sentry_trace_context.dart +++ b/dart/lib/src/protocol/sentry_trace_context.dart @@ -17,6 +17,9 @@ class SentryTraceContext { /// Id of a parent span final SpanId? parentSpanId; + /// Replay associated with this trace. + final SentryId? replayId; + /// Whether the span is sampled or not final bool? sampled; @@ -45,6 +48,9 @@ class SentryTraceContext { ? null : SpanId.fromId(json['parent_span_id'] as String), traceId: SentryId.fromId(json['trace_id'] as String), + replayId: json['replay_id'] == null + ? null + : SentryId.fromId(json['replay_id'] as String), description: json['description'] as String?, status: json['status'] == null ? null @@ -61,6 +67,7 @@ class SentryTraceContext { 'trace_id': traceId.toString(), 'op': operation, if (parentSpanId != null) 'parent_span_id': parentSpanId!.toString(), + if (replayId != null) 'replay_id': replayId!.toString(), if (description != null) 'description': description, if (status != null) 'status': status!.toString(), if (origin != null) 'origin': origin, @@ -76,6 +83,7 @@ class SentryTraceContext { parentSpanId: parentSpanId, sampled: sampled, origin: origin, + replayId: replayId, ); SentryTraceContext({ @@ -87,6 +95,7 @@ class SentryTraceContext { this.description, this.status, this.origin, + this.replayId, }) : traceId = traceId ?? SentryId.newId(), spanId = spanId ?? SpanId.newId(); @@ -94,9 +103,9 @@ class SentryTraceContext { factory SentryTraceContext.fromPropagationContext( PropagationContext propagationContext) { return SentryTraceContext( - traceId: propagationContext.traceId, - spanId: propagationContext.spanId, - operation: 'default', - ); + traceId: propagationContext.traceId, + spanId: propagationContext.spanId, + operation: 'default', + replayId: propagationContext.baggage?.getReplayId()); } } diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 3fef9a92a2..a4b6ecf79a 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -97,6 +97,16 @@ class Scope { /// they must be JSON-serializable. Map get extra => Map.unmodifiable(_extra); + SentryId? _replayId; + + /// Get the active replay recording. + @internal + SentryId? get replayId => _replayId; + + /// Set the active replay recording id. + @internal + set replayId(SentryId? value) => _replayId = value; + final Contexts _contexts = Contexts(); /// Unmodifiable map of the scope contexts key/value @@ -237,6 +247,7 @@ class Scope { _tags.clear(); _extra.clear(); _eventProcessors.clear(); + _replayId = null; _clearBreadcrumbsSync(); _setUserSync(null); @@ -425,7 +436,8 @@ class Scope { ..fingerprint = List.from(fingerprint) .._transaction = _transaction ..span = span - .._enableScopeSync = false; + .._enableScopeSync = false + .._replayId = _replayId; clone._setUserSync(user); diff --git a/dart/lib/src/sentry_baggage.dart b/dart/lib/src/sentry_baggage.dart index 25aab900f4..3ae6a2a2ac 100644 --- a/dart/lib/src/sentry_baggage.dart +++ b/dart/lib/src/sentry_baggage.dart @@ -109,6 +109,9 @@ class SentryBaggage { if (scope.user?.segment != null) { setUserSegment(scope.user!.segment!); } + if (scope.replayId != null && scope.replayId != SentryId.empty()) { + setReplayId(scope.replayId.toString()); + } } static Map _extractKeyValuesFromBaggageString( @@ -201,5 +204,12 @@ class SentryBaggage { return double.tryParse(sampleRate); } + void setReplayId(String value) => set('sentry-replay_id', value); + + SentryId? getReplayId() { + final replayId = get('sentry-replay_id'); + return replayId == null ? null : SentryId.fromId(replayId); + } + Map get keyValues => Map.unmodifiable(_keyValues); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 59610993ad..757e976d2d 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -142,15 +142,15 @@ class SentryClient { var traceContext = scope?.span?.traceContext(); if (traceContext == null) { - if (scope?.propagationContext.baggage == null) { - scope?.propagationContext.baggage = - SentryBaggage({}, logger: _options.logger); - scope?.propagationContext.baggage?.setValuesFromScope(scope, _options); - } if (scope != null) { + scope.propagationContext.baggage ??= + SentryBaggage({}, logger: _options.logger) + ..setValuesFromScope(scope, _options); traceContext = SentryTraceContextHeader.fromBaggage( scope.propagationContext.baggage!); } + } else { + traceContext.replayId = scope?.replayId; } final envelope = SentryEnvelope.fromEvent( diff --git a/dart/lib/src/sentry_trace_context_header.dart b/dart/lib/src/sentry_trace_context_header.dart index bcb1d0b1bb..11186990cb 100644 --- a/dart/lib/src/sentry_trace_context_header.dart +++ b/dart/lib/src/sentry_trace_context_header.dart @@ -1,3 +1,5 @@ +import 'package:meta/meta.dart'; + import 'protocol/sentry_id.dart'; import 'sentry_baggage.dart'; import 'sentry_options.dart'; @@ -13,6 +15,7 @@ class SentryTraceContextHeader { this.transaction, this.sampleRate, this.sampled, + this.replayId, }); final SentryId traceId; @@ -25,6 +28,9 @@ class SentryTraceContextHeader { final String? sampleRate; final String? sampled; + @internal + SentryId? replayId; + /// Deserializes a [SentryTraceContextHeader] from JSON [Map]. factory SentryTraceContextHeader.fromJson(Map json) { return SentryTraceContextHeader( @@ -37,6 +43,7 @@ class SentryTraceContextHeader { transaction: json['transaction'], sampleRate: json['sample_rate'], sampled: json['sampled'], + replayId: json['replay_id'], ); } @@ -52,6 +59,7 @@ class SentryTraceContextHeader { if (transaction != null) 'transaction': transaction, if (sampleRate != null) 'sample_rate': sampleRate, if (sampled != null) 'sampled': sampled, + if (replayId != null) 'replay_id': replayId.toString(), }; } @@ -83,6 +91,9 @@ class SentryTraceContextHeader { if (sampled != null) { baggage.setSampled(sampled!); } + if (replayId != null) { + baggage.setReplayId(replayId.toString()); + } return baggage; } @@ -92,6 +103,7 @@ class SentryTraceContextHeader { baggage.get('sentry-public_key').toString(), release: baggage.get('sentry-release'), environment: baggage.get('sentry-environment'), + replayId: baggage.getReplayId(), ); } } diff --git a/dart/test/protocol/sentry_baggage_header_test.dart b/dart/test/protocol/sentry_baggage_header_test.dart index 38428be41a..cb4f0be6bf 100644 --- a/dart/test/protocol/sentry_baggage_header_test.dart +++ b/dart/test/protocol/sentry_baggage_header_test.dart @@ -21,11 +21,23 @@ void main() { baggage.setTransaction('transaction'); baggage.setSampleRate('1.0'); baggage.setSampled('false'); + final replayId = SentryId.newId().toString(); + baggage.setReplayId(replayId); final baggageHeader = SentryBaggageHeader.fromBaggage(baggage); - expect(baggageHeader.value, - 'sentry-trace_id=$id,sentry-public_key=publicKey,sentry-release=release,sentry-environment=environment,sentry-user_id=userId,sentry-user_segment=userSegment,sentry-transaction=transaction,sentry-sample_rate=1.0,sentry-sampled=false'); + expect( + baggageHeader.value, + 'sentry-trace_id=$id,' + 'sentry-public_key=publicKey,' + 'sentry-release=release,' + 'sentry-environment=environment,' + 'sentry-user_id=userId,' + 'sentry-user_segment=userSegment,' + 'sentry-transaction=transaction,' + 'sentry-sample_rate=1.0,' + 'sentry-sampled=false,' + 'sentry-replay_id=$replayId'); }); }); } diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt index 3d57b12d77..453f138cdb 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt @@ -27,6 +27,7 @@ internal class SentryFlutterReplayRecorder( "width" to config.recordingWidth, "height" to config.recordingHeight, "frameRate" to config.frameRate, + "replayId" to integration.getReplayId().toString() ), ) } catch (ignored: Exception) { diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 0fa822ff54..7744a884e7 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -91,6 +91,7 @@ Future setupSentry( options.navigatorKey = navigatorKey; options.experimental.replay.sessionSampleRate = 1.0; + options.experimental.replay.errorSampleRate = 1.0; _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { diff --git a/flutter/lib/src/event_processor/replay_event_processor.dart b/flutter/lib/src/event_processor/replay_event_processor.dart index 65ed67141c..4be68a4d00 100644 --- a/flutter/lib/src/event_processor/replay_event_processor.dart +++ b/flutter/lib/src/event_processor/replay_event_processor.dart @@ -13,11 +13,9 @@ class ReplayEventProcessor implements EventProcessor { Future apply(SentryEvent event, Hint hint) async { if (event.eventId != SentryId.empty() && event.exceptions?.isNotEmpty == true) { - final isCrash = event.exceptions! - .any((element) => element.mechanism?.handled == false); - // ignore: unused_local_variable - final replayId = - await _binding.sendReplayForEvent(event.eventId, isCrash); + final isCrash = + event.exceptions!.any((e) => e.mechanism?.handled == false); + await _binding.sendReplayForEvent(event.eventId, isCrash); } return event; } diff --git a/flutter/lib/src/native/java/sentry_native_java.dart b/flutter/lib/src/native/java/sentry_native_java.dart index 241957e5d7..5cdcbc3ed3 100644 --- a/flutter/lib/src/native/java/sentry_native_java.dart +++ b/flutter/lib/src/native/java/sentry_native_java.dart @@ -33,6 +33,9 @@ class SentryNativeJava extends SentryNativeChannel { channel.setMethodCallHandler((call) async { switch (call.method) { case 'ReplayRecorder.start': + final replayId = + SentryId.fromId(call.arguments['replayId'] as String); + _startRecorder( call.arguments['directory'] as String, ScreenshotRecorderConfig( @@ -41,10 +44,22 @@ class SentryNativeJava extends SentryNativeChannel { frameRate: call.arguments['frameRate'] as int, ), ); + + Sentry.configureScope((s) { + // ignore: invalid_use_of_internal_member + s.replayId = replayId; + }); + break; case 'ReplayRecorder.stop': await _replayRecorder?.stop(); _replayRecorder = null; + + Sentry.configureScope((s) { + // ignore: invalid_use_of_internal_member + s.replayId = null; + }); + break; case 'ReplayRecorder.pause': await _replayRecorder?.stop();