Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixes

- Android not sending events when `autoInitializedNativeSdk` is disabled ([#3420](https://github.com/getsentry/sentry-dart/pull/3420))

## 9.9.1

### Fixes
Expand Down
90 changes: 52 additions & 38 deletions packages/flutter/lib/src/native/java/android_envelope_sender.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ import '../../utils/internal_logger.dart';
import 'binding.dart' as native;

class AndroidEnvelopeSender {
final SentryFlutterOptions _options;
final WorkerConfig _config;
final SpawnWorkerFn _spawn;

bool _isClosed = false;
Worker? _worker;

AndroidEnvelopeSender(this._options, {SpawnWorkerFn? spawn})
AndroidEnvelopeSender(SentryOptions options, {SpawnWorkerFn? spawn})
: _config = WorkerConfig(
debugName: 'SentryAndroidEnvelopeSender',
debug: _options.debug,
diagnosticLevel: _options.diagnosticLevel,
automatedTestMode: _options.automatedTestMode,
debug: options.debug,
diagnosticLevel: options.diagnosticLevel,
// ignore: invalid_use_of_internal_member
automatedTestMode: options.automatedTestMode,
),
_spawn = spawn ?? spawnWorker;

Expand All @@ -30,30 +32,41 @@ class AndroidEnvelopeSender {
AndroidEnvelopeSender.new;

FutureOr<void> start() async {
if (_isClosed) return;
if (_worker != null) return;
_worker = await _spawn(_config, _entryPoint);
final worker = await _spawn(_config, _entryPoint);
// Guard against close() being called during spawn.
if (_isClosed) {
worker.close();
return;
}
_worker = worker;
}

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

/// Fire-and-forget send of envelope bytes to the worker.
void captureEnvelope(
Uint8List envelopeData, bool containsUnhandledException) {
if (_isClosed) return;

final client = _worker;
if (client == null) {
_options.log(
SentryLevel.warning,
'captureEnvelope called before worker started; dropping',
if (client != null) {
client.send((
TransferableTypedData.fromList([envelopeData]),
containsUnhandledException
));
} else {
internalLogger.info(
'captureEnvelope called before worker started: sending envelope in main isolate instead',
);
return;
_captureEnvelope(envelopeData, containsUnhandledException,
automatedTestMode: _config.automatedTestMode);
}
client.send((
TransferableTypedData.fromList([envelopeData]),
containsUnhandledException
));
}

static void _entryPoint((SendPort, WorkerConfig) init) {
Expand All @@ -72,35 +85,36 @@ class _AndroidEnvelopeHandler extends WorkerHandler {
if (msg is (TransferableTypedData, bool)) {
final (transferable, containsUnhandledException) = msg;
final data = transferable.materialize().asUint8List();
_captureEnvelope(data, containsUnhandledException);
_captureEnvelope(data, containsUnhandledException,
automatedTestMode: _config.automatedTestMode);
} else {
internalLogger
.warning('${_config.debugName}: unexpected message type: $msg');
}
}
}

void _captureEnvelope(
Uint8List envelopeData, bool containsUnhandledException) {
JObject? id;
JByteArray? byteArray;
try {
byteArray = JByteArray.from(envelopeData);
id = native.InternalSentrySdk.captureEnvelope(
byteArray, containsUnhandledException);

if (id == null) {
internalLogger.error(
'${_config.debugName}: native Android SDK returned null when capturing envelope');
}
} catch (exception, stackTrace) {
internalLogger.error('${_config.debugName}: failed to capture envelope',
error: exception, stackTrace: stackTrace);
if (_config.automatedTestMode) {
rethrow;
}
} finally {
byteArray?.release();
id?.release();
void _captureEnvelope(Uint8List envelopeData, bool containsUnhandledException,
{bool automatedTestMode = false}) {
JObject? id;
JByteArray? byteArray;
try {
byteArray = JByteArray.from(envelopeData);
id = native.InternalSentrySdk.captureEnvelope(
byteArray, containsUnhandledException);

if (id == null) {
internalLogger
.error('Native Android SDK returned null when capturing envelope');
}
} catch (exception, stackTrace) {
internalLogger.error('Failed to capture envelope',
error: exception, stackTrace: stackTrace);
if (automatedTestMode) {
rethrow;
}
} finally {
byteArray?.release();
id?.release();
}
}
12 changes: 7 additions & 5 deletions packages/flutter/lib/src/native/java/sentry_native_java.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ class SentryNativeJava extends SentryNativeChannel {
AndroidEnvelopeSender? _envelopeSender;
native.ReplayIntegration? _nativeReplay;

SentryNativeJava(super.options);
SentryNativeJava(super.options) {
// Initialize envelope sender here in the ctor instead of init().
// Ensures it starts when autoInitializeNativeSdk is enabled and disabled.
_envelopeSender = AndroidEnvelopeSender.factory(options);
_envelopeSender?.start();
}

@override
bool get supportsReplay => true;
Expand All @@ -36,11 +41,8 @@ class SentryNativeJava extends SentryNativeChannel {
AndroidReplayRecorder? get testRecorder => _replayRecorder;

@override
Future<void> init(Hub hub) async {
void init(Hub hub) {
initSentryAndroid(hub: hub, options: options, owner: this);

_envelopeSender = AndroidEnvelopeSender.factory(options);
await _envelopeSender?.start();
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,31 @@ import 'package:sentry_flutter/src/isolate/isolate_worker.dart';

void main() {
group('AndroidEnvelopeSender host behavior', () {
test('warns and drops when not started', () {
test('logs when sending envelopes in main isolate', () {
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));
};
SentryInternalLogger.configure(
isEnabled: true,
minLevel: SentryLevel.debug,
logOutput: ({
required String name,
required SentryLevel level,
required String message,
Object? error,
StackTrace? stackTrace,
}) {
logs.add((level, message.toString()));
},
);

final sender = AndroidEnvelopeSender(options);
sender.captureEnvelope(Uint8List.fromList([1, 2, 3]), false);

expect(
logs.any((e) =>
e.$1 == SentryLevel.warning &&
e.$1 == SentryLevel.info &&
e.$2.contains(
'captureEnvelope called before worker started; dropping')),
'captureEnvelope called before worker started: sending envelope in main isolate instead')),
isTrue,
);
});
Expand All @@ -40,30 +48,6 @@ void main() {
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;
Expand All @@ -88,7 +72,7 @@ void main() {
spawnCount = 0;

await sender.start();
expect(spawnCount, 1);
expect(spawnCount, 0);

// Close twice should be safe.
expect(() => sender.close(), returnsNormally);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
// 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.
});
// 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.
}
51 changes: 2 additions & 49 deletions packages/flutter/test/native/sentry_native_java_test.dart
Original file line number Diff line number Diff line change
@@ -1,49 +1,2 @@
@TestOn('vm')
library;

import 'package:flutter_test/flutter_test.dart';
import 'sentry_native_java_web_stub.dart'
if (dart.library.io) 'package:sentry_flutter/src/native/java/sentry_native_java.dart';

void main() {
// the ReplaySizeAdjustment tests assumes a constant video block size of 16
group('ReplaySizeAdjustment', () {
test('rounds down when remainder is less than or equal to half block size',
() {
expect(0.0.adjustReplaySizeToBlockSize(), 0.0);
expect(8.0.adjustReplaySizeToBlockSize(), 0.0);
expect(16.0.adjustReplaySizeToBlockSize(), 16.0);
expect(24.0.adjustReplaySizeToBlockSize(), 16.0);
expect(100.0.adjustReplaySizeToBlockSize(), 96.0);
});

test('rounds up when remainder is greater than half block size', () {
expect(9.0.adjustReplaySizeToBlockSize(), 16.0);
expect(15.0.adjustReplaySizeToBlockSize(), 16.0);
expect(25.0.adjustReplaySizeToBlockSize(), 32.0);
expect(108.0.adjustReplaySizeToBlockSize(), 112.0);
expect(109.0.adjustReplaySizeToBlockSize(), 112.0);
});

test('returns exact value when already multiple of block size', () {
expect(32.0.adjustReplaySizeToBlockSize(), 32.0);
expect(48.0.adjustReplaySizeToBlockSize(), 48.0);
expect(64.0.adjustReplaySizeToBlockSize(), 64.0);
expect(128.0.adjustReplaySizeToBlockSize(), 128.0);
});

test('handles edge cases at half block size boundaries', () {
expect(8.0.adjustReplaySizeToBlockSize(), 0.0);
expect(24.0.adjustReplaySizeToBlockSize(), 16.0);
expect(40.0.adjustReplaySizeToBlockSize(), 32.0);
});

test('handles fractional values', () {
expect(7.5.adjustReplaySizeToBlockSize(), 0.0);
expect(8.5.adjustReplaySizeToBlockSize(), 16.0);
expect(15.5.adjustReplaySizeToBlockSize(), 16.0);
expect(16.5.adjustReplaySizeToBlockSize(), 16.0);
expect(24.5.adjustReplaySizeToBlockSize(), 32.0);
});
});
}
export 'sentry_native_java_test_real.dart'
if (dart.library.js_interop) 'sentry_native_java_test_web.dart';
Loading
Loading