Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
03f8118
Update
buenaflor Sep 2, 2025
6928f3a
Update
buenaflor Sep 3, 2025
a91cbae
Update
buenaflor Sep 3, 2025
a6bd3cc
Update
buenaflor Sep 3, 2025
b9269c7
Update
buenaflor Sep 3, 2025
6ef9960
Update
buenaflor Sep 4, 2025
a43f2e1
Configure diagnostic log
buenaflor Sep 4, 2025
e334269
Update log messages
buenaflor Sep 4, 2025
aa728e7
Update
buenaflor Sep 4, 2025
45cc8c3
Update
buenaflor Sep 4, 2025
a603960
Update
buenaflor Sep 4, 2025
147da01
Update
buenaflor Sep 4, 2025
2b11149
Update
buenaflor Sep 4, 2025
8325952
Update
buenaflor Sep 4, 2025
3dbe751
Update
buenaflor Sep 4, 2025
71ba593
Update
buenaflor Sep 4, 2025
fe7f6df
Update
buenaflor Sep 4, 2025
39a951e
Update
buenaflor Oct 7, 2025
884642f
Fix test
buenaflor Oct 7, 2025
f5f5401
Update
buenaflor Oct 7, 2025
de232c6
Update
buenaflor Oct 7, 2025
69d5111
Update
buenaflor Oct 7, 2025
79d9ff9
Merge branch 'main' into enh/long-lived-envelope-worker
buenaflor Oct 7, 2025
62bb12b
Add automatedTestMode option
buenaflor Oct 7, 2025
532d2b3
Merge branch 'main' into enh/long-lived-envelope-worker
buenaflor Oct 7, 2025
53c6036
Update
buenaflor Oct 7, 2025
06ee227
Fix web tests
buenaflor Oct 7, 2025
10d9419
Update
buenaflor Oct 7, 2025
e6771bb
Update
buenaflor Oct 7, 2025
e2ae6a3
Add close
buenaflor Oct 7, 2025
ae9b24c
Review
buenaflor Oct 8, 2025
7efb747
Merge branch 'main' into enh/long-lived-envelope-worker
buenaflor Oct 8, 2025
4b440d0
Review
buenaflor Oct 8, 2025
91b0298
Update
buenaflor Oct 8, 2025
2cd6dd8
Update
buenaflor Oct 8, 2025
2f1840d
Update
buenaflor Oct 8, 2025
7750213
Update
buenaflor Oct 9, 2025
cd554e4
Update
buenaflor Oct 9, 2025
9708168
Update
buenaflor Oct 9, 2025
99b5584
Update
buenaflor Oct 9, 2025
eead0ce
Update
buenaflor Oct 9, 2025
780a59c
Update
buenaflor Oct 9, 2025
143414f
Update
buenaflor Oct 9, 2025
60f9674
Update
buenaflor Oct 9, 2025
3eeda60
Fix tests
buenaflor Oct 9, 2025
a40112f
Update
buenaflor Oct 9, 2025
83f8ea4
Update
buenaflor Oct 9, 2025
f9edebc
Update
buenaflor Oct 9, 2025
895068c
Merge branch 'main' into enh/app-start-refresh-rate-jni-ffi
buenaflor Oct 13, 2025
1ffba6f
Update
buenaflor Oct 14, 2025
9733bc8
Merge branch 'main' into enh/app-start-refresh-rate-jni-ffi
buenaflor Oct 14, 2025
958fa0d
Update
buenaflor Oct 16, 2025
84542ab
Update
buenaflor Oct 16, 2025
e744a5e
Update
buenaflor Oct 16, 2025
9912628
Update
buenaflor Oct 16, 2025
dee9256
Update
buenaflor Oct 16, 2025
ab2f4e9
Update
buenaflor Oct 16, 2025
ed51094
Update
buenaflor Oct 16, 2025
0d378b3
Update
buenaflor Oct 16, 2025
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 @@ -41,10 +41,6 @@ class SentryFlutterPlugin :
ActivityAware {
private lateinit var channel: MethodChannel
private lateinit var context: Context
private lateinit var sentryFlutter: SentryFlutter

private var activity: WeakReference<Activity>? = null
private var pluginRegistrationTime: Long? = null

override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
pluginRegistrationTime = System.currentTimeMillis()
Expand All @@ -65,7 +61,6 @@ class SentryFlutterPlugin :
when (call.method) {
"initNativeSdk" -> initNativeSdk(call, result)
"closeNativeSdk" -> closeNativeSdk(result)
"fetchNativeAppStart" -> fetchNativeAppStart(result)
"setContexts" -> setContexts(call.argument("key"), call.argument("value"), result)
"removeContexts" -> removeContexts(call.argument("key"), result)
"setUser" -> setUser(call.argument("user"), result)
Expand All @@ -75,7 +70,6 @@ class SentryFlutterPlugin :
"removeExtra" -> removeExtra(call.argument("key"), result)
"setTag" -> setTag(call.argument("key"), call.argument("value"), result)
"removeTag" -> removeTag(call.argument("key"), result)
"displayRefreshRate" -> displayRefreshRate(result)
"nativeCrash" -> crash()
"setReplayConfig" -> setReplayConfig(call, result)
"captureReplay" -> captureReplay(result)
Expand Down Expand Up @@ -151,106 +145,6 @@ class SentryFlutterPlugin :
}
}

