Skip to content

Commit

Permalink
refactor: use JNI for Android integration (#2670)
Browse files Browse the repository at this point in the history
* add JNI and use in replay.onScreenshotRecorded

* exlude bindings from coverage

* use jni bitmap

* move all native to android_replay_recorder

* android screenshot isolate

* cleanup

* use persistent isolate

* update replay native tests

* update high-risk-code list

* remove print()

* remove package:file dependency

* JNI benchmark

* ktlint format

* fixup ci

* fixup

* fix web tests

* dart format

* try to fix ci

* try to fix ci

* chore: exclude binding from coverage

* comments

* chore: changelog

* move uint8list creation to separate isolate

* Update CHANGELOG.md

Co-authored-by: Giancarlo Buenaflor <giancarlo_buenaflor@yahoo.com>

---------

Co-authored-by: Giancarlo Buenaflor <giancarlo_buenaflor@yahoo.com>
  • Loading branch information
vaind and buenaflor authored Feb 13, 2025
1 parent 3ab3095 commit e1f690d
Show file tree
Hide file tree
Showing 18 changed files with 4,490 additions and 126 deletions.
2 changes: 1 addition & 1 deletion .github/actions/coverage/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ runs:
with:
path: './${{ inputs.directory }}/coverage/lcov.info'
min_coverage: ${{ inputs.min-coverage }}
exclude: 'lib/src/native/cocoa/binding.dart'
exclude: 'lib/src/native/**/binding.dart lib/src/native/java/android_replay_recorder.dart'
1 change: 1 addition & 0 deletions .github/file-filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ high_risk_code: &high_risk_code
- "flutter/ios/Classes/SentryFlutterPluginApple.swift"
- "flutter/lib/src/screenshot/recorder.dart"
- "flutter/lib/src/screenshot/widget_filter.dart"
- "flutter/lib/src/native/java/android_replay_recorder.dart"
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased 9.0.0

### Breaking changes

- Remove `SentryDisplayWidget` and manual TTID implementation ([#2668](https://github.com/getsentry/sentry-dart/pull/2668))
- Increase minimum SDK version requirements to Dart v3.5.0 and Flutter v3.24.0 ([#2643](https://github.com/getsentry/sentry-dart/pull/2643))
- Remove screenshot option `attachScreenshotOnlyWhenResumed` ([#2664](https://github.com/getsentry/sentry-dart/pull/2664))
Expand All @@ -12,6 +14,10 @@
- Add hint for transactions ([#2675](https://github.com/getsentry/sentry-dart/pull/2675))
- `BeforeSendTransactionCallback` now has a `Hint` parameter

### Enhancements

- Replay: improve Android native interop performance by using JNI ([#2670](https://github.com/getsentry/sentry-dart/pull/2670))

### Dependencies

- Bump Android SDK from v7.20.1 to v8.1.0 ([#2650](https://github.com/getsentry/sentry-dart/pull/2650))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.flutter

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
Expand Down Expand Up @@ -35,7 +36,6 @@ import io.sentry.protocol.DebugImage
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

Expand All @@ -49,7 +49,6 @@ class SentryFlutterPlugin :
private lateinit var channel: MethodChannel
private lateinit var context: Context
private lateinit var sentryFlutter: SentryFlutter
private lateinit var replay: ReplayIntegration

// Note: initial config because we don't yet have the numbers of the actual Flutter widget.
// See how SentryFlutterReplayRecorder.start() handles it. New settings will be set by setReplayConfig() method below.
Expand Down Expand Up @@ -103,7 +102,6 @@ class SentryFlutterPlugin :
"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 @@ -164,15 +162,13 @@ class SentryFlutterPlugin :
private fun setupReplay(options: SentryAndroidOptions) {
// Replace the default ReplayIntegration with a Flutter-specific recorder.
options.integrations.removeAll { it is ReplayIntegration }
val cacheDirPath = options.cacheDirPath
val replayOptions = options.sessionReplay
val isReplayEnabled = replayOptions.isSessionReplayEnabled || replayOptions.isSessionReplayForErrorsEnabled
if (cacheDirPath != null && isReplayEnabled) {
if (replayOptions.isSessionReplayEnabled || replayOptions.isSessionReplayForErrorsEnabled) {
replay =
ReplayIntegration(
context,
context.applicationContext,
dateProvider = CurrentDateProvider.getInstance(),
recorderProvider = { SentryFlutterReplayRecorder(channel, replay) },
recorderProvider = { SentryFlutterReplayRecorder(channel, replay!!) },
recorderConfigProvider = {
Log.i(
"Sentry",
Expand All @@ -187,8 +183,8 @@ class SentryFlutterPlugin :
},
replayCacheProvider = null,
)
replay.breadcrumbConverter = SentryFlutterReplayBreadcrumbConverter()
options.addIntegration(replay)
replay!!.breadcrumbConverter = SentryFlutterReplayBreadcrumbConverter()
options.addIntegration(replay!!)
options.setReplayController(replay)
} else {
options.setReplayController(null)
Expand Down Expand Up @@ -517,8 +513,13 @@ class SentryFlutterPlugin :
}

companion object {
@SuppressLint("StaticFieldLeak")
private var replay: ReplayIntegration? = null

private const val NATIVE_CRASH_WAIT_TIME = 500L

@JvmStatic fun privateSentryGetReplayIntegration(): ReplayIntegration? = replay

private fun crash() {
val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException")
val mainThread = Looper.getMainLooper().thread
Expand Down Expand Up @@ -552,19 +553,6 @@ class SentryFlutterPlugin :
result.success(serializedScope)
}

private fun addReplayScreenshot(
path: String?,
timestamp: Long?,
result: Result,
) {
if (path == null || timestamp == null) {
result.error("5", "Arguments are null", null)
return
}
replay.onScreenshotRecorded(File(path), timestamp)
result.success("")
}

private fun setReplayConfig(
call: MethodCall,
result: Result,
Expand Down Expand Up @@ -614,7 +602,7 @@ class SentryFlutterPlugin :
replayConfig.bitRate,
),
)
replay.onConfigurationChanged(Configuration())
replay!!.onConfigurationChanged(Configuration())
result.success("")
}

Expand All @@ -626,7 +614,7 @@ class SentryFlutterPlugin :
result.error("5", "Arguments are null", null)
return
}
replay.captureReplay(isCrash)
result.success(replay.getReplayId().toString())
replay!!.captureReplay(isCrash)
result.success(replay!!.getReplayId().toString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,11 @@ internal class SentryFlutterReplayRecorder(
return
}

val cacheDirPath = integration.replayCacheDir?.absolutePath
if (cacheDirPath == null) {
Log.w("Sentry", "Replay cache directory is null, can't start replay recorder.")
return
}
Handler(Looper.getMainLooper()).post {
try {
channel.invokeMethod(
"ReplayRecorder.start",
mapOf(
"directory" to cacheDirPath,
"width" to recorderConfig.recordingWidth,
"height" to recorderConfig.recordingHeight,
"frameRate" to recorderConfig.frameRate,
Expand Down
18 changes: 18 additions & 0 deletions flutter/ffi-jni.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
android_sdk_config:
add_gradle_deps: true
android_example: 'example/'

# summarizer:
# backend: asm

output:
dart:
path: lib/src/native/java/binding.dart
structure: single_file

log_level: all

classes:
- io.sentry.android.replay.ReplayIntegration
- io.sentry.flutter.SentryFlutterPlugin
- android.graphics.Bitmap
Loading

0 comments on commit e1f690d

Please # to comment.