From d79f8f97917c668dd26d735ed62675ab16d7cf96 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 31 Oct 2025 08:54:19 +0000 Subject: [PATCH 01/27] Add support for captureReplay --- packages/flutter/ffi-jni.yaml | 1 + .../sentry_flutter/SentryFlutterPlugin.swift | 17 +- .../sentry_flutter_objc/SentryFlutterPlugin.h | 1 + .../flutter/lib/src/native/cocoa/binding.dart | 10 + .../src/native/cocoa/sentry_native_cocoa.dart | 22 +- .../flutter/lib/src/native/java/binding.dart | 747 +++++++++++++++++- .../src/native/java/sentry_native_java.dart | 66 +- 7 files changed, 847 insertions(+), 17 deletions(-) diff --git a/packages/flutter/ffi-jni.yaml b/packages/flutter/ffi-jni.yaml index 203493f18d..c7abfaa2e5 100644 --- a/packages/flutter/ffi-jni.yaml +++ b/packages/flutter/ffi-jni.yaml @@ -15,6 +15,7 @@ log_level: all classes: - io.sentry.android.core.InternalSentrySdk - io.sentry.android.replay.ReplayIntegration + - io.sentry.android.replay.ScreenshotRecorderConfig - io.sentry.flutter.SentryFlutterPlugin - io.sentry.Sentry - io.sentry.Breadcrumb diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift index 50012639d9..723ec6397d 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -83,14 +83,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { collectProfile(call, result) #endif - case "captureReplay": -#if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS)) - PrivateSentrySDKOnly.captureReplay() - result(PrivateSentrySDKOnly.getReplayId()) -#else - result(nil) -#endif - default: result(FlutterMethodNotImplemented) } @@ -274,6 +266,15 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { // // Purpose: Called from the Flutter plugin's native bridge (FFI) - bindings are created from SentryFlutterPlugin.h + @objc public class func captureReplay() -> String? { + #if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS)) + PrivateSentrySDKOnly.captureReplay() + return PrivateSentrySDKOnly.getReplayId() + #else + return nil + #endif + } + #if os(iOS) // Taken from the Flutter engine: // https://github.com/flutter/engine/blob/main/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm#L150 diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterPlugin.h b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterPlugin.h index 6f2e25eb54..39ffdb81a0 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterPlugin.h +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter_objc/SentryFlutterPlugin.h @@ -8,5 +8,6 @@ + (nullable NSData *)fetchNativeAppStartAsBytes; + (nullable NSData *)loadContextsAsBytes; + (nullable NSData *)loadDebugImagesAsBytes:(NSSet *)instructionAddresses; ++ (nullable NSString *)captureReplay; @end #endif diff --git a/packages/flutter/lib/src/native/cocoa/binding.dart b/packages/flutter/lib/src/native/cocoa/binding.dart index 98b943ac70..b509202267 100644 --- a/packages/flutter/lib/src/native/cocoa/binding.dart +++ b/packages/flutter/lib/src/native/cocoa/binding.dart @@ -1510,6 +1510,7 @@ late final _sel_fetchNativeAppStartAsBytes = late final _sel_loadContextsAsBytes = objc.registerName("loadContextsAsBytes"); late final _sel_loadDebugImagesAsBytes_ = objc.registerName("loadDebugImagesAsBytes:"); +late final _sel_captureReplay = objc.registerName("captureReplay"); /// SentryFlutterPlugin class SentryFlutterPlugin extends objc.NSObject { @@ -1568,6 +1569,15 @@ class SentryFlutterPlugin extends objc.NSObject { : objc.NSData.castFromPointer(_ret, retain: true, release: true); } + /// captureReplay + static objc.NSString? captureReplay() { + final _ret = + _objc_msgSend_151sglz(_class_SentryFlutterPlugin, _sel_captureReplay); + return _ret.address == 0 + ? null + : objc.NSString.castFromPointer(_ret, retain: true, release: true); + } + /// init SentryFlutterPlugin init() { objc.checkOsVersionInternal('SentryFlutterPlugin.init', diff --git a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart index cdf0358389..516074033f 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -67,13 +67,6 @@ class SentryNativeCocoa extends SentryNativeChannel { return super.init(hub); } - @override - FutureOr captureReplay() async { - final replayId = await super.captureReplay(); - _replayId = replayId; - return replayId; - } - @override Future close() async { await _envelopeSender?.close(); @@ -305,6 +298,21 @@ class SentryNativeCocoa extends SentryNativeChannel { scope.removeExtraForKey(key.toNSString()); })); }); + + @override + SentryId captureReplay() => + tryCatchSync('captureReplay', () { + final value = cocoa.SentryFlutterPlugin.captureReplay()?.toDartString(); + SentryId id; + if (value == null) { + id = SentryId.empty(); + } else { + id = SentryId.fromId(value); + } + _replayId = id; + return id; + }) ?? + SentryId.empty(); } // The default conversion does not handle bool so we will add it ourselves diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index 949bb60638..5720447220 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -1174,7 +1174,7 @@ class ReplayIntegration extends jni$_.JObject { /// from: `public final void onConfigurationChanged(io.sentry.android.replay.ScreenshotRecorderConfig screenshotRecorderConfig)` void onConfigurationChanged( - jni$_.JObject screenshotRecorderConfig, + ScreenshotRecorderConfig screenshotRecorderConfig, ) { final _$screenshotRecorderConfig = screenshotRecorderConfig.reference; _onConfigurationChanged( @@ -1261,6 +1261,751 @@ final class $ReplayIntegration$Type extends jni$_.JObjType { } } +/// from: `io.sentry.android.replay.ScreenshotRecorderConfig$Companion` +class ScreenshotRecorderConfig$Companion extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType $type; + + @jni$_.internal + ScreenshotRecorderConfig$Companion.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'io/sentry/android/replay/ScreenshotRecorderConfig$Companion'); + + /// The type which includes information such as the signature of this class. + static const nullableType = + $ScreenshotRecorderConfig$Companion$NullableType(); + static const type = $ScreenshotRecorderConfig$Companion$Type(); + static final _id_fromSize = _class.instanceMethodId( + r'fromSize', + r'(Landroid/content/Context;Lio/sentry/SentryReplayOptions;II)Lio/sentry/android/replay/ScreenshotRecorderConfig;', + ); + + static final _fromSize = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int32, + jni$_.Int32 + )>)>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + int, + int)>(); + + /// from: `public final io.sentry.android.replay.ScreenshotRecorderConfig fromSize(android.content.Context context, io.sentry.SentryReplayOptions sentryReplayOptions, int i, int i1)` + /// The returned object must be released after use, by calling the [release] method. + ScreenshotRecorderConfig fromSize( + jni$_.JObject context, + jni$_.JObject sentryReplayOptions, + int i, + int i1, + ) { + final _$context = context.reference; + final _$sentryReplayOptions = sentryReplayOptions.reference; + return _fromSize(reference.pointer, _id_fromSize as jni$_.JMethodIDPtr, + _$context.pointer, _$sentryReplayOptions.pointer, i, i1) + .object( + const $ScreenshotRecorderConfig$Type()); + } + + static final _id_new$ = _class.constructorId( + r'(Lkotlin/jvm/internal/DefaultConstructorMarker;)V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `synthetic public void (kotlin.jvm.internal.DefaultConstructorMarker defaultConstructorMarker)` + /// The returned object must be released after use, by calling the [release] method. + factory ScreenshotRecorderConfig$Companion( + jni$_.JObject? defaultConstructorMarker, + ) { + final _$defaultConstructorMarker = + defaultConstructorMarker?.reference ?? jni$_.jNullReference; + return ScreenshotRecorderConfig$Companion.fromReference(_new$( + _class.reference.pointer, + _id_new$ as jni$_.JMethodIDPtr, + _$defaultConstructorMarker.pointer) + .reference); + } +} + +final class $ScreenshotRecorderConfig$Companion$NullableType + extends jni$_.JObjType { + @jni$_.internal + const $ScreenshotRecorderConfig$Companion$NullableType(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lio/sentry/android/replay/ScreenshotRecorderConfig$Companion;'; + + @jni$_.internal + @core$_.override + ScreenshotRecorderConfig$Companion? fromReference( + jni$_.JReference reference) => + reference.isNull + ? null + : ScreenshotRecorderConfig$Companion.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => + ($ScreenshotRecorderConfig$Companion$NullableType).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == + ($ScreenshotRecorderConfig$Companion$NullableType) && + other is $ScreenshotRecorderConfig$Companion$NullableType; + } +} + +final class $ScreenshotRecorderConfig$Companion$Type + extends jni$_.JObjType { + @jni$_.internal + const $ScreenshotRecorderConfig$Companion$Type(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lio/sentry/android/replay/ScreenshotRecorderConfig$Companion;'; + + @jni$_.internal + @core$_.override + ScreenshotRecorderConfig$Companion fromReference( + jni$_.JReference reference) => + ScreenshotRecorderConfig$Companion.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => + const $ScreenshotRecorderConfig$Companion$NullableType(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ScreenshotRecorderConfig$Companion$Type).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ScreenshotRecorderConfig$Companion$Type) && + other is $ScreenshotRecorderConfig$Companion$Type; + } +} + +/// from: `io.sentry.android.replay.ScreenshotRecorderConfig` +class ScreenshotRecorderConfig extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType $type; + + @jni$_.internal + ScreenshotRecorderConfig.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'io/sentry/android/replay/ScreenshotRecorderConfig'); + + /// The type which includes information such as the signature of this class. + static const nullableType = $ScreenshotRecorderConfig$NullableType(); + static const type = $ScreenshotRecorderConfig$Type(); + static final _id_Companion = _class.staticFieldId( + r'Companion', + r'Lio/sentry/android/replay/ScreenshotRecorderConfig$Companion;', + ); + + /// from: `static public final io.sentry.android.replay.ScreenshotRecorderConfig$Companion Companion` + /// The returned object must be released after use, by calling the [release] method. + static ScreenshotRecorderConfig$Companion get Companion => _id_Companion.get( + _class, const $ScreenshotRecorderConfig$Companion$Type()); + + static final _id_new$ = _class.constructorId( + r'(IIFFII)V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Int32, + jni$_.Int32, + jni$_.Double, + jni$_.Double, + jni$_.Int32, + jni$_.Int32 + )>)>>('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, int, int, double, double, int, int)>(); + + /// from: `public void (int i, int i1, float f, float f1, int i2, int i3)` + /// The returned object must be released after use, by calling the [release] method. + factory ScreenshotRecorderConfig( + int i, + int i1, + double f, + double f1, + int i2, + int i3, + ) { + return ScreenshotRecorderConfig.fromReference(_new$( + _class.reference.pointer, + _id_new$ as jni$_.JMethodIDPtr, + i, + i1, + f, + f1, + i2, + i3) + .reference); + } + + static final _id_getRecordingWidth = _class.instanceMethodId( + r'getRecordingWidth', + r'()I', + ); + + static final _getRecordingWidth = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final int getRecordingWidth()` + int getRecordingWidth() { + return _getRecordingWidth( + reference.pointer, _id_getRecordingWidth as jni$_.JMethodIDPtr) + .integer; + } + + static final _id_getRecordingHeight = _class.instanceMethodId( + r'getRecordingHeight', + r'()I', + ); + + static final _getRecordingHeight = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final int getRecordingHeight()` + int getRecordingHeight() { + return _getRecordingHeight( + reference.pointer, _id_getRecordingHeight as jni$_.JMethodIDPtr) + .integer; + } + + static final _id_getScaleFactorX = _class.instanceMethodId( + r'getScaleFactorX', + r'()F', + ); + + static final _getScaleFactorX = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallFloatMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final float getScaleFactorX()` + double getScaleFactorX() { + return _getScaleFactorX( + reference.pointer, _id_getScaleFactorX as jni$_.JMethodIDPtr) + .float; + } + + static final _id_getScaleFactorY = _class.instanceMethodId( + r'getScaleFactorY', + r'()F', + ); + + static final _getScaleFactorY = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallFloatMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final float getScaleFactorY()` + double getScaleFactorY() { + return _getScaleFactorY( + reference.pointer, _id_getScaleFactorY as jni$_.JMethodIDPtr) + .float; + } + + static final _id_getFrameRate = _class.instanceMethodId( + r'getFrameRate', + r'()I', + ); + + static final _getFrameRate = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final int getFrameRate()` + int getFrameRate() { + return _getFrameRate( + reference.pointer, _id_getFrameRate as jni$_.JMethodIDPtr) + .integer; + } + + static final _id_getBitRate = _class.instanceMethodId( + r'getBitRate', + r'()I', + ); + + static final _getBitRate = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final int getBitRate()` + int getBitRate() { + return _getBitRate(reference.pointer, _id_getBitRate as jni$_.JMethodIDPtr) + .integer; + } + + static final _id_new$1 = _class.constructorId( + r'(FF)V', + ); + + static final _new$1 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Double, jni$_.Double)>)>>( + 'globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, jni$_.JMethodIDPtr, double, double)>(); + + /// from: `public void (float f, float f1)` + /// The returned object must be released after use, by calling the [release] method. + factory ScreenshotRecorderConfig.new$1( + double f, + double f1, + ) { + return ScreenshotRecorderConfig.fromReference( + _new$1(_class.reference.pointer, _id_new$1 as jni$_.JMethodIDPtr, f, f1) + .reference); + } + + static final _id_component1 = _class.instanceMethodId( + r'component1', + r'()I', + ); + + static final _component1 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final int component1()` + int component1() { + return _component1(reference.pointer, _id_component1 as jni$_.JMethodIDPtr) + .integer; + } + + static final _id_component2 = _class.instanceMethodId( + r'component2', + r'()I', + ); + + static final _component2 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final int component2()` + int component2() { + return _component2(reference.pointer, _id_component2 as jni$_.JMethodIDPtr) + .integer; + } + + static final _id_component3 = _class.instanceMethodId( + r'component3', + r'()F', + ); + + static final _component3 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallFloatMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final float component3()` + double component3() { + return _component3(reference.pointer, _id_component3 as jni$_.JMethodIDPtr) + .float; + } + + static final _id_component4 = _class.instanceMethodId( + r'component4', + r'()F', + ); + + static final _component4 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallFloatMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final float component4()` + double component4() { + return _component4(reference.pointer, _id_component4 as jni$_.JMethodIDPtr) + .float; + } + + static final _id_component5 = _class.instanceMethodId( + r'component5', + r'()I', + ); + + static final _component5 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final int component5()` + int component5() { + return _component5(reference.pointer, _id_component5 as jni$_.JMethodIDPtr) + .integer; + } + + static final _id_component6 = _class.instanceMethodId( + r'component6', + r'()I', + ); + + static final _component6 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final int component6()` + int component6() { + return _component6(reference.pointer, _id_component6 as jni$_.JMethodIDPtr) + .integer; + } + + static final _id_copy = _class.instanceMethodId( + r'copy', + r'(IIFFII)Lio/sentry/android/replay/ScreenshotRecorderConfig;', + ); + + static final _copy = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Int32, + jni$_.Int32, + jni$_.Double, + jni$_.Double, + jni$_.Int32, + jni$_.Int32 + )>)>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, int, int, double, double, int, int)>(); + + /// from: `public final io.sentry.android.replay.ScreenshotRecorderConfig copy(int i, int i1, float f, float f1, int i2, int i3)` + /// The returned object must be released after use, by calling the [release] method. + ScreenshotRecorderConfig copy( + int i, + int i1, + double f, + double f1, + int i2, + int i3, + ) { + return _copy(reference.pointer, _id_copy as jni$_.JMethodIDPtr, i, i1, f, + f1, i2, i3) + .object( + const $ScreenshotRecorderConfig$Type()); + } + + static final _id_toString$1 = _class.instanceMethodId( + r'toString', + r'()Ljava/lang/String;', + ); + + static final _toString$1 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public java.lang.String toString()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString toString$1() { + return _toString$1(reference.pointer, _id_toString$1 as jni$_.JMethodIDPtr) + .object(const jni$_.JStringType()); + } + + static final _id_hashCode$1 = _class.instanceMethodId( + r'hashCode', + r'()I', + ); + + static final _hashCode$1 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public int hashCode()` + int hashCode$1() { + return _hashCode$1(reference.pointer, _id_hashCode$1 as jni$_.JMethodIDPtr) + .integer; + } + + static final _id_equals = _class.instanceMethodId( + r'equals', + r'(Ljava/lang/Object;)Z', + ); + + static final _equals = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `public boolean equals(java.lang.Object object)` + bool equals( + jni$_.JObject? object, + ) { + final _$object = object?.reference ?? jni$_.jNullReference; + return _equals(reference.pointer, _id_equals as jni$_.JMethodIDPtr, + _$object.pointer) + .boolean; + } +} + +final class $ScreenshotRecorderConfig$NullableType + extends jni$_.JObjType { + @jni$_.internal + const $ScreenshotRecorderConfig$NullableType(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lio/sentry/android/replay/ScreenshotRecorderConfig;'; + + @jni$_.internal + @core$_.override + ScreenshotRecorderConfig? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : ScreenshotRecorderConfig.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ScreenshotRecorderConfig$NullableType).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ScreenshotRecorderConfig$NullableType) && + other is $ScreenshotRecorderConfig$NullableType; + } +} + +final class $ScreenshotRecorderConfig$Type + extends jni$_.JObjType { + @jni$_.internal + const $ScreenshotRecorderConfig$Type(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lio/sentry/android/replay/ScreenshotRecorderConfig;'; + + @jni$_.internal + @core$_.override + ScreenshotRecorderConfig fromReference(jni$_.JReference reference) => + ScreenshotRecorderConfig.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => + const $ScreenshotRecorderConfig$NullableType(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($ScreenshotRecorderConfig$Type).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($ScreenshotRecorderConfig$Type) && + other is $ScreenshotRecorderConfig$Type; + } +} + /// from: `io.sentry.flutter.SentryFlutterPlugin$Companion` class SentryFlutterPlugin$Companion extends jni$_.JObject { @jni$_.internal diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index 64bb9dc6ec..a4811a4581 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -5,6 +5,7 @@ import 'package:jni/jni.dart'; import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; +import '../../replay/replay_config.dart'; import '../../replay/scheduled_recorder_config.dart'; import '../native_app_start.dart'; import '../sentry_native_channel.dart'; @@ -18,6 +19,7 @@ import 'binding.dart' as native; class SentryNativeJava extends SentryNativeChannel { AndroidReplayRecorder? _replayRecorder; AndroidEnvelopeSender? _envelopeSender; + native.ReplayIntegration? _nativeReplay; SentryNativeJava(super.options); @@ -48,7 +50,8 @@ class SentryNativeJava extends SentryNativeChannel { : false; _replayId = replayId; - + _nativeReplay = native.SentryFlutterPlugin.Companion + .privateSentryGetReplayIntegration(); _replayRecorder = AndroidReplayRecorder.factory(options); await _replayRecorder!.start(); hub.configureScope((s) { @@ -358,6 +361,54 @@ class SentryNativeJava extends SentryNativeChannel { native.Sentry.removeExtra(jKey); }); }); + + @override + void setReplayConfig(ReplayConfig config) => + tryCatchSync('setReplayConfig', () { + final invalidConfig = config.width == 0.0 || + config.height == 0.0 || + config.windowWidth == 0.0 || + config.windowHeight == 0.0; + if (invalidConfig) { + options.log( + SentryLevel.error, + 'Replay config is not valid: ' + 'width: ${config.width}, ' + 'height: ${config.height}, ' + 'windowWidth: ${config.windowWidth}, ' + 'windowHeight: ${config.windowHeight}'); + return; + } + + var adjWidth = config.width; + var adjHeight = config.height; + + // First update the smaller dimension, as changing that will affect the screen ratio more. + if (adjWidth < adjHeight) { + final newWidth = adjWidth.adjustReplaySizeToBlockSize(); + final scale = newWidth / adjWidth; + final newHeight = (adjHeight * scale).adjustReplaySizeToBlockSize(); + adjWidth = newWidth; + adjHeight = newHeight; + } else { + final newHeight = adjHeight.adjustReplaySizeToBlockSize(); + final scale = newHeight / adjHeight; + final newWidth = (adjWidth * scale).adjustReplaySizeToBlockSize(); + adjHeight = newHeight; + adjWidth = newWidth; + } + + final replayConfig = native.ScreenshotRecorderConfig( + adjWidth.toInt(), + adjHeight.toInt(), + adjWidth / config.windowWidth, + adjHeight / config.windowHeight, + config.frameRate, + 0, // bitRate is currently not used + ); + + _nativeReplay?.onConfigurationChanged(replayConfig); + }); } JObject? _dartToJObject(Object? value, Arena arena) => switch (value) { @@ -393,3 +444,16 @@ JMap _dartToJMap(Map json, Arena arena) { return jmap; } + +const _videoBlockSize = 16; + +extension _ReplaySizeAdjustment on double { + double adjustReplaySizeToBlockSize() { + final remainder = this % _videoBlockSize; + if (remainder <= _videoBlockSize / 2) { + return this - remainder; + } else { + return this + (_videoBlockSize - remainder); + } + } +} From aadb6b96821dfab66f13a30ab0d9c9e7507e941c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 31 Oct 2025 08:55:51 +0000 Subject: [PATCH 02/27] Remove method channel methods --- .../io/sentry/flutter/SentryFlutterPlugin.kt | 71 ------------------- 1 file changed, 71 deletions(-) diff --git a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index 4d47d974de..2226d64e83 100644 --- a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -61,8 +61,6 @@ class SentryFlutterPlugin : when (call.method) { "initNativeSdk" -> initNativeSdk(call, result) "closeNativeSdk" -> closeNativeSdk(result) - "setReplayConfig" -> setReplayConfig(call, result) - "captureReplay" -> captureReplay(result) else -> result.notImplemented() } } @@ -354,73 +352,4 @@ class SentryFlutterPlugin : } } } - - private fun setReplayConfig( - call: MethodCall, - result: Result, - ) { - // Since codec block size is 16, so we have to adjust the width and height to it, - // otherwise the codec might fail to configure on some devices, see - // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/MediaCodecInfo.java;l=1999-2001 - val windowWidth = call.argument("windowWidth") as? Double ?: 0.0 - val windowHeight = call.argument("windowHeight") as? Double ?: 0.0 - - var width = call.argument("width") as? Double ?: 0.0 - var height = call.argument("height") as? Double ?: 0.0 - - val invalidConfig = - width == 0.0 || - height == 0.0 || - windowWidth == 0.0 || - windowHeight == 0.0 - - if (invalidConfig) { - result.error( - "5", - "Replay config is not valid: width: $width, height: $height, " + - "windowWidth: $windowWidth, windowHeight: $windowHeight", - null, - ) - return - } - - // First update the smaller dimension, as changing that will affect the screen ratio more. - if (width < height) { - val newWidth = width.adjustReplaySizeToBlockSize() - height = (height * (newWidth / width)).adjustReplaySizeToBlockSize() - width = newWidth - } else { - val newHeight = height.adjustReplaySizeToBlockSize() - width = (width * (newHeight / height)).adjustReplaySizeToBlockSize() - height = newHeight - } - - val replayConfig = - ScreenshotRecorderConfig( - recordingWidth = width.roundToInt(), - recordingHeight = height.roundToInt(), - scaleFactorX = width.toFloat() / windowWidth.toFloat(), - scaleFactorY = height.toFloat() / windowHeight.toFloat(), - frameRate = call.argument("frameRate") as? Int ?: 0, - bitRate = call.argument("bitRate") as? Int ?: 0, - ) - Log.i( - "Sentry", - "Configuring replay: %dx%d at %d FPS, %d BPS".format( - replayConfig.recordingWidth, - replayConfig.recordingHeight, - replayConfig.frameRate, - replayConfig.bitRate, - ), - ) - replay?.onConfigurationChanged(replayConfig) - result.success("") - } - - private fun captureReplay( - result: Result, - ) { - replay!!.captureReplay(isTerminating = false) - result.success(replay!!.getReplayId().toString()) - } } From 74346c063b3003bfc20c37712ae709ba321cdb88 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 31 Oct 2025 10:14:37 +0000 Subject: [PATCH 03/27] Update --- .../flutter/lib/src/native/java/binding.dart | 590 +++++++++++++++--- .../src/native/java/sentry_native_java.dart | 7 - .../lib/src/native/sentry_native_channel.dart | 19 +- .../test/sentry_native_channel_test.dart | 34 +- 4 files changed, 539 insertions(+), 111 deletions(-) diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index 5720447220..ba81d18072 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -167,14 +167,14 @@ class InternalSentrySdk extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureEnvelope(byte[] bs, boolean z)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject? captureEnvelope( + static SentryId? captureEnvelope( jni$_.JByteArray bs, bool z, ) { final _$bs = bs.reference; return _captureEnvelope(_class.reference.pointer, _id_captureEnvelope as jni$_.JMethodIDPtr, _$bs.pointer, z ? 1 : 0) - .object(const jni$_.JObjectNullableType()); + .object(const $SentryId$NullableType()); } static final _id_getAppStartMeasurement = _class.staticMethodId( @@ -797,10 +797,10 @@ class ReplayIntegration extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId getReplayId()` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject getReplayId() { + SentryId getReplayId() { return _getReplayId( reference.pointer, _id_getReplayId as jni$_.JMethodIDPtr) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_setBreadcrumbConverter = _class.instanceMethodId( @@ -3598,13 +3598,13 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureEvent(io.sentry.SentryEvent sentryEvent)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureEvent( + static SentryId captureEvent( jni$_.JObject sentryEvent, ) { final _$sentryEvent = sentryEvent.reference; return _captureEvent(_class.reference.pointer, _id_captureEvent as jni$_.JMethodIDPtr, _$sentryEvent.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureEvent$1 = _class.staticMethodId( @@ -3631,7 +3631,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureEvent(io.sentry.SentryEvent sentryEvent, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureEvent$1( + static SentryId captureEvent$1( jni$_.JObject sentryEvent, ScopeCallback scopeCallback, ) { @@ -3642,7 +3642,7 @@ class Sentry extends jni$_.JObject { _id_captureEvent$1 as jni$_.JMethodIDPtr, _$sentryEvent.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureEvent$2 = _class.staticMethodId( @@ -3669,7 +3669,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureEvent(io.sentry.SentryEvent sentryEvent, io.sentry.Hint hint)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureEvent$2( + static SentryId captureEvent$2( jni$_.JObject sentryEvent, jni$_.JObject? hint, ) { @@ -3680,7 +3680,7 @@ class Sentry extends jni$_.JObject { _id_captureEvent$2 as jni$_.JMethodIDPtr, _$sentryEvent.pointer, _$hint.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureEvent$3 = _class.staticMethodId( @@ -3709,7 +3709,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureEvent(io.sentry.SentryEvent sentryEvent, io.sentry.Hint hint, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureEvent$3( + static SentryId captureEvent$3( jni$_.JObject sentryEvent, jni$_.JObject? hint, ScopeCallback scopeCallback, @@ -3723,7 +3723,7 @@ class Sentry extends jni$_.JObject { _$sentryEvent.pointer, _$hint.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureMessage = _class.staticMethodId( @@ -3744,13 +3744,13 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureMessage(java.lang.String string)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureMessage( + static SentryId captureMessage( jni$_.JString string, ) { final _$string = string.reference; return _captureMessage(_class.reference.pointer, _id_captureMessage as jni$_.JMethodIDPtr, _$string.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureMessage$1 = _class.staticMethodId( @@ -3777,7 +3777,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureMessage(java.lang.String string, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureMessage$1( + static SentryId captureMessage$1( jni$_.JString string, ScopeCallback scopeCallback, ) { @@ -3788,7 +3788,7 @@ class Sentry extends jni$_.JObject { _id_captureMessage$1 as jni$_.JMethodIDPtr, _$string.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureMessage$2 = _class.staticMethodId( @@ -3815,7 +3815,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureMessage(java.lang.String string, io.sentry.SentryLevel sentryLevel)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureMessage$2( + static SentryId captureMessage$2( jni$_.JString string, jni$_.JObject sentryLevel, ) { @@ -3826,7 +3826,7 @@ class Sentry extends jni$_.JObject { _id_captureMessage$2 as jni$_.JMethodIDPtr, _$string.pointer, _$sentryLevel.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureMessage$3 = _class.staticMethodId( @@ -3855,7 +3855,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureMessage(java.lang.String string, io.sentry.SentryLevel sentryLevel, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureMessage$3( + static SentryId captureMessage$3( jni$_.JString string, jni$_.JObject sentryLevel, ScopeCallback scopeCallback, @@ -3869,7 +3869,7 @@ class Sentry extends jni$_.JObject { _$string.pointer, _$sentryLevel.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureFeedback = _class.staticMethodId( @@ -3890,13 +3890,13 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureFeedback(io.sentry.protocol.Feedback feedback)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureFeedback( + static SentryId captureFeedback( jni$_.JObject feedback, ) { final _$feedback = feedback.reference; return _captureFeedback(_class.reference.pointer, _id_captureFeedback as jni$_.JMethodIDPtr, _$feedback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureFeedback$1 = _class.staticMethodId( @@ -3923,7 +3923,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureFeedback(io.sentry.protocol.Feedback feedback, io.sentry.Hint hint)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureFeedback$1( + static SentryId captureFeedback$1( jni$_.JObject feedback, jni$_.JObject? hint, ) { @@ -3934,7 +3934,7 @@ class Sentry extends jni$_.JObject { _id_captureFeedback$1 as jni$_.JMethodIDPtr, _$feedback.pointer, _$hint.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureFeedback$2 = _class.staticMethodId( @@ -3963,7 +3963,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureFeedback(io.sentry.protocol.Feedback feedback, io.sentry.Hint hint, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureFeedback$2( + static SentryId captureFeedback$2( jni$_.JObject feedback, jni$_.JObject? hint, ScopeCallback? scopeCallback, @@ -3977,7 +3977,7 @@ class Sentry extends jni$_.JObject { _$feedback.pointer, _$hint.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureException = _class.staticMethodId( @@ -3998,13 +3998,13 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureException(java.lang.Throwable throwable)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureException( + static SentryId captureException( jni$_.JObject throwable, ) { final _$throwable = throwable.reference; return _captureException(_class.reference.pointer, _id_captureException as jni$_.JMethodIDPtr, _$throwable.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureException$1 = _class.staticMethodId( @@ -4031,7 +4031,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureException(java.lang.Throwable throwable, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureException$1( + static SentryId captureException$1( jni$_.JObject throwable, ScopeCallback scopeCallback, ) { @@ -4042,7 +4042,7 @@ class Sentry extends jni$_.JObject { _id_captureException$1 as jni$_.JMethodIDPtr, _$throwable.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureException$2 = _class.staticMethodId( @@ -4069,7 +4069,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureException(java.lang.Throwable throwable, io.sentry.Hint hint)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureException$2( + static SentryId captureException$2( jni$_.JObject throwable, jni$_.JObject? hint, ) { @@ -4080,7 +4080,7 @@ class Sentry extends jni$_.JObject { _id_captureException$2 as jni$_.JMethodIDPtr, _$throwable.pointer, _$hint.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureException$3 = _class.staticMethodId( @@ -4109,7 +4109,7 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureException(java.lang.Throwable throwable, io.sentry.Hint hint, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureException$3( + static SentryId captureException$3( jni$_.JObject throwable, jni$_.JObject? hint, ScopeCallback scopeCallback, @@ -4123,7 +4123,7 @@ class Sentry extends jni$_.JObject { _$throwable.pointer, _$hint.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureUserFeedback = _class.staticMethodId( @@ -4547,10 +4547,10 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId getLastEventId()` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject getLastEventId() { + static SentryId getLastEventId() { return _getLastEventId( _class.reference.pointer, _id_getLastEventId as jni$_.JMethodIDPtr) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_pushScope = _class.staticMethodId( @@ -5285,13 +5285,13 @@ class Sentry extends jni$_.JObject { /// from: `static public io.sentry.protocol.SentryId captureCheckIn(io.sentry.CheckIn checkIn)` /// The returned object must be released after use, by calling the [release] method. - static jni$_.JObject captureCheckIn( + static SentryId captureCheckIn( jni$_.JObject checkIn, ) { final _$checkIn = checkIn.reference; return _captureCheckIn(_class.reference.pointer, _id_captureCheckIn as jni$_.JMethodIDPtr, _$checkIn.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_logger = _class.staticMethodId( @@ -5419,7 +5419,7 @@ class Sentry extends jni$_.JObject { /// from: `static public void showUserFeedbackDialog(io.sentry.protocol.SentryId sentryId, io.sentry.SentryFeedbackOptions$OptionsConfigurator optionsConfigurator)` static void showUserFeedbackDialog$2( - jni$_.JObject? sentryId, + SentryId? sentryId, jni$_.JObject? optionsConfigurator, ) { final _$sentryId = sentryId?.reference ?? jni$_.jNullReference; @@ -7333,7 +7333,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureEvent(io.sentry.SentryEvent sentryEvent, io.sentry.Hint hint)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureEvent( + SentryId captureEvent( jni$_.JObject sentryEvent, jni$_.JObject? hint, ) { @@ -7344,7 +7344,7 @@ class ScopesAdapter extends jni$_.JObject { _id_captureEvent as jni$_.JMethodIDPtr, _$sentryEvent.pointer, _$hint.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureEvent$1 = _class.instanceMethodId( @@ -7373,7 +7373,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureEvent(io.sentry.SentryEvent sentryEvent, io.sentry.Hint hint, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureEvent$1( + SentryId captureEvent$1( jni$_.JObject sentryEvent, jni$_.JObject? hint, ScopeCallback scopeCallback, @@ -7387,7 +7387,7 @@ class ScopesAdapter extends jni$_.JObject { _$sentryEvent.pointer, _$hint.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureMessage = _class.instanceMethodId( @@ -7414,7 +7414,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureMessage(java.lang.String string, io.sentry.SentryLevel sentryLevel)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureMessage( + SentryId captureMessage( jni$_.JString string, jni$_.JObject sentryLevel, ) { @@ -7425,7 +7425,7 @@ class ScopesAdapter extends jni$_.JObject { _id_captureMessage as jni$_.JMethodIDPtr, _$string.pointer, _$sentryLevel.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureMessage$1 = _class.instanceMethodId( @@ -7454,7 +7454,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureMessage(java.lang.String string, io.sentry.SentryLevel sentryLevel, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureMessage$1( + SentryId captureMessage$1( jni$_.JString string, jni$_.JObject sentryLevel, ScopeCallback scopeCallback, @@ -7468,7 +7468,7 @@ class ScopesAdapter extends jni$_.JObject { _$string.pointer, _$sentryLevel.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureFeedback = _class.instanceMethodId( @@ -7489,13 +7489,13 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureFeedback(io.sentry.protocol.Feedback feedback)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureFeedback( + SentryId captureFeedback( jni$_.JObject feedback, ) { final _$feedback = feedback.reference; return _captureFeedback(reference.pointer, _id_captureFeedback as jni$_.JMethodIDPtr, _$feedback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureFeedback$1 = _class.instanceMethodId( @@ -7522,7 +7522,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureFeedback(io.sentry.protocol.Feedback feedback, io.sentry.Hint hint)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureFeedback$1( + SentryId captureFeedback$1( jni$_.JObject feedback, jni$_.JObject? hint, ) { @@ -7533,7 +7533,7 @@ class ScopesAdapter extends jni$_.JObject { _id_captureFeedback$1 as jni$_.JMethodIDPtr, _$feedback.pointer, _$hint.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureFeedback$2 = _class.instanceMethodId( @@ -7562,7 +7562,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureFeedback(io.sentry.protocol.Feedback feedback, io.sentry.Hint hint, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureFeedback$2( + SentryId captureFeedback$2( jni$_.JObject feedback, jni$_.JObject? hint, ScopeCallback? scopeCallback, @@ -7576,7 +7576,7 @@ class ScopesAdapter extends jni$_.JObject { _$feedback.pointer, _$hint.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureEnvelope = _class.instanceMethodId( @@ -7603,7 +7603,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureEnvelope(io.sentry.SentryEnvelope sentryEnvelope, io.sentry.Hint hint)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureEnvelope( + SentryId captureEnvelope( jni$_.JObject sentryEnvelope, jni$_.JObject? hint, ) { @@ -7614,7 +7614,7 @@ class ScopesAdapter extends jni$_.JObject { _id_captureEnvelope as jni$_.JMethodIDPtr, _$sentryEnvelope.pointer, _$hint.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureException = _class.instanceMethodId( @@ -7641,7 +7641,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureException(java.lang.Throwable throwable, io.sentry.Hint hint)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureException( + SentryId captureException( jni$_.JObject throwable, jni$_.JObject? hint, ) { @@ -7652,7 +7652,7 @@ class ScopesAdapter extends jni$_.JObject { _id_captureException as jni$_.JMethodIDPtr, _$throwable.pointer, _$hint.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureException$1 = _class.instanceMethodId( @@ -7681,7 +7681,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureException(java.lang.Throwable throwable, io.sentry.Hint hint, io.sentry.ScopeCallback scopeCallback)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureException$1( + SentryId captureException$1( jni$_.JObject throwable, jni$_.JObject? hint, ScopeCallback scopeCallback, @@ -7695,7 +7695,7 @@ class ScopesAdapter extends jni$_.JObject { _$throwable.pointer, _$hint.pointer, _$scopeCallback.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureUserFeedback = _class.instanceMethodId( @@ -8144,10 +8144,10 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId getLastEventId()` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject getLastEventId() { + SentryId getLastEventId() { return _getLastEventId( reference.pointer, _id_getLastEventId as jni$_.JMethodIDPtr) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_pushScope = _class.instanceMethodId( @@ -8663,7 +8663,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureTransaction(io.sentry.protocol.SentryTransaction sentryTransaction, io.sentry.TraceContext traceContext, io.sentry.Hint hint, io.sentry.ProfilingTraceData profilingTraceData)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureTransaction( + SentryId captureTransaction( jni$_.JObject sentryTransaction, jni$_.JObject? traceContext, jni$_.JObject? hint, @@ -8681,7 +8681,7 @@ class ScopesAdapter extends jni$_.JObject { _$traceContext.pointer, _$hint.pointer, _$profilingTraceData.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_captureProfileChunk = _class.instanceMethodId( @@ -8702,7 +8702,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureProfileChunk(io.sentry.ProfileChunk profileChunk)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureProfileChunk( + SentryId captureProfileChunk( jni$_.JObject profileChunk, ) { final _$profileChunk = profileChunk.reference; @@ -8710,7 +8710,7 @@ class ScopesAdapter extends jni$_.JObject { reference.pointer, _id_captureProfileChunk as jni$_.JMethodIDPtr, _$profileChunk.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_startTransaction = _class.instanceMethodId( @@ -9088,13 +9088,13 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureCheckIn(io.sentry.CheckIn checkIn)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureCheckIn( + SentryId captureCheckIn( jni$_.JObject checkIn, ) { final _$checkIn = checkIn.reference; return _captureCheckIn(reference.pointer, _id_captureCheckIn as jni$_.JMethodIDPtr, _$checkIn.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_getRateLimiter = _class.instanceMethodId( @@ -9146,7 +9146,7 @@ class ScopesAdapter extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId captureReplay(io.sentry.SentryReplayEvent sentryReplayEvent, io.sentry.Hint hint)` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject captureReplay( + SentryId captureReplay( jni$_.JObject sentryReplayEvent, jni$_.JObject? hint, ) { @@ -9157,7 +9157,7 @@ class ScopesAdapter extends jni$_.JObject { _id_captureReplay as jni$_.JMethodIDPtr, _$sentryReplayEvent.pointer, _$hint.pointer) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_logger = _class.instanceMethodId( @@ -10049,10 +10049,10 @@ class Scope extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId getReplayId()` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject getReplayId() { + SentryId getReplayId() { return _getReplayId( reference.pointer, _id_getReplayId as jni$_.JMethodIDPtr) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_setReplayId = _class.instanceMethodId( @@ -10073,7 +10073,7 @@ class Scope extends jni$_.JObject { /// from: `public void setReplayId(io.sentry.protocol.SentryId sentryId)` void setReplayId( - jni$_.JObject sentryId, + SentryId sentryId, ) { final _$sentryId = sentryId.reference; _setReplayId(reference.pointer, _id_setReplayId as jni$_.JMethodIDPtr, @@ -11280,7 +11280,7 @@ class Scope extends jni$_.JObject { /// from: `public void setLastEventId(io.sentry.protocol.SentryId sentryId)` void setLastEventId( - jni$_.JObject sentryId, + SentryId sentryId, ) { final _$sentryId = sentryId.reference; _setLastEventId(reference.pointer, _id_setLastEventId as jni$_.JMethodIDPtr, @@ -11307,10 +11307,10 @@ class Scope extends jni$_.JObject { /// from: `public io.sentry.protocol.SentryId getLastEventId()` /// The returned object must be released after use, by calling the [release] method. - jni$_.JObject getLastEventId() { + SentryId getLastEventId() { return _getLastEventId( reference.pointer, _id_getLastEventId as jni$_.JMethodIDPtr) - .object(const jni$_.JObjectType()); + .object(const $SentryId$Type()); } static final _id_bindClient = _class.instanceMethodId( @@ -12750,6 +12750,446 @@ final class $User$Type extends jni$_.JObjType { } } +/// from: `io.sentry.protocol.SentryId$Deserializer` +class SentryId$Deserializer extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType $type; + + @jni$_.internal + SentryId$Deserializer.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = + jni$_.JClass.forName(r'io/sentry/protocol/SentryId$Deserializer'); + + /// The type which includes information such as the signature of this class. + static const nullableType = $SentryId$Deserializer$NullableType(); + static const type = $SentryId$Deserializer$Type(); + static final _id_new$ = _class.constructorId( + r'()V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory SentryId$Deserializer() { + return SentryId$Deserializer.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr) + .reference); + } + + static final _id_deserialize = _class.instanceMethodId( + r'deserialize', + r'(Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/SentryId;', + ); + + static final _deserialize = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer + )>)>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer)>(); + + /// from: `public io.sentry.protocol.SentryId deserialize(io.sentry.ObjectReader objectReader, io.sentry.ILogger iLogger)` + /// The returned object must be released after use, by calling the [release] method. + SentryId deserialize( + jni$_.JObject objectReader, + jni$_.JObject iLogger, + ) { + final _$objectReader = objectReader.reference; + final _$iLogger = iLogger.reference; + return _deserialize( + reference.pointer, + _id_deserialize as jni$_.JMethodIDPtr, + _$objectReader.pointer, + _$iLogger.pointer) + .object(const $SentryId$Type()); + } +} + +final class $SentryId$Deserializer$NullableType + extends jni$_.JObjType { + @jni$_.internal + const $SentryId$Deserializer$NullableType(); + + @jni$_.internal + @core$_.override + String get signature => r'Lio/sentry/protocol/SentryId$Deserializer;'; + + @jni$_.internal + @core$_.override + SentryId$Deserializer? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : SentryId$Deserializer.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($SentryId$Deserializer$NullableType).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($SentryId$Deserializer$NullableType) && + other is $SentryId$Deserializer$NullableType; + } +} + +final class $SentryId$Deserializer$Type + extends jni$_.JObjType { + @jni$_.internal + const $SentryId$Deserializer$Type(); + + @jni$_.internal + @core$_.override + String get signature => r'Lio/sentry/protocol/SentryId$Deserializer;'; + + @jni$_.internal + @core$_.override + SentryId$Deserializer fromReference(jni$_.JReference reference) => + SentryId$Deserializer.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => + const $SentryId$Deserializer$NullableType(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($SentryId$Deserializer$Type).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($SentryId$Deserializer$Type) && + other is $SentryId$Deserializer$Type; + } +} + +/// from: `io.sentry.protocol.SentryId` +class SentryId extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType $type; + + @jni$_.internal + SentryId.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName(r'io/sentry/protocol/SentryId'); + + /// The type which includes information such as the signature of this class. + static const nullableType = $SentryId$NullableType(); + static const type = $SentryId$Type(); + static final _id_EMPTY_ID = _class.staticFieldId( + r'EMPTY_ID', + r'Lio/sentry/protocol/SentryId;', + ); + + /// from: `static public final io.sentry.protocol.SentryId EMPTY_ID` + /// The returned object must be released after use, by calling the [release] method. + static SentryId? get EMPTY_ID => + _id_EMPTY_ID.get(_class, const $SentryId$NullableType()); + + static final _id_new$ = _class.constructorId( + r'()V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory SentryId() { + return SentryId.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr) + .reference); + } + + static final _id_new$1 = _class.constructorId( + r'(Ljava/util/UUID;)V', + ); + + static final _new$1 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `public void (java.util.UUID uUID)` + /// The returned object must be released after use, by calling the [release] method. + factory SentryId.new$1( + jni$_.JObject? uUID, + ) { + final _$uUID = uUID?.reference ?? jni$_.jNullReference; + return SentryId.fromReference(_new$1(_class.reference.pointer, + _id_new$1 as jni$_.JMethodIDPtr, _$uUID.pointer) + .reference); + } + + static final _id_new$2 = _class.constructorId( + r'(Ljava/lang/String;)V', + ); + + static final _new$2 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `public void (java.lang.String string)` + /// The returned object must be released after use, by calling the [release] method. + factory SentryId.new$2( + jni$_.JString string, + ) { + final _$string = string.reference; + return SentryId.fromReference(_new$2(_class.reference.pointer, + _id_new$2 as jni$_.JMethodIDPtr, _$string.pointer) + .reference); + } + + static final _id_toString$1 = _class.instanceMethodId( + r'toString', + r'()Ljava/lang/String;', + ); + + static final _toString$1 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public java.lang.String toString()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JString? toString$1() { + return _toString$1(reference.pointer, _id_toString$1 as jni$_.JMethodIDPtr) + .object(const jni$_.JStringNullableType()); + } + + static final _id_equals = _class.instanceMethodId( + r'equals', + r'(Ljava/lang/Object;)Z', + ); + + static final _equals = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `public boolean equals(java.lang.Object object)` + bool equals( + jni$_.JObject? object, + ) { + final _$object = object?.reference ?? jni$_.jNullReference; + return _equals(reference.pointer, _id_equals as jni$_.JMethodIDPtr, + _$object.pointer) + .boolean; + } + + static final _id_hashCode$1 = _class.instanceMethodId( + r'hashCode', + r'()I', + ); + + static final _hashCode$1 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public int hashCode()` + int hashCode$1() { + return _hashCode$1(reference.pointer, _id_hashCode$1 as jni$_.JMethodIDPtr) + .integer; + } + + static final _id_serialize = _class.instanceMethodId( + r'serialize', + r'(Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V', + ); + + static final _serialize = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer + )>)>>('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer)>(); + + /// from: `public void serialize(io.sentry.ObjectWriter objectWriter, io.sentry.ILogger iLogger)` + void serialize( + jni$_.JObject objectWriter, + jni$_.JObject iLogger, + ) { + final _$objectWriter = objectWriter.reference; + final _$iLogger = iLogger.reference; + _serialize(reference.pointer, _id_serialize as jni$_.JMethodIDPtr, + _$objectWriter.pointer, _$iLogger.pointer) + .check(); + } +} + +final class $SentryId$NullableType extends jni$_.JObjType { + @jni$_.internal + const $SentryId$NullableType(); + + @jni$_.internal + @core$_.override + String get signature => r'Lio/sentry/protocol/SentryId;'; + + @jni$_.internal + @core$_.override + SentryId? fromReference(jni$_.JReference reference) => reference.isNull + ? null + : SentryId.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($SentryId$NullableType).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($SentryId$NullableType) && + other is $SentryId$NullableType; + } +} + +final class $SentryId$Type extends jni$_.JObjType { + @jni$_.internal + const $SentryId$Type(); + + @jni$_.internal + @core$_.override + String get signature => r'Lio/sentry/protocol/SentryId;'; + + @jni$_.internal + @core$_.override + SentryId fromReference(jni$_.JReference reference) => SentryId.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => const $SentryId$NullableType(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($SentryId$Type).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($SentryId$Type) && other is $SentryId$Type; + } +} + /// from: `android.graphics.Bitmap$CompressFormat` class Bitmap$CompressFormat extends jni$_.JObject { @jni$_.internal diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index a4811a4581..cad810d396 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -100,13 +100,6 @@ class SentryNativeJava extends SentryNativeChannel { return super.init(hub); } - @override - FutureOr captureReplay() async { - final replayId = await super.captureReplay(); - _replayId = replayId; - return replayId; - } - @override FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { diff --git a/packages/flutter/lib/src/native/sentry_native_channel.dart b/packages/flutter/lib/src/native/sentry_native_channel.dart index 89bc6a18e4..3349ca761d 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -221,19 +221,16 @@ class SentryNativeChannel SentryId? get replayId => null; @override - FutureOr setReplayConfig(ReplayConfig config) => - channel.invokeMethod('setReplayConfig', { - 'windowWidth': config.windowWidth, - 'windowHeight': config.windowHeight, - 'width': config.width, - 'height': config.height, - 'frameRate': config.frameRate, - }); + FutureOr setReplayConfig(ReplayConfig config) { + assert( + false, 'setReplayConfig should not be used through method channels.'); + } @override - FutureOr captureReplay() => channel - .invokeMethod('captureReplay') - .then((value) => SentryId.fromId(value as String)); + FutureOr captureReplay() { + assert(false, 'captureReplay should not be used through method channels.'); + return SentryId.empty(); + } @override FutureOr captureSession() { diff --git a/packages/flutter/test/sentry_native_channel_test.dart b/packages/flutter/test/sentry_native_channel_test.dart index d46b164edd..fe0d3da794 100644 --- a/packages/flutter/test/sentry_native_channel_test.dart +++ b/packages/flutter/test/sentry_native_channel_test.dart @@ -302,8 +302,11 @@ void main() { }); test('setReplayConfig', () async { - when(channel.invokeMethod('setReplayConfig', any)) - .thenAnswer((_) => Future.value()); + final matcher = _nativeUnavailableMatcher( + mockPlatform, + includeLookupSymbol: true, + includeFailedToLoadClassException: true, + ); final config = ReplayConfig( windowWidth: 110, @@ -311,31 +314,26 @@ void main() { width: 1.1, height: 2.2, frameRate: 3); - await sut.setReplayConfig(config); if (mockPlatform.isAndroid) { - verify(channel.invokeMethod('setReplayConfig', { - 'windowWidth': config.windowWidth, - 'windowHeight': config.windowHeight, - 'width': config.width, - 'height': config.height, - 'frameRate': config.frameRate, - })); + expect(() => sut.setReplayConfig(config), matcher); } else { - verifyNever(channel.invokeMethod('setReplayConfig', any)); + expect(() => sut.setReplayConfig(config), returnsNormally); } + + verifyZeroInteractions(channel); }); test('captureReplay', () async { - final sentryId = SentryId.newId(); - - when(channel.invokeMethod('captureReplay', any)) - .thenAnswer((_) => Future.value(sentryId.toString())); + final matcher = _nativeUnavailableMatcher( + mockPlatform, + includeLookupSymbol: true, + includeFailedToLoadClassException: true, + ); - final returnedId = await sut.captureReplay(); + expect(() => sut.captureReplay(), matcher); - verify(channel.invokeMethod('captureReplay')); - expect(returnedId, sentryId); + verifyZeroInteractions(channel); }); test('getSession is no-op', () async { From 6764e1ca6364c6174615b09b98fbe33de9d61b12 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 31 Oct 2025 10:29:03 +0000 Subject: [PATCH 04/27] Update --- packages/flutter/ffi-jni.yaml | 1 + .../src/native/java/sentry_native_java.dart | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/packages/flutter/ffi-jni.yaml b/packages/flutter/ffi-jni.yaml index c7abfaa2e5..a06d752da2 100644 --- a/packages/flutter/ffi-jni.yaml +++ b/packages/flutter/ffi-jni.yaml @@ -23,4 +23,5 @@ classes: - io.sentry.Scope - io.sentry.ScopeCallback - io.sentry.protocol.User + - io.sentry.protocol.SentryId - android.graphics.Bitmap diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index cad810d396..26f6d924b6 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -355,6 +355,26 @@ class SentryNativeJava extends SentryNativeChannel { }); }); + @override + SentryId captureReplay() { + final id = tryCatchSync('captureReplay', () { + using((arena) { + // The passed parameter is `isTerminating` + _nativeReplay?.captureReplay(false.toJBoolean()..releasedBy(arena)); + final jString = _nativeReplay?.getReplayId().toString$1() + ?..releasedBy(arena); + + if (jString == null) { + return SentryId.empty(); + } else { + return SentryId.fromId(jString.toDartString()); + } + }); + }); + + return id ?? SentryId.empty(); + } + @override void setReplayConfig(ReplayConfig config) => tryCatchSync('setReplayConfig', () { From d1b64efc3645165dfa000dd39f802b20c8627132 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 31 Oct 2025 10:34:19 +0000 Subject: [PATCH 05/27] Update --- packages/flutter/lib/src/native/java/sentry_native_java.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index 26f6d924b6..a0676a87b2 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -358,7 +358,7 @@ class SentryNativeJava extends SentryNativeChannel { @override SentryId captureReplay() { final id = tryCatchSync('captureReplay', () { - using((arena) { + return using((arena) { // The passed parameter is `isTerminating` _nativeReplay?.captureReplay(false.toJBoolean()..releasedBy(arena)); final jString = _nativeReplay?.getReplayId().toString$1() From 55603c8c0339cb8616bdd9af12c095ef67e05dbe Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 3 Nov 2025 10:17:14 +0100 Subject: [PATCH 06/27] Add test for adjustReplaySizeToBlockSize --- packages/flutter/lib/src/native/java/sentry_native_java.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index a0676a87b2..06a2aaee97 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -460,7 +460,8 @@ JMap _dartToJMap(Map json, Arena arena) { const _videoBlockSize = 16; -extension _ReplaySizeAdjustment on double { +@visibleForTesting +extension ReplaySizeAdjustment on double { double adjustReplaySizeToBlockSize() { final remainder = this % _videoBlockSize; if (remainder <= _videoBlockSize / 2) { From 8f1b69e6855d166810bf03062e3753ecac33c1b9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 3 Nov 2025 15:39:56 +0100 Subject: [PATCH 07/27] Update --- .../example/integration_test/replay_test.dart | 203 ++++++++++++++++-- .../src/native/cocoa/sentry_native_cocoa.dart | 19 ++ .../native/java/android_replay_recorder.dart | 12 ++ .../src/native/java/sentry_native_java.dart | 27 +++ .../test/native/sentry_native_java_test.dart | 45 ++++ 5 files changed, 283 insertions(+), 23 deletions(-) create mode 100644 packages/flutter/test/native/sentry_native_java_test.dart diff --git a/packages/flutter/example/integration_test/replay_test.dart b/packages/flutter/example/integration_test/replay_test.dart index d8eff357c2..cc5bb77a4f 100644 --- a/packages/flutter/example/integration_test/replay_test.dart +++ b/packages/flutter/example/integration_test/replay_test.dart @@ -1,39 +1,196 @@ +// ignore_for_file: invalid_use_of_internal_member + +import 'dart:async'; import 'dart:io'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter_example/main.dart'; +import 'package:sentry_flutter/src/native/java/sentry_native_java.dart'; +import 'package:sentry_flutter/src/replay/replay_config.dart'; +import 'package:sentry_flutter/src/native/cocoa/sentry_native_cocoa.dart'; +import 'package:sentry_flutter/src/native/java/android_replay_recorder.dart'; +import 'package:sentry_flutter/src/replay/scheduled_recorder_config.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + IntegrationTestWidgetsFlutterBinding.instance.framePolicy = + LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + + const fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; + + tearDown(() async { + await Sentry.close(); + }); + + Future setupSentryAndApp(WidgetTester tester, {String? dsn}) async { + await setupSentry( + () async { + await tester.pumpWidget(SentryScreenshotWidget( + child: DefaultAssetBundle( + bundle: SentryAssetBundle(enableStructuredDataTracing: true), + child: const MyApp(), + ))); + }, + dsn ?? fakeDsn, + isIntegrationTest: true, + ); + } + group('Replay recording', () { - setUp(() async { - await SentryFlutter.init((options) { - // ignore: invalid_use_of_internal_member - options.automatedTestMode = true; - options.dsn = 'https://abc@def.ingest.sentry.io/1234567'; - options.debug = true; - options.replay.sessionSampleRate = 1.0; + testWidgets('native binding is initialized', (tester) async { + await setupSentryAndApp(tester); + expect(SentryFlutter.native, isNotNull); + }); + + testWidgets('supportsReplay matches platform', (tester) async { + await setupSentryAndApp(tester); + final supports = SentryFlutter.native?.supportsReplay ?? false; + expect(supports, Platform.isAndroid || Platform.isIOS ? isTrue : isFalse); + }); + + testWidgets('captureReplay returns a SentryId', (tester) async { + if (!(Platform.isAndroid || Platform.isIOS)) return; + await setupSentryAndApp(tester); + final id = await SentryFlutter.native?.captureReplay(); + expect(id, isA()); + if (Platform.isIOS) { + final current = SentryFlutter.native?.replayId; + expect(current?.toString(), equals(id?.toString())); + } + }); + + testWidgets('sets replay ID to context (Android/iOS)', (tester) async { + await setupSentryAndApp(tester); + + if (Platform.isAndroid) { + final native = SentryFlutter.native as SentryNativeJava?; + expect(native, isNotNull); + await native!.testSetReplayId('123', replayIsBuffering: false); + } else if (Platform.isIOS) { + final native = SentryFlutter.native as SentryNativeCocoa?; + expect(native, isNotNull); + await native!.testSetReplayId('123', replayIsBuffering: false); + } else { + return; + } + + await Sentry.configureScope((scope) async { + expect(scope.replayId?.toString(), '123'); + }); + }); + + testWidgets('clears replay ID from context (Android only)', (tester) async { + if (!Platform.isAndroid) return; + await setupSentryAndApp(tester); + + final native = SentryFlutter.native as SentryNativeJava?; + expect(native, isNotNull); + await native!.testSetReplayId('123', replayIsBuffering: false); + + await Sentry.configureScope((scope) async { + expect(scope.replayId, isNotNull); + }); + + await native.testClearReplayId(); + + await Sentry.configureScope((scope) async { + expect(scope.replayId, isNull); }); }); - tearDown(() async { - await Sentry.close(); + testWidgets('Android: captures images and pause/resume/stop', + (tester) async { + if (!Platform.isAndroid) return; + await setupSentryAndApp(tester); + final native = SentryFlutter.native as SentryNativeJava?; + expect(native, isNotNull); + final recorder = native!.testRecorder; + + // Configure recorder + await recorder + .onConfigurationChanged(const ScheduledScreenshotRecorderConfig( + width: 800, + height: 600, + frameRate: 1, + )); + + var count = 0; + final completer = Completer(); + AndroidReplayRecorder.onScreenshotAddedForTest = () { + count++; + if (!completer.isCompleted) completer.complete(); + }; + + // Start and wait for first frame + await recorder.start(); + await completer.future; + expect(count > 0, isTrue); + + // Pause and ensure count is stable + final pausedAt = count; + await recorder.pause(); + await Future.delayed(const Duration(milliseconds: 200)); + expect(count, equals(pausedAt)); + + // Resume and ensure count increases + await recorder.resume(); + final resumedCompleter = Completer(); + final startCount = count; + AndroidReplayRecorder.onScreenshotAddedForTest = () { + count++; + if (!resumedCompleter.isCompleted && count > startCount) { + resumedCompleter.complete(); + } + }; + await resumedCompleter.future; + expect(count, greaterThan(startCount)); + + // Stop and ensure no further increments + await recorder.stop(); + final stoppedAt = count; + await Future.delayed(const Duration(milliseconds: 200)); + expect(count, equals(stoppedAt)); + AndroidReplayRecorder.onScreenshotAddedForTest = null; }); - test('native binding is initialized', () async { - // ignore: invalid_use_of_internal_member - expect(SentryFlutter.native, isNotNull); + testWidgets('Android: setReplayConfig applies without error', + (tester) async { + if (!Platform.isAndroid) return; + await setupSentryAndApp(tester); + const config = ReplayConfig( + windowWidth: 1080, + windowHeight: 1920, + width: 800, + height: 600, + frameRate: 1, + ); + // Should not throw + await SentryFlutter.native?.setReplayConfig(config); }); - test('session replay is captured', () async { - // TODO add when the beforeSend callback is implemented for replays. - }, skip: true); + testWidgets('iOS: capture screenshot via test recorder returns metadata', + (tester) async { + if (!Platform.isIOS) return; + await setupSentryAndApp(tester); + final native = SentryFlutter.native as SentryNativeCocoa?; + expect(native, isNotNull); + final json = await native!.testRecorder.captureScreenshot(); + expect(json, isNotNull); + expect(json!['length'], isNotNull); + expect(json['address'], isNotNull); + expect(json['width'], isNotNull); + expect(json['height'], isNotNull); + expect((json['width'] as int) > 0, isTrue); + expect((json['height'] as int) > 0, isTrue); - test('replay is captured on errors', () async { - // TODO we may need an HTTP server for this because Android sends replays - // in a separate envelope. - }, skip: true); - }, - skip: Platform.isAndroid - ? false - : "Replay recording is not supported on this platform"); + // Also verify capture works with null replayId + await (SentryFlutter.native as SentryNativeCocoa) + .testSetReplayId(null, replayIsBuffering: false); + final json2 = await native.testRecorder.captureScreenshot(); + expect(json2, isNotNull); + }); + }); } diff --git a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart index 516074033f..26f176c39e 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -27,6 +27,25 @@ class SentryNativeCocoa extends SentryNativeChannel { @override SentryId? get replayId => _replayId; + // Test-only: expose recorder without MethodChannel. + @visibleForTesting + CocoaReplayRecorder get testRecorder { + return _replayRecorder ??= CocoaReplayRecorder(options); + } + + @visibleForTesting + Future testSetReplayId(String? id, + {bool replayIsBuffering = false}) async { + final newId = id == null ? null : SentryId.fromId(id); + if (_replayId != newId) { + _replayId = newId; + await Sentry.configureScope((s) async { + // ignore: invalid_use_of_internal_member + s.replayId = !replayIsBuffering ? _replayId : null; + }); + } + } + @override Future init(Hub hub) async { // We only need these when replay is enabled (session or error capture) diff --git a/packages/flutter/lib/src/native/java/android_replay_recorder.dart b/packages/flutter/lib/src/native/java/android_replay_recorder.dart index 599ac2fd82..c1031e5658 100644 --- a/packages/flutter/lib/src/native/java/android_replay_recorder.dart +++ b/packages/flutter/lib/src/native/java/android_replay_recorder.dart @@ -24,6 +24,10 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder { static AndroidReplayRecorder Function(SentryFlutterOptions) factory = AndroidReplayRecorder.new; + @internal + @visibleForTesting + static void Function()? onScreenshotAddedForTest; + AndroidReplayRecorder(super.options, {SpawnWorkerFn? spawn}) : _config = WorkerConfig( debugName: 'SentryAndroidReplayRecorder', @@ -67,6 +71,14 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder { width: screenshot.width, height: screenshot.height, )); + final callback = onScreenshotAddedForTest; + if (callback != null) { + try { + callback(); + } catch (_) { + // ignore test callback errors + } + } } catch (error, stackTrace) { options.log( SentryLevel.error, diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index 06a2aaee97..52689706a4 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -30,6 +30,29 @@ class SentryNativeJava extends SentryNativeChannel { SentryId? get replayId => _replayId; SentryId? _replayId; + @visibleForTesting + AndroidReplayRecorder get testRecorder { + return _replayRecorder ??= AndroidReplayRecorder.factory(options); + } + + @visibleForTesting + Future testSetReplayId(String? id, + {bool replayIsBuffering = false}) async { + _replayId = id == null ? null : SentryId.fromId(id); + await Sentry.configureScope((s) async { + // ignore: invalid_use_of_internal_member + s.replayId = !replayIsBuffering ? _replayId : null; + }); + } + + @visibleForTesting + Future testClearReplayId() async { + await Sentry.configureScope((s) async { + // ignore: invalid_use_of_internal_member + s.replayId = null; + }); + } + @override Future init(Hub hub) async { // We only need these when replay is enabled (session or error capture) @@ -359,6 +382,8 @@ class SentryNativeJava extends SentryNativeChannel { SentryId captureReplay() { final id = tryCatchSync('captureReplay', () { return using((arena) { + _nativeReplay ??= native.SentryFlutterPlugin.Companion + .privateSentryGetReplayIntegration(); // The passed parameter is `isTerminating` _nativeReplay?.captureReplay(false.toJBoolean()..releasedBy(arena)); final jString = _nativeReplay?.getReplayId().toString$1() @@ -420,6 +445,8 @@ class SentryNativeJava extends SentryNativeChannel { 0, // bitRate is currently not used ); + _nativeReplay ??= native.SentryFlutterPlugin.Companion + .privateSentryGetReplayIntegration(); _nativeReplay?.onConfigurationChanged(replayConfig); }); } diff --git a/packages/flutter/test/native/sentry_native_java_test.dart b/packages/flutter/test/native/sentry_native_java_test.dart new file mode 100644 index 0000000000..e9d01e5d29 --- /dev/null +++ b/packages/flutter/test/native/sentry_native_java_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/src/native/java/sentry_native_java.dart'; + +void main() { + // the ReplaySizeAdjustment tests assumes a constant video block size of 16 + group('ReplaySizeAdjustment', () { + test('rounds down when remainder is less than or equal to half block size', + () { + expect(0.0.adjustReplaySizeToBlockSize(), 0.0); + expect(8.0.adjustReplaySizeToBlockSize(), 0.0); + expect(16.0.adjustReplaySizeToBlockSize(), 16.0); + expect(24.0.adjustReplaySizeToBlockSize(), 16.0); + expect(100.0.adjustReplaySizeToBlockSize(), 96.0); + }); + + test('rounds up when remainder is greater than half block size', () { + expect(9.0.adjustReplaySizeToBlockSize(), 16.0); + expect(15.0.adjustReplaySizeToBlockSize(), 16.0); + expect(25.0.adjustReplaySizeToBlockSize(), 32.0); + expect(108.0.adjustReplaySizeToBlockSize(), 112.0); + expect(109.0.adjustReplaySizeToBlockSize(), 112.0); + }); + + test('returns exact value when already multiple of block size', () { + expect(32.0.adjustReplaySizeToBlockSize(), 32.0); + expect(48.0.adjustReplaySizeToBlockSize(), 48.0); + expect(64.0.adjustReplaySizeToBlockSize(), 64.0); + expect(128.0.adjustReplaySizeToBlockSize(), 128.0); + }); + + test('handles edge cases at half block size boundaries', () { + expect(8.0.adjustReplaySizeToBlockSize(), 0.0); + expect(24.0.adjustReplaySizeToBlockSize(), 16.0); + expect(40.0.adjustReplaySizeToBlockSize(), 32.0); + }); + + test('handles fractional values', () { + expect(7.5.adjustReplaySizeToBlockSize(), 0.0); + expect(8.5.adjustReplaySizeToBlockSize(), 16.0); + expect(15.5.adjustReplaySizeToBlockSize(), 16.0); + expect(16.5.adjustReplaySizeToBlockSize(), 16.0); + expect(24.5.adjustReplaySizeToBlockSize(), 32.0); + }); + }); +} From b27d058c3c1c9295d144f0289761df874073c5c6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 3 Nov 2025 15:52:08 +0100 Subject: [PATCH 08/27] Add comment --- packages/flutter/lib/src/native/java/sentry_native_java.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index 52689706a4..ea6082b4b7 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -403,6 +403,9 @@ class SentryNativeJava extends SentryNativeChannel { @override void setReplayConfig(ReplayConfig config) => tryCatchSync('setReplayConfig', () { + // Since codec block size is 16, so we have to adjust the width and height to it, + // otherwise the codec might fail to configure on some devices, see + // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/MediaCodecInfo.java;l=1999-2001 final invalidConfig = config.width == 0.0 || config.height == 0.0 || config.windowWidth == 0.0 || From 7137617f73bd33d0f0b7d89b1365cef284a4af6b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 11:55:28 +0100 Subject: [PATCH 09/27] Update tests --- .../example/integration_test/replay_test.dart | 179 +++++++++++------- .../native/java/android_replay_recorder.dart | 14 +- .../src/native/java/sentry_native_java.dart | 22 +-- .../test/replay/replay_native_test.dart | 100 +++------- 4 files changed, 145 insertions(+), 170 deletions(-) diff --git a/packages/flutter/example/integration_test/replay_test.dart b/packages/flutter/example/integration_test/replay_test.dart index cc5bb77a4f..13c79ffd00 100644 --- a/packages/flutter/example/integration_test/replay_test.dart +++ b/packages/flutter/example/integration_test/replay_test.dart @@ -22,6 +22,7 @@ void main() { const fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; tearDown(() async { + AndroidReplayRecorder.onScreenshotAddedForTest = null; await Sentry.close(); }); @@ -62,102 +63,152 @@ void main() { } }); - testWidgets('sets replay ID to context (Android/iOS)', (tester) async { + testWidgets('sets replay ID after capturing exception', (tester) async { await setupSentryAndApp(tester); - if (Platform.isAndroid) { - final native = SentryFlutter.native as SentryNativeJava?; - expect(native, isNotNull); - await native!.testSetReplayId('123', replayIsBuffering: false); - } else if (Platform.isIOS) { - final native = SentryFlutter.native as SentryNativeCocoa?; - expect(native, isNotNull); - await native!.testSetReplayId('123', replayIsBuffering: false); - } else { - return; + try { + throw Exception('boom'); + } catch (e, st) { + await Sentry.captureException(e, stackTrace: st); } + // After capture, ReplayEventProcessor should set scope.replayId await Sentry.configureScope((scope) async { - expect(scope.replayId?.toString(), '123'); + expect( + scope.replayId == null || scope.replayId == const SentryId.empty(), + isFalse); + }); + + final current = SentryFlutter.native?.replayId; + await Sentry.configureScope((scope) async { + expect(current?.toString(), equals(scope.replayId?.toString())); }); }); - testWidgets('clears replay ID from context (Android only)', (tester) async { - if (!Platform.isAndroid) return; + testWidgets( + 'replay recorder start emits frame and stop silences frames on Android', + (tester) async { await setupSentryAndApp(tester); - final native = SentryFlutter.native as SentryNativeJava?; expect(native, isNotNull); - await native!.testSetReplayId('123', replayIsBuffering: false); - await Sentry.configureScope((scope) async { - expect(scope.replayId, isNotNull); - }); + // Wait for replay setup to finish triggered by init + await Future.delayed(const Duration(seconds: 2)); + final recorder = native!.testRecorder; + expect(recorder, isNotNull); - await native.testClearReplayId(); + await recorder! + .onConfigurationChanged(const ScheduledScreenshotRecorderConfig( + width: 800, + height: 600, + frameRate: 1, + )); - await Sentry.configureScope((scope) async { - expect(scope.replayId, isNull); - }); - }); + var frameCount = 0; + final firstFrame = Completer(); + AndroidReplayRecorder.onScreenshotAddedForTest = () { + frameCount++; + if (!firstFrame.isCompleted) firstFrame.complete(); + }; - testWidgets('Android: captures images and pause/resume/stop', + const frameDuration = Duration(seconds: 1); // 1 FPS + Future waitForSilenceByCount( + {required Duration quietFor, required Duration maxWait}) async { + final deadline = DateTime.now().add(maxWait); + var prev = frameCount; + while (DateTime.now().isBefore(deadline)) { + await Future.delayed(quietFor); + if (frameCount == prev) return; // no new frames in quiet window + prev = frameCount; // frames arrived; try another window + } + fail('Expected no frames for $quietFor but count kept changing'); + } + + // START: expect first frame from init (replay enabled). If setup is slow, + // allow a slightly longer timeout. + await tester.pump(); + await firstFrame.future.timeout(const Duration(seconds: 5)); + + // STOP: wait until a quiet window without frames + await recorder.stop(); + await tester.pump(); + await waitForSilenceByCount( + quietFor: frameDuration * 2, maxWait: frameDuration * 12); + }, skip: !Platform.isAndroid); + + testWidgets( + 'replay recorder pause silences and resume restarts frames on Android', (tester) async { - if (!Platform.isAndroid) return; await setupSentryAndApp(tester); final native = SentryFlutter.native as SentryNativeJava?; expect(native, isNotNull); + + // Wait for replay setup to finish triggered by init + await Future.delayed(const Duration(seconds: 2)); final recorder = native!.testRecorder; + expect(recorder, isNotNull); - // Configure recorder - await recorder + await recorder! .onConfigurationChanged(const ScheduledScreenshotRecorderConfig( width: 800, height: 600, frameRate: 1, )); - var count = 0; - final completer = Completer(); + // Hook: count frames and capture first + var frameCount = 0; + final firstFrame = Completer(); AndroidReplayRecorder.onScreenshotAddedForTest = () { - count++; - if (!completer.isCompleted) completer.complete(); + frameCount++; + if (!firstFrame.isCompleted) firstFrame.complete(); }; - // Start and wait for first frame - await recorder.start(); - await completer.future; - expect(count > 0, isTrue); + const frameDuration = Duration(seconds: 1); // 1 FPS + Future waitForSilenceByCount({ + required Duration quietFor, + required Duration maxWait, + }) async { + final deadline = DateTime.now().add(maxWait); + var prev = frameCount; + while (DateTime.now().isBefore(deadline)) { + await Future.delayed(quietFor); + if (frameCount == prev) return; // no new frames in quiet window + prev = frameCount; // frames arrived; try another window + } + fail('Expected no frames for $quietFor but count kept changing'); + } - // Pause and ensure count is stable - final pausedAt = count; + // Ensure recording is running: wait for first frame from init + await tester.pump(); + await firstFrame.future.timeout(const Duration(seconds: 5)); + + // PAUSE: expect silence await recorder.pause(); - await Future.delayed(const Duration(milliseconds: 200)); - expect(count, equals(pausedAt)); + await tester.pump(); + final pausedCount = frameCount; + await waitForSilenceByCount( + quietFor: frameDuration * 2, maxWait: frameDuration * 12); + expect(frameCount, equals(pausedCount)); - // Resume and ensure count increases + // RESUME: expect count to increase await recorder.resume(); - final resumedCompleter = Completer(); - final startCount = count; - AndroidReplayRecorder.onScreenshotAddedForTest = () { - count++; - if (!resumedCompleter.isCompleted && count > startCount) { - resumedCompleter.complete(); - } - }; - await resumedCompleter.future; - expect(count, greaterThan(startCount)); + await tester.pump(); + final resumedBaseline = frameCount; + final resumeDeadline = DateTime.now().add(const Duration(seconds: 6)); + while (DateTime.now().isBefore(resumeDeadline) && + frameCount == resumedBaseline) { + await Future.delayed(const Duration(milliseconds: 100)); + } + expect(frameCount, greaterThan(resumedBaseline)); - // Stop and ensure no further increments + // STOP: final silence await recorder.stop(); - final stoppedAt = count; - await Future.delayed(const Duration(milliseconds: 200)); - expect(count, equals(stoppedAt)); - AndroidReplayRecorder.onScreenshotAddedForTest = null; - }); + await tester.pump(); + await waitForSilenceByCount( + quietFor: frameDuration * 2, maxWait: frameDuration * 12); + }, skip: !Platform.isAndroid); - testWidgets('Android: setReplayConfig applies without error', - (tester) async { + testWidgets('setReplayConfig applies without error on iOS', (tester) async { if (!Platform.isAndroid) return; await setupSentryAndApp(tester); const config = ReplayConfig( @@ -169,9 +220,9 @@ void main() { ); // Should not throw await SentryFlutter.native?.setReplayConfig(config); - }); + }, skip: !Platform.isAndroid); - testWidgets('iOS: capture screenshot via test recorder returns metadata', + testWidgets('capture screenshot via test recorder returns metadata on iOS', (tester) async { if (!Platform.isIOS) return; await setupSentryAndApp(tester); @@ -186,11 +237,9 @@ void main() { expect((json['width'] as int) > 0, isTrue); expect((json['height'] as int) > 0, isTrue); - // Also verify capture works with null replayId - await (SentryFlutter.native as SentryNativeCocoa) - .testSetReplayId(null, replayIsBuffering: false); + // Capture again to ensure subsequent captures still succeed final json2 = await native.testRecorder.captureScreenshot(); expect(json2, isNotNull); - }); + }, skip: !Platform.isIOS); }); } diff --git a/packages/flutter/lib/src/native/java/android_replay_recorder.dart b/packages/flutter/lib/src/native/java/android_replay_recorder.dart index c1031e5658..0c2ccec9c8 100644 --- a/packages/flutter/lib/src/native/java/android_replay_recorder.dart +++ b/packages/flutter/lib/src/native/java/android_replay_recorder.dart @@ -24,7 +24,6 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder { static AndroidReplayRecorder Function(SentryFlutterOptions) factory = AndroidReplayRecorder.new; - @internal @visibleForTesting static void Function()? onScreenshotAddedForTest; @@ -36,7 +35,10 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder { automatedTestMode: options.automatedTestMode, ), _spawn = spawn ?? spawnWorker { - super.callback = _addReplayScreenshot; + super.callback = (screenshot, isNewlyCaptured) { + onScreenshotAddedForTest?.call(); + return _addReplayScreenshot(screenshot, isNewlyCaptured); + }; } @override @@ -71,14 +73,6 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder { width: screenshot.width, height: screenshot.height, )); - final callback = onScreenshotAddedForTest; - if (callback != null) { - try { - callback(); - } catch (_) { - // ignore test callback errors - } - } } catch (error, stackTrace) { options.log( SentryLevel.error, diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index ea6082b4b7..f03b2958db 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -31,26 +31,8 @@ class SentryNativeJava extends SentryNativeChannel { SentryId? _replayId; @visibleForTesting - AndroidReplayRecorder get testRecorder { - return _replayRecorder ??= AndroidReplayRecorder.factory(options); - } - - @visibleForTesting - Future testSetReplayId(String? id, - {bool replayIsBuffering = false}) async { - _replayId = id == null ? null : SentryId.fromId(id); - await Sentry.configureScope((s) async { - // ignore: invalid_use_of_internal_member - s.replayId = !replayIsBuffering ? _replayId : null; - }); - } - - @visibleForTesting - Future testClearReplayId() async { - await Sentry.configureScope((s) async { - // ignore: invalid_use_of_internal_member - s.replayId = null; - }); + AndroidReplayRecorder? get testRecorder { + return _replayRecorder; } @override diff --git a/packages/flutter/test/replay/replay_native_test.dart b/packages/flutter/test/replay/replay_native_test.dart index 978d467bcd..d03b07b2b7 100644 --- a/packages/flutter/test/replay/replay_native_test.dart +++ b/packages/flutter/test/replay/replay_native_test.dart @@ -109,7 +109,8 @@ void main() { await tester.pumpWidget(Container()); await tester.pumpAndSettle(); }); - }); + // Skip on Android since JNI cannot be unit tested yet + }, skip: mockPlatform.isAndroid); test( 'clears replay ID from context on ${mockPlatform.operatingSystem.name}', @@ -137,82 +138,31 @@ void main() { when(hub.configureScope(captureAny)).thenReturn(null); await pumpTestElement(tester); - if (mockPlatform.isAndroid) { - nextFrame({bool wait = true}) async { - final future = mockAndroidRecorder.completer.future; - await tester.pumpAndWaitUntil(future, requiredToComplete: wait); - } - - final Map replayConfig = { - 'scope.replayId': '123', - 'replayId': '456', - }; - final configuration = { - 'width': 800, - 'height': 600, - 'frameRate': 1, - }; - await native.invokeFromNative( - 'ReplayRecorder.start', replayConfig); - - await native.invokeFromNative( - 'ReplayRecorder.onConfigurationChanged', configuration); - - await nextFrame(); - expect(mockAndroidRecorder.captured, isNotEmpty); - final screenshot = mockAndroidRecorder.captured.first; - expect(screenshot.width, configuration['width']); - expect(screenshot.height, configuration['height']); - - await native.invokeFromNative('ReplayRecorder.pause'); - var count = mockAndroidRecorder.captured.length; - - await nextFrame(wait: false); - await Future.delayed(const Duration(milliseconds: 100)); - expect(mockAndroidRecorder.captured.length, equals(count)); - - await nextFrame(wait: false); - expect(mockAndroidRecorder.captured.length, equals(count)); - - await native.invokeFromNative('ReplayRecorder.resume'); - - await nextFrame(); - expect(mockAndroidRecorder.captured.length, greaterThan(count)); - - await native.invokeFromNative('ReplayRecorder.stop'); - count = mockAndroidRecorder.captured.length; - await Future.delayed(const Duration(milliseconds: 100)); - await nextFrame(wait: false); - expect(mockAndroidRecorder.captured.length, equals(count)); - } else if (mockPlatform.isIOS) { - final Map replayConfig = { - 'scope.replayId': '123' - }; - - Future captureAndVerify() async { - final future = native.invokeFromNative( - 'captureReplayScreenshot', replayConfig); - final json = (await tester.pumpAndWaitUntil(future)) - as Map; - - expect(json['length'], greaterThan(3000)); - expect(json['address'], greaterThan(0)); - expect(json['width'], 640); - expect(json['height'], 480); - NativeMemory.fromJson(json).free(); - } - - await captureAndVerify(); - - // Check everything works if session-replay rate is 0, - // which causes replayId to be 0 as well. - replayConfig['scope.replayId'] = null; - await captureAndVerify(); - } else { - fail('unsupported platform'); + final Map replayConfig = {'scope.replayId': '123'}; + + Future captureAndVerify() async { + final future = native.invokeFromNative( + 'captureReplayScreenshot', replayConfig); + final json = (await tester.pumpAndWaitUntil(future)) + as Map; + + expect(json['length'], greaterThan(3000)); + expect(json['address'], greaterThan(0)); + expect(json['width'], 640); + expect(json['height'], 480); + NativeMemory.fromJson(json).free(); } + + await captureAndVerify(); + + // Check everything works if session-replay rate is 0, + // which causes replayId to be 0 as well. + replayConfig['scope.replayId'] = null; + await captureAndVerify(); }); - }, timeout: Timeout(Duration(seconds: 10))); + }, + timeout: Timeout(Duration(seconds: 10)), + skip: mockPlatform.isAndroid); }); }); } From 9da5e554be296802413c55d47f469e3d3a591216 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 11:55:53 +0100 Subject: [PATCH 10/27] Clean up --- packages/flutter/lib/src/native/java/sentry_native_java.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index f03b2958db..aa5f1ab9e6 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -31,9 +31,7 @@ class SentryNativeJava extends SentryNativeChannel { SentryId? _replayId; @visibleForTesting - AndroidReplayRecorder? get testRecorder { - return _replayRecorder; - } + AndroidReplayRecorder? get testRecorder => _replayRecorder; @override Future init(Hub hub) async { From f57259778285b23683583b39a4828c82822b6d53 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 13:17:29 +0100 Subject: [PATCH 11/27] Update tests --- .../flutter/test/native/sentry_native_java_test.dart | 6 +++++- .../test/native/sentry_native_java_web_stub.dart | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 packages/flutter/test/native/sentry_native_java_web_stub.dart diff --git a/packages/flutter/test/native/sentry_native_java_test.dart b/packages/flutter/test/native/sentry_native_java_test.dart index e9d01e5d29..a11e552fa6 100644 --- a/packages/flutter/test/native/sentry_native_java_test.dart +++ b/packages/flutter/test/native/sentry_native_java_test.dart @@ -1,5 +1,9 @@ +@TestOn('vm') +library; + import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry_flutter/src/native/java/sentry_native_java.dart'; +import 'sentry_native_java_web_stub.dart' + if (dart.library.io) 'package:sentry_flutter/src/native/java/sentry_native_java.dart'; void main() { // the ReplaySizeAdjustment tests assumes a constant video block size of 16 diff --git a/packages/flutter/test/native/sentry_native_java_web_stub.dart b/packages/flutter/test/native/sentry_native_java_web_stub.dart new file mode 100644 index 0000000000..81c60ea2ac --- /dev/null +++ b/packages/flutter/test/native/sentry_native_java_web_stub.dart @@ -0,0 +1,12 @@ +// Web stub for sentry_native_java.dart +// This file provides only the parts needed for testing on web platform +// without importing JNI or other FFI dependencies. + +import 'package:meta/meta.dart'; + +@visibleForTesting +extension ReplaySizeAdjustment on double { + double adjustReplaySizeToBlockSize() { + return 0; + } +} From c41e9d1dff9563026b7c6ad1e8bc40dee799daca Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 13:18:42 +0100 Subject: [PATCH 12/27] Update tests --- packages/flutter/test/native/sentry_native_java_web_stub.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/test/native/sentry_native_java_web_stub.dart b/packages/flutter/test/native/sentry_native_java_web_stub.dart index 81c60ea2ac..cbc7ad2f32 100644 --- a/packages/flutter/test/native/sentry_native_java_web_stub.dart +++ b/packages/flutter/test/native/sentry_native_java_web_stub.dart @@ -1,5 +1,5 @@ // Web stub for sentry_native_java.dart -// This file provides only the parts needed for testing on web platform +// This file provides only the parts needed for being able to compile on Web // without importing JNI or other FFI dependencies. import 'package:meta/meta.dart'; From 0d578c92f29d289acb6bc0b68f893fd8249c83d6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 13:35:48 +0100 Subject: [PATCH 13/27] Update tests --- packages/flutter/test/replay/replay_native_test.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/flutter/test/replay/replay_native_test.dart b/packages/flutter/test/replay/replay_native_test.dart index d03b07b2b7..40f978e4b5 100644 --- a/packages/flutter/test/replay/replay_native_test.dart +++ b/packages/flutter/test/replay/replay_native_test.dart @@ -160,9 +160,7 @@ void main() { replayConfig['scope.replayId'] = null; await captureAndVerify(); }); - }, - timeout: Timeout(Duration(seconds: 10)), - skip: mockPlatform.isAndroid); + }, timeout: Timeout(Duration(seconds: 10)), skip: !mockPlatform.isIOS); }); }); } From 244c51c93432769d595a83a2164ede77e0daf55e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 13:37:12 +0100 Subject: [PATCH 14/27] Update tests --- packages/flutter/example/integration_test/replay_test.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/flutter/example/integration_test/replay_test.dart b/packages/flutter/example/integration_test/replay_test.dart index 13c79ffd00..347cc2c9dc 100644 --- a/packages/flutter/example/integration_test/replay_test.dart +++ b/packages/flutter/example/integration_test/replay_test.dart @@ -63,6 +63,9 @@ void main() { } }); + // We would like to add a test that ensures a native-initiated replay stop + // clears the replay ID from the scope. Currently we can't add that test + // because FFI/JNI cannot be mocked in this environment. testWidgets('sets replay ID after capturing exception', (tester) async { await setupSentryAndApp(tester); From 247313b9721cfe4d96dd2824837aad77feeba383 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 13:41:50 +0100 Subject: [PATCH 15/27] Update tests --- packages/flutter/example/integration_test/replay_test.dart | 5 ++--- .../flutter/lib/src/native/java/android_replay_recorder.dart | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/flutter/example/integration_test/replay_test.dart b/packages/flutter/example/integration_test/replay_test.dart index 347cc2c9dc..19604c595b 100644 --- a/packages/flutter/example/integration_test/replay_test.dart +++ b/packages/flutter/example/integration_test/replay_test.dart @@ -22,7 +22,6 @@ void main() { const fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; tearDown(() async { - AndroidReplayRecorder.onScreenshotAddedForTest = null; await Sentry.close(); }); @@ -109,7 +108,7 @@ void main() { var frameCount = 0; final firstFrame = Completer(); - AndroidReplayRecorder.onScreenshotAddedForTest = () { + recorder.onScreenshotAddedForTest = () { frameCount++; if (!firstFrame.isCompleted) firstFrame.complete(); }; @@ -161,7 +160,7 @@ void main() { // Hook: count frames and capture first var frameCount = 0; final firstFrame = Completer(); - AndroidReplayRecorder.onScreenshotAddedForTest = () { + recorder.onScreenshotAddedForTest = () { frameCount++; if (!firstFrame.isCompleted) firstFrame.complete(); }; diff --git a/packages/flutter/lib/src/native/java/android_replay_recorder.dart b/packages/flutter/lib/src/native/java/android_replay_recorder.dart index 0c2ccec9c8..0a99968a82 100644 --- a/packages/flutter/lib/src/native/java/android_replay_recorder.dart +++ b/packages/flutter/lib/src/native/java/android_replay_recorder.dart @@ -25,7 +25,7 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder { AndroidReplayRecorder.new; @visibleForTesting - static void Function()? onScreenshotAddedForTest; + void Function()? onScreenshotAddedForTest; AndroidReplayRecorder(super.options, {SpawnWorkerFn? spawn}) : _config = WorkerConfig( From 20b100be399d5f11d5df1671b6c7c5eff13ac9d3 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 13:58:41 +0100 Subject: [PATCH 16/27] Update tests --- .../example/integration_test/replay_test.dart | 58 +++---------------- .../test/replay/replay_native_test.dart | 2 + 2 files changed, 10 insertions(+), 50 deletions(-) diff --git a/packages/flutter/example/integration_test/replay_test.dart b/packages/flutter/example/integration_test/replay_test.dart index 19604c595b..76936b484a 100644 --- a/packages/flutter/example/integration_test/replay_test.dart +++ b/packages/flutter/example/integration_test/replay_test.dart @@ -11,7 +11,6 @@ import 'package:sentry_flutter_example/main.dart'; import 'package:sentry_flutter/src/native/java/sentry_native_java.dart'; import 'package:sentry_flutter/src/replay/replay_config.dart'; import 'package:sentry_flutter/src/native/cocoa/sentry_native_cocoa.dart'; -import 'package:sentry_flutter/src/native/java/android_replay_recorder.dart'; import 'package:sentry_flutter/src/replay/scheduled_recorder_config.dart'; void main() { @@ -94,7 +93,6 @@ void main() { final native = SentryFlutter.native as SentryNativeJava?; expect(native, isNotNull); - // Wait for replay setup to finish triggered by init await Future.delayed(const Duration(seconds: 2)); final recorder = native!.testRecorder; expect(recorder, isNotNull); @@ -113,29 +111,14 @@ void main() { if (!firstFrame.isCompleted) firstFrame.complete(); }; - const frameDuration = Duration(seconds: 1); // 1 FPS - Future waitForSilenceByCount( - {required Duration quietFor, required Duration maxWait}) async { - final deadline = DateTime.now().add(maxWait); - var prev = frameCount; - while (DateTime.now().isBefore(deadline)) { - await Future.delayed(quietFor); - if (frameCount == prev) return; // no new frames in quiet window - prev = frameCount; // frames arrived; try another window - } - fail('Expected no frames for $quietFor but count kept changing'); - } - - // START: expect first frame from init (replay enabled). If setup is slow, - // allow a slightly longer timeout. await tester.pump(); await firstFrame.future.timeout(const Duration(seconds: 5)); - // STOP: wait until a quiet window without frames await recorder.stop(); await tester.pump(); - await waitForSilenceByCount( - quietFor: frameDuration * 2, maxWait: frameDuration * 12); + final afterStopCount = frameCount; + await Future.delayed(const Duration(seconds: 2)); + expect(frameCount, equals(afterStopCount)); }, skip: !Platform.isAndroid); testWidgets( @@ -145,7 +128,6 @@ void main() { final native = SentryFlutter.native as SentryNativeJava?; expect(native, isNotNull); - // Wait for replay setup to finish triggered by init await Future.delayed(const Duration(seconds: 2)); final recorder = native!.testRecorder; expect(recorder, isNotNull); @@ -157,7 +139,6 @@ void main() { frameRate: 1, )); - // Hook: count frames and capture first var frameCount = 0; final firstFrame = Completer(); recorder.onScreenshotAddedForTest = () { @@ -165,49 +146,26 @@ void main() { if (!firstFrame.isCompleted) firstFrame.complete(); }; - const frameDuration = Duration(seconds: 1); // 1 FPS - Future waitForSilenceByCount({ - required Duration quietFor, - required Duration maxWait, - }) async { - final deadline = DateTime.now().add(maxWait); - var prev = frameCount; - while (DateTime.now().isBefore(deadline)) { - await Future.delayed(quietFor); - if (frameCount == prev) return; // no new frames in quiet window - prev = frameCount; // frames arrived; try another window - } - fail('Expected no frames for $quietFor but count kept changing'); - } - - // Ensure recording is running: wait for first frame from init await tester.pump(); await firstFrame.future.timeout(const Duration(seconds: 5)); - // PAUSE: expect silence await recorder.pause(); await tester.pump(); final pausedCount = frameCount; - await waitForSilenceByCount( - quietFor: frameDuration * 2, maxWait: frameDuration * 12); + await Future.delayed(const Duration(seconds: 2)); expect(frameCount, equals(pausedCount)); - // RESUME: expect count to increase await recorder.resume(); await tester.pump(); final resumedBaseline = frameCount; - final resumeDeadline = DateTime.now().add(const Duration(seconds: 6)); - while (DateTime.now().isBefore(resumeDeadline) && - frameCount == resumedBaseline) { - await Future.delayed(const Duration(milliseconds: 100)); - } + await Future.delayed(const Duration(seconds: 3)); expect(frameCount, greaterThan(resumedBaseline)); - // STOP: final silence await recorder.stop(); await tester.pump(); - await waitForSilenceByCount( - quietFor: frameDuration * 2, maxWait: frameDuration * 12); + final afterStopCount = frameCount; + await Future.delayed(const Duration(seconds: 2)); + expect(frameCount, equals(afterStopCount)); }, skip: !Platform.isAndroid); testWidgets('setReplayConfig applies without error on iOS', (tester) async { diff --git a/packages/flutter/test/replay/replay_native_test.dart b/packages/flutter/test/replay/replay_native_test.dart index 40f978e4b5..7c86884917 100644 --- a/packages/flutter/test/replay/replay_native_test.dart +++ b/packages/flutter/test/replay/replay_native_test.dart @@ -171,6 +171,8 @@ class _MockAndroidReplayRecorder extends ScheduledScreenshotRecorder final captured = []; var completer = Completer(); + void Function()? onScreenshotAddedForTest; + _MockAndroidReplayRecorder(super.options) { super.callback = (screenshot, _) async { captured.add(screenshot); From 336e06c789cdd6cf2a7e2af28060df0a914db384 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 3 Nov 2025 08:51:14 +0000 Subject: [PATCH 17/27] release: 9.8.0 --- CHANGELOG.md | 2 +- docs/sdk-versions.md | 1 + packages/dart/lib/src/version.dart | 2 +- packages/dart/pubspec.yaml | 2 +- packages/dio/lib/src/version.dart | 2 +- packages/dio/pubspec.yaml | 4 ++-- packages/drift/lib/src/version.dart | 2 +- packages/drift/pubspec.yaml | 4 ++-- packages/file/lib/src/version.dart | 2 +- packages/file/pubspec.yaml | 4 ++-- packages/firebase_remote_config/pubspec.yaml | 4 ++-- packages/flutter/example/pubspec.yaml | 2 +- packages/flutter/lib/src/version.dart | 2 +- packages/flutter/pubspec.yaml | 4 ++-- packages/hive/lib/src/version.dart | 2 +- packages/hive/pubspec.yaml | 4 ++-- packages/isar/lib/src/version.dart | 2 +- packages/isar/pubspec.yaml | 4 ++-- packages/link/pubspec.yaml | 4 ++-- packages/logging/lib/src/version.dart | 2 +- packages/logging/pubspec.yaml | 4 ++-- packages/sqflite/lib/src/version.dart | 2 +- packages/sqflite/pubspec.yaml | 4 ++-- 23 files changed, 33 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2715fb3186..927bf21f24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 9.8.0 ### Features diff --git a/docs/sdk-versions.md b/docs/sdk-versions.md index 37d3310912..6a380435ee 100644 --- a/docs/sdk-versions.md +++ b/docs/sdk-versions.md @@ -6,6 +6,7 @@ This document shows which version of the various Sentry SDKs are used in which S | Sentry Flutter SDK | Sentry Android SDK | Sentry Cocoa SDK | Sentry JavaScript SDK | Sentry Native SDK | | ------------------ | ------------------ | ---------------- | --------------------- | ----------------- | +| 9.8.0 | 8.21.1 | 8.56.2 | 10.6.0 | 0.10.0 | | 9.7.0 | 8.21.1 | 8.56.2 | 10.6.0 | 0.10.0 | | 9.7.0-beta.5 | 8.21.1 | 8.55.1 | 10.6.0 | 0.10.0 | | 9.6.0 | 8.17.0 | 8.52.1 | 9.40.0 | 0.9.1 | diff --git a/packages/dart/lib/src/version.dart b/packages/dart/lib/src/version.dart index 435d745c40..b5b1e82418 100644 --- a/packages/dart/lib/src/version.dart +++ b/packages/dart/lib/src/version.dart @@ -9,7 +9,7 @@ library; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '9.7.0'; +const String sdkVersion = '9.8.0'; String sdkName(bool isWeb) => isWeb ? _browserSdkName : _ioSdkName; diff --git a/packages/dart/pubspec.yaml b/packages/dart/pubspec.yaml index 3b1a513cc9..d4b539a1d2 100644 --- a/packages/dart/pubspec.yaml +++ b/packages/dart/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 9.7.0 +version: 9.8.0 description: > A crash reporting library for Dart that sends crash reports to Sentry.io. This library supports Dart VM and Web. For Flutter consider sentry_flutter instead. diff --git a/packages/dio/lib/src/version.dart b/packages/dio/lib/src/version.dart index 7a6463fd8e..84a721f3fe 100644 --- a/packages/dio/lib/src/version.dart +++ b/packages/dio/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '9.7.0'; +const String sdkVersion = '9.8.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_dio'; diff --git a/packages/dio/pubspec.yaml b/packages/dio/pubspec.yaml index 1add18b61a..90801ad814 100644 --- a/packages/dio/pubspec.yaml +++ b/packages/dio/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_dio description: An integration which adds support for performance tracing for the Dio package. -version: 9.7.0 +version: 9.8.0 homepage: https://docs.sentry.io/platforms/dart/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -19,7 +19,7 @@ platforms: dependencies: dio: ^5.2.0 - sentry: 9.7.0 + sentry: 9.8.0 dev_dependencies: meta: ^1.3.0 diff --git a/packages/drift/lib/src/version.dart b/packages/drift/lib/src/version.dart index cdcae8e4c0..c2de9746d5 100644 --- a/packages/drift/lib/src/version.dart +++ b/packages/drift/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '9.7.0'; +const String sdkVersion = '9.8.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_drift'; diff --git a/packages/drift/pubspec.yaml b/packages/drift/pubspec.yaml index bf72c0666f..6e7948b144 100644 --- a/packages/drift/pubspec.yaml +++ b/packages/drift/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_drift description: An integration which adds support for performance tracing for the drift package. -version: 9.7.0 +version: 9.8.0 homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -17,7 +17,7 @@ platforms: web: dependencies: - sentry: 9.7.0 + sentry: 9.8.0 meta: ^1.3.0 drift: ^2.24.0 diff --git a/packages/file/lib/src/version.dart b/packages/file/lib/src/version.dart index d97b8815e4..ed0f724c4a 100644 --- a/packages/file/lib/src/version.dart +++ b/packages/file/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '9.7.0'; +const String sdkVersion = '9.8.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_file'; diff --git a/packages/file/pubspec.yaml b/packages/file/pubspec.yaml index a5c26abee9..9263c8575f 100644 --- a/packages/file/pubspec.yaml +++ b/packages/file/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_file description: An integration which adds support for performance tracing for dart.io.File. -version: 9.7.0 +version: 9.8.0 homepage: https://docs.sentry.io/platforms/dart/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -17,7 +17,7 @@ platforms: windows: dependencies: - sentry: 9.7.0 + sentry: 9.8.0 meta: ^1.3.0 dev_dependencies: diff --git a/packages/firebase_remote_config/pubspec.yaml b/packages/firebase_remote_config/pubspec.yaml index aa6c16cb83..23f6b84272 100644 --- a/packages/firebase_remote_config/pubspec.yaml +++ b/packages/firebase_remote_config/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_firebase_remote_config description: "Sentry integration to use feature flags from Firebase Remote Config." -version: 9.7.0 +version: 9.8.0 homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter firebase_remote_config: '>=5.4.3 <7.0.0' - sentry: 9.7.0 + sentry: 9.8.0 dev_dependencies: flutter_test: diff --git a/packages/flutter/example/pubspec.yaml b/packages/flutter/example/pubspec.yaml index 1d54bb831b..4122299125 100644 --- a/packages/flutter/example/pubspec.yaml +++ b/packages/flutter/example/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_flutter_example description: Demonstrates how to use the sentry_flutter plugin. -version: 9.7.0 +version: 9.8.0 publish_to: 'none' # Remove this line if you wish to publish to pub.dev diff --git a/packages/flutter/lib/src/version.dart b/packages/flutter/lib/src/version.dart index 6da082c7f8..58b1d0f635 100644 --- a/packages/flutter/lib/src/version.dart +++ b/packages/flutter/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '9.7.0'; +const String sdkVersion = '9.8.0'; /// The default SDK name reported to Sentry.io in the submitted events. const String sdkName = 'sentry.dart.flutter'; diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index 390f913e1e..2345f64233 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry_flutter -version: 9.7.0 +version: 9.8.0 description: Sentry SDK for Flutter. This package aims to support different Flutter targets by relying on the many platforms supported by Sentry with native SDKs. homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart @@ -23,7 +23,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - sentry: 9.7.0 + sentry: 9.8.0 package_info_plus: '>=1.0.0' meta: ^1.3.0 ffi: ^2.0.0 diff --git a/packages/hive/lib/src/version.dart b/packages/hive/lib/src/version.dart index aed4f93142..72504f247a 100644 --- a/packages/hive/lib/src/version.dart +++ b/packages/hive/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '9.7.0'; +const String sdkVersion = '9.8.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_hive'; diff --git a/packages/hive/pubspec.yaml b/packages/hive/pubspec.yaml index 0cb235d79d..0987b84d72 100644 --- a/packages/hive/pubspec.yaml +++ b/packages/hive/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_hive description: An integration which adds support for performance tracing for the hive package. -version: 9.7.0 +version: 9.8.0 homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -17,7 +17,7 @@ platforms: web: dependencies: - sentry: 9.7.0 + sentry: 9.8.0 hive: ^2.2.3 meta: ^1.3.0 diff --git a/packages/isar/lib/src/version.dart b/packages/isar/lib/src/version.dart index 605f7dba6b..eb9b09a75c 100644 --- a/packages/isar/lib/src/version.dart +++ b/packages/isar/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '9.7.0'; +const String sdkVersion = '9.8.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_isar'; diff --git a/packages/isar/pubspec.yaml b/packages/isar/pubspec.yaml index 9e57e8daac..91db38f933 100644 --- a/packages/isar/pubspec.yaml +++ b/packages/isar/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_isar description: An integration which adds support for performance tracing for the isar package. -version: 9.7.0 +version: 9.8.0 homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -20,7 +20,7 @@ platforms: dependencies: isar: ^3.1.0 isar_flutter_libs: ^3.1.0 # contains Isar Core - sentry: 9.7.0 + sentry: 9.8.0 meta: ^1.3.0 path: ^1.8.3 diff --git a/packages/link/pubspec.yaml b/packages/link/pubspec.yaml index 01d95f1120..0628e6e2a5 100644 --- a/packages/link/pubspec.yaml +++ b/packages/link/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_link description: Automatic capture of exceptions and GraphQL errors for the gql eco-system, like graphql and ferry -version: 9.7.0 +version: 9.8.0 homepage: https://docs.sentry.io/platforms/dart/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -13,7 +13,7 @@ dependencies: gql_exec: ">=0.4.4 <2.0.0" gql_link: ">=0.5.0 <2.0.0" gql: ">=0.14.0 <2.0.0" - sentry: 9.7.0 + sentry: 9.8.0 dev_dependencies: lints: ^4.0.0 diff --git a/packages/logging/lib/src/version.dart b/packages/logging/lib/src/version.dart index 3933c741b3..4eb732ac57 100644 --- a/packages/logging/lib/src/version.dart +++ b/packages/logging/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '9.7.0'; +const String sdkVersion = '9.8.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_logging'; diff --git a/packages/logging/pubspec.yaml b/packages/logging/pubspec.yaml index 31f22596e5..86973e140a 100644 --- a/packages/logging/pubspec.yaml +++ b/packages/logging/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_logging description: An integration which adds support for recording log from the logging package. -version: 9.7.0 +version: 9.8.0 homepage: https://docs.sentry.io/platforms/dart/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -19,7 +19,7 @@ platforms: dependencies: logging: ^1.0.0 - sentry: 9.7.0 + sentry: 9.8.0 meta: ^1.3.0 dev_dependencies: diff --git a/packages/sqflite/lib/src/version.dart b/packages/sqflite/lib/src/version.dart index 66280764a0..6d2f9e139b 100644 --- a/packages/sqflite/lib/src/version.dart +++ b/packages/sqflite/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '9.7.0'; +const String sdkVersion = '9.8.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_sqflite'; diff --git a/packages/sqflite/pubspec.yaml b/packages/sqflite/pubspec.yaml index 442f99cbcb..ce1b41c3c6 100644 --- a/packages/sqflite/pubspec.yaml +++ b/packages/sqflite/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_sqflite description: An integration which adds support for performance tracing for the sqflite package. -version: 9.7.0 +version: 9.8.0 homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -15,7 +15,7 @@ platforms: macos: dependencies: - sentry: 9.7.0 + sentry: 9.8.0 sqflite: ^2.2.8 sqflite_common: ^2.0.0 meta: ^1.3.0 From f196368a4a3709211c8cc176fac6e48b8c03dfe0 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 3 Nov 2025 15:42:39 +0100 Subject: [PATCH 18/27] Replace Android emulator test step with unit test (#3319) --- .github/workflows/flutter_test.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flutter_test.yml b/.github/workflows/flutter_test.yml index d6d27fd131..35ebdfd3bd 100644 --- a/.github/workflows/flutter_test.yml +++ b/.github/workflows/flutter_test.yml @@ -82,18 +82,9 @@ jobs: disable-animations: true script: flutter drive --driver=integration_test/test_driver/driver.dart --target=integration_test/sentry_widgets_flutter_binding_test.dart --profile -d emulator-5554 - - name: launch android emulator & run android native test - uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed #pin@v2.34.0 - with: - working-directory: packages/flutter/example/android - api-level: 31 - profile: Nexus 6 - arch: x86_64 - force-avd-creation: false - avd-name: avd-x86_64-31 - emulator-options: -no-snapshot-save -no-window -accel on -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - script: ./gradlew testDebugUnitTest + - name: Run Android native unit tests + working-directory: packages/flutter/example/android + run: ./gradlew testDebugUnitTest - name: build apk working-directory: packages/flutter/example/android From 1753710425ffb5be07cf0f89bef216c5725caa1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:26:39 +0100 Subject: [PATCH 19/27] build(deps): bump actions/upload-artifact from 4 to 5 (#3315) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/flutter-symbols.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-symbols.yml b/.github/workflows/flutter-symbols.yml index 0ab67c6c98..fd7f10871d 100644 --- a/.github/workflows/flutter-symbols.yml +++ b/.github/workflows/flutter-symbols.yml @@ -52,7 +52,7 @@ jobs: FLUTTER_VERSION: ${{ inputs.flutter_version || '3.*.*' }} - name: Upload updated status cache of processed files - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: always() with: name: flutter-symbol-collector-database From 0248c076a3f16fff0b11de89afccee6f186bd55d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:27:01 +0100 Subject: [PATCH 20/27] build(deps): bump ruby/setup-ruby from 1.263.0 to 1.267.0 (#3316) Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.263.0 to 1.267.0. - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/0481980f17b760ef6bca5e8c55809102a0af1e5a...d5126b9b3579e429dd52e51e68624dda2e05be25) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.267.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/min_version_test.yml | 2 +- .github/workflows/testflight.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/min_version_test.yml b/.github/workflows/min_version_test.yml index 844c214543..21c8d5c714 100644 --- a/.github/workflows/min_version_test.yml +++ b/.github/workflows/min_version_test.yml @@ -51,7 +51,7 @@ jobs: with: flutter-version: '3.24.0' - - uses: ruby/setup-ruby@0481980f17b760ef6bca5e8c55809102a0af1e5a # pin@v1.263.0 + - uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # pin@v1.267.0 with: ruby-version: '3.1.2' # https://github.com/flutter/flutter/issues/109385#issuecomment-1212614125 diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 264f2d5d31..898f4a92fc 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v5 - uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # pin@v2.21.0 - run: xcodes select 16.3 - - uses: ruby/setup-ruby@0481980f17b760ef6bca5e8c55809102a0af1e5a # pin@v1.263.0 + - uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # pin@v1.267.0 with: ruby-version: '2.7.5' bundler-cache: true From e5f85f184b5ad3cf0c02d0777eed8d2962a9df03 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 14:09:22 +0100 Subject: [PATCH 21/27] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 927bf21f24..884fe275f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Enhancements + +- Refactor `captureReplay` and `setReplayConfig` to use FFI/JNI ([#3318](https://github.com/getsentry/sentry-dart/pull/3318)) + ## 9.8.0 ### Features From daacf583967ed72f61eba44908fe4eec586310a3 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 14:19:30 +0100 Subject: [PATCH 22/27] Use round() instead of toInt() --- packages/flutter/lib/src/native/java/sentry_native_java.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index aa5f1ab9e6..a1726cbfe7 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -420,8 +420,8 @@ class SentryNativeJava extends SentryNativeChannel { } final replayConfig = native.ScreenshotRecorderConfig( - adjWidth.toInt(), - adjHeight.toInt(), + adjWidth.round(), + adjHeight.round(), adjWidth / config.windowWidth, adjHeight / config.windowHeight, config.frameRate, From 54a4ef1201e89694531885b4183c7ebf93b631da Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 14:26:14 +0100 Subject: [PATCH 23/27] Fix CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46917eb955..884fe275f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 9.8.0 +## Unreleased ### Enhancements From e8ce749068baef4feaac1d471f1e63c9998a5e79 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 14:53:27 +0100 Subject: [PATCH 24/27] Update --- packages/flutter/lib/src/native/java/sentry_native_java.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index a1726cbfe7..c70db9294e 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -240,6 +240,7 @@ class SentryNativeJava extends SentryNativeChannel { Future close() async { await _replayRecorder?.stop(); await _envelopeSender?.close(); + _nativeReplay?.release(); return super.close(); } @@ -431,6 +432,8 @@ class SentryNativeJava extends SentryNativeChannel { _nativeReplay ??= native.SentryFlutterPlugin.Companion .privateSentryGetReplayIntegration(); _nativeReplay?.onConfigurationChanged(replayConfig); + + replayConfig.release(); }); } From b18a997be45e382acbb89e0060633230ebae4e5c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 15:00:50 +0100 Subject: [PATCH 25/27] Bug bot reviews --- .../example/integration_test/replay_test.dart | 8 ++++---- .../src/native/cocoa/sentry_native_cocoa.dart | 18 +---------------- .../src/native/java/sentry_native_java.dart | 20 +++++++++++++------ 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/packages/flutter/example/integration_test/replay_test.dart b/packages/flutter/example/integration_test/replay_test.dart index 76936b484a..4ac9b1c141 100644 --- a/packages/flutter/example/integration_test/replay_test.dart +++ b/packages/flutter/example/integration_test/replay_test.dart @@ -168,8 +168,8 @@ void main() { expect(frameCount, equals(afterStopCount)); }, skip: !Platform.isAndroid); - testWidgets('setReplayConfig applies without error on iOS', (tester) async { - if (!Platform.isAndroid) return; + testWidgets('setReplayConfig applies without error on Android', + (tester) async { await setupSentryAndApp(tester); const config = ReplayConfig( windowWidth: 1080, @@ -188,7 +188,7 @@ void main() { await setupSentryAndApp(tester); final native = SentryFlutter.native as SentryNativeCocoa?; expect(native, isNotNull); - final json = await native!.testRecorder.captureScreenshot(); + final json = await native!.testRecorder?.captureScreenshot(); expect(json, isNotNull); expect(json!['length'], isNotNull); expect(json['address'], isNotNull); @@ -198,7 +198,7 @@ void main() { expect((json['height'] as int) > 0, isTrue); // Capture again to ensure subsequent captures still succeed - final json2 = await native.testRecorder.captureScreenshot(); + final json2 = await native.testRecorder?.captureScreenshot(); expect(json2, isNotNull); }, skip: !Platform.isIOS); }); diff --git a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart index 26f176c39e..ef11eaf06c 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -27,24 +27,8 @@ class SentryNativeCocoa extends SentryNativeChannel { @override SentryId? get replayId => _replayId; - // Test-only: expose recorder without MethodChannel. @visibleForTesting - CocoaReplayRecorder get testRecorder { - return _replayRecorder ??= CocoaReplayRecorder(options); - } - - @visibleForTesting - Future testSetReplayId(String? id, - {bool replayIsBuffering = false}) async { - final newId = id == null ? null : SentryId.fromId(id); - if (_replayId != newId) { - _replayId = newId; - await Sentry.configureScope((s) async { - // ignore: invalid_use_of_internal_member - s.replayId = !replayIsBuffering ? _replayId : null; - }); - } - } + CocoaReplayRecorder? get testRecorder => _replayRecorder; @override Future init(Hub hub) async { diff --git a/packages/flutter/lib/src/native/java/sentry_native_java.dart b/packages/flutter/lib/src/native/java/sentry_native_java.dart index c70db9294e..e7784ba814 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -367,14 +367,22 @@ class SentryNativeJava extends SentryNativeChannel { .privateSentryGetReplayIntegration(); // The passed parameter is `isTerminating` _nativeReplay?.captureReplay(false.toJBoolean()..releasedBy(arena)); - final jString = _nativeReplay?.getReplayId().toString$1() - ?..releasedBy(arena); - if (jString == null) { - return SentryId.empty(); - } else { - return SentryId.fromId(jString.toDartString()); + final nativeReplayId = _nativeReplay?.getReplayId(); + nativeReplayId?.releasedBy(arena); + + JString? jString; + if (nativeReplayId != null) { + jString = nativeReplayId.toString$1(); + jString?.releasedBy(arena); } + + final result = jString == null + ? SentryId.empty() + : SentryId.fromId(jString.toDartString()); + + _replayId = result; + return result; }); }); From 84910f6e1eaf911d7ab326476abe45a52f3db565 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 15:06:21 +0100 Subject: [PATCH 26/27] Add test --- .../flutter/example/integration_test/replay_test.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/flutter/example/integration_test/replay_test.dart b/packages/flutter/example/integration_test/replay_test.dart index 4ac9b1c141..b35f9bd75e 100644 --- a/packages/flutter/example/integration_test/replay_test.dart +++ b/packages/flutter/example/integration_test/replay_test.dart @@ -61,6 +61,15 @@ void main() { } }); + testWidgets('captureReplay sets native replay ID', (tester) async { + if (!(Platform.isAndroid || Platform.isIOS)) return; + await setupSentryAndApp(tester); + final id = await SentryFlutter.native?.captureReplay(); + expect(id, isA()); + expect(SentryFlutter.native?.replayId, isNotNull); + expect(SentryFlutter.native?.replayId, isNot(const SentryId.empty())); + }); + // We would like to add a test that ensures a native-initiated replay stop // clears the replay ID from the scope. Currently we can't add that test // because FFI/JNI cannot be mocked in this environment. From 58910107bd6b937266fe94634d1d332eb31a7fed Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 4 Nov 2025 15:36:04 +0100 Subject: [PATCH 27/27] Fix test --- packages/flutter/example/integration_test/replay_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/flutter/example/integration_test/replay_test.dart b/packages/flutter/example/integration_test/replay_test.dart index b35f9bd75e..3217a4a6f6 100644 --- a/packages/flutter/example/integration_test/replay_test.dart +++ b/packages/flutter/example/integration_test/replay_test.dart @@ -187,16 +187,19 @@ void main() { height: 600, frameRate: 1, ); + await Future.delayed(const Duration(seconds: 2)); + // Should not throw await SentryFlutter.native?.setReplayConfig(config); }, skip: !Platform.isAndroid); testWidgets('capture screenshot via test recorder returns metadata on iOS', (tester) async { - if (!Platform.isIOS) return; await setupSentryAndApp(tester); final native = SentryFlutter.native as SentryNativeCocoa?; expect(native, isNotNull); + + await Future.delayed(const Duration(seconds: 2)); final json = await native!.testRecorder?.captureScreenshot(); expect(json, isNotNull); expect(json!['length'], isNotNull);