From 03f81187e23a01845d06f3b0719ebfbf48dfc7aa Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 3 Sep 2025 01:25:16 +0200 Subject: [PATCH 01/61] Update --- .../native/java/android_envelope_worker.dart | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 packages/flutter/lib/src/native/java/android_envelope_worker.dart diff --git a/packages/flutter/lib/src/native/java/android_envelope_worker.dart b/packages/flutter/lib/src/native/java/android_envelope_worker.dart new file mode 100644 index 0000000000..b87d8cf92f --- /dev/null +++ b/packages/flutter/lib/src/native/java/android_envelope_worker.dart @@ -0,0 +1,96 @@ +class AndroidEnvelopeWorker extends WorkerIsolate { + AndroidEnvelopeWorker(super.config); + + static Future spawn(WorkerConfig config) async { + // 1) Create a ReceivePort the worker can talk to immediately. + final init = ReceivePort(); + + // 2) Pass BOTH the config and init.sendPort into the isolate. + await Isolate.spawn<(WorkerConfig, SendPort)>( + AndroidEnvelopeWorker.entryPoint, + (config, init.sendPort), + debugName: 'SentryAndroidEnvelopeWorker', + ); + + // 3) First message from worker is its inbox SendPort. + final SendPort workerInbox = await init.first as SendPort; + return workerInbox; + } + + void startMessageLoop() { + final receivePort = ReceivePort(); + + // Handshake: tell host how to send messages to this worker. + hostPort.send(receivePort.sendPort); + + receivePort.listen((message) { + try { + processMessage(message); + } catch (e, st) { + // sendError(e, st); + } + }); + } + + void processMessage(dynamic message) { + IsolateDiagnosticLog.log(SentryLevel.warning, + 'EnvelopeWorker invoked; starting captureEnvelope'); + + if (message is TransferableTypedData) { + final envelopeData = message.materialize().asUint8List(); + _captureEnvelope(envelopeData, false); + } + } + + void _captureEnvelope( + Uint8List envelopeData, bool containsUnhandledException) { + JObject? id; + JByteArray? byteArray; + try { + byteArray = JByteArray.from(envelopeData); + id = native.InternalSentrySdk.captureEnvelope( + byteArray, containsUnhandledException); + + if (id == null) { + IsolateDiagnosticLog.log(SentryLevel.error, + 'Native Android SDK returned null id when capturing envelope'); + } + } catch (exception, stackTrace) { + IsolateDiagnosticLog.log(SentryLevel.error, 'Failed to capture envelope', + exception: exception, stackTrace: stackTrace); + // if (options.automatedTestMode) { + // rethrow; + // } + } finally { + byteArray?.release(); + id?.release(); + } + } + + void send(Object message) => hostPort.send(message); + + static void entryPoint((WorkerConfig, SendPort) args) { + final (config, hostPort) = args; + + final level = config.environment['logLevel'] as SentryLevel; + final debug = config.environment['debug'] as bool; + IsolateDiagnosticLog.configure(debug: debug, level: level); + IsolateDiagnosticLog.log( + SentryLevel.warning, 'AndroidEnvelopeWorker started'); + + // Construct worker with the hostPort we just received. + final worker = AndroidEnvelopeWorker(config); + + // Start loop and complete the handshake by sending our inbox SendPort. + final receivePort = ReceivePort(); + hostPort.send(receivePort.sendPort); // <- completes init.first in spawn() + + // Option A: reuse startMessageLoop’s listener: + receivePort.listen(worker.processMessage); + + // Option B: if you prefer your existing method, you can: + // worker.startMessageLoop(); + // but then remove the duplicate handshake above from startMessageLoop, or + // let startMessageLoop accept the already-created receivePort. + } +} From 6928f3a92a3c43f0b1c64618f7445dc164bdd84a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 3 Sep 2025 15:03:19 +0200 Subject: [PATCH 02/61] Update --- .../native/java/android_envelope_worker.dart | 107 +++++----- .../src/native/java/sentry_native_java.dart | 31 +-- packages/flutter/lib/src/worker_isolate.dart | 201 ++++++++++++++++++ 3 files changed, 268 insertions(+), 71 deletions(-) create mode 100644 packages/flutter/lib/src/worker_isolate.dart diff --git a/packages/flutter/lib/src/native/java/android_envelope_worker.dart b/packages/flutter/lib/src/native/java/android_envelope_worker.dart index b87d8cf92f..7e2d76e88e 100644 --- a/packages/flutter/lib/src/native/java/android_envelope_worker.dart +++ b/packages/flutter/lib/src/native/java/android_envelope_worker.dart @@ -1,38 +1,69 @@ -class AndroidEnvelopeWorker extends WorkerIsolate { - AndroidEnvelopeWorker(super.config); +import 'dart:async'; +import 'dart:isolate'; +import 'dart:typed_data'; - static Future spawn(WorkerConfig config) async { - // 1) Create a ReceivePort the worker can talk to immediately. - final init = ReceivePort(); +import 'package:jni/jni.dart'; +import 'package:meta/meta.dart'; - // 2) Pass BOTH the config and init.sendPort into the isolate. - await Isolate.spawn<(WorkerConfig, SendPort)>( - AndroidEnvelopeWorker.entryPoint, - (config, init.sendPort), +import '../../../sentry_flutter.dart'; +import '../../worker_isolate.dart'; +import 'binding.dart' as native; + +/// Host-side proxy for the Android envelope worker isolate. +class AndroidEnvelopeWorker { + AndroidEnvelopeWorker(this._options); + + final SentryFlutterOptions _options; + + WorkerClient? _client; + + @internal // visible for testing/mocking + static AndroidEnvelopeWorker Function(SentryFlutterOptions) factory = + AndroidEnvelopeWorker.new; + + Future start() async { + if (_client != null) return; + final config = WorkerConfig( + debug: _options.debug, + logLevel: _options.diagnosticLevel, debugName: 'SentryAndroidEnvelopeWorker', ); - - // 3) First message from worker is its inbox SendPort. - final SendPort workerInbox = await init.first as SendPort; - return workerInbox; + final (_, port) = await WorkerIsolate.spawn( + config, + AndroidEnvelopeWorkerIsolate.entryPoint, + ); + _client = WorkerClient(port); } - void startMessageLoop() { - final receivePort = ReceivePort(); + Future stop() async { + _close(); + } - // Handshake: tell host how to send messages to this worker. - hostPort.send(receivePort.sendPort); + /// Fire-and-forget send of envelope bytes to the worker. + void captureEnvelope(Uint8List envelopeData) { + final client = _client; + if (client == null) { + _options.log( + SentryLevel.warning, + 'AndroidEnvelopeWorker.captureEnvelope called before start; dropping', + ); + return; + } + client.send(TransferableTypedData.fromList([envelopeData])); + } - receivePort.listen((message) { - try { - processMessage(message); - } catch (e, st) { - // sendError(e, st); - } - }); + void _close() { + _client?.close(); + _client = null; } +} + +/// Worker isolate implementation handling envelope capture via JNI. +class AndroidEnvelopeWorkerIsolate extends WorkerIsolate { + AndroidEnvelopeWorkerIsolate(super.host); - void processMessage(dynamic message) { + @override + FutureOr handleMessage(Object? message) { IsolateDiagnosticLog.log(SentryLevel.warning, 'EnvelopeWorker invoked; starting captureEnvelope'); @@ -67,30 +98,8 @@ class AndroidEnvelopeWorker extends WorkerIsolate { } } - void send(Object message) => hostPort.send(message); - static void entryPoint((WorkerConfig, SendPort) args) { - final (config, hostPort) = args; - - final level = config.environment['logLevel'] as SentryLevel; - final debug = config.environment['debug'] as bool; - IsolateDiagnosticLog.configure(debug: debug, level: level); - IsolateDiagnosticLog.log( - SentryLevel.warning, 'AndroidEnvelopeWorker started'); - - // Construct worker with the hostPort we just received. - final worker = AndroidEnvelopeWorker(config); - - // Start loop and complete the handshake by sending our inbox SendPort. - final receivePort = ReceivePort(); - hostPort.send(receivePort.sendPort); // <- completes init.first in spawn() - - // Option A: reuse startMessageLoop’s listener: - receivePort.listen(worker.processMessage); - - // Option B: if you prefer your existing method, you can: - // worker.startMessageLoop(); - // but then remove the duplicate handshake above from startMessageLoop, or - // let startMessageLoop accept the already-created receivePort. + final (config, host) = args; + WorkerIsolate.bootstrap(config, host, AndroidEnvelopeWorkerIsolate(host)); } } 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 0c57f79179..509a86d66f 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -6,8 +6,10 @@ import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; import '../../replay/scheduled_recorder_config.dart'; +import '../../worker_isolate.dart'; import '../sentry_native_channel.dart'; import '../utils/utf8_json.dart'; +import 'android_envelope_worker.dart'; import 'android_replay_recorder.dart'; import 'binding.dart' as native; @@ -71,34 +73,18 @@ class SentryNativeJava extends SentryNativeChannel { }); } + envelopeWorker = AndroidEnvelopeWorker.factory(options); + await envelopeWorker.start(); + return super.init(hub); } + late AndroidEnvelopeWorker envelopeWorker; + @override FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { - JObject? id; - JByteArray? byteArray; - try { - byteArray = JByteArray.from(envelopeData); - id = native.InternalSentrySdk.captureEnvelope( - byteArray, containsUnhandledException); - - if (id == null) { - options.log(SentryLevel.error, - 'Native Android SDK returned null id when capturing envelope'); - } - } catch (exception, stackTrace) { - options.log(SentryLevel.error, 'Failed to capture envelope', - exception: exception, stackTrace: stackTrace); - - if (options.automatedTestMode) { - rethrow; - } - } finally { - byteArray?.release(); - id?.release(); - } + envelopeWorker.captureEnvelope(envelopeData); } @override @@ -189,6 +175,7 @@ class SentryNativeJava extends SentryNativeChannel { @override Future close() async { await _replayRecorder?.stop(); + envelopeWorker.close(); return super.close(); } } diff --git a/packages/flutter/lib/src/worker_isolate.dart b/packages/flutter/lib/src/worker_isolate.dart new file mode 100644 index 0000000000..db15ab3932 --- /dev/null +++ b/packages/flutter/lib/src/worker_isolate.dart @@ -0,0 +1,201 @@ +import 'dart:developer' as developer; +import 'dart:async'; +import 'dart:isolate'; + +import 'package:meta/meta.dart'; + +import '../sentry_flutter.dart'; + +class WorkerConfig { + final bool debug; + final SentryLevel logLevel; + final String? debugName; + + const WorkerConfig({ + required this.debug, + required this.logLevel, + this.debugName, + }); +} + +class IsolateDiagnosticLog { + IsolateDiagnosticLog._(); + + static late final bool _debug; + static late final SentryLevel _level; + + static void configure({required bool debug, required SentryLevel level}) { + _debug = debug; + _level = level; + } + + static void log( + SentryLevel level, + String message, { + String? logger, + Object? exception, + StackTrace? stackTrace, + }) { + if (_isEnabled(level)) { + developer.log( + '[${level.name}] $message', + level: level.toDartLogLevel(), + name: logger ?? 'sentry', + time: DateTime.now(), + error: exception, + stackTrace: stackTrace, + ); + } + } + + static bool _isEnabled(SentryLevel level) { + return _debug && level.ordinal >= _level.ordinal || + level == SentryLevel.fatal; + } +} + +/// Unified V3 worker API combining the robustness of the native replay worker +/// pattern (request/response with correlation IDs) with the minimal +/// WorkerIsolateBase bootstrap/spawn flow. +abstract class WorkerIsolate { + static const String shutdownMessage = 'shutdown'; + + @protected + final SendPort hostPort; + + WorkerIsolate(this.hostPort); + + /// Handle fire-and-forget messages from host → worker. + FutureOr handleMessage(Object? message); + + /// Handle a request expecting a response. Default implementation returns null. + FutureOr handleRequest(Object? payload) => null; + + /// Worker-side bootstrap: configures logging, handshakes, starts loop. + static void bootstrap( + WorkerConfig config, + SendPort hostPort, + WorkerIsolate worker, + ) { + IsolateDiagnosticLog.configure( + debug: config.debug, + level: config.logLevel, + ); + final receivePort = ReceivePort(); + + // Handshake: provide worker's inbox to host. + hostPort.send(receivePort.sendPort); + + receivePort.listen((message) { + if (message == shutdownMessage) { + IsolateDiagnosticLog.log( + SentryLevel.debug, 'Worker V3 received shutdown request'); + try { + receivePort.close(); + } catch (e, st) { + IsolateDiagnosticLog.log( + SentryLevel.error, + 'Worker V3 ReceivePort close error', + exception: e, + stackTrace: st, + ); + } + IsolateDiagnosticLog.log(SentryLevel.debug, 'Worker V3 closed'); + return; + } + + // Minimal RPC pattern: (id, payload, replyTo) + if (message is (int, Object?, SendPort)) { + final (id, payload, replyTo) = message; + Future.sync(() => worker.handleRequest(payload)) + .then((result) => replyTo.send((id, result))) + .catchError((Object error, StackTrace stackTrace) { + // RemoteError is a simple, transferable error container. + replyTo + .send((id, RemoteError(error.toString(), stackTrace.toString()))); + }); + return; + } + + // Fire-and-forget path + try { + worker.handleMessage(message); + } catch (e, st) { + IsolateDiagnosticLog.log( + SentryLevel.error, + 'Worker V3 error while handling message', + exception: e, + stackTrace: st, + ); + } + }); + } + + /// Host-side spawn: returns worker inbox SendPort after handshake + static Future<(Isolate isolate, SendPort workerPort)> spawn( + WorkerConfig cfg, + void Function((WorkerConfig, SendPort)) entryPoint, + ) async { + final init = ReceivePort(); + final isolate = await Isolate.spawn<(WorkerConfig, SendPort)>( + entryPoint, + (cfg, init.sendPort), + debugName: cfg.debugName, + ); + final SendPort workerPort = await init.first as SendPort; + return (isolate, workerPort); + } +} + +/// Host-side helper for workers to perform minimal request/response. +class WorkerClient { + WorkerClient(this._workerPort) { + _responses.listen(_handleResponse); + } + + final SendPort _workerPort; + final ReceivePort _responses = ReceivePort(); + final Map> _pending = {}; + int _idCounter = 0; + bool _closed = false; + + /// Fire-and-forget send to the worker. + void send(Object? message) { + _workerPort.send(message); + } + + /// Send a request to the worker and await a response. + Future request(Object? payload) { + if (_closed) throw StateError('WorkerClientV3 is closed'); + final id = _idCounter++; + final completer = Completer.sync(); + _pending[id] = completer; + _workerPort.send((id, payload, _responses.sendPort)); + return completer.future; + } + + void close() { + if (_closed) return; + _closed = true; + _workerPort.send(WorkerIsolate.shutdownMessage); + if (_pending.isEmpty) { + _responses.close(); + } + } + + void _handleResponse(dynamic message) { + final (int id, Object? response) = message as (int, Object?); + final completer = _pending.remove(id); + if (completer == null) return; + + if (response is RemoteError) { + completer.completeError(response); + } else { + completer.complete(response); + } + + if (_closed && _pending.isEmpty) { + _responses.close(); + } + } +} From a91cbae5ff2db8dbe921814013a4afafb55bc026 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 3 Sep 2025 17:30:19 +0200 Subject: [PATCH 03/61] Update --- .../lib/src/isolate_diagnostic_log.dart | 39 +++ .../native/java/android_envelope_worker.dart | 80 +++--- .../src/native/java/sentry_native_java.dart | 2 +- packages/flutter/lib/src/worker_isolate.dart | 237 ++++++++---------- 4 files changed, 184 insertions(+), 174 deletions(-) create mode 100644 packages/flutter/lib/src/isolate_diagnostic_log.dart diff --git a/packages/flutter/lib/src/isolate_diagnostic_log.dart b/packages/flutter/lib/src/isolate_diagnostic_log.dart new file mode 100644 index 0000000000..96ac1e5e69 --- /dev/null +++ b/packages/flutter/lib/src/isolate_diagnostic_log.dart @@ -0,0 +1,39 @@ +import 'dart:developer' as developer; + +import '../sentry_flutter.dart'; + +class IsolateDiagnosticLog { + IsolateDiagnosticLog._(); + + static late final bool _debug; + static late final SentryLevel _level; + + static void configure({required bool debug, required SentryLevel level}) { + _debug = debug; + _level = level; + } + + static void log( + SentryLevel level, + String message, { + String? logger, + Object? exception, + StackTrace? stackTrace, + }) { + if (_isEnabled(level)) { + developer.log( + '[${level.name}] $message', + level: level.toDartLogLevel(), + name: logger ?? 'sentry', + time: DateTime.now(), + error: exception, + stackTrace: stackTrace, + ); + } + } + + static bool _isEnabled(SentryLevel level) { + return _debug && level.ordinal >= _level.ordinal || + level == SentryLevel.fatal; + } +} diff --git a/packages/flutter/lib/src/native/java/android_envelope_worker.dart b/packages/flutter/lib/src/native/java/android_envelope_worker.dart index 7e2d76e88e..65f5d80305 100644 --- a/packages/flutter/lib/src/native/java/android_envelope_worker.dart +++ b/packages/flutter/lib/src/native/java/android_envelope_worker.dart @@ -7,40 +7,39 @@ import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; import '../../worker_isolate.dart'; +import '../../isolate_diagnostic_log.dart'; import 'binding.dart' as native; -/// Host-side proxy for the Android envelope worker isolate. -class AndroidEnvelopeWorker { - AndroidEnvelopeWorker(this._options); - +class AndroidEnvelopeWorker implements WorkerHandle { final SentryFlutterOptions _options; + final IsolateConfig _config; + IsolateClient? _client; - WorkerClient? _client; + AndroidEnvelopeWorker(this._options) + : _config = IsolateConfig( + debug: _options.debug, + logLevel: _options.diagnosticLevel, + debugName: 'SentryAndroidEnvelopeWorker', + ); @internal // visible for testing/mocking static AndroidEnvelopeWorker Function(SentryFlutterOptions) factory = AndroidEnvelopeWorker.new; - Future start() async { + @override + FutureOr start() async { if (_client != null) return; - final config = WorkerConfig( - debug: _options.debug, - logLevel: _options.diagnosticLevel, - debugName: 'SentryAndroidEnvelopeWorker', - ); - final (_, port) = await WorkerIsolate.spawn( - config, - AndroidEnvelopeWorkerIsolate.entryPoint, - ); - _client = WorkerClient(port); + _client = await spawnIsolate(_config, _entryPoint); } - Future stop() async { - _close(); + static void _entryPoint((SendPort, IsolateConfig) init) { + final (host, config) = init; + runIsolate(config, host, _AndroidEnvelopeMessageHandler()); } /// Fire-and-forget send of envelope bytes to the worker. - void captureEnvelope(Uint8List envelopeData) { + void captureEnvelope( + Uint8List envelopeData, bool containsUnhandledException) { final client = _client; if (client == null) { _options.log( @@ -49,27 +48,30 @@ class AndroidEnvelopeWorker { ); return; } - client.send(TransferableTypedData.fromList([envelopeData])); + client.send(( + TransferableTypedData.fromList([envelopeData]), + containsUnhandledException + )); } - void _close() { + @override + FutureOr close() { _client?.close(); _client = null; } } -/// Worker isolate implementation handling envelope capture via JNI. -class AndroidEnvelopeWorkerIsolate extends WorkerIsolate { - AndroidEnvelopeWorkerIsolate(super.host); - +class _AndroidEnvelopeMessageHandler implements IsolateMessageHandler { @override - FutureOr handleMessage(Object? message) { - IsolateDiagnosticLog.log(SentryLevel.warning, - 'EnvelopeWorker invoked; starting captureEnvelope'); - - if (message is TransferableTypedData) { - final envelopeData = message.materialize().asUint8List(); - _captureEnvelope(envelopeData, false); + FutureOr onMessage(Object? msg) { + if (msg is (TransferableTypedData, bool)) { + final (transferable, containsUnhandledException) = msg; + final data = transferable.materialize().asUint8List(); + _captureEnvelope(data, containsUnhandledException); + } else { + IsolateDiagnosticLog.log(SentryLevel.warning, + 'Unexpected message type while handling a message: $msg', + logger: 'SentryAndroidEnvelopeWorker'); } } @@ -84,11 +86,15 @@ class AndroidEnvelopeWorkerIsolate extends WorkerIsolate { if (id == null) { IsolateDiagnosticLog.log(SentryLevel.error, - 'Native Android SDK returned null id when capturing envelope'); + 'Native Android SDK returned null id when capturing envelope', + logger: 'SentryAndroidEnvelopeWorker'); } } catch (exception, stackTrace) { IsolateDiagnosticLog.log(SentryLevel.error, 'Failed to capture envelope', - exception: exception, stackTrace: stackTrace); + exception: exception, + stackTrace: stackTrace, + logger: 'SentryAndroidEnvelopeWorker'); + // TODO: // if (options.automatedTestMode) { // rethrow; // } @@ -98,8 +104,6 @@ class AndroidEnvelopeWorkerIsolate extends WorkerIsolate { } } - static void entryPoint((WorkerConfig, SendPort) args) { - final (config, host) = args; - WorkerIsolate.bootstrap(config, host, AndroidEnvelopeWorkerIsolate(host)); - } + @override + FutureOr onRequest(Object? payload) => null; // not used for now } 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 509a86d66f..380292d375 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -84,7 +84,7 @@ class SentryNativeJava extends SentryNativeChannel { @override FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { - envelopeWorker.captureEnvelope(envelopeData); + envelopeWorker.captureEnvelope(envelopeData, containsUnhandledException); } @override diff --git a/packages/flutter/lib/src/worker_isolate.dart b/packages/flutter/lib/src/worker_isolate.dart index db15ab3932..8ad6921ee5 100644 --- a/packages/flutter/lib/src/worker_isolate.dart +++ b/packages/flutter/lib/src/worker_isolate.dart @@ -5,155 +5,39 @@ import 'dart:isolate'; import 'package:meta/meta.dart'; import '../sentry_flutter.dart'; +import 'isolate_diagnostic_log.dart'; -class WorkerConfig { +// ------------------------------------------- +// HOST-SIDE API (runs on the main isolate) +// ------------------------------------------- + +/// Uniform lifecycle for any host-facing worker facade. +abstract class WorkerHandle { + FutureOr start(); + FutureOr close(); +} + +/// Minimal config passed to isolates. Extend as needed. +class IsolateConfig { final bool debug; final SentryLevel logLevel; final String? debugName; - const WorkerConfig({ + const IsolateConfig({ required this.debug, required this.logLevel, this.debugName, }); } -class IsolateDiagnosticLog { - IsolateDiagnosticLog._(); - - static late final bool _debug; - static late final SentryLevel _level; - - static void configure({required bool debug, required SentryLevel level}) { - _debug = debug; - _level = level; - } - - static void log( - SentryLevel level, - String message, { - String? logger, - Object? exception, - StackTrace? stackTrace, - }) { - if (_isEnabled(level)) { - developer.log( - '[${level.name}] $message', - level: level.toDartLogLevel(), - name: logger ?? 'sentry', - time: DateTime.now(), - error: exception, - stackTrace: stackTrace, - ); - } - } - - static bool _isEnabled(SentryLevel level) { - return _debug && level.ordinal >= _level.ordinal || - level == SentryLevel.fatal; - } -} - -/// Unified V3 worker API combining the robustness of the native replay worker -/// pattern (request/response with correlation IDs) with the minimal -/// WorkerIsolateBase bootstrap/spawn flow. -abstract class WorkerIsolate { - static const String shutdownMessage = 'shutdown'; - - @protected - final SendPort hostPort; - - WorkerIsolate(this.hostPort); - - /// Handle fire-and-forget messages from host → worker. - FutureOr handleMessage(Object? message); - - /// Handle a request expecting a response. Default implementation returns null. - FutureOr handleRequest(Object? payload) => null; - - /// Worker-side bootstrap: configures logging, handshakes, starts loop. - static void bootstrap( - WorkerConfig config, - SendPort hostPort, - WorkerIsolate worker, - ) { - IsolateDiagnosticLog.configure( - debug: config.debug, - level: config.logLevel, - ); - final receivePort = ReceivePort(); - - // Handshake: provide worker's inbox to host. - hostPort.send(receivePort.sendPort); - - receivePort.listen((message) { - if (message == shutdownMessage) { - IsolateDiagnosticLog.log( - SentryLevel.debug, 'Worker V3 received shutdown request'); - try { - receivePort.close(); - } catch (e, st) { - IsolateDiagnosticLog.log( - SentryLevel.error, - 'Worker V3 ReceivePort close error', - exception: e, - stackTrace: st, - ); - } - IsolateDiagnosticLog.log(SentryLevel.debug, 'Worker V3 closed'); - return; - } - - // Minimal RPC pattern: (id, payload, replyTo) - if (message is (int, Object?, SendPort)) { - final (id, payload, replyTo) = message; - Future.sync(() => worker.handleRequest(payload)) - .then((result) => replyTo.send((id, result))) - .catchError((Object error, StackTrace stackTrace) { - // RemoteError is a simple, transferable error container. - replyTo - .send((id, RemoteError(error.toString(), stackTrace.toString()))); - }); - return; - } - - // Fire-and-forget path - try { - worker.handleMessage(message); - } catch (e, st) { - IsolateDiagnosticLog.log( - SentryLevel.error, - 'Worker V3 error while handling message', - exception: e, - stackTrace: st, - ); - } - }); - } - - /// Host-side spawn: returns worker inbox SendPort after handshake - static Future<(Isolate isolate, SendPort workerPort)> spawn( - WorkerConfig cfg, - void Function((WorkerConfig, SendPort)) entryPoint, - ) async { - final init = ReceivePort(); - final isolate = await Isolate.spawn<(WorkerConfig, SendPort)>( - entryPoint, - (cfg, init.sendPort), - debugName: cfg.debugName, - ); - final SendPort workerPort = await init.first as SendPort; - return (isolate, workerPort); - } -} - /// Host-side helper for workers to perform minimal request/response. -class WorkerClient { - WorkerClient(this._workerPort) { +class IsolateClient { + IsolateClient(this._workerPort) { _responses.listen(_handleResponse); } final SendPort _workerPort; + SendPort get port => _workerPort; final ReceivePort _responses = ReceivePort(); final Map> _pending = {}; int _idCounter = 0; @@ -166,7 +50,7 @@ class WorkerClient { /// Send a request to the worker and await a response. Future request(Object? payload) { - if (_closed) throw StateError('WorkerClientV3 is closed'); + if (_closed) throw StateError('IsolateClient is closed'); final id = _idCounter++; final completer = Completer.sync(); _pending[id] = completer; @@ -176,8 +60,8 @@ class WorkerClient { void close() { if (_closed) return; + _workerPort.send(_Ctl.shutdown); _closed = true; - _workerPort.send(WorkerIsolate.shutdownMessage); if (_pending.isEmpty) { _responses.close(); } @@ -199,3 +83,86 @@ class WorkerClient { } } } + +class _Ctl { + static const shutdown = '_shutdown_'; +} + +/// Isolate entry-point signature. +typedef IsolateEntry = void Function((SendPort, IsolateConfig)); + +/// Spawn an isolate and handshake to obtain its SendPort. +Future spawnIsolate( + IsolateConfig config, + IsolateEntry entry, +) async { + final receivePort = ReceivePort(); + await Isolate.spawn<(SendPort, IsolateConfig)>( + entry, + (receivePort.sendPort, config), + debugName: config.debugName, + ); + final workerPort = await receivePort.first as SendPort; + return IsolateClient(workerPort); +} + +// ------------------------------------------- +// ISOLATE-SIDE API (runs inside the worker isolate) +// ------------------------------------------- + +/// Domain behavior contract implemented INSIDE the worker isolate. +abstract class IsolateMessageHandler { + FutureOr onMessage(Object? message); + FutureOr onRequest(Object? payload) => null; +} + +/// Generic isolate runtime. Reuse for every Sentry worker. +void runIsolate( + IsolateConfig config, + SendPort host, + IsolateMessageHandler logic, +) { + // TODO: we might want to configure this at init overall since we shouldn't need isolate specific log setups + IsolateDiagnosticLog.configure( + debug: config.debug, + level: config.logLevel, + ); + + final inbox = ReceivePort(); + host.send(inbox.sendPort); + + inbox.listen((msg) async { + if (msg == _Ctl.shutdown) { + IsolateDiagnosticLog.log( + SentryLevel.debug, 'Isolate received shutdown request', + logger: config.debugName); + inbox.close(); + IsolateDiagnosticLog.log(SentryLevel.debug, 'Isolate closed.', + logger: config.debugName); + return; + } + + // RPC: (id, payload, replyTo) + if (msg is (int, Object?, SendPort)) { + final (id, payload, replyTo) = msg; + try { + final result = await logic.onRequest(payload); + replyTo.send((id, result)); + } catch (e, st) { + replyTo.send((id, RemoteError(e.toString(), st.toString()))); + } + return; + } + + // Fire-and-forget + try { + await logic.onMessage(msg); + } catch (exception, stackTrace) { + IsolateDiagnosticLog.log( + SentryLevel.error, 'Isolate error while handling message', + exception: exception, + stackTrace: stackTrace, + logger: config.debugName); + } + }); +} From a6bd3ccc542b60570d57185a400a50ff29d0923d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 3 Sep 2025 17:47:52 +0200 Subject: [PATCH 04/61] Update --- .../native/cocoa/cococa_envelope_worker.dart | 101 ++++++++++++++++++ .../src/native/cocoa/sentry_native_cocoa.dart | 23 ++-- .../native/java/android_envelope_worker.dart | 7 +- 3 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 packages/flutter/lib/src/native/cocoa/cococa_envelope_worker.dart diff --git a/packages/flutter/lib/src/native/cocoa/cococa_envelope_worker.dart b/packages/flutter/lib/src/native/cocoa/cococa_envelope_worker.dart new file mode 100644 index 0000000000..91191a14fc --- /dev/null +++ b/packages/flutter/lib/src/native/cocoa/cococa_envelope_worker.dart @@ -0,0 +1,101 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:jni/jni.dart'; +import 'package:meta/meta.dart'; +import 'package:objective_c/objective_c.dart'; + +import '../../../sentry_flutter.dart'; +import '../../worker_isolate.dart'; +import '../../isolate_diagnostic_log.dart'; +import 'binding.dart' as cocoa; + +class CocoaEnvelopeWorker implements Worker { + final SentryFlutterOptions _options; + final IsolateConfig _config; + IsolateClient? _client; + + CocoaEnvelopeWorker(this._options) + : _config = IsolateConfig( + debug: _options.debug, + logLevel: _options.diagnosticLevel, + debugName: 'SentryCocoaEnvelopeWorker', + ); + + @internal // visible for testing/mocking + static CocoaEnvelopeWorker Function(SentryFlutterOptions) factory = + CocoaEnvelopeWorker.new; + + @override + FutureOr start() async { + if (_client != null) return; + _client = await spawnIsolate(_config, _entryPoint); + } + + static void _entryPoint((SendPort, IsolateConfig) init) { + final (host, config) = init; + runIsolate(config, host, _CocoaEnvelopeMessageHandler()); + } + + /// Fire-and-forget send of envelope bytes to the worker. + void captureEnvelope(Uint8List envelopeData) { + final client = _client; + if (client == null) { + _options.log( + SentryLevel.warning, + 'CocoaEnvelopeWorker.captureEnvelope called before start; dropping', + ); + return; + } + client.send(TransferableTypedData.fromList([envelopeData])); + } + + @override + FutureOr close() { + _client?.close(); + _client = null; + } +} + +class _CocoaEnvelopeMessageHandler extends IsolateMessageHandler { + @override + FutureOr onMessage(Object? msg) { + if (msg is TransferableTypedData) { + final data = msg.materialize().asUint8List(); + _captureEnvelope(data); + } else { + IsolateDiagnosticLog.log(SentryLevel.warning, + 'Unexpected message type while handling a message: $msg', + logger: 'SentryCocoaEnvelopeWorker'); + } + } + + void _captureEnvelope(Uint8List envelopeData) { + JObject? id; + JByteArray? byteArray; + try { + final nsData = envelopeData.toNSData(); + final envelope = cocoa.PrivateSentrySDKOnly.envelopeWithData(nsData); + if (envelope != null) { + cocoa.PrivateSentrySDKOnly.captureEnvelope(envelope); + } else { + IsolateDiagnosticLog.log(SentryLevel.error, + 'Native Cocoa SDK returned null when capturing envelope', + logger: 'SentryCocoaEnvelopeWorker'); + } + } catch (exception, stackTrace) { + IsolateDiagnosticLog.log(SentryLevel.error, 'Failed to capture envelope', + exception: exception, + stackTrace: stackTrace, + logger: 'SentryCocoaEnvelopeWorker'); + // TODO: + // if (options.automatedTestMode) { + // rethrow; + // } + } finally { + byteArray?.release(); + id?.release(); + } + } +} 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 7145f39f71..4bd66e81ab 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -9,10 +9,12 @@ import '../sentry_native_channel.dart'; import '../utils/utf8_json.dart'; import 'binding.dart' as cocoa; import 'cocoa_replay_recorder.dart'; +import 'cococa_envelope_worker.dart'; @internal class SentryNativeCocoa extends SentryNativeChannel { CocoaReplayRecorder? _replayRecorder; + CocoaEnvelopeWorker? _envelopeWorker; SentryId? _replayId; SentryNativeCocoa(super.options); @@ -49,29 +51,16 @@ class SentryNativeCocoa extends SentryNativeChannel { }); } + _envelopeWorker = CocoaEnvelopeWorker(options); + _envelopeWorker?.start(); + return super.init(hub); } @override FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { - try { - final nsData = envelopeData.toNSData(); - final envelope = cocoa.PrivateSentrySDKOnly.envelopeWithData(nsData); - if (envelope != null) { - cocoa.PrivateSentrySDKOnly.captureEnvelope(envelope); - } else { - options.log( - SentryLevel.error, 'Failed to capture envelope: envelope is null'); - } - } catch (exception, stackTrace) { - options.log(SentryLevel.error, 'Failed to capture envelope', - exception: exception, stackTrace: stackTrace); - - if (options.automatedTestMode) { - rethrow; - } - } + _envelopeWorker?.captureEnvelope(envelopeData); } @override diff --git a/packages/flutter/lib/src/native/java/android_envelope_worker.dart b/packages/flutter/lib/src/native/java/android_envelope_worker.dart index 65f5d80305..f19f338539 100644 --- a/packages/flutter/lib/src/native/java/android_envelope_worker.dart +++ b/packages/flutter/lib/src/native/java/android_envelope_worker.dart @@ -10,7 +10,7 @@ import '../../worker_isolate.dart'; import '../../isolate_diagnostic_log.dart'; import 'binding.dart' as native; -class AndroidEnvelopeWorker implements WorkerHandle { +class AndroidEnvelopeWorker implements Worker { final SentryFlutterOptions _options; final IsolateConfig _config; IsolateClient? _client; @@ -61,7 +61,7 @@ class AndroidEnvelopeWorker implements WorkerHandle { } } -class _AndroidEnvelopeMessageHandler implements IsolateMessageHandler { +class _AndroidEnvelopeMessageHandler extends IsolateMessageHandler { @override FutureOr onMessage(Object? msg) { if (msg is (TransferableTypedData, bool)) { @@ -103,7 +103,4 @@ class _AndroidEnvelopeMessageHandler implements IsolateMessageHandler { id?.release(); } } - - @override - FutureOr onRequest(Object? payload) => null; // not used for now } From b9269c7dd28ec4b6faea963c07ffcc2451ebabd9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 00:01:49 +0200 Subject: [PATCH 05/61] Update --- .../integrations/native_sdk_integration.dart | 7 ++ ...worker.dart => cocoa_envelope_sender.dart} | 61 +++++++--------- .../src/native/cocoa/sentry_native_cocoa.dart | 10 +-- ...rker.dart => android_envelope_sender.dart} | 51 ++++++------- .../src/native/java/sentry_native_java.dart | 11 +-- packages/flutter/lib/src/worker_isolate.dart | 73 +++++++++---------- 6 files changed, 105 insertions(+), 108 deletions(-) rename packages/flutter/lib/src/native/cocoa/{cococa_envelope_worker.dart => cocoa_envelope_sender.dart} (59%) rename packages/flutter/lib/src/native/java/{android_envelope_worker.dart => android_envelope_sender.dart} (68%) diff --git a/packages/flutter/lib/src/integrations/native_sdk_integration.dart b/packages/flutter/lib/src/integrations/native_sdk_integration.dart index 76c91eda6e..14164c6617 100644 --- a/packages/flutter/lib/src/integrations/native_sdk_integration.dart +++ b/packages/flutter/lib/src/integrations/native_sdk_integration.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; +import '../isolate_diagnostic_log.dart'; import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; @@ -25,6 +26,12 @@ class NativeSdkIntegration implements Integration { return; } + // Configure static Isolate logger before spawning isolates + IsolateDiagnosticLog.configure( + debug: options.debug, + level: options.diagnosticLevel, + ); + try { await _native.init(hub); options.sdk.addIntegration('nativeSdkIntegration'); diff --git a/packages/flutter/lib/src/native/cocoa/cococa_envelope_worker.dart b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart similarity index 59% rename from packages/flutter/lib/src/native/cocoa/cococa_envelope_worker.dart rename to packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart index 91191a14fc..33e48f9662 100644 --- a/packages/flutter/lib/src/native/cocoa/cococa_envelope_worker.dart +++ b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:isolate'; import 'dart:typed_data'; -import 'package:jni/jni.dart'; import 'package:meta/meta.dart'; import 'package:objective_c/objective_c.dart'; @@ -11,54 +10,55 @@ import '../../worker_isolate.dart'; import '../../isolate_diagnostic_log.dart'; import 'binding.dart' as cocoa; -class CocoaEnvelopeWorker implements Worker { +class CocoaEnvelopeSender implements WorkerHost { final SentryFlutterOptions _options; - final IsolateConfig _config; - IsolateClient? _client; + final WorkerConfig _config; + Worker? _worker; - CocoaEnvelopeWorker(this._options) - : _config = IsolateConfig( - debug: _options.debug, - logLevel: _options.diagnosticLevel, - debugName: 'SentryCocoaEnvelopeWorker', + static final String name = 'SentryCocoaEnvelopeSender'; + + CocoaEnvelopeSender(this._options) + : _config = WorkerConfig( + debugName: name, ); @internal // visible for testing/mocking - static CocoaEnvelopeWorker Function(SentryFlutterOptions) factory = - CocoaEnvelopeWorker.new; + static CocoaEnvelopeSender Function(SentryFlutterOptions) factory = + CocoaEnvelopeSender.new; @override FutureOr start() async { - if (_client != null) return; - _client = await spawnIsolate(_config, _entryPoint); + if (_worker != null) return; + _worker = await spawnWorker(_config, _entryPoint); } - static void _entryPoint((SendPort, IsolateConfig) init) { - final (host, config) = init; - runIsolate(config, host, _CocoaEnvelopeMessageHandler()); + @override + FutureOr close() { + _worker?.close(); + _worker = null; } /// Fire-and-forget send of envelope bytes to the worker. void captureEnvelope(Uint8List envelopeData) { - final client = _client; + final client = _worker; if (client == null) { _options.log( SentryLevel.warning, - 'CocoaEnvelopeWorker.captureEnvelope called before start; dropping', + 'captureEnvelope called before start; dropping', + logger: name, ); return; } client.send(TransferableTypedData.fromList([envelopeData])); } - @override - FutureOr close() { - _client?.close(); - _client = null; + static void _entryPoint((SendPort, WorkerConfig) init) { + final (host, config) = init; + runWorker(config, host, _CocoaEnvelopeHandler()); } } -class _CocoaEnvelopeMessageHandler extends IsolateMessageHandler { +class _CocoaEnvelopeHandler extends WorkerHandler { @override FutureOr onMessage(Object? msg) { if (msg is TransferableTypedData) { @@ -67,13 +67,11 @@ class _CocoaEnvelopeMessageHandler extends IsolateMessageHandler { } else { IsolateDiagnosticLog.log(SentryLevel.warning, 'Unexpected message type while handling a message: $msg', - logger: 'SentryCocoaEnvelopeWorker'); + logger: CocoaEnvelopeSender.name); } } void _captureEnvelope(Uint8List envelopeData) { - JObject? id; - JByteArray? byteArray; try { final nsData = envelopeData.toNSData(); final envelope = cocoa.PrivateSentrySDKOnly.envelopeWithData(nsData); @@ -82,20 +80,13 @@ class _CocoaEnvelopeMessageHandler extends IsolateMessageHandler { } else { IsolateDiagnosticLog.log(SentryLevel.error, 'Native Cocoa SDK returned null when capturing envelope', - logger: 'SentryCocoaEnvelopeWorker'); + logger: CocoaEnvelopeSender.name); } } catch (exception, stackTrace) { IsolateDiagnosticLog.log(SentryLevel.error, 'Failed to capture envelope', exception: exception, stackTrace: stackTrace, - logger: 'SentryCocoaEnvelopeWorker'); - // TODO: - // if (options.automatedTestMode) { - // rethrow; - // } - } finally { - byteArray?.release(); - id?.release(); + logger: CocoaEnvelopeSender.name); } } } 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 4bd66e81ab..6707817f2d 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -9,12 +9,12 @@ import '../sentry_native_channel.dart'; import '../utils/utf8_json.dart'; import 'binding.dart' as cocoa; import 'cocoa_replay_recorder.dart'; -import 'cococa_envelope_worker.dart'; +import 'cocoa_envelope_sender.dart'; @internal class SentryNativeCocoa extends SentryNativeChannel { CocoaReplayRecorder? _replayRecorder; - CocoaEnvelopeWorker? _envelopeWorker; + CocoaEnvelopeSender? _envelopeSender; SentryId? _replayId; SentryNativeCocoa(super.options); @@ -51,8 +51,8 @@ class SentryNativeCocoa extends SentryNativeChannel { }); } - _envelopeWorker = CocoaEnvelopeWorker(options); - _envelopeWorker?.start(); + _envelopeSender = CocoaEnvelopeSender(options); + await _envelopeSender?.start(); return super.init(hub); } @@ -60,7 +60,7 @@ class SentryNativeCocoa extends SentryNativeChannel { @override FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { - _envelopeWorker?.captureEnvelope(envelopeData); + _envelopeSender?.captureEnvelope(envelopeData); } @override diff --git a/packages/flutter/lib/src/native/java/android_envelope_worker.dart b/packages/flutter/lib/src/native/java/android_envelope_sender.dart similarity index 68% rename from packages/flutter/lib/src/native/java/android_envelope_worker.dart rename to packages/flutter/lib/src/native/java/android_envelope_sender.dart index f19f338539..71171badba 100644 --- a/packages/flutter/lib/src/native/java/android_envelope_worker.dart +++ b/packages/flutter/lib/src/native/java/android_envelope_sender.dart @@ -10,41 +10,43 @@ import '../../worker_isolate.dart'; import '../../isolate_diagnostic_log.dart'; import 'binding.dart' as native; -class AndroidEnvelopeWorker implements Worker { +class AndroidEnvelopeSender implements WorkerHost { final SentryFlutterOptions _options; - final IsolateConfig _config; - IsolateClient? _client; + final WorkerConfig _config; + Worker? _worker; - AndroidEnvelopeWorker(this._options) - : _config = IsolateConfig( - debug: _options.debug, - logLevel: _options.diagnosticLevel, - debugName: 'SentryAndroidEnvelopeWorker', + static final String name = 'SentryAndroidEnvelopeSender'; + + AndroidEnvelopeSender(this._options) + : _config = WorkerConfig( + debugName: name, ); @internal // visible for testing/mocking - static AndroidEnvelopeWorker Function(SentryFlutterOptions) factory = - AndroidEnvelopeWorker.new; + static AndroidEnvelopeSender Function(SentryFlutterOptions) factory = + AndroidEnvelopeSender.new; @override FutureOr start() async { - if (_client != null) return; - _client = await spawnIsolate(_config, _entryPoint); + if (_worker != null) return; + _worker = await spawnWorker(_config, _entryPoint); } - static void _entryPoint((SendPort, IsolateConfig) init) { - final (host, config) = init; - runIsolate(config, host, _AndroidEnvelopeMessageHandler()); + @override + FutureOr close() { + _worker?.close(); + _worker = null; } /// Fire-and-forget send of envelope bytes to the worker. void captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { - final client = _client; + final client = _worker; if (client == null) { _options.log( SentryLevel.warning, - 'AndroidEnvelopeWorker.captureEnvelope called before start; dropping', + 'captureEnvelope called before worker started; dropping', + logger: name, ); return; } @@ -54,14 +56,13 @@ class AndroidEnvelopeWorker implements Worker { )); } - @override - FutureOr close() { - _client?.close(); - _client = null; + static void _entryPoint((SendPort, WorkerConfig) init) { + final (host, config) = init; + runWorker(config, host, _AndroidEnvelopeHandler()); } } -class _AndroidEnvelopeMessageHandler extends IsolateMessageHandler { +class _AndroidEnvelopeHandler extends WorkerHandler { @override FutureOr onMessage(Object? msg) { if (msg is (TransferableTypedData, bool)) { @@ -71,7 +72,7 @@ class _AndroidEnvelopeMessageHandler extends IsolateMessageHandler { } else { IsolateDiagnosticLog.log(SentryLevel.warning, 'Unexpected message type while handling a message: $msg', - logger: 'SentryAndroidEnvelopeWorker'); + logger: AndroidEnvelopeSender.name); } } @@ -87,13 +88,13 @@ class _AndroidEnvelopeMessageHandler extends IsolateMessageHandler { if (id == null) { IsolateDiagnosticLog.log(SentryLevel.error, 'Native Android SDK returned null id when capturing envelope', - logger: 'SentryAndroidEnvelopeWorker'); + logger: AndroidEnvelopeSender.name); } } catch (exception, stackTrace) { IsolateDiagnosticLog.log(SentryLevel.error, 'Failed to capture envelope', exception: exception, stackTrace: stackTrace, - logger: 'SentryAndroidEnvelopeWorker'); + logger: AndroidEnvelopeSender.name); // TODO: // if (options.automatedTestMode) { // rethrow; 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 380292d375..a6df55c0af 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -6,16 +6,17 @@ import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; import '../../replay/scheduled_recorder_config.dart'; -import '../../worker_isolate.dart'; import '../sentry_native_channel.dart'; import '../utils/utf8_json.dart'; -import 'android_envelope_worker.dart'; +import 'android_envelope_sender.dart'; import 'android_replay_recorder.dart'; import 'binding.dart' as native; @internal class SentryNativeJava extends SentryNativeChannel { AndroidReplayRecorder? _replayRecorder; + AndroidEnvelopeSender? _envelopeSender; + SentryNativeJava(super.options); @override @@ -73,13 +74,13 @@ class SentryNativeJava extends SentryNativeChannel { }); } - envelopeWorker = AndroidEnvelopeWorker.factory(options); - await envelopeWorker.start(); + _envelopeSender = AndroidEnvelopeSender.factory(options); + await _envelopeSender?.start(); return super.init(hub); } - late AndroidEnvelopeWorker envelopeWorker; + late AndroidEnvelopeSender envelopeWorker; @override FutureOr captureEnvelope( diff --git a/packages/flutter/lib/src/worker_isolate.dart b/packages/flutter/lib/src/worker_isolate.dart index 8ad6921ee5..d087d94efa 100644 --- a/packages/flutter/lib/src/worker_isolate.dart +++ b/packages/flutter/lib/src/worker_isolate.dart @@ -1,9 +1,6 @@ -import 'dart:developer' as developer; import 'dart:async'; import 'dart:isolate'; -import 'package:meta/meta.dart'; - import '../sentry_flutter.dart'; import 'isolate_diagnostic_log.dart'; @@ -11,28 +8,27 @@ import 'isolate_diagnostic_log.dart'; // HOST-SIDE API (runs on the main isolate) // ------------------------------------------- -/// Uniform lifecycle for any host-facing worker facade. -abstract class WorkerHandle { +/// Host-side lifecycle interface for a worker isolate. +/// +/// Responsible for spawning the worker isolate, sending messages, +/// and shutting it down. It does not define the worker logic. +abstract class WorkerHost { FutureOr start(); FutureOr close(); } /// Minimal config passed to isolates. Extend as needed. -class IsolateConfig { - final bool debug; - final SentryLevel logLevel; +class WorkerConfig { final String? debugName; - const IsolateConfig({ - required this.debug, - required this.logLevel, - this.debugName, + const WorkerConfig({ + required this.debugName, }); } /// Host-side helper for workers to perform minimal request/response. -class IsolateClient { - IsolateClient(this._workerPort) { +class Worker { + Worker(this._workerPort) { _responses.listen(_handleResponse); } @@ -50,7 +46,7 @@ class IsolateClient { /// Send a request to the worker and await a response. Future request(Object? payload) { - if (_closed) throw StateError('IsolateClient is closed'); + if (_closed) throw StateError('WorkerClient is closed'); final id = _idCounter++; final completer = Completer.sync(); _pending[id] = completer; @@ -88,46 +84,47 @@ class _Ctl { static const shutdown = '_shutdown_'; } -/// Isolate entry-point signature. -typedef IsolateEntry = void Function((SendPort, IsolateConfig)); +/// Worker (isolate) entry-point signature. +typedef WorkerEntry = void Function((SendPort, WorkerConfig)); -/// Spawn an isolate and handshake to obtain its SendPort. -Future spawnIsolate( - IsolateConfig config, - IsolateEntry entry, +/// Spawn a worker isolate and handshake to obtain its SendPort. +Future spawnWorker( + WorkerConfig config, + WorkerEntry entry, ) async { final receivePort = ReceivePort(); - await Isolate.spawn<(SendPort, IsolateConfig)>( + await Isolate.spawn<(SendPort, WorkerConfig)>( entry, (receivePort.sendPort, config), debugName: config.debugName, ); final workerPort = await receivePort.first as SendPort; - return IsolateClient(workerPort); + return Worker(workerPort); } // ------------------------------------------- // ISOLATE-SIDE API (runs inside the worker isolate) // ------------------------------------------- -/// Domain behavior contract implemented INSIDE the worker isolate. -abstract class IsolateMessageHandler { +/// Message/request handler that runs inside the worker isolate. +/// +/// This does not represent the isolate lifecycle; it only defines how +/// the worker processes incoming messages and optional request/response. +abstract class WorkerHandler { + /// Handle fire-and-forget messages sent from the host. FutureOr onMessage(Object? message); - FutureOr onRequest(Object? payload) => null; + + /// Handle request/response payloads sent from the host. + /// Return value is sent back to the host. Default: no-op. + FutureOr onRequest(Object? payload) => {}; } -/// Generic isolate runtime. Reuse for every Sentry worker. -void runIsolate( - IsolateConfig config, +/// Generic worker runtime. Reuse for every Sentry worker. +void runWorker( + WorkerConfig config, SendPort host, - IsolateMessageHandler logic, + WorkerHandler handler, ) { - // TODO: we might want to configure this at init overall since we shouldn't need isolate specific log setups - IsolateDiagnosticLog.configure( - debug: config.debug, - level: config.logLevel, - ); - final inbox = ReceivePort(); host.send(inbox.sendPort); @@ -146,7 +143,7 @@ void runIsolate( if (msg is (int, Object?, SendPort)) { final (id, payload, replyTo) = msg; try { - final result = await logic.onRequest(payload); + final result = await handler.onRequest(payload); replyTo.send((id, result)); } catch (e, st) { replyTo.send((id, RemoteError(e.toString(), st.toString()))); @@ -156,7 +153,7 @@ void runIsolate( // Fire-and-forget try { - await logic.onMessage(msg); + await handler.onMessage(msg); } catch (exception, stackTrace) { IsolateDiagnosticLog.log( SentryLevel.error, 'Isolate error while handling message', From 6ef9960d313ff7994679ae9fc92e53745faedbc3 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 11:47:31 +0200 Subject: [PATCH 06/61] Update --- packages/flutter/lib/src/worker_isolate.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/worker_isolate.dart b/packages/flutter/lib/src/worker_isolate.dart index d087d94efa..3f9a2ec3e3 100644 --- a/packages/flutter/lib/src/worker_isolate.dart +++ b/packages/flutter/lib/src/worker_isolate.dart @@ -10,8 +10,8 @@ import 'isolate_diagnostic_log.dart'; /// Host-side lifecycle interface for a worker isolate. /// -/// Responsible for spawning the worker isolate, sending messages, -/// and shutting it down. It does not define the worker logic. +/// Responsible for spawning the worker isolate, and shutting it down. +/// It does not define the worker logic. abstract class WorkerHost { FutureOr start(); FutureOr close(); From a43f2e1d03367f40496eed9740fd20aa94bdc334 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 12:45:09 +0200 Subject: [PATCH 07/61] Configure diagnostic log --- .../lib/src/integrations/native_sdk_integration.dart | 6 ------ packages/flutter/lib/src/worker_isolate.dart | 9 +++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/integrations/native_sdk_integration.dart b/packages/flutter/lib/src/integrations/native_sdk_integration.dart index 14164c6617..9cdb8f65d7 100644 --- a/packages/flutter/lib/src/integrations/native_sdk_integration.dart +++ b/packages/flutter/lib/src/integrations/native_sdk_integration.dart @@ -26,12 +26,6 @@ class NativeSdkIntegration implements Integration { return; } - // Configure static Isolate logger before spawning isolates - IsolateDiagnosticLog.configure( - debug: options.debug, - level: options.diagnosticLevel, - ); - try { await _native.init(hub); options.sdk.addIntegration('nativeSdkIntegration'); diff --git a/packages/flutter/lib/src/worker_isolate.dart b/packages/flutter/lib/src/worker_isolate.dart index 3f9a2ec3e3..2c15e0f425 100644 --- a/packages/flutter/lib/src/worker_isolate.dart +++ b/packages/flutter/lib/src/worker_isolate.dart @@ -19,9 +19,13 @@ abstract class WorkerHost { /// Minimal config passed to isolates. Extend as needed. class WorkerConfig { + final bool debug; + final SentryLevel diagnosticLevel; final String? debugName; const WorkerConfig({ + required this.debug, + required this.diagnosticLevel, required this.debugName, }); } @@ -125,6 +129,11 @@ void runWorker( SendPort host, WorkerHandler handler, ) { + IsolateDiagnosticLog.configure( + debug: config.debug, + level: config.diagnosticLevel, + ); + final inbox = ReceivePort(); host.send(inbox.sendPort); From e334269ece7adbf36efdf228b3bdd4d25544a478 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 13:28:07 +0200 Subject: [PATCH 08/61] Update log messages --- .../integrations/native_sdk_integration.dart | 2 +- .../integrations/thread_info_integration.dart | 2 +- .../lib/src/{ => isolate}/isolate_helper.dart | 0 .../lib/src/isolate/isolate_logger.dart | 66 +++++++++++++++++++ .../isolate_worker.dart} | 23 +++---- .../lib/src/isolate_diagnostic_log.dart | 39 ----------- .../native/cocoa/cocoa_envelope_sender.dart | 26 +++----- .../native/java/android_envelope_sender.dart | 26 +++----- 8 files changed, 97 insertions(+), 87 deletions(-) rename packages/flutter/lib/src/{ => isolate}/isolate_helper.dart (100%) create mode 100644 packages/flutter/lib/src/isolate/isolate_logger.dart rename packages/flutter/lib/src/{worker_isolate.dart => isolate/isolate_worker.dart} (87%) delete mode 100644 packages/flutter/lib/src/isolate_diagnostic_log.dart diff --git a/packages/flutter/lib/src/integrations/native_sdk_integration.dart b/packages/flutter/lib/src/integrations/native_sdk_integration.dart index 9cdb8f65d7..edb17e0c8b 100644 --- a/packages/flutter/lib/src/integrations/native_sdk_integration.dart +++ b/packages/flutter/lib/src/integrations/native_sdk_integration.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; -import '../isolate_diagnostic_log.dart'; +import '../isolate_logger.dart'; import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; diff --git a/packages/flutter/lib/src/integrations/thread_info_integration.dart b/packages/flutter/lib/src/integrations/thread_info_integration.dart index 94ad83bb8c..a647b1b10e 100644 --- a/packages/flutter/lib/src/integrations/thread_info_integration.dart +++ b/packages/flutter/lib/src/integrations/thread_info_integration.dart @@ -3,7 +3,7 @@ import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; -import '../isolate_helper.dart'; +import '../isolate/isolate_helper.dart'; /// Integration for adding thread information to spans. /// diff --git a/packages/flutter/lib/src/isolate_helper.dart b/packages/flutter/lib/src/isolate/isolate_helper.dart similarity index 100% rename from packages/flutter/lib/src/isolate_helper.dart rename to packages/flutter/lib/src/isolate/isolate_helper.dart diff --git a/packages/flutter/lib/src/isolate/isolate_logger.dart b/packages/flutter/lib/src/isolate/isolate_logger.dart new file mode 100644 index 0000000000..9abf4e6ac2 --- /dev/null +++ b/packages/flutter/lib/src/isolate/isolate_logger.dart @@ -0,0 +1,66 @@ +import 'dart:developer' as developer; + +import '../../sentry_flutter.dart'; + +/// Isolate-local logger that writes diagnostic messages to `dart:developer.log`. +/// +/// Intended for worker/background isolates where a `SentryOptions` instance +/// or hub may not be available. Because Dart statics are isolate-local, +/// you must call [configure] once per isolate before using [log]. +class IsolateLogger { + IsolateLogger._(); + + static late final bool _debug; + static late final SentryLevel _level; + static late final String _loggerName; + static bool _isConfigured = false; + + /// Configures this logger for the current isolate. + /// + /// Must be called once per isolate before invoking [log]. + /// + /// - [debug]: when false, suppresses all logs except [SentryLevel.fatal]. + /// - [level]: minimum severity threshold (inclusive) when [debug] is true. + /// - [loggerName]: logger name for the call sites + static void configure( + {required bool debug, + required SentryLevel level, + required String loggerName}) { + _debug = debug; + _level = level; + _loggerName = loggerName; + _isConfigured = true; + } + + /// Emits a log entry if enabled for this isolate. + /// + /// Messages are forwarded to [developer.log]. The provided [level] is + /// mapped via [SentryLevel.toDartLogLevel] to a `developer.log` numeric level. + /// If logging is disabled or [level] is below the configured threshold, + /// nothing is emitted. [SentryLevel.fatal] is always emitted. + static void log( + SentryLevel level, + String message, { + String? logger, + Object? exception, + StackTrace? stackTrace, + }) { + assert( + _isConfigured, 'IsolateLogger.configure must be called before logging'); + if (_isEnabled(level)) { + developer.log( + '[${level.name}] $message', + level: level.toDartLogLevel(), + name: logger ?? _loggerName, + time: DateTime.now(), + error: exception, + stackTrace: stackTrace, + ); + } + } + + static bool _isEnabled(SentryLevel level) { + return _debug && level.ordinal >= _level.ordinal || + level == SentryLevel.fatal; + } +} diff --git a/packages/flutter/lib/src/worker_isolate.dart b/packages/flutter/lib/src/isolate/isolate_worker.dart similarity index 87% rename from packages/flutter/lib/src/worker_isolate.dart rename to packages/flutter/lib/src/isolate/isolate_worker.dart index 2c15e0f425..92a924e55c 100644 --- a/packages/flutter/lib/src/worker_isolate.dart +++ b/packages/flutter/lib/src/isolate/isolate_worker.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:isolate'; -import '../sentry_flutter.dart'; -import 'isolate_diagnostic_log.dart'; +import '../../sentry_flutter.dart'; +import 'isolate_logger.dart'; // ------------------------------------------- // HOST-SIDE API (runs on the main isolate) @@ -50,7 +50,7 @@ class Worker { /// Send a request to the worker and await a response. Future request(Object? payload) { - if (_closed) throw StateError('WorkerClient is closed'); + if (_closed) throw StateError('Worker is closed'); final id = _idCounter++; final completer = Completer.sync(); _pending[id] = completer; @@ -129,9 +129,10 @@ void runWorker( SendPort host, WorkerHandler handler, ) { - IsolateDiagnosticLog.configure( + IsolateLogger.configure( debug: config.debug, level: config.diagnosticLevel, + loggerName: config.debugName ?? 'SentryIsolateWorker', ); final inbox = ReceivePort(); @@ -139,12 +140,9 @@ void runWorker( inbox.listen((msg) async { if (msg == _Ctl.shutdown) { - IsolateDiagnosticLog.log( - SentryLevel.debug, 'Isolate received shutdown request', - logger: config.debugName); + IsolateLogger.log(SentryLevel.debug, 'Isolate received shutdown'); inbox.close(); - IsolateDiagnosticLog.log(SentryLevel.debug, 'Isolate closed.', - logger: config.debugName); + IsolateLogger.log(SentryLevel.debug, 'Isolate closed'); return; } @@ -164,11 +162,8 @@ void runWorker( try { await handler.onMessage(msg); } catch (exception, stackTrace) { - IsolateDiagnosticLog.log( - SentryLevel.error, 'Isolate error while handling message', - exception: exception, - stackTrace: stackTrace, - logger: config.debugName); + IsolateLogger.log(SentryLevel.error, 'Isolate failed to handle message', + exception: exception, stackTrace: stackTrace); } }); } diff --git a/packages/flutter/lib/src/isolate_diagnostic_log.dart b/packages/flutter/lib/src/isolate_diagnostic_log.dart deleted file mode 100644 index 96ac1e5e69..0000000000 --- a/packages/flutter/lib/src/isolate_diagnostic_log.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:developer' as developer; - -import '../sentry_flutter.dart'; - -class IsolateDiagnosticLog { - IsolateDiagnosticLog._(); - - static late final bool _debug; - static late final SentryLevel _level; - - static void configure({required bool debug, required SentryLevel level}) { - _debug = debug; - _level = level; - } - - static void log( - SentryLevel level, - String message, { - String? logger, - Object? exception, - StackTrace? stackTrace, - }) { - if (_isEnabled(level)) { - developer.log( - '[${level.name}] $message', - level: level.toDartLogLevel(), - name: logger ?? 'sentry', - time: DateTime.now(), - error: exception, - stackTrace: stackTrace, - ); - } - } - - static bool _isEnabled(SentryLevel level) { - return _debug && level.ordinal >= _level.ordinal || - level == SentryLevel.fatal; - } -} diff --git a/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart index 33e48f9662..4f6de62582 100644 --- a/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart +++ b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart @@ -6,8 +6,8 @@ import 'package:meta/meta.dart'; import 'package:objective_c/objective_c.dart'; import '../../../sentry_flutter.dart'; -import '../../worker_isolate.dart'; -import '../../isolate_diagnostic_log.dart'; +import '../../isolate/isolate_worker.dart'; +import '../../isolate/isolate_logger.dart'; import 'binding.dart' as cocoa; class CocoaEnvelopeSender implements WorkerHost { @@ -15,11 +15,11 @@ class CocoaEnvelopeSender implements WorkerHost { final WorkerConfig _config; Worker? _worker; - static final String name = 'SentryCocoaEnvelopeSender'; - CocoaEnvelopeSender(this._options) : _config = WorkerConfig( - debugName: name, + debugName: 'SentryCocoaEnvelopeSender', + debug: _options.debug, + diagnosticLevel: _options.diagnosticLevel, ); @internal // visible for testing/mocking @@ -45,7 +45,6 @@ class CocoaEnvelopeSender implements WorkerHost { _options.log( SentryLevel.warning, 'captureEnvelope called before start; dropping', - logger: name, ); return; } @@ -65,9 +64,7 @@ class _CocoaEnvelopeHandler extends WorkerHandler { final data = msg.materialize().asUint8List(); _captureEnvelope(data); } else { - IsolateDiagnosticLog.log(SentryLevel.warning, - 'Unexpected message type while handling a message: $msg', - logger: CocoaEnvelopeSender.name); + IsolateLogger.log(SentryLevel.warning, 'Unexpected message type: $msg'); } } @@ -78,15 +75,12 @@ class _CocoaEnvelopeHandler extends WorkerHandler { if (envelope != null) { cocoa.PrivateSentrySDKOnly.captureEnvelope(envelope); } else { - IsolateDiagnosticLog.log(SentryLevel.error, - 'Native Cocoa SDK returned null when capturing envelope', - logger: CocoaEnvelopeSender.name); + IsolateLogger.log(SentryLevel.error, + 'Native Cocoa SDK returned null when capturing envelope'); } } catch (exception, stackTrace) { - IsolateDiagnosticLog.log(SentryLevel.error, 'Failed to capture envelope', - exception: exception, - stackTrace: stackTrace, - logger: CocoaEnvelopeSender.name); + IsolateLogger.log(SentryLevel.error, 'Failed to capture envelope', + exception: exception, stackTrace: stackTrace); } } } diff --git a/packages/flutter/lib/src/native/java/android_envelope_sender.dart b/packages/flutter/lib/src/native/java/android_envelope_sender.dart index 71171badba..ff1b1b9cc8 100644 --- a/packages/flutter/lib/src/native/java/android_envelope_sender.dart +++ b/packages/flutter/lib/src/native/java/android_envelope_sender.dart @@ -6,8 +6,8 @@ import 'package:jni/jni.dart'; import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; -import '../../worker_isolate.dart'; -import '../../isolate_diagnostic_log.dart'; +import '../../isolate/isolate_worker.dart'; +import '../../isolate/isolate_logger.dart'; import 'binding.dart' as native; class AndroidEnvelopeSender implements WorkerHost { @@ -15,11 +15,11 @@ class AndroidEnvelopeSender implements WorkerHost { final WorkerConfig _config; Worker? _worker; - static final String name = 'SentryAndroidEnvelopeSender'; - AndroidEnvelopeSender(this._options) : _config = WorkerConfig( - debugName: name, + debugName: 'SentryAndroidEnvelopeSender', + debug: _options.debug, + diagnosticLevel: _options.diagnosticLevel, ); @internal // visible for testing/mocking @@ -46,7 +46,6 @@ class AndroidEnvelopeSender implements WorkerHost { _options.log( SentryLevel.warning, 'captureEnvelope called before worker started; dropping', - logger: name, ); return; } @@ -70,9 +69,7 @@ class _AndroidEnvelopeHandler extends WorkerHandler { final data = transferable.materialize().asUint8List(); _captureEnvelope(data, containsUnhandledException); } else { - IsolateDiagnosticLog.log(SentryLevel.warning, - 'Unexpected message type while handling a message: $msg', - logger: AndroidEnvelopeSender.name); + IsolateLogger.log(SentryLevel.warning, 'Unexpected message type: $msg'); } } @@ -86,15 +83,12 @@ class _AndroidEnvelopeHandler extends WorkerHandler { byteArray, containsUnhandledException); if (id == null) { - IsolateDiagnosticLog.log(SentryLevel.error, - 'Native Android SDK returned null id when capturing envelope', - logger: AndroidEnvelopeSender.name); + IsolateLogger.log(SentryLevel.error, + 'Native Android SDK returned null when capturing envelope'); } } catch (exception, stackTrace) { - IsolateDiagnosticLog.log(SentryLevel.error, 'Failed to capture envelope', - exception: exception, - stackTrace: stackTrace, - logger: AndroidEnvelopeSender.name); + IsolateLogger.log(SentryLevel.error, 'Failed to capture envelope', + exception: exception, stackTrace: stackTrace); // TODO: // if (options.automatedTestMode) { // rethrow; From aa728e71c027f6bb8c993e2ea6d6f083cdfdaa7d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 13:43:09 +0200 Subject: [PATCH 09/61] Update --- .../lib/src/isolate/isolate_worker.dart | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/flutter/lib/src/isolate/isolate_worker.dart b/packages/flutter/lib/src/isolate/isolate_worker.dart index 92a924e55c..70d1760e69 100644 --- a/packages/flutter/lib/src/isolate/isolate_worker.dart +++ b/packages/flutter/lib/src/isolate/isolate_worker.dart @@ -4,19 +4,12 @@ import 'dart:isolate'; import '../../sentry_flutter.dart'; import 'isolate_logger.dart'; +const _shutdownCommand = '_shutdown_'; + // ------------------------------------------- // HOST-SIDE API (runs on the main isolate) // ------------------------------------------- -/// Host-side lifecycle interface for a worker isolate. -/// -/// Responsible for spawning the worker isolate, and shutting it down. -/// It does not define the worker logic. -abstract class WorkerHost { - FutureOr start(); - FutureOr close(); -} - /// Minimal config passed to isolates. Extend as needed. class WorkerConfig { final bool debug; @@ -30,6 +23,15 @@ class WorkerConfig { }); } +/// Host-side lifecycle interface for a worker isolate. +/// +/// Responsible for spawning the worker isolate, and shutting it down. +/// It does not define the worker logic. +abstract class WorkerHost { + FutureOr start(); + FutureOr close(); +} + /// Host-side helper for workers to perform minimal request/response. class Worker { Worker(this._workerPort) { @@ -60,7 +62,7 @@ class Worker { void close() { if (_closed) return; - _workerPort.send(_Ctl.shutdown); + _workerPort.send(_shutdownCommand); _closed = true; if (_pending.isEmpty) { _responses.close(); @@ -84,10 +86,6 @@ class Worker { } } -class _Ctl { - static const shutdown = '_shutdown_'; -} - /// Worker (isolate) entry-point signature. typedef WorkerEntry = void Function((SendPort, WorkerConfig)); @@ -123,7 +121,11 @@ abstract class WorkerHandler { FutureOr onRequest(Object? payload) => {}; } -/// Generic worker runtime. Reuse for every Sentry worker. +/// Runs the Sentry worker loop inside a background isolate. +/// +/// Call this only from the worker isolate entry-point spawned via +/// [spawnWorker]. It configures logging, handshakes with the host, and routes +/// messages void runWorker( WorkerConfig config, SendPort host, @@ -139,7 +141,7 @@ void runWorker( host.send(inbox.sendPort); inbox.listen((msg) async { - if (msg == _Ctl.shutdown) { + if (msg == _shutdownCommand) { IsolateLogger.log(SentryLevel.debug, 'Isolate received shutdown'); inbox.close(); IsolateLogger.log(SentryLevel.debug, 'Isolate closed'); From 45cc8c30e99bd4685589694d9992b8f56b155f70 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 13:43:22 +0200 Subject: [PATCH 10/61] Update --- .../flutter/lib/src/integrations/native_sdk_integration.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flutter/lib/src/integrations/native_sdk_integration.dart b/packages/flutter/lib/src/integrations/native_sdk_integration.dart index edb17e0c8b..76c91eda6e 100644 --- a/packages/flutter/lib/src/integrations/native_sdk_integration.dart +++ b/packages/flutter/lib/src/integrations/native_sdk_integration.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; -import '../isolate_logger.dart'; import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; From a603960cc2b420f600eb69b864599f6026444f76 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 15:16:00 +0200 Subject: [PATCH 11/61] Update --- packages/flutter/lib/src/isolate/isolate_worker.dart | 4 ++-- .../lib/src/native/cocoa/cocoa_envelope_sender.dart | 10 +++++++--- .../lib/src/native/java/android_envelope_sender.dart | 10 +++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/flutter/lib/src/isolate/isolate_worker.dart b/packages/flutter/lib/src/isolate/isolate_worker.dart index 70d1760e69..5ad2900b4e 100644 --- a/packages/flutter/lib/src/isolate/isolate_worker.dart +++ b/packages/flutter/lib/src/isolate/isolate_worker.dart @@ -14,7 +14,7 @@ const _shutdownCommand = '_shutdown_'; class WorkerConfig { final bool debug; final SentryLevel diagnosticLevel; - final String? debugName; + final String debugName; const WorkerConfig({ required this.debug, @@ -134,7 +134,7 @@ void runWorker( IsolateLogger.configure( debug: config.debug, level: config.diagnosticLevel, - loggerName: config.debugName ?? 'SentryIsolateWorker', + loggerName: config.debugName, ); final inbox = ReceivePort(); diff --git a/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart index 4f6de62582..be70515ab5 100644 --- a/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart +++ b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart @@ -10,17 +10,21 @@ import '../../isolate/isolate_worker.dart'; import '../../isolate/isolate_logger.dart'; import 'binding.dart' as cocoa; +typedef SpawnWorkerFn = Future Function(WorkerConfig, WorkerEntry); + class CocoaEnvelopeSender implements WorkerHost { final SentryFlutterOptions _options; final WorkerConfig _config; + final SpawnWorkerFn _spawn; Worker? _worker; - CocoaEnvelopeSender(this._options) + CocoaEnvelopeSender(this._options, {SpawnWorkerFn? spawn}) : _config = WorkerConfig( debugName: 'SentryCocoaEnvelopeSender', debug: _options.debug, diagnosticLevel: _options.diagnosticLevel, - ); + ), + _spawn = spawn ?? spawnWorker; @internal // visible for testing/mocking static CocoaEnvelopeSender Function(SentryFlutterOptions) factory = @@ -29,7 +33,7 @@ class CocoaEnvelopeSender implements WorkerHost { @override FutureOr start() async { if (_worker != null) return; - _worker = await spawnWorker(_config, _entryPoint); + _worker = await _spawn(_config, _entryPoint); } @override diff --git a/packages/flutter/lib/src/native/java/android_envelope_sender.dart b/packages/flutter/lib/src/native/java/android_envelope_sender.dart index ff1b1b9cc8..e77c398311 100644 --- a/packages/flutter/lib/src/native/java/android_envelope_sender.dart +++ b/packages/flutter/lib/src/native/java/android_envelope_sender.dart @@ -10,17 +10,21 @@ import '../../isolate/isolate_worker.dart'; import '../../isolate/isolate_logger.dart'; import 'binding.dart' as native; +typedef SpawnWorkerFn = Future Function(WorkerConfig, WorkerEntry); + class AndroidEnvelopeSender implements WorkerHost { final SentryFlutterOptions _options; final WorkerConfig _config; + final SpawnWorkerFn _spawn; Worker? _worker; - AndroidEnvelopeSender(this._options) + AndroidEnvelopeSender(this._options, {SpawnWorkerFn? spawn}) : _config = WorkerConfig( debugName: 'SentryAndroidEnvelopeSender', debug: _options.debug, diagnosticLevel: _options.diagnosticLevel, - ); + ), + _spawn = spawn ?? spawnWorker; @internal // visible for testing/mocking static AndroidEnvelopeSender Function(SentryFlutterOptions) factory = @@ -29,7 +33,7 @@ class AndroidEnvelopeSender implements WorkerHost { @override FutureOr start() async { if (_worker != null) return; - _worker = await spawnWorker(_config, _entryPoint); + _worker = await _spawn(_config, _entryPoint); } @override From 147da011587df77e6644302e77fe715cbd96930b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 15:20:52 +0200 Subject: [PATCH 12/61] Update --- 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 a6df55c0af..df84f63978 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -80,12 +80,10 @@ class SentryNativeJava extends SentryNativeChannel { return super.init(hub); } - late AndroidEnvelopeSender envelopeWorker; - @override FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { - envelopeWorker.captureEnvelope(envelopeData, containsUnhandledException); + _envelopeSender?.captureEnvelope(envelopeData, containsUnhandledException); } @override From 2b11149a0239bf3506796d6a943d7e3eec920392 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 15:21:29 +0200 Subject: [PATCH 13/61] 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 df84f63978..58fb8657ce 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -174,7 +174,7 @@ class SentryNativeJava extends SentryNativeChannel { @override Future close() async { await _replayRecorder?.stop(); - envelopeWorker.close(); + await _envelopeSender?.close(); return super.close(); } } From 83259527ff83f22a4107a0a16a01c1e91bb145bc Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 15:27:55 +0200 Subject: [PATCH 14/61] Update --- .../test/isolate/isolate_logger_test.dart | 52 +++++ .../test/isolate/isolate_worker_test.dart | 203 ++++++++++++++++++ .../native/android_envelope_sender_test.dart | 191 ++++++++++++++++ .../native/cocoa_envelope_sender_test.dart | 188 ++++++++++++++++ 4 files changed, 634 insertions(+) create mode 100644 packages/flutter/test/isolate/isolate_logger_test.dart create mode 100644 packages/flutter/test/isolate/isolate_worker_test.dart create mode 100644 packages/flutter/test/native/android_envelope_sender_test.dart create mode 100644 packages/flutter/test/native/cocoa_envelope_sender_test.dart diff --git a/packages/flutter/test/isolate/isolate_logger_test.dart b/packages/flutter/test/isolate/isolate_logger_test.dart new file mode 100644 index 0000000000..5e804bdf5c --- /dev/null +++ b/packages/flutter/test/isolate/isolate_logger_test.dart @@ -0,0 +1,52 @@ +@TestOn('vm') +library; + +import 'dart:isolate'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/isolate/isolate_logger.dart'; + +void _entryUnconfigured(SendPort sendPort) { + try { + IsolateLogger.log(SentryLevel.info, 'x'); + sendPort.send('no-error'); + } catch (e) { + sendPort.send(e.runtimeType.toString()); + } +} + +void main() { + test('configure required before log (debug builds)', () async { + final rp = ReceivePort(); + await Isolate.spawn(_entryUnconfigured, rp.sendPort, + debugName: 'LoggerUnconfigured'); + final result = await rp.first; + rp.close(); + + // In debug mode, assert triggers AssertionError before any late fields are read. + expect(result, 'AssertionError'); + }); + + test('fatal logs even when debug=false', () { + IsolateLogger.configure( + debug: false, + level: SentryLevel.error, + loggerName: 't', + ); + expect(() => IsolateLogger.log(SentryLevel.fatal, 'fatal ok'), + returnsNormally); + }); + + test('threshold gating (no-throw at info below warning)', () { + IsolateLogger.configure( + debug: true, + level: SentryLevel.warning, + loggerName: 't', + ); + expect( + () => IsolateLogger.log(SentryLevel.info, 'info ok'), returnsNormally); + expect(() => IsolateLogger.log(SentryLevel.warning, 'warn ok'), + returnsNormally); + }); +} diff --git a/packages/flutter/test/isolate/isolate_worker_test.dart b/packages/flutter/test/isolate/isolate_worker_test.dart new file mode 100644 index 0000000000..be5784a4a2 --- /dev/null +++ b/packages/flutter/test/isolate/isolate_worker_test.dart @@ -0,0 +1,203 @@ +@TestOn('vm') +library; + +import 'dart:async'; +import 'dart:isolate'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/isolate/isolate_worker.dart'; + +class _EchoHandler extends WorkerHandler { + @override + Future onMessage(Object? message) async { + if (message is (SendPort, Object?)) { + message.$1.send(message.$2); + } + } + + @override + Future onRequest(Object? payload) async => payload; +} + +class _ErrorHandler extends WorkerHandler { + @override + Future onMessage(Object? message) async {} + + @override + Future onRequest(Object? payload) async { + throw Exception('boom'); + } +} + +class _DelayHandler extends WorkerHandler { + @override + Future onMessage(Object? message) async {} + + @override + Future onRequest(Object? payload) async { + final milliseconds = payload as int; + await Future.delayed(Duration(milliseconds: milliseconds)); + return 'd:$milliseconds'; + } +} + +class _DebugNameHandler extends WorkerHandler { + @override + Future onMessage(Object? message) async {} + + @override + Future onRequest(Object? payload) async { + return Isolate.current.debugName; + } +} + +void _entryEcho((SendPort, WorkerConfig) init) { + final (host, config) = init; + runWorker(config, host, _EchoHandler()); +} + +void _entryError((SendPort, WorkerConfig) init) { + final (host, config) = init; + runWorker(config, host, _ErrorHandler()); +} + +void _entryDelay((SendPort, WorkerConfig) init) { + final (host, config) = init; + runWorker(config, host, _DelayHandler()); +} + +void _entryDebugName((SendPort, WorkerConfig) init) { + final (host, config) = init; + runWorker(config, host, _DebugNameHandler()); +} + +void main() { + group('Worker isolate', () { + test('request/response echoes', () async { + final worker = await spawnWorker( + const WorkerConfig( + debug: true, + diagnosticLevel: SentryLevel.debug, + debugName: 'EchoWorker', + ), + _entryEcho, + ); + try { + final result = await worker.request('ping'); + expect(result, 'ping'); + } finally { + worker.close(); + } + }); + + test('fire-and-forget can ack via SendPort', () async { + final worker = await spawnWorker( + const WorkerConfig( + debug: true, + diagnosticLevel: SentryLevel.debug, + debugName: 'AckWorker', + ), + _entryEcho, + ); + try { + final rp = ReceivePort(); + worker.send((rp.sendPort, 'ok')); + expect(await rp.first, 'ok'); + rp.close(); + } finally { + worker.close(); + } + }); + + test('request errors propagate as RemoteError', () async { + final worker = await spawnWorker( + const WorkerConfig( + debug: true, + diagnosticLevel: SentryLevel.debug, + debugName: 'ErrorWorker', + ), + _entryError, + ); + try { + expect(() => worker.request('any'), throwsA(isA())); + } finally { + worker.close(); + } + }); + + test('concurrent requests are correlated', () async { + final worker = await spawnWorker( + const WorkerConfig( + debug: true, + diagnosticLevel: SentryLevel.debug, + debugName: 'DelayWorker', + ), + _entryDelay, + ); + try { + final futures = >[ + worker.request(50), + worker.request(10), + worker.request(30), + ]; + final results = await Future.wait(futures); + expect(results, ['d:50', 'd:10', 'd:30']); + } finally { + worker.close(); + } + }); + + test('close rejects new requests; in-flight completes', () async { + final worker = await spawnWorker( + const WorkerConfig( + debug: true, + diagnosticLevel: SentryLevel.debug, + debugName: 'CloseWorker', + ), + _entryDelay, + ); + try { + final inFlight = worker.request(30); + worker.close(); + expect(() => worker.request(1), throwsA(isA())); + expect(await inFlight, 'd:30'); + } finally { + // idempotent + worker.close(); + } + }); + + test('send after close is a no-op and does not throw', () async { + final worker = await spawnWorker( + const WorkerConfig( + debug: true, + diagnosticLevel: SentryLevel.debug, + debugName: 'NoThrowSendAfterCloseWorker', + ), + _entryEcho, + ); + worker.close(); + // Fire-and-forget send should be safe and not throw even after close. + expect(() => worker.send('ignored'), returnsNormally); + }); + + test('debugName propagates to worker isolate', () async { + const debugName = 'DebugNameWorker'; + final worker = await spawnWorker( + const WorkerConfig( + debug: true, + diagnosticLevel: SentryLevel.debug, + debugName: debugName, + ), + _entryDebugName, + ); + try { + final result = await worker.request(null); + expect(result, debugName); + } finally { + worker.close(); + } + }); + }); +} diff --git a/packages/flutter/test/native/android_envelope_sender_test.dart b/packages/flutter/test/native/android_envelope_sender_test.dart new file mode 100644 index 0000000000..d1ba80e282 --- /dev/null +++ b/packages/flutter/test/native/android_envelope_sender_test.dart @@ -0,0 +1,191 @@ +@TestOn('vm') +// ignore_for_file: invalid_use_of_internal_member +library; + +import 'dart:async'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/native/java/android_envelope_sender.dart'; +import 'package:sentry_flutter/src/isolate/isolate_worker.dart'; + +void main() { + group('AndroidEnvelopeSender host behavior', () { + test('warns and drops when not started', () { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + final logs = <(SentryLevel, String)>[]; + options.log = (level, message, {logger, exception, stackTrace}) { + logs.add((level, message)); + }; + + final sender = AndroidEnvelopeSender(options); + sender.captureEnvelope(Uint8List.fromList([1, 2, 3]), false); + + expect( + logs.any((e) => + e.$1 == SentryLevel.warning && + e.$2.contains( + 'captureEnvelope called before worker started; dropping')), + isTrue, + ); + }); + + test('close is a no-op when not started', () { + final options = SentryFlutterOptions(); + final sender = AndroidEnvelopeSender(options); + expect(() => sender.close(), returnsNormally); + expect(() => sender.close(), returnsNormally); + }); + + test('warns and drops after close', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + final logs = <(SentryLevel, String)>[]; + options.log = (level, message, {logger, exception, stackTrace}) { + logs.add((level, message)); + }; + + final sender = AndroidEnvelopeSender(options); + await sender.start(); + sender.close(); + + sender.captureEnvelope(Uint8List.fromList([9]), false); + + expect( + logs.any((e) => + e.$1 == SentryLevel.warning && + e.$2.contains( + 'captureEnvelope called before worker started; dropping')), + isTrue, + ); + }); + + test('start is a no-op when already started', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + var spawnCount = 0; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + spawnCount++; + final inbox = ReceivePort(); + addTearDown(() => inbox.close()); + return Worker(inbox.sendPort); + } + + final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); + + await sender.start(); + await sender.start(); + expect(spawnCount, 1); + + sender.close(); + spawnCount = 0; + + await sender.start(); + expect(spawnCount, 1); + + // Close twice should be safe. + expect(() => sender.close(), returnsNormally); + expect(() => sender.close(), returnsNormally); + }); + + test('delivers tuple to worker after start', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + final inboxes = []; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + final inbox = ReceivePort(); + inboxes.add(inbox); + addTearDown(() => inbox.close()); + return Worker(inbox.sendPort); + } + + final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + final payload = Uint8List.fromList([4, 5, 6]); + sender.captureEnvelope(payload, true); + + final msg = await inboxes.last.first; + expect(msg, isA<(TransferableTypedData, bool)>()); + final (transferable, containsUnhandled) = + msg as (TransferableTypedData, bool); + expect(containsUnhandled, true); + final data = transferable.materialize().asUint8List(); + expect(data, [4, 5, 6]); + + sender.close(); + }); + + test('uses expected WorkerConfig', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + WorkerConfig? seenConfig; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + seenConfig = config; + final inbox = ReceivePort(); + addTearDown(() => inbox.close()); + return Worker(inbox.sendPort); + } + + final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + expect(seenConfig, isNotNull); + expect(seenConfig!.debugName, 'SentryAndroidEnvelopeSender'); + expect(seenConfig!.debug, options.debug); + expect(seenConfig!.diagnosticLevel, options.diagnosticLevel); + + sender.close(); + }); + + test('sends are delivered sequentially with flags', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + final inboxes = []; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + final inbox = ReceivePort(); + inboxes.add(inbox); + addTearDown(() => inbox.close()); + return Worker(inbox.sendPort); + } + + final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + sender.captureEnvelope(Uint8List.fromList([10]), true); + sender.captureEnvelope(Uint8List.fromList([11]), false); + + final inbox = inboxes.last; + final msgs = await inbox.take(2).toList(); + final msg1 = msgs[0]; + final msg2 = msgs[1]; + + expect(msg1, isA<(TransferableTypedData, bool)>()); + expect(msg2, isA<(TransferableTypedData, bool)>()); + + final (t1, f1) = msg1 as (TransferableTypedData, bool); + final (t2, f2) = msg2 as (TransferableTypedData, bool); + expect(f1, true); + expect(f2, false); + final data1 = t1.materialize().asUint8List(); + final data2 = t2.materialize().asUint8List(); + expect(data1, [10]); + expect(data2, [11]); + + sender.close(); + }); + }); +} diff --git a/packages/flutter/test/native/cocoa_envelope_sender_test.dart b/packages/flutter/test/native/cocoa_envelope_sender_test.dart new file mode 100644 index 0000000000..37aed72847 --- /dev/null +++ b/packages/flutter/test/native/cocoa_envelope_sender_test.dart @@ -0,0 +1,188 @@ +@TestOn('vm') +// ignore_for_file: invalid_use_of_internal_member +library; + +import 'dart:async'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/native/cocoa/cocoa_envelope_sender.dart'; +import 'package:sentry_flutter/src/isolate/isolate_worker.dart'; + +void main() { + group('CocoaEnvelopeSender host behavior', () { + test('warns and drops when not started', () { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + final logs = <(SentryLevel, String)>[]; + options.log = (level, message, {logger, exception, stackTrace}) { + logs.add((level, message)); + }; + + final sender = CocoaEnvelopeSender(options); + sender.captureEnvelope(Uint8List.fromList([1, 2, 3])); + + expect( + logs.any((e) => + e.$1 == SentryLevel.warning && + e.$2.contains('captureEnvelope called before start; dropping')), + isTrue, + ); + }); + + test('close is a no-op when not started', () { + final options = SentryFlutterOptions(); + final sender = CocoaEnvelopeSender(options); + expect(() => sender.close(), returnsNormally); + expect(() => sender.close(), returnsNormally); + }); + + test('start is a no-op when already started', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + var spawnCount = 0; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + spawnCount++; + final inbox = ReceivePort(); + late final StreamSubscription sub; + sub = inbox.listen((msg) async { + if (msg == '_shutdown_') { + await sub.cancel(); + inbox.close(); + } + }); + return Worker(inbox.sendPort); + } + + final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); + + await sender.start(); + await sender.start(); + expect(spawnCount, 1); + + sender.close(); + spawnCount = 0; + + await sender.start(); + expect(spawnCount, 1); + + // Close twice should be safe. + expect(() => sender.close(), returnsNormally); + expect(() => sender.close(), returnsNormally); + }); + + test('warns and drops after close', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + final logs = <(SentryLevel, String)>[]; + options.log = (level, message, {logger, exception, stackTrace}) { + logs.add((level, message)); + }; + + final sender = CocoaEnvelopeSender(options); + await sender.start(); + sender.close(); + + sender.captureEnvelope(Uint8List.fromList([9])); + + expect( + logs.any((e) => + e.$1 == SentryLevel.warning && + e.$2.contains('captureEnvelope called before start; dropping')), + isTrue, + ); + }); + + test('sends are delivered sequentially', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + final inboxes = []; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + final inbox = ReceivePort(); + inboxes.add(inbox); + addTearDown(() => inbox.close()); + return Worker(inbox.sendPort); + } + + final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + sender.captureEnvelope(Uint8List.fromList([10])); + sender.captureEnvelope(Uint8List.fromList([11])); + + final inbox = inboxes.last; + final msgs = await inbox.take(2).toList(); + final msg1 = msgs[0]; + final msg2 = msgs[1]; + + expect(msg1, isA()); + expect(msg2, isA()); + + final data1 = (msg1 as TransferableTypedData).materialize().asUint8List(); + final data2 = (msg2 as TransferableTypedData).materialize().asUint8List(); + expect(data1, [10]); + expect(data2, [11]); + + sender.close(); + }); + + test('delivers to worker after start', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + final inboxes = []; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + final inbox = ReceivePort(); + inboxes.add(inbox); + addTearDown(() => inbox.close()); + return Worker(inbox.sendPort); + } + + final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + final payload = Uint8List.fromList([1, 2, 3]); + sender.captureEnvelope(payload); + + final msg = await inboxes.last.first; + expect(msg, isA()); + final data = (msg as TransferableTypedData).materialize().asUint8List(); + expect(data, [1, 2, 3]); + + sender.close(); + }); + + test('uses expected WorkerConfig', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + WorkerConfig? seenConfig; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + seenConfig = config; + final inbox = ReceivePort(); + addTearDown(() => inbox.close()); + return Worker(inbox.sendPort); + } + + final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + expect(seenConfig, isNotNull); + expect(seenConfig!.debugName, 'SentryCocoaEnvelopeSender'); + expect(seenConfig!.debug, options.debug); + expect(seenConfig!.diagnosticLevel, options.diagnosticLevel); + + sender.close(); + }); + }); +} From 3dbe75120cff66962ba9565b814b7e630366c6ab Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 15:56:48 +0200 Subject: [PATCH 15/61] Update --- .../lib/src/isolate/isolate_worker.dart | 34 ++++++++++++------- .../native/android_envelope_sender_test.dart | 12 ++++--- .../native/cocoa_envelope_sender_test.dart | 12 ++++--- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/flutter/lib/src/isolate/isolate_worker.dart b/packages/flutter/lib/src/isolate/isolate_worker.dart index 5ad2900b4e..31ca49cec9 100644 --- a/packages/flutter/lib/src/isolate/isolate_worker.dart +++ b/packages/flutter/lib/src/isolate/isolate_worker.dart @@ -33,14 +33,15 @@ abstract class WorkerHost { } /// Host-side helper for workers to perform minimal request/response. +/// Adapted from https://dart.dev/language/isolates#robust-ports-example class Worker { - Worker(this._workerPort) { + Worker(this._workerPort, this._responses) { _responses.listen(_handleResponse); } final SendPort _workerPort; SendPort get port => _workerPort; - final ReceivePort _responses = ReceivePort(); + final ReceivePort _responses; final Map> _pending = {}; int _idCounter = 0; bool _closed = false; @@ -56,7 +57,7 @@ class Worker { final id = _idCounter++; final completer = Completer.sync(); _pending[id] = completer; - _workerPort.send((id, payload, _responses.sendPort)); + _workerPort.send((id, payload)); return completer.future; } @@ -94,14 +95,23 @@ Future spawnWorker( WorkerConfig config, WorkerEntry entry, ) async { - final receivePort = ReceivePort(); + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (SendPort commandPort) { + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + await Isolate.spawn<(SendPort, WorkerConfig)>( entry, - (receivePort.sendPort, config), + (initPort.sendPort, config), debugName: config.debugName, ); - final workerPort = await receivePort.first as SendPort; - return Worker(workerPort); + + final (ReceivePort receivePort, SendPort sendPort) = await connection.future; + return Worker(sendPort, receivePort); } // ------------------------------------------- @@ -148,14 +158,14 @@ void runWorker( return; } - // RPC: (id, payload, replyTo) - if (msg is (int, Object?, SendPort)) { - final (id, payload, replyTo) = msg; + // RPC: (id, payload) + if (msg is (int, Object?)) { + final (id, payload) = msg; try { final result = await handler.onRequest(payload); - replyTo.send((id, result)); + host.send((id, result)); } catch (e, st) { - replyTo.send((id, RemoteError(e.toString(), st.toString()))); + host.send((id, RemoteError(e.toString(), st.toString()))); } return; } diff --git a/packages/flutter/test/native/android_envelope_sender_test.dart b/packages/flutter/test/native/android_envelope_sender_test.dart index d1ba80e282..dcd157c3e6 100644 --- a/packages/flutter/test/native/android_envelope_sender_test.dart +++ b/packages/flutter/test/native/android_envelope_sender_test.dart @@ -75,7 +75,8 @@ void main() { spawnCount++; final inbox = ReceivePort(); addTearDown(() => inbox.close()); - return Worker(inbox.sendPort); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); } final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); @@ -105,7 +106,8 @@ void main() { final inbox = ReceivePort(); inboxes.add(inbox); addTearDown(() => inbox.close()); - return Worker(inbox.sendPort); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); } final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); @@ -135,7 +137,8 @@ void main() { seenConfig = config; final inbox = ReceivePort(); addTearDown(() => inbox.close()); - return Worker(inbox.sendPort); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); } final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); @@ -159,7 +162,8 @@ void main() { final inbox = ReceivePort(); inboxes.add(inbox); addTearDown(() => inbox.close()); - return Worker(inbox.sendPort); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); } final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); diff --git a/packages/flutter/test/native/cocoa_envelope_sender_test.dart b/packages/flutter/test/native/cocoa_envelope_sender_test.dart index 37aed72847..31c18373c3 100644 --- a/packages/flutter/test/native/cocoa_envelope_sender_test.dart +++ b/packages/flutter/test/native/cocoa_envelope_sender_test.dart @@ -56,7 +56,8 @@ void main() { inbox.close(); } }); - return Worker(inbox.sendPort); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); } final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); @@ -109,7 +110,8 @@ void main() { final inbox = ReceivePort(); inboxes.add(inbox); addTearDown(() => inbox.close()); - return Worker(inbox.sendPort); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); } final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); @@ -144,7 +146,8 @@ void main() { final inbox = ReceivePort(); inboxes.add(inbox); addTearDown(() => inbox.close()); - return Worker(inbox.sendPort); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); } final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); @@ -171,7 +174,8 @@ void main() { seenConfig = config; final inbox = ReceivePort(); addTearDown(() => inbox.close()); - return Worker(inbox.sendPort); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); } final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); From 71ba593d163e856e30819ed8dc9bb181069e4ef7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 16:01:55 +0200 Subject: [PATCH 16/61] Update --- packages/flutter/lib/src/isolate/isolate_worker.dart | 9 --------- .../lib/src/native/cocoa/cocoa_envelope_sender.dart | 4 +--- .../lib/src/native/java/android_envelope_sender.dart | 4 +--- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/flutter/lib/src/isolate/isolate_worker.dart b/packages/flutter/lib/src/isolate/isolate_worker.dart index 31ca49cec9..d76f4b6b33 100644 --- a/packages/flutter/lib/src/isolate/isolate_worker.dart +++ b/packages/flutter/lib/src/isolate/isolate_worker.dart @@ -23,15 +23,6 @@ class WorkerConfig { }); } -/// Host-side lifecycle interface for a worker isolate. -/// -/// Responsible for spawning the worker isolate, and shutting it down. -/// It does not define the worker logic. -abstract class WorkerHost { - FutureOr start(); - FutureOr close(); -} - /// Host-side helper for workers to perform minimal request/response. /// Adapted from https://dart.dev/language/isolates#robust-ports-example class Worker { diff --git a/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart index be70515ab5..a371518235 100644 --- a/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart +++ b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart @@ -12,7 +12,7 @@ import 'binding.dart' as cocoa; typedef SpawnWorkerFn = Future Function(WorkerConfig, WorkerEntry); -class CocoaEnvelopeSender implements WorkerHost { +class CocoaEnvelopeSender { final SentryFlutterOptions _options; final WorkerConfig _config; final SpawnWorkerFn _spawn; @@ -30,13 +30,11 @@ class CocoaEnvelopeSender implements WorkerHost { static CocoaEnvelopeSender Function(SentryFlutterOptions) factory = CocoaEnvelopeSender.new; - @override FutureOr start() async { if (_worker != null) return; _worker = await _spawn(_config, _entryPoint); } - @override FutureOr close() { _worker?.close(); _worker = null; diff --git a/packages/flutter/lib/src/native/java/android_envelope_sender.dart b/packages/flutter/lib/src/native/java/android_envelope_sender.dart index e77c398311..05be049268 100644 --- a/packages/flutter/lib/src/native/java/android_envelope_sender.dart +++ b/packages/flutter/lib/src/native/java/android_envelope_sender.dart @@ -12,7 +12,7 @@ import 'binding.dart' as native; typedef SpawnWorkerFn = Future Function(WorkerConfig, WorkerEntry); -class AndroidEnvelopeSender implements WorkerHost { +class AndroidEnvelopeSender { final SentryFlutterOptions _options; final WorkerConfig _config; final SpawnWorkerFn _spawn; @@ -30,13 +30,11 @@ class AndroidEnvelopeSender implements WorkerHost { static AndroidEnvelopeSender Function(SentryFlutterOptions) factory = AndroidEnvelopeSender.new; - @override FutureOr start() async { if (_worker != null) return; _worker = await _spawn(_config, _entryPoint); } - @override FutureOr close() { _worker?.close(); _worker = null; From fe7f6dfccd60e50ad01b4508acb8d34911502a80 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 4 Sep 2025 16:04:52 +0200 Subject: [PATCH 17/61] Update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a567ca68f6..df6db27aca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Enhancements + +- Offload `captureEnvelope` to background isolate for iOS and Android ([#3232](https://github.com/getsentry/sentry-dart/pull/3232)) + ## 9.7.0-beta.2 ### Features From 39a951e49fcbe53ef4785897637156a730637d08 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 11:08:19 +0200 Subject: [PATCH 18/61] Update --- packages/flutter/lib/src/isolate/isolate_logger.dart | 4 ++-- packages/flutter/lib/src/isolate/isolate_worker.dart | 4 +--- .../flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart | 2 +- .../flutter/lib/src/native/java/android_envelope_sender.dart | 2 +- packages/flutter/test/isolate/isolate_worker_test.dart | 1 - 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/flutter/lib/src/isolate/isolate_logger.dart b/packages/flutter/lib/src/isolate/isolate_logger.dart index 9abf4e6ac2..5fe97dff86 100644 --- a/packages/flutter/lib/src/isolate/isolate_logger.dart +++ b/packages/flutter/lib/src/isolate/isolate_logger.dart @@ -2,7 +2,7 @@ import 'dart:developer' as developer; import '../../sentry_flutter.dart'; -/// Isolate-local logger that writes diagnostic messages to `dart:developer.log`. +/// Static logger for Isolates that writes diagnostic messages to `dart:developer.log`. /// /// Intended for worker/background isolates where a `SentryOptions` instance /// or hub may not be available. Because Dart statics are isolate-local, @@ -32,7 +32,7 @@ class IsolateLogger { _isConfigured = true; } - /// Emits a log entry if enabled for this isolate. + /// Emits a log entry if enabled. /// /// Messages are forwarded to [developer.log]. The provided [level] is /// mapped via [SentryLevel.toDartLogLevel] to a `developer.log` numeric level. diff --git a/packages/flutter/lib/src/isolate/isolate_worker.dart b/packages/flutter/lib/src/isolate/isolate_worker.dart index d76f4b6b33..434e4b232e 100644 --- a/packages/flutter/lib/src/isolate/isolate_worker.dart +++ b/packages/flutter/lib/src/isolate/isolate_worker.dart @@ -10,7 +10,7 @@ const _shutdownCommand = '_shutdown_'; // HOST-SIDE API (runs on the main isolate) // ------------------------------------------- -/// Minimal config passed to isolates. Extend as needed. +/// Minimal config passed to isolates - extend as needed. class WorkerConfig { final bool debug; final SentryLevel diagnosticLevel; @@ -149,7 +149,6 @@ void runWorker( return; } - // RPC: (id, payload) if (msg is (int, Object?)) { final (id, payload) = msg; try { @@ -161,7 +160,6 @@ void runWorker( return; } - // Fire-and-forget try { await handler.onMessage(msg); } catch (exception, stackTrace) { diff --git a/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart index a371518235..72f3eb74e6 100644 --- a/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart +++ b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart @@ -26,7 +26,7 @@ class CocoaEnvelopeSender { ), _spawn = spawn ?? spawnWorker; - @internal // visible for testing/mocking + @internal static CocoaEnvelopeSender Function(SentryFlutterOptions) factory = CocoaEnvelopeSender.new; diff --git a/packages/flutter/lib/src/native/java/android_envelope_sender.dart b/packages/flutter/lib/src/native/java/android_envelope_sender.dart index 05be049268..59d53a3960 100644 --- a/packages/flutter/lib/src/native/java/android_envelope_sender.dart +++ b/packages/flutter/lib/src/native/java/android_envelope_sender.dart @@ -26,7 +26,7 @@ class AndroidEnvelopeSender { ), _spawn = spawn ?? spawnWorker; - @internal // visible for testing/mocking + @internal static AndroidEnvelopeSender Function(SentryFlutterOptions) factory = AndroidEnvelopeSender.new; diff --git a/packages/flutter/test/isolate/isolate_worker_test.dart b/packages/flutter/test/isolate/isolate_worker_test.dart index be5784a4a2..f1b6d630fb 100644 --- a/packages/flutter/test/isolate/isolate_worker_test.dart +++ b/packages/flutter/test/isolate/isolate_worker_test.dart @@ -1,7 +1,6 @@ @TestOn('vm') library; -import 'dart:async'; import 'dart:isolate'; import 'package:flutter_test/flutter_test.dart'; From 884642f5bd105bbaa36870fa1febdb7a085d1bed Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 11:31:31 +0200 Subject: [PATCH 19/61] Fix test --- packages/flutter/test/sentry_native_channel_test.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/flutter/test/sentry_native_channel_test.dart b/packages/flutter/test/sentry_native_channel_test.dart index c0a49c0aa4..f0ab1bceb6 100644 --- a/packages/flutter/test/sentry_native_channel_test.dart +++ b/packages/flutter/test/sentry_native_channel_test.dart @@ -194,6 +194,7 @@ void main() { }); test('startProfiler', () { + sut.startProfiler(SentryId.newId()); final matcher = _nativeUnavailableMatcher( mockPlatform, androidUnsupported: true, @@ -238,13 +239,8 @@ void main() { when(channel.invokeMethod('captureEnvelope', any)) .thenAnswer((_) async => {}); - final matcher = _nativeUnavailableMatcher( - mockPlatform, - includeLookupSymbol: true, - ); - final data = Uint8List.fromList([1, 2, 3]); - expect(() => sut.captureEnvelope(data, false), matcher); + sut.captureEnvelope(data, false); verifyZeroInteractions(channel); }, @@ -267,7 +263,7 @@ void main() { mockPlatform, includeLookupSymbol: true, ); - + sut.loadDebugImages(SentryStackTrace(frames: [])); expect( () => sut.loadDebugImages(SentryStackTrace(frames: [])), matcher); From f5f5401069b3eef5de55d10dfbd79f8e061022b4 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 11:40:39 +0200 Subject: [PATCH 20/61] Update --- .../lib/src/isolate/isolate_logger.dart | 21 +++++++++++++++--- .../test/isolate/isolate_logger_test.dart | 22 +++++++++++++++++-- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/isolate/isolate_logger.dart b/packages/flutter/lib/src/isolate/isolate_logger.dart index 5fe97dff86..bb8a8b7fe0 100644 --- a/packages/flutter/lib/src/isolate/isolate_logger.dart +++ b/packages/flutter/lib/src/isolate/isolate_logger.dart @@ -1,5 +1,7 @@ import 'dart:developer' as developer; +import 'package:meta/meta.dart'; + import '../../sentry_flutter.dart'; /// Static logger for Isolates that writes diagnostic messages to `dart:developer.log`. @@ -10,14 +12,15 @@ import '../../sentry_flutter.dart'; class IsolateLogger { IsolateLogger._(); - static late final bool _debug; - static late final SentryLevel _level; - static late final String _loggerName; + static late bool _debug; + static late SentryLevel _level; + static late String _loggerName; static bool _isConfigured = false; /// Configures this logger for the current isolate. /// /// Must be called once per isolate before invoking [log]. + /// Throws [StateError] if called more than once without calling [reset] first. /// /// - [debug]: when false, suppresses all logs except [SentryLevel.fatal]. /// - [level]: minimum severity threshold (inclusive) when [debug] is true. @@ -26,12 +29,24 @@ class IsolateLogger { {required bool debug, required SentryLevel level, required String loggerName}) { + if (_isConfigured) { + throw StateError( + 'IsolateLogger.configure has already been called. It can only be configured once per isolate.'); + } _debug = debug; _level = level; _loggerName = loggerName; _isConfigured = true; } + /// Resets the logger state to allow reconfiguration. + /// + /// This is intended for testing purposes only. + @visibleForTesting + static void reset() { + _isConfigured = false; + } + /// Emits a log entry if enabled. /// /// Messages are forwarded to [developer.log]. The provided [level] is diff --git a/packages/flutter/test/isolate/isolate_logger_test.dart b/packages/flutter/test/isolate/isolate_logger_test.dart index 5e804bdf5c..5d6fddfc96 100644 --- a/packages/flutter/test/isolate/isolate_logger_test.dart +++ b/packages/flutter/test/isolate/isolate_logger_test.dart @@ -17,6 +17,10 @@ void _entryUnconfigured(SendPort sendPort) { } void main() { + setUp(() { + IsolateLogger.reset(); + }); + test('configure required before log (debug builds)', () async { final rp = ReceivePort(); await Isolate.spawn(_entryUnconfigured, rp.sendPort, @@ -24,8 +28,7 @@ void main() { final result = await rp.first; rp.close(); - // In debug mode, assert triggers AssertionError before any late fields are read. - expect(result, 'AssertionError'); + expect(result, '_AssertionError'); }); test('fatal logs even when debug=false', () { @@ -49,4 +52,19 @@ void main() { expect(() => IsolateLogger.log(SentryLevel.warning, 'warn ok'), returnsNormally); }); + + test('prevents reconfiguration without reset', () { + IsolateLogger.configure( + debug: true, + level: SentryLevel.info, + loggerName: 't', + ); + expect( + () => IsolateLogger.configure( + debug: false, + level: SentryLevel.error, + loggerName: 't2', + ), + throwsStateError); + }); } From de232c6ef9958eee4bcb65c8e5935f6fb52cfe6c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 11:42:42 +0200 Subject: [PATCH 21/61] Update --- .../flutter/test/integrations/thread_info_integration_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/test/integrations/thread_info_integration_test.dart b/packages/flutter/test/integrations/thread_info_integration_test.dart index 3615e84814..75e4a8595b 100644 --- a/packages/flutter/test/integrations/thread_info_integration_test.dart +++ b/packages/flutter/test/integrations/thread_info_integration_test.dart @@ -4,8 +4,8 @@ library; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/src/integrations/thread_info_integration.dart'; -import 'package:sentry_flutter/src/isolate_helper.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/isolate/isolate_helper.dart'; import '../mocks.mocks.dart'; From 69d51119dc220ce4479a59ee79cf477416099240 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 12:01:22 +0200 Subject: [PATCH 22/61] Update --- packages/flutter/test/native/android_envelope_sender_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flutter/test/native/android_envelope_sender_test.dart b/packages/flutter/test/native/android_envelope_sender_test.dart index dcd157c3e6..64067db2bb 100644 --- a/packages/flutter/test/native/android_envelope_sender_test.dart +++ b/packages/flutter/test/native/android_envelope_sender_test.dart @@ -2,7 +2,6 @@ // ignore_for_file: invalid_use_of_internal_member library; -import 'dart:async'; import 'dart:isolate'; import 'dart:typed_data'; From 62bb12bbf0c42aa56cd31d1fd43c9878394b6b02 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 12:13:27 +0200 Subject: [PATCH 23/61] Add automatedTestMode option --- .../flutter/lib/src/isolate/isolate_worker.dart | 2 ++ .../src/native/cocoa/cocoa_envelope_sender.dart | 10 +++++++++- .../src/native/java/android_envelope_sender.dart | 14 +++++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/isolate/isolate_worker.dart b/packages/flutter/lib/src/isolate/isolate_worker.dart index 434e4b232e..bcf3bdbb67 100644 --- a/packages/flutter/lib/src/isolate/isolate_worker.dart +++ b/packages/flutter/lib/src/isolate/isolate_worker.dart @@ -15,11 +15,13 @@ class WorkerConfig { final bool debug; final SentryLevel diagnosticLevel; final String debugName; + final bool automatedTestMode; const WorkerConfig({ required this.debug, required this.diagnosticLevel, required this.debugName, + this.automatedTestMode = false, }); } diff --git a/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart index 72f3eb74e6..57f47325c7 100644 --- a/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart +++ b/packages/flutter/lib/src/native/cocoa/cocoa_envelope_sender.dart @@ -23,6 +23,7 @@ class CocoaEnvelopeSender { debugName: 'SentryCocoaEnvelopeSender', debug: _options.debug, diagnosticLevel: _options.diagnosticLevel, + automatedTestMode: _options.automatedTestMode, ), _spawn = spawn ?? spawnWorker; @@ -55,11 +56,15 @@ class CocoaEnvelopeSender { static void _entryPoint((SendPort, WorkerConfig) init) { final (host, config) = init; - runWorker(config, host, _CocoaEnvelopeHandler()); + runWorker(config, host, _CocoaEnvelopeHandler(config)); } } class _CocoaEnvelopeHandler extends WorkerHandler { + final WorkerConfig _config; + + _CocoaEnvelopeHandler(this._config); + @override FutureOr onMessage(Object? msg) { if (msg is TransferableTypedData) { @@ -83,6 +88,9 @@ class _CocoaEnvelopeHandler extends WorkerHandler { } catch (exception, stackTrace) { IsolateLogger.log(SentryLevel.error, 'Failed to capture envelope', exception: exception, stackTrace: stackTrace); + if (_config.automatedTestMode) { + rethrow; + } } } } diff --git a/packages/flutter/lib/src/native/java/android_envelope_sender.dart b/packages/flutter/lib/src/native/java/android_envelope_sender.dart index 59d53a3960..e3855e6f45 100644 --- a/packages/flutter/lib/src/native/java/android_envelope_sender.dart +++ b/packages/flutter/lib/src/native/java/android_envelope_sender.dart @@ -23,6 +23,7 @@ class AndroidEnvelopeSender { debugName: 'SentryAndroidEnvelopeSender', debug: _options.debug, diagnosticLevel: _options.diagnosticLevel, + automatedTestMode: _options.automatedTestMode, ), _spawn = spawn ?? spawnWorker; @@ -59,11 +60,15 @@ class AndroidEnvelopeSender { static void _entryPoint((SendPort, WorkerConfig) init) { final (host, config) = init; - runWorker(config, host, _AndroidEnvelopeHandler()); + runWorker(config, host, _AndroidEnvelopeHandler(config)); } } class _AndroidEnvelopeHandler extends WorkerHandler { + final WorkerConfig _config; + + _AndroidEnvelopeHandler(this._config); + @override FutureOr onMessage(Object? msg) { if (msg is (TransferableTypedData, bool)) { @@ -91,10 +96,9 @@ class _AndroidEnvelopeHandler extends WorkerHandler { } catch (exception, stackTrace) { IsolateLogger.log(SentryLevel.error, 'Failed to capture envelope', exception: exception, stackTrace: stackTrace); - // TODO: - // if (options.automatedTestMode) { - // rethrow; - // } + if (_config.automatedTestMode) { + rethrow; + } } finally { byteArray?.release(); id?.release(); From 53c603611e73e55c0b5c86a78d40be8d1bce8c3e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 14:30:34 +0200 Subject: [PATCH 24/61] Update --- packages/flutter/example/pubspec_overrides.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flutter/example/pubspec_overrides.yaml b/packages/flutter/example/pubspec_overrides.yaml index 8f6b711d3b..7dafca339e 100644 --- a/packages/flutter/example/pubspec_overrides.yaml +++ b/packages/flutter/example/pubspec_overrides.yaml @@ -21,4 +21,3 @@ dependency_overrides: isar_flutter_libs: git: url: https://github.com/MrLittleWhite/isar_flutter_libs.git - From 06ee227757bebda500cd3c3f34f68f760ed52250 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 14:59:28 +0200 Subject: [PATCH 25/61] Fix web tests --- .../native/android_envelope_sender_test.dart | 196 +----------------- .../android_envelope_sender_test_real.dart | 194 +++++++++++++++++ .../android_envelope_sender_test_web.dart | 10 + .../native/cocoa_envelope_sender_test.dart | 194 +---------------- .../cocoa_envelope_sender_test_real.dart | 192 +++++++++++++++++ .../cocoa_envelope_sender_test_web.dart | 10 + 6 files changed, 410 insertions(+), 386 deletions(-) create mode 100644 packages/flutter/test/native/android_envelope_sender_test_real.dart create mode 100644 packages/flutter/test/native/android_envelope_sender_test_web.dart create mode 100644 packages/flutter/test/native/cocoa_envelope_sender_test_real.dart create mode 100644 packages/flutter/test/native/cocoa_envelope_sender_test_web.dart diff --git a/packages/flutter/test/native/android_envelope_sender_test.dart b/packages/flutter/test/native/android_envelope_sender_test.dart index 64067db2bb..9779018acb 100644 --- a/packages/flutter/test/native/android_envelope_sender_test.dart +++ b/packages/flutter/test/native/android_envelope_sender_test.dart @@ -1,194 +1,2 @@ -@TestOn('vm') -// ignore_for_file: invalid_use_of_internal_member -library; - -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/native/java/android_envelope_sender.dart'; -import 'package:sentry_flutter/src/isolate/isolate_worker.dart'; - -void main() { - group('AndroidEnvelopeSender host behavior', () { - test('warns and drops when not started', () { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - final logs = <(SentryLevel, String)>[]; - options.log = (level, message, {logger, exception, stackTrace}) { - logs.add((level, message)); - }; - - final sender = AndroidEnvelopeSender(options); - sender.captureEnvelope(Uint8List.fromList([1, 2, 3]), false); - - expect( - logs.any((e) => - e.$1 == SentryLevel.warning && - e.$2.contains( - 'captureEnvelope called before worker started; dropping')), - isTrue, - ); - }); - - test('close is a no-op when not started', () { - final options = SentryFlutterOptions(); - final sender = AndroidEnvelopeSender(options); - expect(() => sender.close(), returnsNormally); - expect(() => sender.close(), returnsNormally); - }); - - test('warns and drops after close', () async { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - final logs = <(SentryLevel, String)>[]; - options.log = (level, message, {logger, exception, stackTrace}) { - logs.add((level, message)); - }; - - final sender = AndroidEnvelopeSender(options); - await sender.start(); - sender.close(); - - sender.captureEnvelope(Uint8List.fromList([9]), false); - - expect( - logs.any((e) => - e.$1 == SentryLevel.warning && - e.$2.contains( - 'captureEnvelope called before worker started; dropping')), - isTrue, - ); - }); - - test('start is a no-op when already started', () async { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - - var spawnCount = 0; - Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { - spawnCount++; - final inbox = ReceivePort(); - addTearDown(() => inbox.close()); - final replies = ReceivePort(); - return Worker(inbox.sendPort, replies); - } - - final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); - - await sender.start(); - await sender.start(); - expect(spawnCount, 1); - - sender.close(); - spawnCount = 0; - - await sender.start(); - expect(spawnCount, 1); - - // Close twice should be safe. - expect(() => sender.close(), returnsNormally); - expect(() => sender.close(), returnsNormally); - }); - - test('delivers tuple to worker after start', () async { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - - final inboxes = []; - Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { - final inbox = ReceivePort(); - inboxes.add(inbox); - addTearDown(() => inbox.close()); - final replies = ReceivePort(); - return Worker(inbox.sendPort, replies); - } - - final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); - await sender.start(); - - final payload = Uint8List.fromList([4, 5, 6]); - sender.captureEnvelope(payload, true); - - final msg = await inboxes.last.first; - expect(msg, isA<(TransferableTypedData, bool)>()); - final (transferable, containsUnhandled) = - msg as (TransferableTypedData, bool); - expect(containsUnhandled, true); - final data = transferable.materialize().asUint8List(); - expect(data, [4, 5, 6]); - - sender.close(); - }); - - test('uses expected WorkerConfig', () async { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - - WorkerConfig? seenConfig; - Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { - seenConfig = config; - final inbox = ReceivePort(); - addTearDown(() => inbox.close()); - final replies = ReceivePort(); - return Worker(inbox.sendPort, replies); - } - - final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); - await sender.start(); - - expect(seenConfig, isNotNull); - expect(seenConfig!.debugName, 'SentryAndroidEnvelopeSender'); - expect(seenConfig!.debug, options.debug); - expect(seenConfig!.diagnosticLevel, options.diagnosticLevel); - - sender.close(); - }); - - test('sends are delivered sequentially with flags', () async { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - - final inboxes = []; - Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { - final inbox = ReceivePort(); - inboxes.add(inbox); - addTearDown(() => inbox.close()); - final replies = ReceivePort(); - return Worker(inbox.sendPort, replies); - } - - final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); - await sender.start(); - - sender.captureEnvelope(Uint8List.fromList([10]), true); - sender.captureEnvelope(Uint8List.fromList([11]), false); - - final inbox = inboxes.last; - final msgs = await inbox.take(2).toList(); - final msg1 = msgs[0]; - final msg2 = msgs[1]; - - expect(msg1, isA<(TransferableTypedData, bool)>()); - expect(msg2, isA<(TransferableTypedData, bool)>()); - - final (t1, f1) = msg1 as (TransferableTypedData, bool); - final (t2, f2) = msg2 as (TransferableTypedData, bool); - expect(f1, true); - expect(f2, false); - final data1 = t1.materialize().asUint8List(); - final data2 = t2.materialize().asUint8List(); - expect(data1, [10]); - expect(data2, [11]); - - sender.close(); - }); - }); -} +export 'android_envelope_sender_test_real.dart' + if (dart.library.js_interop) 'android_envelope_sender_test_web.dart'; diff --git a/packages/flutter/test/native/android_envelope_sender_test_real.dart b/packages/flutter/test/native/android_envelope_sender_test_real.dart new file mode 100644 index 0000000000..64067db2bb --- /dev/null +++ b/packages/flutter/test/native/android_envelope_sender_test_real.dart @@ -0,0 +1,194 @@ +@TestOn('vm') +// ignore_for_file: invalid_use_of_internal_member +library; + +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/native/java/android_envelope_sender.dart'; +import 'package:sentry_flutter/src/isolate/isolate_worker.dart'; + +void main() { + group('AndroidEnvelopeSender host behavior', () { + test('warns and drops when not started', () { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + final logs = <(SentryLevel, String)>[]; + options.log = (level, message, {logger, exception, stackTrace}) { + logs.add((level, message)); + }; + + final sender = AndroidEnvelopeSender(options); + sender.captureEnvelope(Uint8List.fromList([1, 2, 3]), false); + + expect( + logs.any((e) => + e.$1 == SentryLevel.warning && + e.$2.contains( + 'captureEnvelope called before worker started; dropping')), + isTrue, + ); + }); + + test('close is a no-op when not started', () { + final options = SentryFlutterOptions(); + final sender = AndroidEnvelopeSender(options); + expect(() => sender.close(), returnsNormally); + expect(() => sender.close(), returnsNormally); + }); + + test('warns and drops after close', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + final logs = <(SentryLevel, String)>[]; + options.log = (level, message, {logger, exception, stackTrace}) { + logs.add((level, message)); + }; + + final sender = AndroidEnvelopeSender(options); + await sender.start(); + sender.close(); + + sender.captureEnvelope(Uint8List.fromList([9]), false); + + expect( + logs.any((e) => + e.$1 == SentryLevel.warning && + e.$2.contains( + 'captureEnvelope called before worker started; dropping')), + isTrue, + ); + }); + + test('start is a no-op when already started', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + var spawnCount = 0; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + spawnCount++; + final inbox = ReceivePort(); + addTearDown(() => inbox.close()); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); + } + + final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); + + await sender.start(); + await sender.start(); + expect(spawnCount, 1); + + sender.close(); + spawnCount = 0; + + await sender.start(); + expect(spawnCount, 1); + + // Close twice should be safe. + expect(() => sender.close(), returnsNormally); + expect(() => sender.close(), returnsNormally); + }); + + test('delivers tuple to worker after start', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + final inboxes = []; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + final inbox = ReceivePort(); + inboxes.add(inbox); + addTearDown(() => inbox.close()); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); + } + + final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + final payload = Uint8List.fromList([4, 5, 6]); + sender.captureEnvelope(payload, true); + + final msg = await inboxes.last.first; + expect(msg, isA<(TransferableTypedData, bool)>()); + final (transferable, containsUnhandled) = + msg as (TransferableTypedData, bool); + expect(containsUnhandled, true); + final data = transferable.materialize().asUint8List(); + expect(data, [4, 5, 6]); + + sender.close(); + }); + + test('uses expected WorkerConfig', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + WorkerConfig? seenConfig; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + seenConfig = config; + final inbox = ReceivePort(); + addTearDown(() => inbox.close()); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); + } + + final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + expect(seenConfig, isNotNull); + expect(seenConfig!.debugName, 'SentryAndroidEnvelopeSender'); + expect(seenConfig!.debug, options.debug); + expect(seenConfig!.diagnosticLevel, options.diagnosticLevel); + + sender.close(); + }); + + test('sends are delivered sequentially with flags', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + final inboxes = []; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + final inbox = ReceivePort(); + inboxes.add(inbox); + addTearDown(() => inbox.close()); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); + } + + final sender = AndroidEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + sender.captureEnvelope(Uint8List.fromList([10]), true); + sender.captureEnvelope(Uint8List.fromList([11]), false); + + final inbox = inboxes.last; + final msgs = await inbox.take(2).toList(); + final msg1 = msgs[0]; + final msg2 = msgs[1]; + + expect(msg1, isA<(TransferableTypedData, bool)>()); + expect(msg2, isA<(TransferableTypedData, bool)>()); + + final (t1, f1) = msg1 as (TransferableTypedData, bool); + final (t2, f2) = msg2 as (TransferableTypedData, bool); + expect(f1, true); + expect(f2, false); + final data1 = t1.materialize().asUint8List(); + final data2 = t2.materialize().asUint8List(); + expect(data1, [10]); + expect(data2, [11]); + + sender.close(); + }); + }); +} diff --git a/packages/flutter/test/native/android_envelope_sender_test_web.dart b/packages/flutter/test/native/android_envelope_sender_test_web.dart new file mode 100644 index 0000000000..6b061ee80a --- /dev/null +++ b/packages/flutter/test/native/android_envelope_sender_test_web.dart @@ -0,0 +1,10 @@ +// Stub for web - these tests only run on VM +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Android envelope sender tests are not supported on web', () { + // This test file exists only to satisfy the compiler when running web tests. + // The actual tests in android_envelope_sender_test_real.dart are only + // executed on VM platforms. + }); +} diff --git a/packages/flutter/test/native/cocoa_envelope_sender_test.dart b/packages/flutter/test/native/cocoa_envelope_sender_test.dart index 31c18373c3..15590a436d 100644 --- a/packages/flutter/test/native/cocoa_envelope_sender_test.dart +++ b/packages/flutter/test/native/cocoa_envelope_sender_test.dart @@ -1,192 +1,2 @@ -@TestOn('vm') -// ignore_for_file: invalid_use_of_internal_member -library; - -import 'dart:async'; -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/native/cocoa/cocoa_envelope_sender.dart'; -import 'package:sentry_flutter/src/isolate/isolate_worker.dart'; - -void main() { - group('CocoaEnvelopeSender host behavior', () { - test('warns and drops when not started', () { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - final logs = <(SentryLevel, String)>[]; - options.log = (level, message, {logger, exception, stackTrace}) { - logs.add((level, message)); - }; - - final sender = CocoaEnvelopeSender(options); - sender.captureEnvelope(Uint8List.fromList([1, 2, 3])); - - expect( - logs.any((e) => - e.$1 == SentryLevel.warning && - e.$2.contains('captureEnvelope called before start; dropping')), - isTrue, - ); - }); - - test('close is a no-op when not started', () { - final options = SentryFlutterOptions(); - final sender = CocoaEnvelopeSender(options); - expect(() => sender.close(), returnsNormally); - expect(() => sender.close(), returnsNormally); - }); - - test('start is a no-op when already started', () async { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - - var spawnCount = 0; - Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { - spawnCount++; - final inbox = ReceivePort(); - late final StreamSubscription sub; - sub = inbox.listen((msg) async { - if (msg == '_shutdown_') { - await sub.cancel(); - inbox.close(); - } - }); - final replies = ReceivePort(); - return Worker(inbox.sendPort, replies); - } - - final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); - - await sender.start(); - await sender.start(); - expect(spawnCount, 1); - - sender.close(); - spawnCount = 0; - - await sender.start(); - expect(spawnCount, 1); - - // Close twice should be safe. - expect(() => sender.close(), returnsNormally); - expect(() => sender.close(), returnsNormally); - }); - - test('warns and drops after close', () async { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - final logs = <(SentryLevel, String)>[]; - options.log = (level, message, {logger, exception, stackTrace}) { - logs.add((level, message)); - }; - - final sender = CocoaEnvelopeSender(options); - await sender.start(); - sender.close(); - - sender.captureEnvelope(Uint8List.fromList([9])); - - expect( - logs.any((e) => - e.$1 == SentryLevel.warning && - e.$2.contains('captureEnvelope called before start; dropping')), - isTrue, - ); - }); - - test('sends are delivered sequentially', () async { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - - final inboxes = []; - Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { - final inbox = ReceivePort(); - inboxes.add(inbox); - addTearDown(() => inbox.close()); - final replies = ReceivePort(); - return Worker(inbox.sendPort, replies); - } - - final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); - await sender.start(); - - sender.captureEnvelope(Uint8List.fromList([10])); - sender.captureEnvelope(Uint8List.fromList([11])); - - final inbox = inboxes.last; - final msgs = await inbox.take(2).toList(); - final msg1 = msgs[0]; - final msg2 = msgs[1]; - - expect(msg1, isA()); - expect(msg2, isA()); - - final data1 = (msg1 as TransferableTypedData).materialize().asUint8List(); - final data2 = (msg2 as TransferableTypedData).materialize().asUint8List(); - expect(data1, [10]); - expect(data2, [11]); - - sender.close(); - }); - - test('delivers to worker after start', () async { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - - final inboxes = []; - Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { - final inbox = ReceivePort(); - inboxes.add(inbox); - addTearDown(() => inbox.close()); - final replies = ReceivePort(); - return Worker(inbox.sendPort, replies); - } - - final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); - await sender.start(); - - final payload = Uint8List.fromList([1, 2, 3]); - sender.captureEnvelope(payload); - - final msg = await inboxes.last.first; - expect(msg, isA()); - final data = (msg as TransferableTypedData).materialize().asUint8List(); - expect(data, [1, 2, 3]); - - sender.close(); - }); - - test('uses expected WorkerConfig', () async { - final options = SentryFlutterOptions(); - options.debug = true; - options.diagnosticLevel = SentryLevel.debug; - - WorkerConfig? seenConfig; - Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { - seenConfig = config; - final inbox = ReceivePort(); - addTearDown(() => inbox.close()); - final replies = ReceivePort(); - return Worker(inbox.sendPort, replies); - } - - final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); - await sender.start(); - - expect(seenConfig, isNotNull); - expect(seenConfig!.debugName, 'SentryCocoaEnvelopeSender'); - expect(seenConfig!.debug, options.debug); - expect(seenConfig!.diagnosticLevel, options.diagnosticLevel); - - sender.close(); - }); - }); -} +export 'cocoa_envelope_sender_test_real.dart' + if (dart.library.js_interop) 'cocoa_envelope_sender_test_web.dart'; diff --git a/packages/flutter/test/native/cocoa_envelope_sender_test_real.dart b/packages/flutter/test/native/cocoa_envelope_sender_test_real.dart new file mode 100644 index 0000000000..31c18373c3 --- /dev/null +++ b/packages/flutter/test/native/cocoa_envelope_sender_test_real.dart @@ -0,0 +1,192 @@ +@TestOn('vm') +// ignore_for_file: invalid_use_of_internal_member +library; + +import 'dart:async'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/native/cocoa/cocoa_envelope_sender.dart'; +import 'package:sentry_flutter/src/isolate/isolate_worker.dart'; + +void main() { + group('CocoaEnvelopeSender host behavior', () { + test('warns and drops when not started', () { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + final logs = <(SentryLevel, String)>[]; + options.log = (level, message, {logger, exception, stackTrace}) { + logs.add((level, message)); + }; + + final sender = CocoaEnvelopeSender(options); + sender.captureEnvelope(Uint8List.fromList([1, 2, 3])); + + expect( + logs.any((e) => + e.$1 == SentryLevel.warning && + e.$2.contains('captureEnvelope called before start; dropping')), + isTrue, + ); + }); + + test('close is a no-op when not started', () { + final options = SentryFlutterOptions(); + final sender = CocoaEnvelopeSender(options); + expect(() => sender.close(), returnsNormally); + expect(() => sender.close(), returnsNormally); + }); + + test('start is a no-op when already started', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + var spawnCount = 0; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + spawnCount++; + final inbox = ReceivePort(); + late final StreamSubscription sub; + sub = inbox.listen((msg) async { + if (msg == '_shutdown_') { + await sub.cancel(); + inbox.close(); + } + }); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); + } + + final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); + + await sender.start(); + await sender.start(); + expect(spawnCount, 1); + + sender.close(); + spawnCount = 0; + + await sender.start(); + expect(spawnCount, 1); + + // Close twice should be safe. + expect(() => sender.close(), returnsNormally); + expect(() => sender.close(), returnsNormally); + }); + + test('warns and drops after close', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + final logs = <(SentryLevel, String)>[]; + options.log = (level, message, {logger, exception, stackTrace}) { + logs.add((level, message)); + }; + + final sender = CocoaEnvelopeSender(options); + await sender.start(); + sender.close(); + + sender.captureEnvelope(Uint8List.fromList([9])); + + expect( + logs.any((e) => + e.$1 == SentryLevel.warning && + e.$2.contains('captureEnvelope called before start; dropping')), + isTrue, + ); + }); + + test('sends are delivered sequentially', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + final inboxes = []; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + final inbox = ReceivePort(); + inboxes.add(inbox); + addTearDown(() => inbox.close()); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); + } + + final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + sender.captureEnvelope(Uint8List.fromList([10])); + sender.captureEnvelope(Uint8List.fromList([11])); + + final inbox = inboxes.last; + final msgs = await inbox.take(2).toList(); + final msg1 = msgs[0]; + final msg2 = msgs[1]; + + expect(msg1, isA()); + expect(msg2, isA()); + + final data1 = (msg1 as TransferableTypedData).materialize().asUint8List(); + final data2 = (msg2 as TransferableTypedData).materialize().asUint8List(); + expect(data1, [10]); + expect(data2, [11]); + + sender.close(); + }); + + test('delivers to worker after start', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + final inboxes = []; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + final inbox = ReceivePort(); + inboxes.add(inbox); + addTearDown(() => inbox.close()); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); + } + + final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + final payload = Uint8List.fromList([1, 2, 3]); + sender.captureEnvelope(payload); + + final msg = await inboxes.last.first; + expect(msg, isA()); + final data = (msg as TransferableTypedData).materialize().asUint8List(); + expect(data, [1, 2, 3]); + + sender.close(); + }); + + test('uses expected WorkerConfig', () async { + final options = SentryFlutterOptions(); + options.debug = true; + options.diagnosticLevel = SentryLevel.debug; + + WorkerConfig? seenConfig; + Future fakeSpawn(WorkerConfig config, WorkerEntry entry) async { + seenConfig = config; + final inbox = ReceivePort(); + addTearDown(() => inbox.close()); + final replies = ReceivePort(); + return Worker(inbox.sendPort, replies); + } + + final sender = CocoaEnvelopeSender(options, spawn: fakeSpawn); + await sender.start(); + + expect(seenConfig, isNotNull); + expect(seenConfig!.debugName, 'SentryCocoaEnvelopeSender'); + expect(seenConfig!.debug, options.debug); + expect(seenConfig!.diagnosticLevel, options.diagnosticLevel); + + sender.close(); + }); + }); +} diff --git a/packages/flutter/test/native/cocoa_envelope_sender_test_web.dart b/packages/flutter/test/native/cocoa_envelope_sender_test_web.dart new file mode 100644 index 0000000000..be0d7c08f0 --- /dev/null +++ b/packages/flutter/test/native/cocoa_envelope_sender_test_web.dart @@ -0,0 +1,10 @@ +// Stub for web - these tests only run on VM +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Cocoa envelope sender tests are not supported on web', () { + // This test file exists only to satisfy the compiler when running web tests. + // The actual tests in cocoa_envelope_sender_test_real.dart are only + // executed on VM platforms. + }); +} From 10d9419548d0b160a0facef6cb9455dcbcca0cf5 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 15:02:02 +0200 Subject: [PATCH 26/61] Update --- packages/flutter/test/sentry_native_channel_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/flutter/test/sentry_native_channel_test.dart b/packages/flutter/test/sentry_native_channel_test.dart index f0ab1bceb6..1a5c7dd476 100644 --- a/packages/flutter/test/sentry_native_channel_test.dart +++ b/packages/flutter/test/sentry_native_channel_test.dart @@ -194,7 +194,6 @@ void main() { }); test('startProfiler', () { - sut.startProfiler(SentryId.newId()); final matcher = _nativeUnavailableMatcher( mockPlatform, androidUnsupported: true, @@ -263,7 +262,7 @@ void main() { mockPlatform, includeLookupSymbol: true, ); - sut.loadDebugImages(SentryStackTrace(frames: [])); + expect( () => sut.loadDebugImages(SentryStackTrace(frames: [])), matcher); From e6771bb1d28bed655ca75f584138387fd8740b9d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 16:45:56 +0200 Subject: [PATCH 27/61] Update --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa24a7d360..e8238842ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,8 +79,6 @@ - Replay: continue processing if encountering `InheritedWidget` ([#3200](https://github.com/getsentry/sentry-dart/pull/3200)) - Prevents false debug warnings when using [provider](https://pub.dev/packages/provider) for example which extensively uses `InheritedWidget` ->>> main - ## 9.7.0-beta.2 ### Features From e2ae6a3d481b6c16860b34bbcffd08fa0fcaf130 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 7 Oct 2025 16:48:28 +0200 Subject: [PATCH 28/61] Add close --- .../flutter/lib/src/native/cocoa/sentry_native_cocoa.dart | 6 ++++++ 1 file changed, 6 insertions(+) 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 6707817f2d..0beb6e26b1 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -57,6 +57,12 @@ class SentryNativeCocoa extends SentryNativeChannel { return super.init(hub); } + @override + Future close() async { + await _envelopeSender?.close(); + return super.close(); + } + @override FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { From ae9b24c8e5b9ed1bed382cc4fdd1ebf4423ab953 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 8 Oct 2025 11:07:32 +0200 Subject: [PATCH 29/61] Review --- .../lib/src/isolate/isolate_worker.dart | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/flutter/lib/src/isolate/isolate_worker.dart b/packages/flutter/lib/src/isolate/isolate_worker.dart index bcf3bdbb67..25b3fcb362 100644 --- a/packages/flutter/lib/src/isolate/isolate_worker.dart +++ b/packages/flutter/lib/src/isolate/isolate_worker.dart @@ -45,19 +45,19 @@ class Worker { } /// Send a request to the worker and await a response. - Future request(Object? payload) { + Future request(Object? payload) async { if (_closed) throw StateError('Worker is closed'); final id = _idCounter++; final completer = Completer.sync(); _pending[id] = completer; _workerPort.send((id, payload)); - return completer.future; + return await completer.future; } void close() { if (_closed) return; - _workerPort.send(_shutdownCommand); _closed = true; + _workerPort.send(_shutdownCommand); if (_pending.isEmpty) { _responses.close(); } @@ -97,11 +97,16 @@ Future spawnWorker( )); }; - await Isolate.spawn<(SendPort, WorkerConfig)>( - entry, - (initPort.sendPort, config), - debugName: config.debugName, - ); + try { + await Isolate.spawn<(SendPort, WorkerConfig)>( + entry, + (initPort.sendPort, config), + debugName: config.debugName, + ); + } on Object { + initPort.close(); + rethrow; + } final (ReceivePort receivePort, SendPort sendPort) = await connection.future; return Worker(sendPort, receivePort); From 4b440d01f8aa721b53b520ca9d62f83120399c99 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 8 Oct 2025 12:59:27 +0200 Subject: [PATCH 30/61] Review --- packages/flutter/lib/src/isolate/isolate_logger.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/isolate/isolate_logger.dart b/packages/flutter/lib/src/isolate/isolate_logger.dart index bb8a8b7fe0..2b6d8c3667 100644 --- a/packages/flutter/lib/src/isolate/isolate_logger.dart +++ b/packages/flutter/lib/src/isolate/isolate_logger.dart @@ -75,7 +75,7 @@ class IsolateLogger { } static bool _isEnabled(SentryLevel level) { - return _debug && level.ordinal >= _level.ordinal || + return (_debug && level.ordinal >= _level.ordinal) || level == SentryLevel.fatal; } } From 91b029831279b84f40e3df1115b45475e5d56fe5 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 8 Oct 2025 14:38:06 +0200 Subject: [PATCH 31/61] Update --- .../io/sentry/flutter/SentryFlutterPlugin.kt | 53 ++++++------- .../sentry_flutter/SentryFlutterPlugin.swift | 74 +++++++++---------- .../sentry_flutter_objc/SentryFlutterPlugin.h | 1 + .../flutter/lib/src/native/cocoa/binding.dart | 11 +++ .../src/native/cocoa/sentry_native_cocoa.dart | 12 +++ .../flutter/lib/src/native/java/binding.dart | 50 +++++++++++++ .../src/native/java/sentry_native_java.dart | 7 ++ .../lib/src/native/sentry_native_channel.dart | 7 +- 8 files changed, 149 insertions(+), 66 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 2d24d12f5d..e4a8b38fbc 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 @@ -43,7 +43,6 @@ class SentryFlutterPlugin : private lateinit var context: Context private lateinit var sentryFlutter: SentryFlutter - private var activity: WeakReference? = null private var pluginRegistrationTime: Long? = null override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { @@ -75,7 +74,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) @@ -217,29 +215,6 @@ class SentryFlutterPlugin : } } - 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) { if (startTimestamp == null) return @@ -381,12 +356,40 @@ class SentryFlutterPlugin : @SuppressLint("StaticFieldLeak") private var applicationContext: Context? = null + @SuppressLint("StaticFieldLeak") + private var activity: WeakReference? = null + 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 + } + @JvmStatic fun getApplicationContext(): Context? = applicationContext 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 044e3d4757..31c160cc5e 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -133,9 +133,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { collectProfile(call, result) #endif - case "displayRefreshRate": - displayRefreshRate(result) - case "pauseAppHangTracking": pauseAppHangTracking(result) @@ -514,42 +511,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { result(nil) } - #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 - private func displayRefreshRate(_ result: @escaping FlutterResult) { - let displayLink = CADisplayLink(target: self, selector: #selector(onDisplayLink(_:))) - displayLink.add(to: .main, forMode: .common) - displayLink.isPaused = true - - let preferredFPS = displayLink.preferredFramesPerSecond - displayLink.invalidate() - - if preferredFPS != 0 { - result(preferredFPS) - return - } - - if #available(iOS 13.0, *) { - guard let windowScene = UIApplication.shared.windows.first?.windowScene else { - result(nil) - return - } - result(windowScene.screen.maximumFramesPerSecond) - } else { - result(UIScreen.main.maximumFramesPerSecond) - } - } - - @objc private func onDisplayLink(_ displayLink: CADisplayLink) { - // No-op - } - #elseif os(macOS) - private func displayRefreshRate(_ result: @escaping FlutterResult) { - result(nil) - } - #endif - private func pauseAppHangTracking(_ result: @escaping FlutterResult) { SentrySDK.pauseAppHangTracking() result("") @@ -569,6 +530,41 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { // Group of methods exposed to the Objective-C runtime via `@objc`. // // Purpose: Called from the Flutter plugin's native bridge (FFI) - bindings are created from SentryFlutterPlugin.h + + #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 + @objc public class func getDisplayRefreshRate() -> NSNumber? { + let displayLink = CADisplayLink(target: self, selector: #selector(onDisplayLinkStatic(_:))) + displayLink.add(to: .main, forMode: .common) + displayLink.isPaused = true + + let preferredFPS = displayLink.preferredFramesPerSecond + displayLink.invalidate() + + if preferredFPS != 0 { + return NSNumber(value: preferredFPS) + } + + if #available(iOS 13.0, *) { + guard let windowScene = UIApplication.shared.windows.first?.windowScene else { + return nil + } + return NSNumber(value: windowScene.screen.maximumFramesPerSecond) + } else { + return NSNumber(value: UIScreen.main.maximumFramesPerSecond) + } + } + + @objc private class func onDisplayLinkStatic(_ displayLink: CADisplayLink) { + // No-op + } + #elseif os(macOS) + @objc public class func getDisplayRefreshRate() -> NSNumber? { + return nil + } + #endif + @objc(loadDebugImagesAsBytes:) public class func loadDebugImagesAsBytes(instructionAddresses: Set) -> NSData? { var debugImages: [DebugMeta] = [] 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 a18f29ed10..ace6992371 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 @@ -4,6 +4,7 @@ #import #else @interface SentryFlutterPlugin : NSObject ++ (nullable NSNumber *)getDisplayRefreshRate; + (nullable NSData *)loadContextsAsBytes; + (nullable NSData *)loadDebugImagesAsBytes:(NSSet *)instructionAddresses; @end diff --git a/packages/flutter/lib/src/native/cocoa/binding.dart b/packages/flutter/lib/src/native/cocoa/binding.dart index dfa2b80f88..b5d952c5a5 100644 --- a/packages/flutter/lib/src/native/cocoa/binding.dart +++ b/packages/flutter/lib/src/native/cocoa/binding.dart @@ -1121,6 +1121,8 @@ class SentryId$1 extends objc.NSObject { } late final _class_SentryFlutterPlugin = objc.getClass("SentryFlutterPlugin"); +late final _sel_getDisplayRefreshRate = + objc.registerName("getDisplayRefreshRate"); late final _sel_loadContextsAsBytes = objc.registerName("loadContextsAsBytes"); late final _sel_loadDebugImagesAsBytes_ = objc.registerName("loadDebugImagesAsBytes:"); @@ -1146,6 +1148,15 @@ class SentryFlutterPlugin extends objc.NSObject { obj.ref.pointer, _sel_isKindOfClass_, _class_SentryFlutterPlugin); } + /// getDisplayRefreshRate + static objc.NSNumber? getDisplayRefreshRate() { + final _ret = _objc_msgSend_151sglz( + _class_SentryFlutterPlugin, _sel_getDisplayRefreshRate); + return _ret.address == 0 + ? null + : objc.NSNumber.castFromPointer(_ret, retain: true, release: true); + } + /// loadContextsAsBytes static objc.NSData? loadContextsAsBytes() { final _ret = _objc_msgSend_151sglz( 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 0beb6e26b1..89a92af7e4 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ffi'; import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:objective_c/objective_c.dart'; @@ -153,4 +154,15 @@ class SentryNativeCocoa extends SentryNativeChannel { return startTime; }, ); + + @override + int? displayRefreshRate() => tryCatchSync( + 'displayRefreshRate', + () { + final refreshRate = cocoa.SentryFlutterPlugin.getDisplayRefreshRate(); + if (refreshRate == null) return null; + + return refreshRate.intValue; + }, + ); } diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index 6eb935200c..8c263dc772 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -1305,6 +1305,31 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject { .object(const $ReplayIntegration$NullableType()); } + static final _id_getDisplayRefreshRate = _class.instanceMethodId( + r'getDisplayRefreshRate', + r'()Ljava/lang/Integer;', + ); + + static final _getDisplayRefreshRate = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final java.lang.Integer getDisplayRefreshRate()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JInteger? getDisplayRefreshRate() { + return _getDisplayRefreshRate( + reference.pointer, _id_getDisplayRefreshRate as jni$_.JMethodIDPtr) + .object(const jni$_.JIntegerNullableType()); + } + static final _id_getApplicationContext = _class.instanceMethodId( r'getApplicationContext', r'()Landroid/content/Context;', @@ -1765,6 +1790,31 @@ class SentryFlutterPlugin extends jni$_.JObject { .object(const $ReplayIntegration$NullableType()); } + static final _id_getDisplayRefreshRate = _class.staticMethodId( + r'getDisplayRefreshRate', + r'()Ljava/lang/Integer;', + ); + + static final _getDisplayRefreshRate = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `static public final java.lang.Integer getDisplayRefreshRate()` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JInteger? getDisplayRefreshRate() { + return _getDisplayRefreshRate(_class.reference.pointer, + _id_getDisplayRefreshRate as jni$_.JMethodIDPtr) + .object(const jni$_.JIntegerNullableType()); + } + static final _id_getApplicationContext = _class.staticMethodId( r'getApplicationContext', r'()Landroid/content/Context;', 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 58fb8657ce..05a2d66148 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -171,6 +171,13 @@ class SentryNativeJava extends SentryNativeChannel { return null; } + @override + int? displayRefreshRate() { + return native.SentryFlutterPlugin.Companion + .getDisplayRefreshRate() + ?.intValue(); + } + @override Future close() async { await _replayRecorder?.stop(); diff --git a/packages/flutter/lib/src/native/sentry_native_channel.dart b/packages/flutter/lib/src/native/sentry_native_channel.dart index 20d72dd317..c4266fa0cb 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -223,8 +223,11 @@ class SentryNativeChannel } @override - Future displayRefreshRate() => - channel.invokeMethod('displayRefreshRate'); + FutureOr displayRefreshRate() { + assert(false, + 'displayRefreshRate should not be used through method channels.'); + return null; + } @override Future pauseAppHangTracking() => From 2cd6dd8b7bdf02a30bde927d602b2c610ca55a24 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 8 Oct 2025 15:05:14 +0200 Subject: [PATCH 32/61] Update --- packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart | 1 - 1 file changed, 1 deletion(-) 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 89a92af7e4..70ee6bf3fa 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ffi'; import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:objective_c/objective_c.dart'; From 2f1840d0a62e37c08b73b1e967c5f64095fb2662 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 8 Oct 2025 15:23:10 +0200 Subject: [PATCH 33/61] Update --- .../kotlin/io/sentry/flutter/SentryFlutter.kt | 1 + .../io/sentry/flutter/SentryFlutterPlugin.kt | 165 +++++++++--------- .../flutter/lib/src/native/java/binding.dart | 104 +++++++++++ .../src/native/java/sentry_native_java.dart | 29 +++ .../lib/src/native/sentry_native_channel.dart | 8 +- 5 files changed, 222 insertions(+), 85 deletions(-) diff --git a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt index 19eec63247..a1a7ff5c3a 100644 --- a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt +++ b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt @@ -108,6 +108,7 @@ class SentryFlutter { data.getIfNotNull("enableAutoPerformanceTracing") { enableAutoPerformanceTracing -> if (enableAutoPerformanceTracing) { autoPerformanceTracingEnabled = true + SentryFlutterPlugin.setAutoPerformanceTracingEnabled(true) } } 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 e4a8b38fbc..47f1a3d171 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 @@ -43,10 +43,8 @@ class SentryFlutterPlugin : private lateinit var context: Context private lateinit var sentryFlutter: SentryFlutter - private var pluginRegistrationTime: Long? = null - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - pluginRegistrationTime = System.currentTimeMillis() + Companion.pluginRegistrationTime = System.currentTimeMillis() context = flutterPluginBinding.applicationContext applicationContext = context @@ -64,7 +62,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) @@ -149,83 +146,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( - "pluginRegistrationTime" to pluginRegistrationTime, - "appStartTime" to appStartTimeMillis, - "isColdStart" to isColdStart, - ) - - val androidNativeSpans = mutableMapOf() - - 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 TimeSpan.addToMap(map: MutableMap) { - if (startTimestamp == null) return - - description?.let { description -> - map[description] = - mapOf( - "startTimestampMsSinceEpoch" to startTimestampMs, - "stopTimestampMsSinceEpoch" to projectedStopTimestampMs, - ) - } - } private fun setContexts( key: String?, value: Any?, @@ -359,6 +279,9 @@ class SentryFlutterPlugin : @SuppressLint("StaticFieldLeak") private var activity: WeakReference? = null + private var pluginRegistrationTime: Long? = null + private var autoPerformanceTracingEnabled: Boolean = false + private const val NATIVE_CRASH_WAIT_TIME = 500L @Suppress("unused") // Used by native/jni bindings @@ -390,9 +313,89 @@ class SentryFlutterPlugin : return refreshRate } + @Suppress("unused") // Used by native/jni bindings + @JvmStatic + fun fetchNativeAppStartAsBytes(): ByteArray? { + if (!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( + "pluginRegistrationTime" to pluginRegistrationTime, + "appStartTime" to appStartTimeMillis, + "isColdStart" to isColdStart, + ) + + val androidNativeSpans = mutableMapOf() + + 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) { + if (span.startTimestamp == null) return + + span.description?.let { description -> + map[description] = mapOf( + "startTimestampMsSinceEpoch" to span.startTimestampMs, + "stopTimestampMsSinceEpoch" to span.projectedStopTimestampMs, + ) + } + } + @JvmStatic fun getApplicationContext(): Context? = applicationContext + @JvmStatic + fun setAutoPerformanceTracingEnabled(enabled: Boolean) { + autoPerformanceTracingEnabled = enabled + } + @Suppress("unused") // Used by native/jni bindings @JvmStatic fun loadContextsAsBytes(): ByteArray? { diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index 8c263dc772..aebd47c81c 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -1330,6 +1330,32 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject { .object(const jni$_.JIntegerNullableType()); } + static final _id_fetchNativeAppStartAsBytes = _class.instanceMethodId( + r'fetchNativeAppStartAsBytes', + r'()[B', + ); + + static final _fetchNativeAppStartAsBytes = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final byte[] fetchNativeAppStartAsBytes()` + /// The returned object must be released after use, by calling the [release] method. + jni$_.JByteArray? fetchNativeAppStartAsBytes() { + return _fetchNativeAppStartAsBytes(reference.pointer, + _id_fetchNativeAppStartAsBytes as jni$_.JMethodIDPtr) + .object(const jni$_.JByteArrayNullableType()); + } + static final _id_getApplicationContext = _class.instanceMethodId( r'getApplicationContext', r'()Landroid/content/Context;', @@ -1355,6 +1381,32 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject { .object(const jni$_.JObjectNullableType()); } + static final _id_setAutoPerformanceTracingEnabled = _class.instanceMethodId( + r'setAutoPerformanceTracingEnabled', + r'(Z)V', + ); + + static final _setAutoPerformanceTracingEnabled = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>( + 'globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, jni$_.JMethodIDPtr, int)>(); + + /// from: `public final void setAutoPerformanceTracingEnabled(boolean z)` + void setAutoPerformanceTracingEnabled( + bool z, + ) { + _setAutoPerformanceTracingEnabled( + reference.pointer, + _id_setAutoPerformanceTracingEnabled as jni$_.JMethodIDPtr, + z ? 1 : 0) + .check(); + } + static final _id_loadContextsAsBytes = _class.instanceMethodId( r'loadContextsAsBytes', r'()[B', @@ -1815,6 +1867,32 @@ class SentryFlutterPlugin extends jni$_.JObject { .object(const jni$_.JIntegerNullableType()); } + static final _id_fetchNativeAppStartAsBytes = _class.staticMethodId( + r'fetchNativeAppStartAsBytes', + r'()[B', + ); + + static final _fetchNativeAppStartAsBytes = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `static public final byte[] fetchNativeAppStartAsBytes()` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JByteArray? fetchNativeAppStartAsBytes() { + return _fetchNativeAppStartAsBytes(_class.reference.pointer, + _id_fetchNativeAppStartAsBytes as jni$_.JMethodIDPtr) + .object(const jni$_.JByteArrayNullableType()); + } + static final _id_getApplicationContext = _class.staticMethodId( r'getApplicationContext', r'()Landroid/content/Context;', @@ -1840,6 +1918,32 @@ class SentryFlutterPlugin extends jni$_.JObject { .object(const jni$_.JObjectNullableType()); } + static final _id_setAutoPerformanceTracingEnabled = _class.staticMethodId( + r'setAutoPerformanceTracingEnabled', + r'(Z)V', + ); + + static final _setAutoPerformanceTracingEnabled = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>( + 'globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, jni$_.JMethodIDPtr, int)>(); + + /// from: `static public final void setAutoPerformanceTracingEnabled(boolean z)` + static void setAutoPerformanceTracingEnabled( + bool z, + ) { + _setAutoPerformanceTracingEnabled( + _class.reference.pointer, + _id_setAutoPerformanceTracingEnabled as jni$_.JMethodIDPtr, + z ? 1 : 0) + .check(); + } + static final _id_loadContextsAsBytes = _class.staticMethodId( r'loadContextsAsBytes', r'()[B', 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 05a2d66148..8a5655a40e 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; import '../../replay/scheduled_recorder_config.dart'; +import '../native_app_start.dart'; import '../sentry_native_channel.dart'; import '../utils/utf8_json.dart'; import 'android_envelope_sender.dart'; @@ -178,6 +179,34 @@ class SentryNativeJava extends SentryNativeChannel { ?.intValue(); } + @override + NativeAppStart? fetchNativeAppStart() { + JByteArray? appStartUtf8JsonBytes; + + try { + appStartUtf8JsonBytes = + native.SentryFlutterPlugin.Companion.fetchNativeAppStartAsBytes(); + if (appStartUtf8JsonBytes == null) return null; + + final byteRange = + appStartUtf8JsonBytes.getRange(0, appStartUtf8JsonBytes.length); + final bytes = Uint8List.view( + byteRange.buffer, byteRange.offsetInBytes, byteRange.length); + final appStartMap = decodeUtf8JsonMap(bytes); + return NativeAppStart.fromJson(appStartMap); + } catch (exception, stackTrace) { + options.log(SentryLevel.error, 'JNI: Failed to fetch native app start', + exception: exception, stackTrace: stackTrace); + if (options.automatedTestMode) { + rethrow; + } + } finally { + appStartUtf8JsonBytes?.release(); + } + + return null; + } + @override Future close() async { await _replayRecorder?.stop(); diff --git a/packages/flutter/lib/src/native/sentry_native_channel.dart b/packages/flutter/lib/src/native/sentry_native_channel.dart index c4266fa0cb..384bdcaca7 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -91,10 +91,10 @@ class SentryNativeChannel Future close() async => channel.invokeMethod('closeNativeSdk'); @override - Future fetchNativeAppStart() async { - final json = - await channel.invokeMapMethod('fetchNativeAppStart'); - return (json != null) ? NativeAppStart.fromJson(json) : null; + FutureOr fetchNativeAppStart() async { + assert(false, + 'fetchNativeAppStart should not be used through method channels.'); + return null; } @override From 775021383ca165c14d2b46129ac7e32fe3ae9670 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 11:19:54 +0200 Subject: [PATCH 34/61] Update --- .../io/sentry/flutter/SentryFlutterPlugin.kt | 38 ++--- .../sentry_flutter/SentryFlutterPlugin.swift | 136 ++++++++---------- .../sentry_flutter_objc/SentryFlutterPlugin.h | 1 + .../flutter/lib/src/native/cocoa/binding.dart | 11 ++ .../src/native/cocoa/sentry_native_cocoa.dart | 14 ++ .../test/sentry_native_channel_test.dart | 45 +++--- 6 files changed, 123 insertions(+), 122 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 47f1a3d171..97116c6a02 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 @@ -342,20 +342,22 @@ class SentryFlutterPlugin : } val appStartTimeMillis = DateUtils.nanosToMillis(appStartTime.nanoTimestamp().toDouble()) - val item = mutableMapOf( - "pluginRegistrationTime" to pluginRegistrationTime, - "appStartTime" to appStartTimeMillis, - "isColdStart" to isColdStart, - ) + val item = + mutableMapOf( + "pluginRegistrationTime" to pluginRegistrationTime, + "appStartTime" to appStartTimeMillis, + "isColdStart" to isColdStart, + ) val androidNativeSpans = mutableMapOf() - val processInitSpan = TimeSpan().apply { - description = "Process Initialization" - setStartUnixTimeMs(appStartTimeSpan.startTimestampMs) - setStartedAt(appStartTimeSpan.startUptimeMs) - setStoppedAt(appStartMetrics.classLoadedUptimeMs) - } + val processInitSpan = + TimeSpan().apply { + description = "Process Initialization" + setStartUnixTimeMs(appStartTimeSpan.startTimestampMs) + setStartedAt(appStartTimeSpan.startUptimeMs) + setStoppedAt(appStartMetrics.classLoadedUptimeMs) + } addTimeSpanToMap(processInitSpan, androidNativeSpans) val applicationOnCreateSpan = appStartMetrics.applicationOnCreateTimeSpan @@ -377,14 +379,18 @@ class SentryFlutterPlugin : return json.toByteArray(Charsets.UTF_8) } - private fun addTimeSpanToMap(span: TimeSpan, map: MutableMap) { + private fun addTimeSpanToMap( + span: TimeSpan, + map: MutableMap, + ) { if (span.startTimestamp == null) return span.description?.let { description -> - map[description] = mapOf( - "startTimestampMsSinceEpoch" to span.startTimestampMs, - "stopTimestampMsSinceEpoch" to span.projectedStopTimestampMs, - ) + map[description] = + mapOf( + "startTimestampMsSinceEpoch" to span.startTimestampMs, + "stopTimestampMsSinceEpoch" to span.projectedStopTimestampMs, + ) } } 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 31c160cc5e..6629269211 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -76,9 +76,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { case "closeNativeSdk": closeNativeSdk(call, result: result) - case "fetchNativeAppStart": - fetchNativeAppStart(result: result) - case "setContexts": let arguments = call.arguments as? [String: Any?] let key = arguments?["key"] as? String @@ -291,83 +288,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { return !name.isEmpty } - struct TimeSpan { - var startTimestampMsSinceEpoch: NSNumber - var stopTimestampMsSinceEpoch: NSNumber - var description: String - - func addToMap(_ map: inout [String: Any]) { - map[description] = [ - "startTimestampMsSinceEpoch": startTimestampMsSinceEpoch, - "stopTimestampMsSinceEpoch": stopTimestampMsSinceEpoch - ] - } - } - - private func fetchNativeAppStart(result: @escaping FlutterResult) { - #if os(iOS) || os(tvOS) - guard let appStartMeasurement = PrivateSentrySDKOnly.appStartMeasurement else { - print("warning: appStartMeasurement is null") - result(nil) - return - } - - var nativeSpanTimes: [String: Any] = [:] - - let appStartTimeMs = appStartMeasurement.appStartTimestamp.timeIntervalSince1970.toMilliseconds() - let runtimeInitTimeMs = appStartMeasurement.runtimeInitTimestamp.timeIntervalSince1970.toMilliseconds() - let moduleInitializationTimeMs = - appStartMeasurement.moduleInitializationTimestamp.timeIntervalSince1970.toMilliseconds() - let sdkStartTimeMs = appStartMeasurement.sdkStartTimestamp.timeIntervalSince1970.toMilliseconds() - - if !appStartMeasurement.isPreWarmed { - let preRuntimeInitDescription = "Pre Runtime Init" - let preRuntimeInitSpan = TimeSpan( - startTimestampMsSinceEpoch: NSNumber(value: appStartTimeMs), - stopTimestampMsSinceEpoch: NSNumber(value: runtimeInitTimeMs), - description: preRuntimeInitDescription - ) - preRuntimeInitSpan.addToMap(&nativeSpanTimes) - - let moduleInitializationDescription = "Runtime init to Pre Main initializers" - let moduleInitializationSpan = TimeSpan( - startTimestampMsSinceEpoch: NSNumber(value: runtimeInitTimeMs), - stopTimestampMsSinceEpoch: NSNumber(value: moduleInitializationTimeMs), - description: moduleInitializationDescription - ) - moduleInitializationSpan.addToMap(&nativeSpanTimes) - } - - let uiKitInitDescription = "UIKit init" - let uiKitInitSpan = TimeSpan( - startTimestampMsSinceEpoch: NSNumber(value: moduleInitializationTimeMs), - stopTimestampMsSinceEpoch: NSNumber(value: sdkStartTimeMs), - description: uiKitInitDescription - ) - uiKitInitSpan.addToMap(&nativeSpanTimes) - - // Info: We don't have access to didFinishLaunchingTimestamp, - // On HybridSDKs, the Cocoa SDK misses the didFinishLaunchNotification and the - // didBecomeVisibleNotification. Therefore, we can't set the - // didFinishLaunchingTimestamp - - let appStartTime = appStartMeasurement.appStartTimestamp.timeIntervalSince1970 * 1000 - let isColdStart = appStartMeasurement.type == .cold - - let item: [String: Any] = [ - "pluginRegistrationTime": SentryFlutterPlugin.pluginRegistrationTime, - "appStartTime": appStartTime, - "isColdStart": isColdStart, - "nativeSpanTimes": nativeSpanTimes - ] - - result(item) - #else - print("note: appStartMeasurement not available on this platform") - result(nil) - #endif - } - private func setContexts(key: String?, value: Any?, result: @escaping FlutterResult) { guard let key = key else { result("") @@ -565,6 +485,62 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { } #endif + @objc public class func fetchNativeAppStartAsBytes() -> NSData? { + #if os(iOS) || os(tvOS) + guard let appStartMeasurement = PrivateSentrySDKOnly.appStartMeasurement else { + return nil + } + + var nativeSpanTimes: [String: Any] = [:] + + let appStartTimeMs = appStartMeasurement.appStartTimestamp.timeIntervalSince1970.toMilliseconds() + let runtimeInitTimeMs = appStartMeasurement.runtimeInitTimestamp.timeIntervalSince1970.toMilliseconds() + let moduleInitializationTimeMs = + appStartMeasurement.moduleInitializationTimestamp.timeIntervalSince1970.toMilliseconds() + let sdkStartTimeMs = appStartMeasurement.sdkStartTimestamp.timeIntervalSince1970.toMilliseconds() + + if !appStartMeasurement.isPreWarmed { + let preRuntimeInitDescription = "Pre Runtime Init" + let preRuntimeInitSpan: [String: Any] = [ + "startTimestampMsSinceEpoch": NSNumber(value: appStartTimeMs), + "stopTimestampMsSinceEpoch": NSNumber(value: runtimeInitTimeMs) + ] + nativeSpanTimes[preRuntimeInitDescription] = preRuntimeInitSpan + + let moduleInitializationDescription = "Runtime init to Pre Main initializers" + let moduleInitializationSpan: [String: Any] = [ + "startTimestampMsSinceEpoch": NSNumber(value: runtimeInitTimeMs), + "stopTimestampMsSinceEpoch": NSNumber(value: moduleInitializationTimeMs) + ] + nativeSpanTimes[moduleInitializationDescription] = moduleInitializationSpan + } + + let uiKitInitDescription = "UIKit init" + let uiKitInitSpan: [String: Any] = [ + "startTimestampMsSinceEpoch": NSNumber(value: moduleInitializationTimeMs), + "stopTimestampMsSinceEpoch": NSNumber(value: sdkStartTimeMs) + ] + nativeSpanTimes[uiKitInitDescription] = uiKitInitSpan + + let appStartTime = appStartMeasurement.appStartTimestamp.timeIntervalSince1970 * 1000 + let isColdStart = appStartMeasurement.type == .cold + + let item: [String: Any] = [ + "pluginRegistrationTime": pluginRegistrationTime, + "appStartTime": appStartTime, + "isColdStart": isColdStart, + "nativeSpanTimes": nativeSpanTimes + ] + + if let data = try? JSONSerialization.data(withJSONObject: item, options: []) { + return data as NSData + } + return nil + #else + return nil + #endif + } + @objc(loadDebugImagesAsBytes:) public class func loadDebugImagesAsBytes(instructionAddresses: Set) -> NSData? { var debugImages: [DebugMeta] = [] 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 ace6992371..6f2e25eb54 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 @@ -5,6 +5,7 @@ #else @interface SentryFlutterPlugin : NSObject + (nullable NSNumber *)getDisplayRefreshRate; ++ (nullable NSData *)fetchNativeAppStartAsBytes; + (nullable NSData *)loadContextsAsBytes; + (nullable NSData *)loadDebugImagesAsBytes:(NSSet *)instructionAddresses; @end diff --git a/packages/flutter/lib/src/native/cocoa/binding.dart b/packages/flutter/lib/src/native/cocoa/binding.dart index b5d952c5a5..903062aa10 100644 --- a/packages/flutter/lib/src/native/cocoa/binding.dart +++ b/packages/flutter/lib/src/native/cocoa/binding.dart @@ -1123,6 +1123,8 @@ class SentryId$1 extends objc.NSObject { late final _class_SentryFlutterPlugin = objc.getClass("SentryFlutterPlugin"); late final _sel_getDisplayRefreshRate = objc.registerName("getDisplayRefreshRate"); +late final _sel_fetchNativeAppStartAsBytes = + objc.registerName("fetchNativeAppStartAsBytes"); late final _sel_loadContextsAsBytes = objc.registerName("loadContextsAsBytes"); late final _sel_loadDebugImagesAsBytes_ = objc.registerName("loadDebugImagesAsBytes:"); @@ -1157,6 +1159,15 @@ class SentryFlutterPlugin extends objc.NSObject { : objc.NSNumber.castFromPointer(_ret, retain: true, release: true); } + /// fetchNativeAppStartAsBytes + static objc.NSData? fetchNativeAppStartAsBytes() { + final _ret = _objc_msgSend_151sglz( + _class_SentryFlutterPlugin, _sel_fetchNativeAppStartAsBytes); + return _ret.address == 0 + ? null + : objc.NSData.castFromPointer(_ret, retain: true, release: true); + } + /// loadContextsAsBytes static objc.NSData? loadContextsAsBytes() { final _ret = _objc_msgSend_151sglz( 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 70ee6bf3fa..0a8dc2ee4a 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -5,6 +5,7 @@ import 'package:objective_c/objective_c.dart'; import '../../../sentry_flutter.dart'; import '../../replay/replay_config.dart'; +import '../native_app_start.dart'; import '../sentry_native_channel.dart'; import '../utils/utf8_json.dart'; import 'binding.dart' as cocoa; @@ -164,4 +165,17 @@ class SentryNativeCocoa extends SentryNativeChannel { return refreshRate.intValue; }, ); + + @override + NativeAppStart? fetchNativeAppStart() => tryCatchSync( + 'fetchNativeAppStart', + () { + final appStartUtf8JsonBytes = + cocoa.SentryFlutterPlugin.fetchNativeAppStartAsBytes(); + if (appStartUtf8JsonBytes == null) return null; + + final json = decodeUtf8JsonMap(appStartUtf8JsonBytes.toList()); + return NativeAppStart.fromJson(json); + }, + ); } diff --git a/packages/flutter/test/sentry_native_channel_test.dart b/packages/flutter/test/sentry_native_channel_test.dart index 1a5c7dd476..f35d68ea8b 100644 --- a/packages/flutter/test/sentry_native_channel_test.dart +++ b/packages/flutter/test/sentry_native_channel_test.dart @@ -38,34 +38,15 @@ void main() { // TODO move other methods here, e.g. init_native_sdk_test.dart test('fetchNativeAppStart', () async { - when(channel.invokeMethod('fetchNativeAppStart')) - .thenAnswer((_) async => { - 'pluginRegistrationTime': 1, - 'appStartTime': 0.1, - 'isColdStart': true, - // ignore: inference_failure_on_collection_literal - 'nativeSpanTimes': {}, - }); - - final actual = await sut.fetchNativeAppStart(); - - expect(actual?.appStartTime, 0.1); - expect(actual?.isColdStart, true); - }); - - test('invalid fetchNativeAppStart returns null', () async { - when(channel.invokeMethod('fetchNativeAppStart')) - .thenAnswer((_) async => { - 'pluginRegistrationTime': 'invalid', - 'appStartTime': 'invalid', - 'isColdStart': 'invalid', - // ignore: inference_failure_on_collection_literal - 'nativeSpanTimes': 'invalid', - }); + final matcher = _nativeUnavailableMatcher( + mockPlatform, + includeLookupSymbol: true, + includeFailedToLoadClassException: true, + ); - final actual = await sut.fetchNativeAppStart(); + expect(() => sut.fetchNativeAppStart(), matcher); - expect(actual, isNull); + verifyZeroInteractions(channel); }); test('setUser', () async { @@ -269,6 +250,18 @@ void main() { verifyZeroInteractions(channel); }); + test('displayRefreshRate', () async { + final matcher = _nativeUnavailableMatcher( + mockPlatform, + includeLookupSymbol: true, + includeFailedToLoadClassException: true, + ); + + expect(() => sut.displayRefreshRate(), matcher); + + verifyZeroInteractions(channel); + }); + test('pauseAppHangTracking', () async { when(channel.invokeMethod('pauseAppHangTracking')) .thenAnswer((_) => Future.value()); From cd554e45f712d0c58e488503670a5ce1aec1fa7d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 11:22:19 +0200 Subject: [PATCH 35/61] Update --- .../kotlin/io/sentry/flutter/SentryFlutter.kt | 1 - .../io/sentry/flutter/SentryFlutterPlugin.kt | 5 -- .../flutter/lib/src/native/java/binding.dart | 52 ------------------- 3 files changed, 58 deletions(-) diff --git a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt index a1a7ff5c3a..19eec63247 100644 --- a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt +++ b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt @@ -108,7 +108,6 @@ class SentryFlutter { data.getIfNotNull("enableAutoPerformanceTracing") { enableAutoPerformanceTracing -> if (enableAutoPerformanceTracing) { autoPerformanceTracingEnabled = true - SentryFlutterPlugin.setAutoPerformanceTracingEnabled(true) } } 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 97116c6a02..af431efb94 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 @@ -397,11 +397,6 @@ class SentryFlutterPlugin : @JvmStatic fun getApplicationContext(): Context? = applicationContext - @JvmStatic - fun setAutoPerformanceTracingEnabled(enabled: Boolean) { - autoPerformanceTracingEnabled = enabled - } - @Suppress("unused") // Used by native/jni bindings @JvmStatic fun loadContextsAsBytes(): ByteArray? { diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index aebd47c81c..feae89319b 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -1381,32 +1381,6 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject { .object(const jni$_.JObjectNullableType()); } - static final _id_setAutoPerformanceTracingEnabled = _class.instanceMethodId( - r'setAutoPerformanceTracingEnabled', - r'(Z)V', - ); - - static final _setAutoPerformanceTracingEnabled = - jni$_.ProtectedJniExtensions.lookup< - jni$_.NativeFunction< - jni$_.JThrowablePtr Function(jni$_.Pointer, - jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>( - 'globalEnv_CallVoidMethod') - .asFunction< - jni$_.JThrowablePtr Function( - jni$_.Pointer, jni$_.JMethodIDPtr, int)>(); - - /// from: `public final void setAutoPerformanceTracingEnabled(boolean z)` - void setAutoPerformanceTracingEnabled( - bool z, - ) { - _setAutoPerformanceTracingEnabled( - reference.pointer, - _id_setAutoPerformanceTracingEnabled as jni$_.JMethodIDPtr, - z ? 1 : 0) - .check(); - } - static final _id_loadContextsAsBytes = _class.instanceMethodId( r'loadContextsAsBytes', r'()[B', @@ -1918,32 +1892,6 @@ class SentryFlutterPlugin extends jni$_.JObject { .object(const jni$_.JObjectNullableType()); } - static final _id_setAutoPerformanceTracingEnabled = _class.staticMethodId( - r'setAutoPerformanceTracingEnabled', - r'(Z)V', - ); - - static final _setAutoPerformanceTracingEnabled = - jni$_.ProtectedJniExtensions.lookup< - jni$_.NativeFunction< - jni$_.JThrowablePtr Function(jni$_.Pointer, - jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int32,)>)>>( - 'globalEnv_CallStaticVoidMethod') - .asFunction< - jni$_.JThrowablePtr Function( - jni$_.Pointer, jni$_.JMethodIDPtr, int)>(); - - /// from: `static public final void setAutoPerformanceTracingEnabled(boolean z)` - static void setAutoPerformanceTracingEnabled( - bool z, - ) { - _setAutoPerformanceTracingEnabled( - _class.reference.pointer, - _id_setAutoPerformanceTracingEnabled as jni$_.JMethodIDPtr, - z ? 1 : 0) - .check(); - } - static final _id_loadContextsAsBytes = _class.staticMethodId( r'loadContextsAsBytes', r'()[B', From 9708168987bceddf8e56142cec21bc556d89bcc2 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 11:28:51 +0200 Subject: [PATCH 36/61] Update --- .../src/native/java/sentry_native_java.dart | 26 +++++++------------ .../lib/src/native/sentry_native_invoker.dart | 5 +++- 2 files changed, 13 insertions(+), 18 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 8a5655a40e..b79d8b9db4 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -173,38 +173,30 @@ class SentryNativeJava extends SentryNativeChannel { } @override - int? displayRefreshRate() { - return native.SentryFlutterPlugin.Companion - .getDisplayRefreshRate() - ?.intValue(); - } + int? displayRefreshRate() => tryCatchSync('displayRefreshRate', () { + return native.SentryFlutterPlugin.Companion + .getDisplayRefreshRate() + ?.intValue(); + }); @override NativeAppStart? fetchNativeAppStart() { JByteArray? appStartUtf8JsonBytes; - try { + return tryCatchSync('fetchNativeAppStart', () { appStartUtf8JsonBytes = native.SentryFlutterPlugin.Companion.fetchNativeAppStartAsBytes(); if (appStartUtf8JsonBytes == null) return null; final byteRange = - appStartUtf8JsonBytes.getRange(0, appStartUtf8JsonBytes.length); + appStartUtf8JsonBytes!.getRange(0, appStartUtf8JsonBytes!.length); final bytes = Uint8List.view( byteRange.buffer, byteRange.offsetInBytes, byteRange.length); final appStartMap = decodeUtf8JsonMap(bytes); return NativeAppStart.fromJson(appStartMap); - } catch (exception, stackTrace) { - options.log(SentryLevel.error, 'JNI: Failed to fetch native app start', - exception: exception, stackTrace: stackTrace); - if (options.automatedTestMode) { - rethrow; - } - } finally { + }, finallyFn: () { appStartUtf8JsonBytes?.release(); - } - - return null; + }); } @override diff --git a/packages/flutter/lib/src/native/sentry_native_invoker.dart b/packages/flutter/lib/src/native/sentry_native_invoker.dart index 6b20aff03d..31b1c187b6 100644 --- a/packages/flutter/lib/src/native/sentry_native_invoker.dart +++ b/packages/flutter/lib/src/native/sentry_native_invoker.dart @@ -22,7 +22,8 @@ mixin SentryNativeSafeInvoker { } } - T? tryCatchSync(String nativeMethodName, T? Function() fn) { + T? tryCatchSync(String nativeMethodName, T? Function() fn, + {void Function()? finallyFn}) { try { return fn(); } catch (error, stackTrace) { @@ -31,6 +32,8 @@ mixin SentryNativeSafeInvoker { rethrow; } return null; + } finally { + finallyFn?.call(); } } From 99b5584213c43fd2e53d6e52f756d88b6bb1d8e8 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 11:33:10 +0200 Subject: [PATCH 37/61] Update --- .../src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 af431efb94..57fb29e1c0 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 @@ -269,6 +269,7 @@ class SentryFlutterPlugin : result.success("") } + @Suppress("TooManyFunctions") companion object { @SuppressLint("StaticFieldLeak") private var replay: ReplayIntegration? = null @@ -313,7 +314,7 @@ class SentryFlutterPlugin : return refreshRate } - @Suppress("unused") // Used by native/jni bindings + @Suppress("unused", "ReturnCount") // Used by native/jni bindings @JvmStatic fun fetchNativeAppStartAsBytes(): ByteArray? { if (!autoPerformanceTracingEnabled) { From eead0ce3e49f6375da50d8a4394454d12e906bb7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 11:42:47 +0200 Subject: [PATCH 38/61] Update --- .../src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | 3 +-- 1 file changed, 1 insertion(+), 2 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 57fb29e1c0..da5a113850 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 @@ -44,7 +44,7 @@ class SentryFlutterPlugin : private lateinit var sentryFlutter: SentryFlutter override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - Companion.pluginRegistrationTime = System.currentTimeMillis() + pluginRegistrationTime = System.currentTimeMillis() context = flutterPluginBinding.applicationContext applicationContext = context @@ -281,7 +281,6 @@ class SentryFlutterPlugin : private var activity: WeakReference? = null private var pluginRegistrationTime: Long? = null - private var autoPerformanceTracingEnabled: Boolean = false private const val NATIVE_CRASH_WAIT_TIME = 500L From 780a59c2f8f83b890c5df0fe9bcac9ad67ad5a9b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 11:48:29 +0200 Subject: [PATCH 39/61] Update --- .../integration_test/integration_test.dart | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/flutter/example/integration_test/integration_test.dart b/packages/flutter/example/integration_test/integration_test.dart index 4fa3fc4153..c5d24b352b 100644 --- a/packages/flutter/example/integration_test/integration_test.dart +++ b/packages/flutter/example/integration_test/integration_test.dart @@ -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 || Platform.isMacOS) { + // 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(), + reason: 'isColdStart should be a boolean'); + expect(appStart.nativeSpanTimes, isA(), + 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; From 143414f3688dfd1561bd3aef40ccab89bbe6f9e3 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 11:54:59 +0200 Subject: [PATCH 40/61] Update --- packages/flutter/example/integration_test/integration_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/example/integration_test/integration_test.dart b/packages/flutter/example/integration_test/integration_test.dart index c5d24b352b..82b2c0e703 100644 --- a/packages/flutter/example/integration_test/integration_test.dart +++ b/packages/flutter/example/integration_test/integration_test.dart @@ -604,7 +604,7 @@ void main() { await setupSentryAndApp(tester); }); - if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) { + if (Platform.isAndroid || Platform.isIOS) { // fetchNativeAppStart should return data on mobile platforms final appStart = await SentryFlutter.native?.fetchNativeAppStart(); From 60f96741c7a179e72c3f52f040f0626520e0ce60 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 13:03:08 +0200 Subject: [PATCH 41/61] Update --- .../src/main/kotlin/io/sentry/flutter/SentryFlutter.kt | 4 +--- .../src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt index 19eec63247..1df72b60b2 100644 --- a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt +++ b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt @@ -21,8 +21,6 @@ class SentryFlutter { internal const val NATIVE_SDK = "sentry.native.android.flutter" } - var autoPerformanceTracingEnabled = false - fun updateOptions( options: SentryAndroidOptions, data: Map, @@ -107,7 +105,7 @@ class SentryFlutter { data.getIfNotNull("enableAutoPerformanceTracing") { enableAutoPerformanceTracing -> if (enableAutoPerformanceTracing) { - autoPerformanceTracingEnabled = true + SentryFlutterPlugin.setAutoPerformanceTracingEnabled(true) } } 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 da5a113850..09e8d34122 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 @@ -281,6 +281,7 @@ class SentryFlutterPlugin : private var activity: WeakReference? = null private var pluginRegistrationTime: Long? = null + private var autoPerformanceTracingEnabled: Boolean = false private const val NATIVE_CRASH_WAIT_TIME = 500L @@ -439,6 +440,10 @@ class SentryFlutterPlugin : return json.toByteArray(Charsets.UTF_8) } + internal fun setAutoPerformanceTracingEnabled(enabled: Boolean) { + autoPerformanceTracingEnabled = enabled + } + private fun List?.serialize() = this?.map { it.serialize() } private fun DebugImage.serialize() = From 3eeda60919d43bf4db93af8fb393eff456ec41d7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 13:27:22 +0200 Subject: [PATCH 42/61] Fix tests --- packages/flutter/lib/src/native/native_app_start.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/native/native_app_start.dart b/packages/flutter/lib/src/native/native_app_start.dart index 71f0c1eb58..c4c4efb3f9 100644 --- a/packages/flutter/lib/src/native/native_app_start.dart +++ b/packages/flutter/lib/src/native/native_app_start.dart @@ -9,7 +9,7 @@ class NativeAppStart { required this.isColdStart, required this.nativeSpanTimes}); - double appStartTime; + int appStartTime; int pluginRegistrationTime; bool isColdStart; Map nativeSpanTimes; @@ -20,7 +20,7 @@ class NativeAppStart { final isColdStart = json['isColdStart']; final nativeSpanTimes = json['nativeSpanTimes']; - if (appStartTime is! double || + if (appStartTime is! int || pluginRegistrationTime is! int || isColdStart is! bool || nativeSpanTimes is! Map) { From a40112f0bc462fddcd6ed767c321a48f11aaa6fe Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 13:50:28 +0200 Subject: [PATCH 43/61] Update --- .../src/integrations/native_app_start_handler.dart | 4 ++-- .../flutter/lib/src/native/native_app_start.dart | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/flutter/lib/src/integrations/native_app_start_handler.dart b/packages/flutter/lib/src/integrations/native_app_start_handler.dart index 4eda35c0e7..79ee45d91d 100644 --- a/packages/flutter/lib/src/integrations/native_app_start_handler.dart +++ b/packages/flutter/lib/src/integrations/native_app_start_handler.dart @@ -80,8 +80,8 @@ class NativeAppStartHandler { return null; } - final appStartDateTime = DateTime.fromMillisecondsSinceEpoch( - nativeAppStart.appStartTime.toInt()); + final appStartDateTime = + DateTime.fromMillisecondsSinceEpoch(nativeAppStart.appStartTime); final pluginRegistrationDateTime = DateTime.fromMillisecondsSinceEpoch( nativeAppStart.pluginRegistrationTime); diff --git a/packages/flutter/lib/src/native/native_app_start.dart b/packages/flutter/lib/src/native/native_app_start.dart index c4c4efb3f9..dc56381b68 100644 --- a/packages/flutter/lib/src/native/native_app_start.dart +++ b/packages/flutter/lib/src/native/native_app_start.dart @@ -15,12 +15,22 @@ class NativeAppStart { Map nativeSpanTimes; static NativeAppStart? fromJson(Map json) { - final appStartTime = json['appStartTime']; + final appStartTimeValue = json['appStartTime']; final pluginRegistrationTime = json['pluginRegistrationTime']; final isColdStart = json['isColdStart']; final nativeSpanTimes = json['nativeSpanTimes']; - if (appStartTime is! int || + // Convert appStartTime to int (iOS returns double, Android returns int) + final int? appStartTime; + if (appStartTimeValue is int) { + appStartTime = appStartTimeValue; + } else if (appStartTimeValue is double) { + appStartTime = appStartTimeValue.toInt(); + } else { + appStartTime = null; + } + + if (appStartTime == null || pluginRegistrationTime is! int || isColdStart is! bool || nativeSpanTimes is! Map) { From 83f8ea4a0a95559d64b8b382546166d83c6d89da Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 14:20:12 +0200 Subject: [PATCH 44/61] Update --- .../src/main/kotlin/io/sentry/flutter/SentryFlutter.kt | 2 +- .../main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | 8 +++----- .../test/kotlin/io/sentry/flutter/SentryFlutterTest.kt | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt index 1df72b60b2..e0e35b9ccc 100644 --- a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt +++ b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt @@ -105,7 +105,7 @@ class SentryFlutter { data.getIfNotNull("enableAutoPerformanceTracing") { enableAutoPerformanceTracing -> if (enableAutoPerformanceTracing) { - SentryFlutterPlugin.setAutoPerformanceTracingEnabled(true) + SentryFlutterPlugin.autoPerformanceTracingEnabled = true } } 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 09e8d34122..a62a58fa38 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 @@ -281,7 +281,9 @@ class SentryFlutterPlugin : private var activity: WeakReference? = null private var pluginRegistrationTime: Long? = null - private var autoPerformanceTracingEnabled: Boolean = false + + var autoPerformanceTracingEnabled: Boolean = false + internal set private const val NATIVE_CRASH_WAIT_TIME = 500L @@ -440,10 +442,6 @@ class SentryFlutterPlugin : return json.toByteArray(Charsets.UTF_8) } - internal fun setAutoPerformanceTracingEnabled(enabled: Boolean) { - autoPerformanceTracingEnabled = enabled - } - private fun List?.serialize() = this?.map { it.serialize() } private fun DebugImage.serialize() = diff --git a/packages/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt b/packages/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt index be7eacb873..74c9214a6c 100644 --- a/packages/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt +++ b/packages/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt @@ -62,7 +62,7 @@ class SentryFlutterTest { ) assertEquals("sentry.native.android.flutter", fixture.options.nativeSdkName) - assertEquals(true, sut.autoPerformanceTracingEnabled) + assertEquals(true, SentryFlutterPlugin.autoPerformanceTracingEnabled) assertEquals(9006, fixture.options.connectionTimeoutMillis) assertEquals(9007, fixture.options.readTimeoutMillis) From f9edebc62e659ba9fe4ba42cea7505055617255d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 14:25:50 +0200 Subject: [PATCH 45/61] Update --- .../src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a62a58fa38..b4c4d71fd0 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 @@ -281,7 +281,7 @@ class SentryFlutterPlugin : private var activity: WeakReference? = null private var pluginRegistrationTime: Long? = null - + var autoPerformanceTracingEnabled: Boolean = false internal set From c40ff0cb43691ab50b7041472944459e511758b9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 9 Oct 2025 15:15:07 +0200 Subject: [PATCH 46/61] Update --- .../io/sentry/flutter/SentryFlutterPlugin.kt | 17 +++---- .../sentry_flutter/SentryFlutterPlugin.swift | 35 +++++--------- .../sentry_flutter_objc/SentryFlutterPlugin.h | 3 ++ .../flutter/lib/src/native/cocoa/binding.dart | 30 ++++++++++++ .../src/native/cocoa/sentry_native_cocoa.dart | 19 ++++++++ .../flutter/lib/src/native/java/binding.dart | 47 +++++++++++++++++++ .../src/native/java/sentry_native_java.dart | 15 ++++++ .../lib/src/native/sentry_native_channel.dart | 16 +++++-- .../test/sentry_native_channel_test.dart | 47 +++++++++++++------ 9 files changed, 179 insertions(+), 50 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 09e8d34122..13151ea1a0 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 @@ -71,7 +71,6 @@ class SentryFlutterPlugin : "removeExtra" -> removeExtra(call.argument("key"), result) "setTag" -> setTag(call.argument("key"), call.argument("value"), result) "removeTag" -> removeTag(call.argument("key"), result) - "nativeCrash" -> crash() "setReplayConfig" -> setReplayConfig(call, result) "captureReplay" -> captureReplay(result) else -> result.notImplemented() @@ -289,6 +288,15 @@ class SentryFlutterPlugin : @JvmStatic fun privateSentryGetReplayIntegration(): ReplayIntegration? = replay + @Suppress("unused") // Used by native/jni bindings + @JvmStatic + fun nativeCrash() { + val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException") + val mainThread = Looper.getMainLooper().thread + mainThread.uncaughtExceptionHandler?.uncaughtException(mainThread, exception) + mainThread.join(NATIVE_CRASH_WAIT_TIME) + } + @Suppress("unused") // Used by native/jni bindings @JvmStatic fun getDisplayRefreshRate(): Int? { @@ -457,13 +465,6 @@ class SentryFlutterPlugin : "debug_file" to debugFile, ) - private fun crash() { - val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException") - val mainThread = Looper.getMainLooper().thread - mainThread.uncaughtExceptionHandler?.uncaughtException(mainThread, exception) - mainThread.join(NATIVE_CRASH_WAIT_TIME) - } - private fun Double.adjustReplaySizeToBlockSize(): Double { val remainder = this % VIDEO_BLOCK_SIZE return if (remainder <= VIDEO_BLOCK_SIZE / 2) { 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 6629269211..44bb1eaff0 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -130,15 +130,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { collectProfile(call, result) #endif - case "pauseAppHangTracking": - pauseAppHangTracking(result) - - case "resumeAppHangTracking": - resumeAppHangTracking(result) - - case "nativeCrash": - crash() - case "captureReplay": #if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS)) PrivateSentrySDKOnly.captureReplay() @@ -431,20 +422,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { result(nil) } - private func pauseAppHangTracking(_ result: @escaping FlutterResult) { - SentrySDK.pauseAppHangTracking() - result("") - } - - private func resumeAppHangTracking(_ result: @escaping FlutterResult) { - SentrySDK.resumeAppHangTracking() - result("") - } - - private func crash() { - SentrySDK.crash() - } - // MARK: - Objective-C interoperability // // Group of methods exposed to the Objective-C runtime via `@objc`. @@ -541,6 +518,18 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { #endif } + @objc public class func nativeCrash() { + SentrySDK.crash() + } + + @objc public class func pauseAppHangTracking() { + SentrySDK.pauseAppHangTracking() + } + + @objc public class func resumeAppHangTracking() { + SentrySDK.resumeAppHangTracking() + } + @objc(loadDebugImagesAsBytes:) public class func loadDebugImagesAsBytes(instructionAddresses: Set) -> NSData? { var debugImages: [DebugMeta] = [] 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..61af58310d 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,8 @@ + (nullable NSData *)fetchNativeAppStartAsBytes; + (nullable NSData *)loadContextsAsBytes; + (nullable NSData *)loadDebugImagesAsBytes:(NSSet *)instructionAddresses; ++ (void)nativeCrash; ++ (void)pauseAppHangTracking; ++ (void)resumeAppHangTracking; @end #endif diff --git a/packages/flutter/lib/src/native/cocoa/binding.dart b/packages/flutter/lib/src/native/cocoa/binding.dart index 903062aa10..73d693b44d 100644 --- a/packages/flutter/lib/src/native/cocoa/binding.dart +++ b/packages/flutter/lib/src/native/cocoa/binding.dart @@ -1128,6 +1128,19 @@ late final _sel_fetchNativeAppStartAsBytes = late final _sel_loadContextsAsBytes = objc.registerName("loadContextsAsBytes"); late final _sel_loadDebugImagesAsBytes_ = objc.registerName("loadDebugImagesAsBytes:"); +late final _sel_nativeCrash = objc.registerName("nativeCrash"); +final _objc_msgSend_1pl9qdv = objc.msgSendPointer + .cast< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>() + .asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); +late final _sel_pauseAppHangTracking = + objc.registerName("pauseAppHangTracking"); +late final _sel_resumeAppHangTracking = + objc.registerName("resumeAppHangTracking"); /// SentryFlutterPlugin class SentryFlutterPlugin extends objc.NSObject { @@ -1186,6 +1199,23 @@ class SentryFlutterPlugin extends objc.NSObject { : objc.NSData.castFromPointer(_ret, retain: true, release: true); } + /// nativeCrash + static void nativeCrash() { + _objc_msgSend_1pl9qdv(_class_SentryFlutterPlugin, _sel_nativeCrash); + } + + /// pauseAppHangTracking + static void pauseAppHangTracking() { + _objc_msgSend_1pl9qdv( + _class_SentryFlutterPlugin, _sel_pauseAppHangTracking); + } + + /// resumeAppHangTracking + static void resumeAppHangTracking() { + _objc_msgSend_1pl9qdv( + _class_SentryFlutterPlugin, _sel_resumeAppHangTracking); + } + /// 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 0a8dc2ee4a..097b26bc30 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -178,4 +178,23 @@ class SentryNativeCocoa extends SentryNativeChannel { return NativeAppStart.fromJson(json); }, ); + + @override + void nativeCrash() { + cocoa.SentryFlutterPlugin.nativeCrash(); + } + + @override + void pauseAppHangTracking() { + tryCatchSync('pauseAppHangTracking', () { + cocoa.SentryFlutterPlugin.pauseAppHangTracking(); + }); + } + + @override + void resumeAppHangTracking() { + tryCatchSync('resumeAppHangTracking', () { + cocoa.SentryFlutterPlugin.resumeAppHangTracking(); + }); + } } diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index feae89319b..f9730b691a 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -1305,6 +1305,29 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject { .object(const $ReplayIntegration$NullableType()); } + static final _id_nativeCrash = _class.instanceMethodId( + r'nativeCrash', + r'()V', + ); + + static final _nativeCrash = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public final void nativeCrash()` + void nativeCrash() { + _nativeCrash(reference.pointer, _id_nativeCrash as jni$_.JMethodIDPtr) + .check(); + } + static final _id_getDisplayRefreshRate = _class.instanceMethodId( r'getDisplayRefreshRate', r'()Ljava/lang/Integer;', @@ -1816,6 +1839,30 @@ class SentryFlutterPlugin extends jni$_.JObject { .object(const $ReplayIntegration$NullableType()); } + static final _id_nativeCrash = _class.staticMethodId( + r'nativeCrash', + r'()V', + ); + + static final _nativeCrash = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `static public final void nativeCrash()` + static void nativeCrash() { + _nativeCrash( + _class.reference.pointer, _id_nativeCrash as jni$_.JMethodIDPtr) + .check(); + } + static final _id_getDisplayRefreshRate = _class.staticMethodId( r'getDisplayRefreshRate', r'()Ljava/lang/Integer;', 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 b79d8b9db4..817d51c00d 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -199,6 +199,21 @@ class SentryNativeJava extends SentryNativeChannel { }); } + @override + void nativeCrash() { + native.SentryFlutterPlugin.Companion.nativeCrash(); + } + + @override + void pauseAppHangTracking() { + assert(false, 'pauseAppHangTracking is not supported on Android.'); + } + + @override + void resumeAppHangTracking() { + assert(false, 'resumeAppHangTracking is not supported on Android.'); + } + @override Future close() async { await _replayRecorder?.stop(); diff --git a/packages/flutter/lib/src/native/sentry_native_channel.dart b/packages/flutter/lib/src/native/sentry_native_channel.dart index 384bdcaca7..b1f2f20ab4 100644 --- a/packages/flutter/lib/src/native/sentry_native_channel.dart +++ b/packages/flutter/lib/src/native/sentry_native_channel.dart @@ -230,15 +230,21 @@ class SentryNativeChannel } @override - Future pauseAppHangTracking() => - channel.invokeMethod('pauseAppHangTracking'); + FutureOr pauseAppHangTracking() { + assert(false, + 'pauseAppHangTracking should not be used through method channels.'); + } @override - Future resumeAppHangTracking() => - channel.invokeMethod('resumeAppHangTracking'); + FutureOr resumeAppHangTracking() { + assert(false, + 'resumeAppHangTracking should not be used through method channels.'); + } @override - Future nativeCrash() => channel.invokeMethod('nativeCrash'); + FutureOr nativeCrash() { + assert(false, 'nativeCrash should not be used through method channels.'); + } @override bool get supportsReplay => false; diff --git a/packages/flutter/test/sentry_native_channel_test.dart b/packages/flutter/test/sentry_native_channel_test.dart index f35d68ea8b..7288d00971 100644 --- a/packages/flutter/test/sentry_native_channel_test.dart +++ b/packages/flutter/test/sentry_native_channel_test.dart @@ -263,30 +263,49 @@ void main() { }); test('pauseAppHangTracking', () async { - when(channel.invokeMethod('pauseAppHangTracking')) - .thenAnswer((_) => Future.value()); - - await sut.pauseAppHangTracking(); + if (mockPlatform.isAndroid) { + // Android doesn't support app hang tracking, so it should hit the assertion + expect(() => sut.pauseAppHangTracking(), throwsAssertionError); + } else { + // iOS/macOS should throw FFI exceptions in tests + final matcher = _nativeUnavailableMatcher( + mockPlatform, + includeLookupSymbol: true, + includeFailedToLoadClassException: true, + ); + expect(() => sut.pauseAppHangTracking(), matcher); + } - verify(channel.invokeMethod('pauseAppHangTracking')); + verifyZeroInteractions(channel); }); test('resumeAppHangTracking', () async { - when(channel.invokeMethod('resumeAppHangTracking')) - .thenAnswer((_) => Future.value()); - - await sut.resumeAppHangTracking(); + if (mockPlatform.isAndroid) { + // Android doesn't support app hang tracking, so it should hit the assertion + expect(() => sut.resumeAppHangTracking(), throwsAssertionError); + } else { + // iOS/macOS should throw FFI exceptions in tests + final matcher = _nativeUnavailableMatcher( + mockPlatform, + includeLookupSymbol: true, + includeFailedToLoadClassException: true, + ); + expect(() => sut.resumeAppHangTracking(), matcher); + } - verify(channel.invokeMethod('resumeAppHangTracking')); + verifyZeroInteractions(channel); }); test('nativeCrash', () async { - when(channel.invokeMethod('nativeCrash')) - .thenAnswer((_) => Future.value()); + final matcher = _nativeUnavailableMatcher( + mockPlatform, + includeLookupSymbol: true, + includeFailedToLoadClassException: true, + ); - await sut.nativeCrash(); + expect(() => sut.nativeCrash(), matcher); - verify(channel.invokeMethod('nativeCrash')); + verifyZeroInteractions(channel); }); test('setReplayConfig', () async { From 1ffba6f68752011e6db340110119fbbe6d58d701 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 14 Oct 2025 12:51:56 +0200 Subject: [PATCH 47/61] Update --- .../Sources/sentry_flutter/SentryFlutterPlugin.swift | 3 +++ 1 file changed, 3 insertions(+) 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 6629269211..a3babc3c43 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -535,6 +535,7 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { if let data = try? JSONSerialization.data(withJSONObject: item, options: []) { return data as NSData } + print("Failed to load native app start as bytes") return nil #else return nil @@ -570,6 +571,7 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { if let data = try? JSONSerialization.data(withJSONObject: serializedImages, options: []) { return data as NSData } + print("Failed to load debug images as bytes") return nil } @@ -655,6 +657,7 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { if let data = try? JSONSerialization.data(withJSONObject: infos, options: []) { return data as NSData } + print("Failed to load contexts as bytes") return nil } } From fce96c708a6c5c14770596941286c8b777c11c25 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Oct 2025 14:31:08 +0200 Subject: [PATCH 48/61] Update --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fefedbdabe..751275ef2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # Changelog -## 9.7.0 +## Unreleased + +### Enhancements +- Move app hang and crash apis to use FFI/JNI ([#3289](https://github.com/getsentry/sentry-dart/pull/3289/)) + +## 9.7.0 ### Features From 958fa0d163a4a5c70e7d5c557e1d0de8546f416b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Oct 2025 15:06:56 +0200 Subject: [PATCH 49/61] Update --- .../src/main/kotlin/io/sentry/flutter/SentryFlutter.kt | 4 +++- .../main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | 6 ++---- .../src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt | 2 +- .../Sources/sentry_flutter/SentryFlutterPlugin.swift | 1 - 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt index e0e35b9ccc..19eec63247 100644 --- a/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt +++ b/packages/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt @@ -21,6 +21,8 @@ class SentryFlutter { internal const val NATIVE_SDK = "sentry.native.android.flutter" } + var autoPerformanceTracingEnabled = false + fun updateOptions( options: SentryAndroidOptions, data: Map, @@ -105,7 +107,7 @@ class SentryFlutter { data.getIfNotNull("enableAutoPerformanceTracing") { enableAutoPerformanceTracing -> if (enableAutoPerformanceTracing) { - SentryFlutterPlugin.autoPerformanceTracingEnabled = true + autoPerformanceTracingEnabled = true } } 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 b4c4d71fd0..309581c984 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 @@ -41,7 +41,6 @@ class SentryFlutterPlugin : ActivityAware { private lateinit var channel: MethodChannel private lateinit var context: Context - private lateinit var sentryFlutter: SentryFlutter override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { pluginRegistrationTime = System.currentTimeMillis() @@ -282,8 +281,7 @@ class SentryFlutterPlugin : private var pluginRegistrationTime: Long? = null - var autoPerformanceTracingEnabled: Boolean = false - internal set + private lateinit var sentryFlutter: SentryFlutter private const val NATIVE_CRASH_WAIT_TIME = 500L @@ -319,7 +317,7 @@ class SentryFlutterPlugin : @Suppress("unused", "ReturnCount") // Used by native/jni bindings @JvmStatic fun fetchNativeAppStartAsBytes(): ByteArray? { - if (!autoPerformanceTracingEnabled) { + if (!sentryFlutter.autoPerformanceTracingEnabled) { return null } diff --git a/packages/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt b/packages/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt index 74c9214a6c..be7eacb873 100644 --- a/packages/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt +++ b/packages/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt @@ -62,7 +62,7 @@ class SentryFlutterTest { ) assertEquals("sentry.native.android.flutter", fixture.options.nativeSdkName) - assertEquals(true, SentryFlutterPlugin.autoPerformanceTracingEnabled) + assertEquals(true, sut.autoPerformanceTracingEnabled) assertEquals(9006, fixture.options.connectionTimeoutMillis) assertEquals(9007, fixture.options.readTimeoutMillis) 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 a3babc3c43..54fa3ccd26 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -456,7 +456,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { // https://github.com/flutter/engine/blob/main/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm#L150 @objc public class func getDisplayRefreshRate() -> NSNumber? { let displayLink = CADisplayLink(target: self, selector: #selector(onDisplayLinkStatic(_:))) - displayLink.add(to: .main, forMode: .common) displayLink.isPaused = true let preferredFPS = displayLink.preferredFramesPerSecond From 84542aba130d90c21125de719968cceae1ac653e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Oct 2025 16:01:55 +0200 Subject: [PATCH 50/61] Update --- .../sentry_flutter/SentryFlutterPlugin.swift | 24 ++++++++++++------- .../src/native/cocoa/sentry_native_cocoa.dart | 12 ++++++---- .../src/native/java/sentry_native_java.dart | 1 + 3 files changed, 24 insertions(+), 13 deletions(-) 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 54fa3ccd26..7d275bcca8 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -531,11 +531,13 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { "nativeSpanTimes": nativeSpanTimes ] - if let data = try? JSONSerialization.data(withJSONObject: item, options: []) { + do { + let data = try JSONSerialization.data(withJSONObject: item, options: []) return data as NSData + } catch { + print("Failed to load native app start as bytes: \(error)") + return nil } - print("Failed to load native app start as bytes") - return nil #else return nil #endif @@ -567,11 +569,13 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { } let serializedImages = debugImages.map { $0.serialize() } - if let data = try? JSONSerialization.data(withJSONObject: serializedImages, options: []) { + do { + let data = try JSONSerialization.data(withJSONObject: serializedImages, options: []) return data as NSData + } catch { + print("Failed to load debug images as bytes: \(error)") + return nil } - print("Failed to load debug images as bytes") - return nil } // swiftlint:disable:next cyclomatic_complexity @@ -653,11 +657,13 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { "sdk_name": "cocoapods:sentry-cocoa"] } - if let data = try? JSONSerialization.data(withJSONObject: infos, options: []) { + do { + let data = try JSONSerialization.data(withJSONObject: infos, options: []) return data as NSData + } catch { + print("Failed to load contexts as bytes: \(error)") + return nil } - print("Failed to load contexts as bytes") - return nil } } // swiftlint:enable type_body_length 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 0a8dc2ee4a..7d2926c393 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:meta/meta.dart'; import 'package:objective_c/objective_c.dart'; @@ -118,7 +119,12 @@ class SentryNativeCocoa extends SentryNativeChannel { cocoa.SentryFlutterPlugin.loadContextsAsBytes(); if (contextsUtf8JsonBytes == null) return null; - final contexts = decodeUtf8JsonMap(contextsUtf8JsonBytes.toList()); + // Use Flutter's compute to decode the UTF-8 JSON off the main isolate. + final contexts = await compute, Map?>( + // top-level or static function required by compute; use decode helper wrapper + _decodeUtf8JsonMapEntryPoint, + contextsUtf8JsonBytes.toList(), + ); return contexts; } catch (exception, stackTrace) { options.log(SentryLevel.error, 'FFI: Failed to load contexts', @@ -160,9 +166,7 @@ class SentryNativeCocoa extends SentryNativeChannel { 'displayRefreshRate', () { final refreshRate = cocoa.SentryFlutterPlugin.getDisplayRefreshRate(); - if (refreshRate == null) return null; - - return refreshRate.intValue; + return refreshRate?.intValue; }, ); 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 b79d8b9db4..4679f2af43 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:jni/jni.dart'; import 'package:meta/meta.dart'; From e744a5ec9580e4501d26edc9c29615c993a9a3ff Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Oct 2025 16:02:07 +0200 Subject: [PATCH 51/61] Update --- .../flutter/lib/src/native/cocoa/sentry_native_cocoa.dart | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 7d2926c393..3abd2cb579 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -119,12 +119,7 @@ class SentryNativeCocoa extends SentryNativeChannel { cocoa.SentryFlutterPlugin.loadContextsAsBytes(); if (contextsUtf8JsonBytes == null) return null; - // Use Flutter's compute to decode the UTF-8 JSON off the main isolate. - final contexts = await compute, Map?>( - // top-level or static function required by compute; use decode helper wrapper - _decodeUtf8JsonMapEntryPoint, - contextsUtf8JsonBytes.toList(), - ); + final contexts = decodeUtf8JsonMap(contextsUtf8JsonBytes.toList()); return contexts; } catch (exception, stackTrace) { options.log(SentryLevel.error, 'FFI: Failed to load contexts', From 99126287aa83c5062f4217ac96bb51203734a8aa Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Oct 2025 16:05:49 +0200 Subject: [PATCH 52/61] Update --- .../io/sentry/flutter/SentryFlutterPlugin.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 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 309581c984..967f110f87 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 @@ -413,8 +413,13 @@ 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 @@ -436,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?.serialize() = this?.map { it.serialize() } From dee9256d76aa72443233f85c1d0e6e9d352121c7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Oct 2025 16:13:48 +0200 Subject: [PATCH 53/61] Update --- .../src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | 4 ++-- .../flutter/lib/src/native/cocoa/sentry_native_cocoa.dart | 2 -- packages/flutter/lib/src/native/java/sentry_native_java.dart | 2 -- 3 files changed, 2 insertions(+), 6 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 967f110f87..614d5739ff 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 @@ -314,7 +314,7 @@ class SentryFlutterPlugin : return refreshRate } - @Suppress("unused", "ReturnCount") // Used by native/jni bindings + @Suppress("unused", "ReturnCount", "TooGenericExceptionCaught") // Used by native/jni bindings @JvmStatic fun fetchNativeAppStartAsBytes(): ByteArray? { if (!sentryFlutter.autoPerformanceTracingEnabled) { @@ -398,7 +398,7 @@ class SentryFlutterPlugin : @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 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 3abd2cb579..dfda2f4853 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/foundation.dart'; -import 'package:meta/meta.dart'; import 'package:objective_c/objective_c.dart'; import '../../../sentry_flutter.dart'; 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 4679f2af43..2362b5e13c 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -1,9 +1,7 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:jni/jni.dart'; -import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; import '../../replay/scheduled_recorder_config.dart'; From ab2f4e9cffa81c7d39eab9f807d008d30a4e6055 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Oct 2025 16:15:31 +0200 Subject: [PATCH 54/61] Update --- .../src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 614d5739ff..da755ecd10 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 @@ -422,7 +422,7 @@ class SentryFlutterPlugin : } } - @Suppress("unused") // Used by native/jni bindings + @Suppress("unused", "TooGenericExceptionCaught") // Used by native/jni bindings @JvmStatic fun loadDebugImagesAsBytes(addresses: Set): ByteArray? { val options = ScopesAdapter.getInstance().options as SentryAndroidOptions From ed51094f9f274aa70356aaab0f329aa15e21d9b5 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Oct 2025 16:24:25 +0200 Subject: [PATCH 55/61] Update --- packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart | 3 ++- packages/flutter/lib/src/native/java/sentry_native_java.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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 dfda2f4853..2683ef14f0 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -1,5 +1,6 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; +import 'dart:typed_data'; +import 'package:meta/meta.dart'; import 'package:objective_c/objective_c.dart'; import '../../../sentry_flutter.dart'; 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 2362b5e13c..a71233f89a 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:jni/jni.dart'; +import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; import '../../replay/scheduled_recorder_config.dart'; From 0d378b325e31668faddf99a6676aa9a310d2b4dd Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 16 Oct 2025 16:24:43 +0200 Subject: [PATCH 56/61] Update --- packages/flutter/lib/src/native/java/sentry_native_java.dart | 1 + 1 file changed, 1 insertion(+) 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 a71233f89a..b79d8b9db4 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:jni/jni.dart'; import 'package:meta/meta.dart'; From aacf1ffcaa7923962c66159a0f59e8ceedf31cb2 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 20 Oct 2025 09:30:03 +0200 Subject: [PATCH 57/61] Update --- .../io/sentry/flutter/SentryFlutterPlugin.kt | 2 +- .../flutter/lib/src/native/java/binding.dart | 27 +++++++++---------- .../src/native/java/sentry_native_java.dart | 2 +- 3 files changed, 14 insertions(+), 17 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 c16aa31892..b3a3eb128c 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 @@ -290,7 +290,7 @@ class SentryFlutterPlugin : @Suppress("unused") // Used by native/jni bindings @JvmStatic - fun nativeCrash() { + fun crash() { val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException") val mainThread = Looper.getMainLooper().thread mainThread.uncaughtExceptionHandler?.uncaughtException(mainThread, exception) diff --git a/packages/flutter/lib/src/native/java/binding.dart b/packages/flutter/lib/src/native/java/binding.dart index f9730b691a..9b59a003b3 100644 --- a/packages/flutter/lib/src/native/java/binding.dart +++ b/packages/flutter/lib/src/native/java/binding.dart @@ -1305,12 +1305,12 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject { .object(const $ReplayIntegration$NullableType()); } - static final _id_nativeCrash = _class.instanceMethodId( - r'nativeCrash', + static final _id_crash = _class.instanceMethodId( + r'crash', r'()V', ); - static final _nativeCrash = jni$_.ProtectedJniExtensions.lookup< + static final _crash = jni$_.ProtectedJniExtensions.lookup< jni$_.NativeFunction< jni$_.JThrowablePtr Function( jni$_.Pointer, @@ -1322,10 +1322,9 @@ class SentryFlutterPlugin$Companion extends jni$_.JObject { jni$_.JMethodIDPtr, )>(); - /// from: `public final void nativeCrash()` - void nativeCrash() { - _nativeCrash(reference.pointer, _id_nativeCrash as jni$_.JMethodIDPtr) - .check(); + /// from: `public final void crash()` + void crash() { + _crash(reference.pointer, _id_crash as jni$_.JMethodIDPtr).check(); } static final _id_getDisplayRefreshRate = _class.instanceMethodId( @@ -1839,12 +1838,12 @@ class SentryFlutterPlugin extends jni$_.JObject { .object(const $ReplayIntegration$NullableType()); } - static final _id_nativeCrash = _class.staticMethodId( - r'nativeCrash', + static final _id_crash = _class.staticMethodId( + r'crash', r'()V', ); - static final _nativeCrash = jni$_.ProtectedJniExtensions.lookup< + static final _crash = jni$_.ProtectedJniExtensions.lookup< jni$_.NativeFunction< jni$_.JThrowablePtr Function( jni$_.Pointer, @@ -1856,11 +1855,9 @@ class SentryFlutterPlugin extends jni$_.JObject { jni$_.JMethodIDPtr, )>(); - /// from: `static public final void nativeCrash()` - static void nativeCrash() { - _nativeCrash( - _class.reference.pointer, _id_nativeCrash as jni$_.JMethodIDPtr) - .check(); + /// from: `static public final void crash()` + static void crash() { + _crash(_class.reference.pointer, _id_crash as jni$_.JMethodIDPtr).check(); } static final _id_getDisplayRefreshRate = _class.staticMethodId( 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 817d51c00d..bbf2e2a265 100644 --- a/packages/flutter/lib/src/native/java/sentry_native_java.dart +++ b/packages/flutter/lib/src/native/java/sentry_native_java.dart @@ -201,7 +201,7 @@ class SentryNativeJava extends SentryNativeChannel { @override void nativeCrash() { - native.SentryFlutterPlugin.Companion.nativeCrash(); + native.SentryFlutterPlugin.Companion.crash(); } @override From 714279ea20b70a2cd8ac1f365c999bfd4946b81f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 20 Oct 2025 10:07:20 +0200 Subject: [PATCH 58/61] Update --- packages/flutter/ffi-cocoa.yaml | 8 ++ .../sentry_flutter/SentryFlutterPlugin.swift | 12 --- .../sentry_flutter_objc/SentryFlutterPlugin.h | 3 - .../flutter/lib/src/native/cocoa/binding.dart | 81 +++++++++++++------ .../src/native/cocoa/sentry_native_cocoa.dart | 20 ++--- 5 files changed, 70 insertions(+), 54 deletions(-) diff --git a/packages/flutter/ffi-cocoa.yaml b/packages/flutter/ffi-cocoa.yaml index 72cf5a88e0..bc1a60cdcf 100644 --- a/packages/flutter/ffi-cocoa.yaml +++ b/packages/flutter/ffi-cocoa.yaml @@ -19,8 +19,16 @@ objc-interfaces: - PrivateSentrySDKOnly - SentryId - SentryFlutterPlugin + - SentrySDK module: 'SentryId': 'Sentry' + 'SentrySDK': 'Sentry' + member-filter: + SentrySDK: + include: + - 'crash' + - 'pauseAppHangTracking' + - 'resumeAppHangTracking' preamble: | // ignore_for_file: type=lint, unused_element 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 8e2e644e69..ca10f8140a 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -520,18 +520,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { #endif } - @objc public class func nativeCrash() { - SentrySDK.crash() - } - - @objc public class func pauseAppHangTracking() { - SentrySDK.pauseAppHangTracking() - } - - @objc public class func resumeAppHangTracking() { - SentrySDK.resumeAppHangTracking() - } - @objc(loadDebugImagesAsBytes:) public class func loadDebugImagesAsBytes(instructionAddresses: Set) -> NSData? { var debugImages: [DebugMeta] = [] 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 61af58310d..6f2e25eb54 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,8 +8,5 @@ + (nullable NSData *)fetchNativeAppStartAsBytes; + (nullable NSData *)loadContextsAsBytes; + (nullable NSData *)loadDebugImagesAsBytes:(NSSet *)instructionAddresses; -+ (void)nativeCrash; -+ (void)pauseAppHangTracking; -+ (void)resumeAppHangTracking; @end #endif diff --git a/packages/flutter/lib/src/native/cocoa/binding.dart b/packages/flutter/lib/src/native/cocoa/binding.dart index 73d693b44d..d39050185c 100644 --- a/packages/flutter/lib/src/native/cocoa/binding.dart +++ b/packages/flutter/lib/src/native/cocoa/binding.dart @@ -1120,15 +1120,8 @@ class SentryId$1 extends objc.NSObject { factory SentryId$1() => new$(); } -late final _class_SentryFlutterPlugin = objc.getClass("SentryFlutterPlugin"); -late final _sel_getDisplayRefreshRate = - objc.registerName("getDisplayRefreshRate"); -late final _sel_fetchNativeAppStartAsBytes = - objc.registerName("fetchNativeAppStartAsBytes"); -late final _sel_loadContextsAsBytes = objc.registerName("loadContextsAsBytes"); -late final _sel_loadDebugImagesAsBytes_ = - objc.registerName("loadDebugImagesAsBytes:"); -late final _sel_nativeCrash = objc.registerName("nativeCrash"); +late final _class_SentrySDK = objc.getClass("Sentry.SentrySDK"); +late final _sel_crash = objc.registerName("crash"); final _objc_msgSend_1pl9qdv = objc.msgSendPointer .cast< ffi.NativeFunction< @@ -1142,6 +1135,59 @@ late final _sel_pauseAppHangTracking = late final _sel_resumeAppHangTracking = objc.registerName("resumeAppHangTracking"); +/// The main entry point for the Sentry SDK. +/// We recommend using start(configureOptions:) to initialize Sentry. +class SentrySDK extends objc.NSObject { + SentrySDK._(ffi.Pointer pointer, + {bool retain = false, bool release = false}) + : super.castFromPointer(pointer, retain: retain, release: release); + + /// Constructs a [SentrySDK] that points to the same underlying object as [other]. + SentrySDK.castFrom(objc.ObjCObjectBase other) + : this._(other.ref.pointer, retain: true, release: true); + + /// Constructs a [SentrySDK] that wraps the given raw object pointer. + SentrySDK.castFromPointer(ffi.Pointer other, + {bool retain = false, bool release = false}) + : this._(other, retain: retain, release: release); + + /// Returns whether [obj] is an instance of [SentrySDK]. + static bool isInstance(objc.ObjCObjectBase obj) { + return _objc_msgSend_19nvye5( + obj.ref.pointer, _sel_isKindOfClass_, _class_SentrySDK); + } + + /// This forces a crash, useful to test the SentryCrash integration. + /// note: + /// The SDK can’t report a crash when a debugger is attached. Your application needs to run + /// without a debugger attached to capture the crash and send it to Sentry the next time you launch + /// your application. + static void crash() { + _objc_msgSend_1pl9qdv(_class_SentrySDK, _sel_crash); + } + + /// Pauses sending detected app hangs to Sentry. + /// This method doesn’t close the detection of app hangs. Instead, the app hang detection + /// will ignore detected app hangs until you call resumeAppHangTracking. + static void pauseAppHangTracking() { + _objc_msgSend_1pl9qdv(_class_SentrySDK, _sel_pauseAppHangTracking); + } + + /// Resumes sending detected app hangs to Sentry. + static void resumeAppHangTracking() { + _objc_msgSend_1pl9qdv(_class_SentrySDK, _sel_resumeAppHangTracking); + } +} + +late final _class_SentryFlutterPlugin = objc.getClass("SentryFlutterPlugin"); +late final _sel_getDisplayRefreshRate = + objc.registerName("getDisplayRefreshRate"); +late final _sel_fetchNativeAppStartAsBytes = + objc.registerName("fetchNativeAppStartAsBytes"); +late final _sel_loadContextsAsBytes = objc.registerName("loadContextsAsBytes"); +late final _sel_loadDebugImagesAsBytes_ = + objc.registerName("loadDebugImagesAsBytes:"); + /// SentryFlutterPlugin class SentryFlutterPlugin extends objc.NSObject { SentryFlutterPlugin._(ffi.Pointer pointer, @@ -1199,23 +1245,6 @@ class SentryFlutterPlugin extends objc.NSObject { : objc.NSData.castFromPointer(_ret, retain: true, release: true); } - /// nativeCrash - static void nativeCrash() { - _objc_msgSend_1pl9qdv(_class_SentryFlutterPlugin, _sel_nativeCrash); - } - - /// pauseAppHangTracking - static void pauseAppHangTracking() { - _objc_msgSend_1pl9qdv( - _class_SentryFlutterPlugin, _sel_pauseAppHangTracking); - } - - /// resumeAppHangTracking - static void resumeAppHangTracking() { - _objc_msgSend_1pl9qdv( - _class_SentryFlutterPlugin, _sel_resumeAppHangTracking); - } - /// 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 3901503d4a..a156be1e33 100644 --- a/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/packages/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -178,21 +178,15 @@ class SentryNativeCocoa extends SentryNativeChannel { ); @override - void nativeCrash() { - cocoa.SentryFlutterPlugin.nativeCrash(); - } + void nativeCrash() => cocoa.SentrySDK.crash(); @override - void pauseAppHangTracking() { - tryCatchSync('pauseAppHangTracking', () { - cocoa.SentryFlutterPlugin.pauseAppHangTracking(); - }); - } + void pauseAppHangTracking() => tryCatchSync('pauseAppHangTracking', () { + cocoa.SentrySDK.pauseAppHangTracking(); + }); @override - void resumeAppHangTracking() { - tryCatchSync('resumeAppHangTracking', () { - cocoa.SentryFlutterPlugin.resumeAppHangTracking(); - }); - } + void resumeAppHangTracking() => tryCatchSync('resumeAppHangTracking', () { + cocoa.SentrySDK.resumeAppHangTracking(); + }); } From dc580bb6931712b49fa39f55d148dbbe19daf1c2 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 23 Oct 2025 12:16:07 +0200 Subject: [PATCH 59/61] Update --- .../io/sentry/flutter/SentryFlutterPlugin.kt | 106 ------------------ 1 file changed, 106 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 c03faab11a..bd0843006c 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 @@ -288,112 +288,6 @@ class SentryFlutterPlugin : @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( - "pluginRegistrationTime" to pluginRegistrationTime, - "appStartTime" to appStartTimeMillis, - "isColdStart" to isColdStart, - ) - - val androidNativeSpans = mutableMapOf() - - 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, - ) { - if (span.startTimestamp == null) return - - span.description?.let { description -> - map[description] = - mapOf( - "startTimestampMsSinceEpoch" to span.startTimestampMs, - "stopTimestampMsSinceEpoch" to span.projectedStopTimestampMs, - ) - } - } - @JvmStatic fun crash() { val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException") From 3b4c2d38e7f01ecbf6ba769bc64a1e1e916fb25f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 23 Oct 2025 12:18:36 +0200 Subject: [PATCH 60/61] Update --- .../sentry_flutter/SentryFlutterPlugin.swift | 98 ------------------- 1 file changed, 98 deletions(-) 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 f38550a9ae..ca10f8140a 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -520,104 +520,6 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { #endif } - // MARK: - Objective-C interoperability - // - // Group of methods exposed to the Objective-C runtime via `@objc`. - // - // Purpose: Called from the Flutter plugin's native bridge (FFI) - bindings are created from SentryFlutterPlugin.h - - #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 - @objc public class func getDisplayRefreshRate() -> NSNumber? { - let displayLink = CADisplayLink(target: self, selector: #selector(onDisplayLinkStatic(_:))) - displayLink.isPaused = true - - let preferredFPS = displayLink.preferredFramesPerSecond - displayLink.invalidate() - - if preferredFPS != 0 { - return NSNumber(value: preferredFPS) - } - - if #available(iOS 13.0, *) { - guard let windowScene = UIApplication.shared.windows.first?.windowScene else { - return nil - } - return NSNumber(value: windowScene.screen.maximumFramesPerSecond) - } else { - return NSNumber(value: UIScreen.main.maximumFramesPerSecond) - } - } - - @objc private class func onDisplayLinkStatic(_ displayLink: CADisplayLink) { - // No-op - } - #elseif os(macOS) - @objc public class func getDisplayRefreshRate() -> NSNumber? { - return nil - } - #endif - - @objc public class func fetchNativeAppStartAsBytes() -> NSData? { - #if os(iOS) || os(tvOS) - guard let appStartMeasurement = PrivateSentrySDKOnly.appStartMeasurement else { - return nil - } - - var nativeSpanTimes: [String: Any] = [:] - - let appStartTimeMs = appStartMeasurement.appStartTimestamp.timeIntervalSince1970.toMilliseconds() - let runtimeInitTimeMs = appStartMeasurement.runtimeInitTimestamp.timeIntervalSince1970.toMilliseconds() - let moduleInitializationTimeMs = - appStartMeasurement.moduleInitializationTimestamp.timeIntervalSince1970.toMilliseconds() - let sdkStartTimeMs = appStartMeasurement.sdkStartTimestamp.timeIntervalSince1970.toMilliseconds() - - if !appStartMeasurement.isPreWarmed { - let preRuntimeInitDescription = "Pre Runtime Init" - let preRuntimeInitSpan: [String: Any] = [ - "startTimestampMsSinceEpoch": NSNumber(value: appStartTimeMs), - "stopTimestampMsSinceEpoch": NSNumber(value: runtimeInitTimeMs) - ] - nativeSpanTimes[preRuntimeInitDescription] = preRuntimeInitSpan - - let moduleInitializationDescription = "Runtime init to Pre Main initializers" - let moduleInitializationSpan: [String: Any] = [ - "startTimestampMsSinceEpoch": NSNumber(value: runtimeInitTimeMs), - "stopTimestampMsSinceEpoch": NSNumber(value: moduleInitializationTimeMs) - ] - nativeSpanTimes[moduleInitializationDescription] = moduleInitializationSpan - } - - let uiKitInitDescription = "UIKit init" - let uiKitInitSpan: [String: Any] = [ - "startTimestampMsSinceEpoch": NSNumber(value: moduleInitializationTimeMs), - "stopTimestampMsSinceEpoch": NSNumber(value: sdkStartTimeMs) - ] - nativeSpanTimes[uiKitInitDescription] = uiKitInitSpan - - let appStartTime = appStartMeasurement.appStartTimestamp.timeIntervalSince1970 * 1000 - let isColdStart = appStartMeasurement.type == .cold - - let item: [String: Any] = [ - "pluginRegistrationTime": pluginRegistrationTime, - "appStartTime": appStartTime, - "isColdStart": isColdStart, - "nativeSpanTimes": nativeSpanTimes - ] - - do { - let data = try JSONSerialization.data(withJSONObject: item, options: []) - return data as NSData - } catch { - print("Failed to load native app start as bytes: \(error)") - return nil - } - #else - return nil - #endif - } - @objc(loadDebugImagesAsBytes:) public class func loadDebugImagesAsBytes(instructionAddresses: Set) -> NSData? { var debugImages: [DebugMeta] = [] From e82e153721b9209e55a27f7f9aa1485314a23d22 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 23 Oct 2025 12:19:54 +0200 Subject: [PATCH 61/61] Update --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 270f0f05c4..ed0208bf9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,10 @@ ### Enhancements -- Move app hang and crash apis to use FFI/JNI ([#3289](https://github.com/getsentry/sentry-dart/pull/3289/)) -- Refactor `AndroidReplayRecorder` to use the new worker isolate api [#3296](https://github.com/getsentry/sentry-dart/pull/3296/) -- Refactor fetching app start and display refresh rate to use FFI and JNI [#3288](https://github.com/getsentry/sentry-dart/pull/3288/) -- Offload `captureEnvelope` to background isolate for Cocoa and Android [#3232](https://github.com/getsentry/sentry-dart/pull/3232) +- Refactor app hang and crash apis to use FFI/JNI ([#3289](https://github.com/getsentry/sentry-dart/pull/3289/)) +- Refactor `AndroidReplayRecorder` to use the new worker isolate api ([#3296](https://github.com/getsentry/sentry-dart/pull/3296/)) +- Refactor fetching app start and display refresh rate to use FFI and JNI ([#3288](https://github.com/getsentry/sentry-dart/pull/3288/)) +- Offload `captureEnvelope` to background isolate for Cocoa and Android ([#3232](https://github.com/getsentry/sentry-dart/pull/3232)) - Add `sentry.replay_id` to flutter logs ([#3257](https://github.com/getsentry/sentry-dart/pull/3257)) ## 9.7.0