private fun fetchNativeAppStart(result: Result) {
if (!sentryFlutter.autoPerformanceTracingEnabled) {
result.success(null)
return
}

val appStartMetrics = AppStartMetrics.getInstance()

if (!appStartMetrics.isAppLaunchedInForeground ||
appStartMetrics.appStartTimeSpan.durationMs > APP_START_MAX_DURATION_MS
) {
Log.w(
"Sentry",
"Invalid app start data: app not launched in foreground or app start took too long (>60s)",
)
result.success(null)
return
}

val appStartTimeSpan = appStartMetrics.appStartTimeSpan
val appStartTime = appStartTimeSpan.startTimestamp
val isColdStart = appStartMetrics.appStartType == AppStartMetrics.AppStartType.COLD

if (appStartTime == null) {
Log.w("Sentry", "App start won't be sent due to missing appStartTime")
result.success(null)
} else {
val appStartTimeMillis = DateUtils.nanosToMillis(appStartTime.nanoTimestamp().toDouble())
val item =

mutableMapOf<String, Any?>(
"pluginRegistrationTime" to pluginRegistrationTime,
"appStartTime" to appStartTimeMillis,
"isColdStart" to isColdStart,
)

val androidNativeSpans = mutableMapOf<String, Any?>()

val processInitSpan =
TimeSpan().apply {
description = "Process Initialization"
setStartUnixTimeMs(appStartTimeSpan.startTimestampMs)
setStartedAt(appStartTimeSpan.startUptimeMs)
setStoppedAt(appStartMetrics.classLoadedUptimeMs)
}
processInitSpan.addToMap(androidNativeSpans)

val applicationOnCreateSpan = appStartMetrics.applicationOnCreateTimeSpan
applicationOnCreateSpan.addToMap(androidNativeSpans)

val contentProviderSpans = appStartMetrics.contentProviderOnCreateTimeSpans
contentProviderSpans.forEach { span ->
span.addToMap(androidNativeSpans)
}

appStartMetrics.activityLifecycleTimeSpans.forEach { span ->
span.onCreate.addToMap(androidNativeSpans)
span.onStart.addToMap(androidNativeSpans)
}

item["nativeSpanTimes"] = androidNativeSpans

result.success(item)
}
}

private fun displayRefreshRate(result: Result) {
var refreshRate: Int? = null

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val display = activity?.get()?.display
if (display != null) {
refreshRate = display.refreshRate.toInt()
}
} else {
val display =
activity
?.get()
?.window
?.windowManager
?.defaultDisplay
if (display != null) {
refreshRate = display.refreshRate.toInt()
}
}

result.success(refreshRate)
}

