Skip to content
Open
Show file tree
Hide file tree
Changes from 19 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
### Enhancements

- Prefix firebase remote config feature flags with `firebase:` ([#3258](https://github.com/getsentry/sentry-dart/pull/3258))
- Add `sentry.replay_id` to flutter logs ([#3257](https://github.com/getsentry/sentry-dart/pull/3257))
- Replay: continue processing if encountering `InheritedWidget` ([#3200](https://github.com/getsentry/sentry-dart/pull/3200))
- Prevents false debug warnings when using [provider](https://pub.dev/packages/provider) for example which extensively uses `InheritedWidget`
- Add `DioException` response data to error breadcrumb ([#3164](https://github.com/getsentry/sentry-dart/pull/3164))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import io.flutter.plugin.common.MethodChannel
import io.sentry.Sentry
import io.sentry.android.replay.Recorder
import io.sentry.android.replay.ReplayIntegration
import io.sentry.android.replay.ScreenshotRecorderConfig
Expand All @@ -15,10 +16,17 @@ internal class SentryFlutterReplayRecorder(
override fun start() {
Handler(Looper.getMainLooper()).post {
try {
val replayId = integration.getReplayId().toString()
var replayIsBuffering = false
Sentry.configureScope { scope ->
// Buffering mode: we have a replay ID but it's not set on scope yet
replayIsBuffering = replayId != null && scope.replayId == null
}
channel.invokeMethod(
"ReplayRecorder.start",
mapOf(
"replayId" to integration.getReplayId().toString(),
"replayId" to replayId,
"replayIsBuffering" to replayIsBuffering,
),
)
} catch (ignored: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@ - (void)imageWithView:(UIView *_Nonnull)view
// Replay ID may be null if session replay is disabled.
// Replay is still captured for on-error replays.
NSString *replayId = [PrivateSentrySDKOnly getReplayId];
// On iOS, we only have access to scope's replay ID, so we cannot detect buffer mode
// If replay ID exists, it's always in active session mode (not buffering)
BOOL replayIsBuffering = NO;
[self->channel
invokeMethod:@"captureReplayScreenshot"
arguments:@{@"replayId" : replayId ? replayId : [NSNull null]}
arguments:@{
@"replayId" : replayId ? replayId : [NSNull null],
@"replayIsBuffering" : @(replayIsBuffering)
}
result:^(id value) {
if (value == nil) {
NSLog(@"SentryFlutterReplayScreenshotProvider received null "
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// ignore_for_file: invalid_use_of_internal_member

import 'package:sentry/sentry.dart';
import '../sentry_flutter_options.dart';
import '../native/sentry_native_binding.dart';

/// Integration that adds replay-related information to logs using lifecycle callbacks
class ReplayLogIntegration implements Integration<SentryFlutterOptions> {
static const String integrationName = 'ReplayLog';

final SentryNativeBinding? _native;
ReplayLogIntegration(this._native);

SentryFlutterOptions? _options;
SdkLifecycleCallback<OnBeforeCaptureLog>? _addReplayInformation;

@override
Future<void> call(Hub hub, SentryFlutterOptions options) async {
if (!options.replay.isEnabled) {
return;
}
final sessionSampleRate = options.replay.sessionSampleRate ?? 0;
final onErrorSampleRate = options.replay.onErrorSampleRate ?? 0;

_options = options;
_addReplayInformation = (OnBeforeCaptureLog event) {
final scopeReplayId = hub.scope.replayId;
final replayId = scopeReplayId ?? _native?.replayId;
final replayIsBuffering = replayId != null && scopeReplayId == null;

if (sessionSampleRate > 0 && replayId != null && !replayIsBuffering) {
event.log.attributes['sentry.replay_id'] = SentryLogAttribute.string(
scopeReplayId.toString(),
);
} else if (onErrorSampleRate > 0 &&
replayId != null &&
replayIsBuffering) {
event.log.attributes['sentry.replay_id'] = SentryLogAttribute.string(
replayId.toString(),
);
event.log.attributes['sentry._internal.replay_is_buffering'] =
SentryLogAttribute.bool(true);
}
Comment on lines +26 to +43
Copy link
Contributor

Choose a reason for hiding this comment

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

should we add a little note that replayIsBuffering will always result to false for Cocoa until we get the chance to update the Cocoa SDK?

Copy link
Contributor

@buenaflor buenaflor Oct 16, 2025

Choose a reason for hiding this comment

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

or at least we can create a new issue for it here

};
options.lifecycleRegistry
.registerCallback<OnBeforeCaptureLog>(_addReplayInformation!);
options.sdk.addIntegration(integrationName);
}

@override
Future<void> close() async {
final options = _options;
final addReplayInformation = _addReplayInformation;

if (options != null && addReplayInformation != null) {
options.lifecycleRegistry
.removeCallback<OnBeforeCaptureLog>(addReplayInformation);
}

_options = null;
_addReplayInformation = null;
}
}
3 changes: 3 additions & 0 deletions packages/flutter/lib/src/native/c/sentry_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding {
@override
bool get supportsReplay => false;

@override
SentryId? get replayId => null;

@override
FutureOr<void> setReplayConfig(ReplayConfig config) {
_logNotSupported('replay config');
Expand Down
20 changes: 17 additions & 3 deletions packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
@override
bool get supportsReplay => options.platform.isIOS;

@override
SentryId? get replayId => _replayId;

@override
Future<void> init(Hub hub) async {
// We only need these when replay is enabled (session or error capture)
Expand All @@ -32,15 +35,20 @@
case 'captureReplayScreenshot':
_replayRecorder ??= CocoaReplayRecorder(options);

final replayId = call.arguments['replayId'] == null
final replayIdArg = call.arguments['replayId'];
final replayIsBuffering =
call.arguments['replayIsBuffering'] as bool? ?? false;

final replayId = replayIdArg == null
? null
: SentryId.fromId(call.arguments['replayId'] as String);
: SentryId.fromId(replayIdArg as String);

if (_replayId != replayId) {
_replayId = replayId;
hub.configureScope((s) {
// Only set replay ID on scope if not buffering (active session mode)
// ignore: invalid_use_of_internal_member
s.replayId = replayId;
s.replayId = !replayIsBuffering ? replayId : null;
});
}

Expand All @@ -58,7 +66,13 @@
}

@override
FutureOr<SentryId> captureReplay() async {
final replayId = await super.captureReplay();
_replayId = replayId;
return replayId;
}

Future<void> close() async {

Check notice on line 75 in packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

The member 'close' overrides an inherited member but isn't annotated with '@override'.

Try adding the '@OverRide' annotation. See https://dart.dev/diagnostics/annotate_overrides to learn more about this problem.
await _envelopeSender?.close();
return super.close();
}
Expand Down
24 changes: 21 additions & 3 deletions packages/flutter/lib/src/native/java/sentry_native_java.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class SentryNativeJava extends SentryNativeChannel {
@override
bool get supportsReplay => true;

@override
SentryId? get replayId => _replayId;
SentryId? _replayId;

@override
Future<void> init(Hub hub) async {
// We only need these when replay is enabled (session or error capture)
Expand All @@ -30,14 +34,21 @@ class SentryNativeJava extends SentryNativeChannel {
channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'ReplayRecorder.start':
final replayId =
SentryId.fromId(call.arguments['replayId'] as String);
final replayIdArg = call.arguments['replayId'];
final replayIsBuffering =
call.arguments['replayIsBuffering'] as bool? ?? false;

final replayId = replayIdArg != null
? SentryId.fromId(replayIdArg as String)
: null;
_replayId = replayId;

_replayRecorder = AndroidReplayRecorder.factory(options);
await _replayRecorder!.start();
hub.configureScope((s) {
// Only set replay ID on scope if not buffering (active session mode)
// ignore: invalid_use_of_internal_member
s.replayId = replayId;
s.replayId = !replayIsBuffering ? replayId : null;
});
break;
case 'ReplayRecorder.onConfigurationChanged':
Expand Down Expand Up @@ -80,6 +91,13 @@ class SentryNativeJava extends SentryNativeChannel {
return super.init(hub);
}

@override
FutureOr<SentryId> captureReplay() async {
final replayId = await super.captureReplay();
_replayId = replayId;
return replayId;
}

@override
FutureOr<void> captureEnvelope(
Uint8List envelopeData, bool containsUnhandledException) {
Expand Down
2 changes: 2 additions & 0 deletions packages/flutter/lib/src/native/sentry_native_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ abstract class SentryNativeBinding {

bool get supportsReplay;

SentryId? get replayId;

FutureOr<void> setReplayConfig(ReplayConfig config);

FutureOr<SentryId> captureReplay();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ class SentryNativeChannel
@override
bool get supportsReplay => false;

@override
SentryId? get replayId => null;

@override
FutureOr<void> setReplayConfig(ReplayConfig config) =>
channel.invokeMethod('setReplayConfig', {
Expand All @@ -251,7 +254,7 @@ class SentryNativeChannel
});

@override
Future<SentryId> captureReplay() => channel
FutureOr<SentryId> captureReplay() => channel
.invokeMethod('captureReplay')
.then((value) => SentryId.fromId(value as String));

Expand Down
6 changes: 6 additions & 0 deletions packages/flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'integrations/flutter_framework_feature_flag_integration.dart';
import 'integrations/frames_tracking_integration.dart';
import 'integrations/integrations.dart';
import 'integrations/native_app_start_handler.dart';
import 'integrations/replay_log_integration.dart';
import 'integrations/screenshot_integration.dart';
import 'integrations/generic_app_start_integration.dart';
import 'integrations/thread_info_integration.dart';
Expand Down Expand Up @@ -230,6 +231,11 @@ mixin SentryFlutter {

integrations.add(DebugPrintIntegration());

// Only add ReplayLogIntegration on platforms that support replay
if (native != null && native.supportsReplay) {
integrations.add(ReplayLogIntegration(native));
}

if (!platform.isWeb) {
integrations.add(ThreadInfoIntegration());
}
Expand Down
3 changes: 3 additions & 0 deletions packages/flutter/lib/src/web/sentry_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ class SentryWeb with SentryNativeSafeInvoker implements SentryNativeBinding {
@override
bool get supportsReplay => false;

@override
SentryId? get replayId => null;

@override
SentryFlutterOptions get options => _options;
}
Loading
Loading