Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replay orientation change #2462

Merged
merged 23 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.sentry.flutter

import android.app.Activity
import android.content.Context
import android.content.res.Configuration;
import android.os.Build
import android.os.Looper
import android.util.Log
Expand All @@ -27,13 +28,15 @@ import io.sentry.android.core.SentryAndroidOptions
import io.sentry.android.core.performance.AppStartMetrics
import io.sentry.android.core.performance.TimeSpan
import io.sentry.android.replay.ReplayIntegration
import io.sentry.android.replay.ScreenshotRecorderConfig
import io.sentry.protocol.DebugImage
import io.sentry.protocol.SdkVersion
import io.sentry.protocol.SentryId
import io.sentry.protocol.User
import io.sentry.transport.CurrentDateProvider
import java.io.File
import java.lang.ref.WeakReference
import kotlin.math.roundToInt

private const val APP_START_MAX_DURATION_MS = 60000

Expand All @@ -42,6 +45,14 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var context: Context
private lateinit var sentryFlutter: SentryFlutter
private lateinit var replay: ReplayIntegration
private var replayConfig = ScreenshotRecorderConfig(
recordingWidth = 0,
recordingHeight = 0,
scaleFactorX = 1.0f,
scaleFactorY = 1.0f,
frameRate = 0,
bitRate = 0
)

private var activity: WeakReference<Activity>? = null
private var framesTracker: ActivityFramesTracker? = null
Expand Down Expand Up @@ -86,6 +97,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
"loadContexts" -> loadContexts(result)
"displayRefreshRate" -> displayRefreshRate(result)
"nativeCrash" -> crash()
"setReplayConfig" -> setReplayConfig(call, result)
"addReplayScreenshot" -> addReplayScreenshot(call.argument("path"), call.argument("timestamp"), result)
"captureReplay" -> captureReplay(call.argument("isCrash"), result)
else -> result.notImplemented()
Expand Down Expand Up @@ -152,7 +164,13 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
context,
dateProvider = CurrentDateProvider.getInstance(),
recorderProvider = { SentryFlutterReplayRecorder(channel, replay) },
recorderConfigProvider = null,
recorderConfigProvider = {
Log.i(
"Sentry",
"Replay configuration requested. Returning: %dx%d at %d FPS, %d BPS".format(replayConfig.recordingWidth, replayConfig.recordingHeight, replayConfig.frameRate, replayConfig.bitRate)
)
replayConfig
},
replayCacheProvider = null,
)
replay.breadcrumbConverter = SentryFlutterReplayBreadcrumbConverter()
Expand Down Expand Up @@ -181,6 +199,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
"Invalid app start data: app not launched in foreground or app start took too long (>60s)",
)
result.success(null)
return
}

val appStartTimeSpan = appStartMetrics.appStartTimeSpan
Expand Down Expand Up @@ -546,6 +565,20 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
mainThread.uncaughtExceptionHandler.uncaughtException(mainThread, exception)
mainThread.join(NATIVE_CRASH_WAIT_TIME)
}

/**
* 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
*/
private fun Int.adjustReplaySizeToBlockSize(): Int {
val remainder = this % 16
return if (remainder <= 8) {
this - remainder
} else {
this + (16 - remainder)
}
}
}

private fun loadContexts(result: Result) {
Expand Down Expand Up @@ -577,6 +610,23 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.success("")
}

