Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 9.7.0


### Features

- Add W3C `traceparent` header support ([#3246](https://github.com/getsentry/sentry-dart/pull/3246))
Expand Down
1 change: 0 additions & 1 deletion packages/flutter/example/pubspec_overrides.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,3 @@ dependency_overrides:
isar_flutter_libs:
git:
url: https://github.com/MrLittleWhite/isar_flutter_libs.git

Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
81 changes: 81 additions & 0 deletions packages/flutter/lib/src/isolate/isolate_logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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`.
///
/// 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 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.
/// - [loggerName]: logger name for the call sites
static void configure(
{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.');
}
Comment on lines +7 to +35
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We gotta use a static logger since we cannot pass SentryFlutterOptions to another isolate as it's not serializable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the alternative would be not to log in isolates at all

_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
/// 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;
}
}
177 changes: 177 additions & 0 deletions packages/flutter/lib/src/isolate/isolate_worker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import 'dart:async';
import 'dart:isolate';

import '../../sentry_flutter.dart';
import 'isolate_logger.dart';

const _shutdownCommand = '_shutdown_';

// -------------------------------------------
// HOST-SIDE API (runs on the main isolate)
// -------------------------------------------

/// Minimal config passed to isolates - extend as needed.
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,
});
}

/// 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, this._responses) {
_responses.listen(_handleResponse);
}

final SendPort _workerPort;
SendPort get port => _workerPort;
final ReceivePort _responses;
final Map<int, Completer<Object?>> _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<Object?> request(Object? payload) async {
if (_closed) throw StateError('Worker is closed');
final id = _idCounter++;
final completer = Completer<Object?>.sync();
_pending[id] = completer;
_workerPort.send((id, payload));
return await completer.future;
}

void close() {
if (_closed) return;
_closed = true;
_workerPort.send(_shutdownCommand);
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();
}
}
}

/// Worker (isolate) entry-point signature.
typedef WorkerEntry = void Function((SendPort, WorkerConfig));

/// Spawn a worker isolate and handshake to obtain its SendPort.
Future<Worker> spawnWorker(
WorkerConfig config,
WorkerEntry entry,
) async {
final initPort = RawReceivePort();
final connection = Completer<(ReceivePort, SendPort)>.sync();
initPort.handler = (SendPort commandPort) {
connection.complete((
ReceivePort.fromRawReceivePort(initPort),
commandPort,
));
};

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);
}

// -------------------------------------------
// ISOLATE-SIDE API (runs inside the worker isolate)
// -------------------------------------------

/// 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<void> onMessage(Object? message);

/// Handle request/response payloads sent from the host.
/// Return value is sent back to the host. Default: no-op.
FutureOr<Object?> onRequest(Object? payload) => {};
}

/// 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,
WorkerHandler handler,
) {
IsolateLogger.configure(
debug: config.debug,
level: config.diagnosticLevel,
loggerName: config.debugName,
);

final inbox = ReceivePort();
host.send(inbox.sendPort);

inbox.listen((msg) async {
if (msg == _shutdownCommand) {
IsolateLogger.log(SentryLevel.debug, 'Isolate received shutdown');
inbox.close();
IsolateLogger.log(SentryLevel.debug, 'Isolate closed');
return;
}

if (msg is (int, Object?)) {
final (id, payload) = msg;
try {
final result = await handler.onRequest(payload);
host.send((id, result));
} catch (e, st) {
host.send((id, RemoteError(e.toString(), st.toString())));
}
return;
}

try {
await handler.onMessage(msg);
} catch (exception, stackTrace) {
IsolateLogger.log(SentryLevel.error, 'Isolate failed to handle message',
exception: exception, stackTrace: stackTrace);
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import 'dart:async';
import 'dart:isolate';
import 'dart:typed_data';

import 'package:meta/meta.dart';
import 'package:objective_c/objective_c.dart';

import '../../../sentry_flutter.dart';
import '../../isolate/isolate_worker.dart';
import '../../isolate/isolate_logger.dart';
import 'binding.dart' as cocoa;

typedef SpawnWorkerFn = Future<Worker> Function(WorkerConfig, WorkerEntry);

class CocoaEnvelopeSender {
final SentryFlutterOptions _options;
final WorkerConfig _config;
final SpawnWorkerFn _spawn;
Worker? _worker;

CocoaEnvelopeSender(this._options, {SpawnWorkerFn? spawn})
: _config = WorkerConfig(
debugName: 'SentryCocoaEnvelopeSender',
debug: _options.debug,
diagnosticLevel: _options.diagnosticLevel,
automatedTestMode: _options.automatedTestMode,
),
_spawn = spawn ?? spawnWorker;

@internal
static CocoaEnvelopeSender Function(SentryFlutterOptions) factory =
CocoaEnvelopeSender.new;

FutureOr<void> start() async {
if (_worker != null) return;
_worker = await _spawn(_config, _entryPoint);
}

FutureOr<void> close() {
_worker?.close();
_worker = null;
}

/// Fire-and-forget send of envelope bytes to the worker.
void captureEnvelope(Uint8List envelopeData) {
final client = _worker;
if (client == null) {
_options.log(
SentryLevel.warning,
'captureEnvelope called before start; dropping',
);
return;
}
client.send(TransferableTypedData.fromList([envelopeData]));
}

static void _entryPoint((SendPort, WorkerConfig) init) {
final (host, config) = init;
runWorker(config, host, _CocoaEnvelopeHandler(config));
}
}

class _CocoaEnvelopeHandler extends WorkerHandler {
final WorkerConfig _config;

_CocoaEnvelopeHandler(this._config);

@override
FutureOr<void> onMessage(Object? msg) {
if (msg is TransferableTypedData) {
final data = msg.materialize().asUint8List();
_captureEnvelope(data);
} else {
IsolateLogger.log(SentryLevel.warning, 'Unexpected message type: $msg');
}
}

void _captureEnvelope(Uint8List envelopeData) {
try {
final nsData = envelopeData.toNSData();
final envelope = cocoa.PrivateSentrySDKOnly.envelopeWithData(nsData);
if (envelope != null) {
cocoa.PrivateSentrySDKOnly.captureEnvelope(envelope);
} else {
IsolateLogger.log(SentryLevel.error,
'Native Cocoa SDK returned null when capturing envelope');
}
} catch (exception, stackTrace) {
IsolateLogger.log(SentryLevel.error, 'Failed to capture envelope',
exception: exception, stackTrace: stackTrace);
if (_config.automatedTestMode) {
rethrow;
}
}
}
}
Loading
Loading