Skip to content
Open
Show file tree
Hide file tree
Changes from 17 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 @@ -15,6 +15,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))
Copy link
Contributor

Choose a reason for hiding this comment

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

  • 🚫 The changelog entry seems to be part of an already released section ## 9.7.0.
    Consider moving the entry to the ## Unreleased section, please.

- 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,9 +16,14 @@ internal class SentryFlutterReplayRecorder(
override fun start() {
Handler(Looper.getMainLooper()).post {
try {
var scopeReplayId: String? = null
Sentry.configureScope { scope ->
scopeReplayId = scope.replayId?.toString()
}
channel.invokeMethod(
"ReplayRecorder.start",
mapOf(
"scope.replayId" to scopeReplayId,
"replayId" to integration.getReplayId().toString(),
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ - (void)imageWithView:(UIView *_Nonnull)view
onComplete:(void (^_Nonnull)(UIImage *_Nonnull))onComplete {
// Replay ID may be null if session replay is disabled.
// Replay is still captured for on-error replays.
NSString *replayId = [PrivateSentrySDKOnly getReplayId];
NSString *scopeReplayId = [PrivateSentrySDKOnly getReplayId];
[self->channel
invokeMethod:@"captureReplayScreenshot"
arguments:@{@"replayId" : replayId ? replayId : [NSNull null]}
arguments:@{@"scope.replayId" : scopeReplayId ? scopeReplayId : [NSNull null]}
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,60 @@
// 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 = _native?.replayId;

if (sessionSampleRate > 0 && scopeReplayId != null) {
event.log.attributes['sentry.replay_id'] = SentryLogAttribute.string(
scopeReplayId.toString(),
);
} else if (onErrorSampleRate > 0 && replayId != null) {
event.log.attributes['sentry.replay_id'] = SentryLogAttribute.string(
replayId.toString(),
);
event.log.attributes['sentry._internal.replay_is_buffering'] =
SentryLogAttribute.bool(true);
}
};
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
22 changes: 17 additions & 5 deletions packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class SentryNativeCocoa extends SentryNativeChannel {
@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 @@ -30,15 +33,17 @@ class SentryNativeCocoa extends SentryNativeChannel {
case 'captureReplayScreenshot':
_replayRecorder ??= CocoaReplayRecorder(options);

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

// TODO: We need the replayId from cocoa integration, so we can determine if we are in buffer mode.

if (_replayId != replayId) {
_replayId = replayId;
if (_replayId != scopeReplayId) {
_replayId = scopeReplayId;
hub.configureScope((s) {
// ignore: invalid_use_of_internal_member
s.replayId = replayId;
s.replayId = scopeReplayId;
});
}

Expand All @@ -52,6 +57,13 @@ class SentryNativeCocoa 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
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 @@ -19,6 +19,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 @@ -27,14 +31,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 scopeReplayIdArg = call.arguments['scope.replayId'];
final replayIdArg = call.arguments['replayId'];

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

_replayRecorder = AndroidReplayRecorder.factory(options);
await _replayRecorder!.start();
hub.configureScope((s) {
// ignore: invalid_use_of_internal_member
s.replayId = replayId;
s.replayId = scopeReplayId;
});
break;
case 'ReplayRecorder.onConfigurationChanged':
Expand Down Expand Up @@ -74,6 +85,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