private fun TimeSpan.addToMap(map: MutableMap<String, Any?>) {
if (startTimestamp == null) return

description?.let { description ->
map[description] =
mapOf<String, Any?>(
"startTimestampMsSinceEpoch" to startTimestampMs,
"stopTimestampMsSinceEpoch" to projectedStopTimestampMs,
)
}
}
private fun setContexts(
key: String?,
value: Any?,
Expand Down Expand Up @@ -374,23 +268,137 @@ class SentryFlutterPlugin :
result.success("")
}

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

@SuppressLint("StaticFieldLeak")
private var applicationContext: Context? = null

@SuppressLint("StaticFieldLeak")
private var activity: WeakReference<Activity>? = null

private var pluginRegistrationTime: Long? = null

private lateinit var sentryFlutter: SentryFlutter

private const val NATIVE_CRASH_WAIT_TIME = 500L

@Suppress("unused") // Used by native/jni bindings
@JvmStatic
fun privateSentryGetReplayIntegration(): ReplayIntegration? = replay

@Suppress("unused") // Used by native/jni bindings
@JvmStatic
fun getDisplayRefreshRate(): Int? {
var refreshRate: Int? = null

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val display = activity?.get()?.display
if (display != null) {
refreshRate = display.refreshRate.toInt()
}
} else {
val display =
activity
?.get()
?.window
?.windowManager
?.defaultDisplay
if (display != null) {
refreshRate = display.refreshRate.toInt()
}
}

return refreshRate
}

@Suppress("unused", "ReturnCount", "TooGenericExceptionCaught") // Used by native/jni bindings
@JvmStatic
fun fetchNativeAppStartAsBytes(): ByteArray? {
if (!sentryFlutter.autoPerformanceTracingEnabled) {
return null
}

val appStartMetrics = AppStartMetrics.getInstance()

if (!appStartMetrics.isAppLaunchedInForeground ||
appStartMetrics.appStartTimeSpan.durationMs > APP_START_MAX_DURATION_MS
) {
Log.w(
"Sentry",
"Invalid app start data: app not launched in foreground or app start took too long (>60s)",
)
return null
}

val appStartTimeSpan = appStartMetrics.appStartTimeSpan
val appStartTime = appStartTimeSpan.startTimestamp
val isColdStart = appStartMetrics.appStartType == AppStartMetrics.AppStartType.COLD

if (appStartTime == null) {
Log.w("Sentry", "App start won't be sent due to missing appStartTime")
return null
}

val appStartTimeMillis = DateUtils.nanosToMillis(appStartTime.nanoTimestamp().toDouble())
val item =
mutableMapOf<String, Any?>(
"pluginRegistrationTime" to pluginRegistrationTime,
"appStartTime" to appStartTimeMillis,
"isColdStart" to isColdStart,
)

val androidNativeSpans = mutableMapOf<String, Any?>()

val processInitSpan =
TimeSpan().apply {
description = "Process Initialization"
setStartUnixTimeMs(appStartTimeSpan.startTimestampMs)
setStartedAt(appStartTimeSpan.startUptimeMs)
setStoppedAt(appStartMetrics.classLoadedUptimeMs)
}
addTimeSpanToMap(processInitSpan, androidNativeSpans)

val applicationOnCreateSpan = appStartMetrics.applicationOnCreateTimeSpan
addTimeSpanToMap(applicationOnCreateSpan, androidNativeSpans)

val contentProviderSpans = appStartMetrics.contentProviderOnCreateTimeSpans
contentProviderSpans.forEach { span ->
addTimeSpanToMap(span, androidNativeSpans)
}

appStartMetrics.activityLifecycleTimeSpans.forEach { span ->
addTimeSpanToMap(span.onCreate, androidNativeSpans)
addTimeSpanToMap(span.onStart, androidNativeSpans)
}

item["nativeSpanTimes"] = androidNativeSpans

val json = JSONObject(item).toString()
return json.toByteArray(Charsets.UTF_8)
}

private fun addTimeSpanToMap(
span: TimeSpan,
map: MutableMap<String, Any?>,
) {
if (span.startTimestamp == null) return

span.description?.let { description ->
map[description] =
mapOf<String, Any?>(
"startTimestampMsSinceEpoch" to span.startTimestampMs,
"stopTimestampMsSinceEpoch" to span.projectedStopTimestampMs,
)
}
}