private fun setReplayConfig(call: MethodCall, result: Result) {
replayConfig = ScreenshotRecorderConfig(
recordingWidth = (call.argument("width") as? Int)?.adjustReplaySizeToBlockSize() ?: 0,
recordingHeight = (call.argument("height") as? Int)?.adjustReplaySizeToBlockSize() ?: 0,
scaleFactorX = 1.0f,
scaleFactorY = 1.0f,
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(Configuration())
result.success("")
}

private fun captureReplay(
isCrash: Boolean?,
result: Result,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ internal class SentryFlutterReplayRecorder(
private val integration: ReplayIntegration,
) : Recorder {
override fun start(recorderConfig: ScreenshotRecorderConfig) {
// Ignore if this is the initial call before we actually got the configuration from Flutter.
// We'll get another call here when the configuration is set.
if (recorderConfig.recordingHeight == 0 && recorderConfig.recordingWidth == 0) {
return;
}

val cacheDirPath = integration.replayCacheDir?.absolutePath
if (cacheDirPath == null) {
Log.w("Sentry", "Replay cache directory is null, can't start replay recorder.")
Expand Down
9 changes: 9 additions & 0 deletions flutter/lib/src/native/c/sentry_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
import '../../replay/replay_config.dart';
import '../native_app_start.dart';
import '../native_frames.dart';
import '../sentry_native_binding.dart';
Expand Down Expand Up @@ -268,6 +269,14 @@
Pointer.fromAddress(1).cast<Utf8>().toDartString();
}

@override
bool get supportsReplay => false;

@override

Check warning on line 275 in flutter/lib/src/native/c/sentry_native.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/c/sentry_native.dart#L275

Added line #L275 was not covered by tests
FutureOr<void> setReplayConfig(ReplayConfig config) {
_logNotSupported('replay config');

Check warning on line 277 in flutter/lib/src/native/c/sentry_native.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/c/sentry_native.dart#L277

Added line #L277 was not covered by tests
}

@override
FutureOr<SentryId> captureReplay(bool isCrash) {
_logNotSupported('capturing replay');
Expand Down
15 changes: 4 additions & 11 deletions flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import 'dart:ui';
import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
import '../../event_processor/replay_event_processor.dart';
import '../../screenshot/recorder.dart';
import '../../screenshot/recorder_config.dart';
import '../../replay/integration.dart';
import '../sentry_native_channel.dart';
import 'binding.dart' as cocoa;

Expand All @@ -20,19 +18,14 @@ class SentryNativeCocoa extends SentryNativeChannel {

SentryNativeCocoa(super.options);

@override
bool get supportsReplay => options.platformChecker.platform.isIOS;

@override
Future<void> init(Hub hub) async {
// We only need these when replay is enabled (session or error capture)
// so let's set it up conditionally. This allows Dart to trim the code.
if (options.experimental.replay.isEnabled &&
options.platformChecker.platform.isIOS) {
options.sdk.addIntegration(replayIntegrationName);

// We only need the integration when error-replay capture is enabled.
if ((options.experimental.replay.onErrorSampleRate ?? 0) > 0) {
options.addEventProcessor(ReplayEventProcessor(hub, this));
}

if (options.experimental.replay.isEnabled) {
channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'captureReplayScreenshot':
Expand Down
12 changes: 3 additions & 9 deletions flutter/lib/src/native/java/sentry_native_java.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import 'package:flutter/services.dart';
import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
import '../../event_processor/replay_event_processor.dart';
import '../../replay/integration.dart';
import '../../replay/scheduled_recorder.dart';
import '../../replay/scheduled_recorder_config.dart';
import '../sentry_native_channel.dart';
Expand All @@ -19,18 +17,14 @@ class SentryNativeJava extends SentryNativeChannel {
_IdleFrameFiller? _idleFrameFiller;
SentryNativeJava(super.options);

@override
bool get supportsReplay => true;

@override
Future<void> init(Hub hub) async {
// We only need these when replay is enabled (session or error capture)
// so let's set it up conditionally. This allows Dart to trim the code.
if (options.experimental.replay.isEnabled) {
options.sdk.addIntegration(replayIntegrationName);

// We only need the integration when error-replay capture is enabled.
if ((options.experimental.replay.onErrorSampleRate ?? 0) > 0) {
options.addEventProcessor(ReplayEventProcessor(hub, this));
}

channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'ReplayRecorder.start':
Expand Down
5 changes: 5 additions & 0 deletions flutter/lib/src/native/sentry_native_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
import '../replay/replay_config.dart';
import 'native_app_start.dart';
import 'native_frames.dart';

Expand Down Expand Up @@ -64,5 +65,9 @@ abstract class SentryNativeBinding {

FutureOr<void> nativeCrash();

bool get supportsReplay;

FutureOr<void> setReplayConfig(ReplayConfig config);

FutureOr<SentryId> captureReplay(bool isCrash);
}
13 changes: 13 additions & 0 deletions flutter/lib/src/native/sentry_native_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
import '../replay/replay_config.dart';
import 'native_app_start.dart';
import 'native_frames.dart';
import 'method_channel_helper.dart';
Expand Down Expand Up @@ -216,6 +217,18 @@
@override
Future<void> nativeCrash() => channel.invokeMethod('nativeCrash');

@override

Check warning on line 220 in flutter/lib/src/native/sentry_native_channel.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/sentry_native_channel.dart#L220

Added line #L220 was not covered by tests
bool get supportsReplay => false;

@override

Check warning on line 223 in flutter/lib/src/native/sentry_native_channel.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/sentry_native_channel.dart#L223

Added line #L223 was not covered by tests
Future<void> setReplayConfig(ReplayConfig config) =>
channel.invokeMethod('setReplayConfig', {
'width': config.width,
'height': config.height,
'frameRate': config.frameRate,
'bitRate': config.bitRate,

Check warning on line 229 in flutter/lib/src/native/sentry_native_channel.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/sentry_native_channel.dart#L225-L229

Added lines #L225 - L229 were not covered by tests
});

@override
Future<SentryId> captureReplay(bool isCrash) =>
channel.invokeMethod('captureReplay', {
Expand Down
40 changes: 38 additions & 2 deletions flutter/lib/src/replay/integration.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
import 'dart:async';

import 'package:meta/meta.dart';

/// We don't actually have an integration, just want to have a name reported
/// on events so we can filter them out.
import '../../sentry_flutter.dart';
import '../event_processor/replay_event_processor.dart';
import '../native/sentry_native_binding.dart';
import 'replay_config.dart';

@internal
const replayIntegrationName = 'ReplayIntegration';

@internal
class ReplayIntegration extends Integration<SentryFlutterOptions> {
final SentryNativeBinding _native;

ReplayIntegration(this._native);

@override
FutureOr<void> call(Hub hub, SentryFlutterOptions options) {
if (_native.supportsReplay && options.experimental.replay.isEnabled) {
options.sdk.addIntegration(replayIntegrationName);

// We only need the integration when error-replay capture is enabled.
if ((options.experimental.replay.onErrorSampleRate ?? 0) > 0) {
options.addEventProcessor(ReplayEventProcessor(hub, _native));
}

SentryScreenshotWidget.onBuild((status, prevStatus) {
if (status != prevStatus) {
_native.setReplayConfig(ReplayConfig(
width: status.size?.width.round() ?? 0,
height: status.size?.height.round() ?? 0,

Check warning on line 33 in flutter/lib/src/replay/integration.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/replay/integration.dart#L30-L33

Added lines #L30 - L33 were not covered by tests
frameRate: 1,
bitRate: 75000, // TODO replay quality config
));
}
return true;
});
}
}
}
22 changes: 22 additions & 0 deletions flutter/lib/src/replay/replay_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:meta/meta.dart';

import 'scheduled_recorder_config.dart';

@immutable
@internal
class ReplayConfig extends ScheduledScreenshotRecorderConfig {
@override
int get width => super.width!;

Check warning on line 9 in flutter/lib/src/replay/replay_config.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/replay/replay_config.dart#L8-L9

Added lines #L8 - L9 were not covered by tests

@override
int get height => super.height!;

Check warning on line 12 in flutter/lib/src/replay/replay_config.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/replay/replay_config.dart#L11-L12

Added lines #L11 - L12 were not covered by tests

final int bitRate;

const ReplayConfig({

Check warning on line 16 in flutter/lib/src/replay/replay_config.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/replay/replay_config.dart#L16

Added line #L16 was not covered by tests
required int super.width,
required int super.height,
required super.frameRate,
required this.bitRate,
});
}
2 changes: 1 addition & 1 deletion flutter/lib/src/replay/scheduled_recorder_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import '../screenshot/recorder_config.dart';
class ScheduledScreenshotRecorderConfig extends ScreenshotRecorderConfig {
final int frameRate;

ScheduledScreenshotRecorderConfig({
const ScheduledScreenshotRecorderConfig({
super.width,
super.height,
required this.frameRate,
Expand Down
2 changes: 1 addition & 1 deletion flutter/lib/src/screenshot/recorder_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ScreenshotRecorderConfig {
final int? width;
final int? height;

ScreenshotRecorderConfig({
const ScreenshotRecorderConfig({
this.width,
this.height,
});
Expand Down
Loading
Loading