@JvmStatic
fun getApplicationContext(): Context? = applicationContext

@Suppress("unused") // Used by native/jni bindings
@Suppress("unused", "ReturnCount", "TooGenericExceptionCaught") // Used by native/jni bindings
@JvmStatic
fun loadContextsAsBytes(): ByteArray? {
val options = ScopesAdapter.getInstance().options
Expand All @@ -405,11 +413,16 @@ class SentryFlutterPlugin :
options,
currentScope,
)
val json = JSONObject(serializedScope).toString()
return json.toByteArray(Charsets.UTF_8)
try {
val json = JSONObject(serializedScope).toString()
return json.toByteArray(Charsets.UTF_8)
} catch (e: Exception) {
Log.e("Sentry", "Failed to serialize scope", e)
return null
}
}

@Suppress("unused") // Used by native/jni bindings
@Suppress("unused", "TooGenericExceptionCaught") // Used by native/jni bindings
@JvmStatic
fun loadDebugImagesAsBytes(addresses: Set<String>): ByteArray? {
val options = ScopesAdapter.getInstance().options as SentryAndroidOptions
Expand All @@ -428,8 +441,13 @@ class SentryFlutterPlugin :
.serialize()
}

val json = JSONArray(debugImages).toString()
return json.toByteArray(Charsets.UTF_8)
try {
val json = JSONArray(debugImages).toString()
return json.toByteArray(Charsets.UTF_8)
} catch (e: Exception) {
Log.e("Sentry", "Failed to serialize debug images", e)
return null
}
}

private fun List<DebugImage>?.serialize() = this?.map { it.serialize() }
Expand Down
54 changes: 54 additions & 0 deletions packages/flutter/example/integration_test/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,60 @@ void main() {
expect(debugImageByStacktrace.first.imageAddr, expectedImage.imageAddr);
});

testWidgets('fetchNativeAppStart returns app start data', (tester) async {
await restoreFlutterOnErrorAfter(() async {
await setupSentryAndApp(tester);
});

if (Platform.isAndroid || Platform.isIOS) {
// fetchNativeAppStart should return data on mobile platforms
final appStart = await SentryFlutter.native?.fetchNativeAppStart();

expect(appStart, isNotNull, reason: 'App start data should be available');

if (appStart != null) {
expect(appStart.appStartTime, greaterThan(0),
reason: 'App start time should be positive');
expect(appStart.pluginRegistrationTime, greaterThan(0),
reason: 'Plugin registration time should be positive');
expect(appStart.isColdStart, isA<bool>(),
reason: 'isColdStart should be a boolean');
expect(appStart.nativeSpanTimes, isA<Map>(),
reason: 'Native span times should be a map');
}
} else {
// On other platforms, it should return null
final appStart = await SentryFlutter.native?.fetchNativeAppStart();
expect(appStart, isNull,
reason: 'App start should be null on non-mobile platforms');
}
});

testWidgets('displayRefreshRate returns valid refresh rate', (tester) async {
await restoreFlutterOnErrorAfter(() async {
await setupSentryAndApp(tester);
});

if (Platform.isAndroid || Platform.isIOS) {
final refreshRate = await SentryFlutter.native?.displayRefreshRate();

// Refresh rate should be available on mobile platforms
expect(refreshRate, isNotNull,
reason: 'Display refresh rate should be available');

if (refreshRate != null) {
expect(refreshRate, greaterThan(0),
reason: 'Refresh rate should be positive');
expect(refreshRate, lessThanOrEqualTo(1000),
reason: 'Refresh rate should be reasonable (<=1000Hz)');
}
} else {
final refreshRate = await SentryFlutter.native?.displayRefreshRate();
expect(refreshRate, isNull,
reason: 'Refresh rate should be null or positive on other platforms');
}
});

group('e2e', () {
var output = find.byKey(const Key('output'));
late Fixture fixture;
Expand Down
Loading
Loading