Skip to content

Commit f4d9e97

Browse files
buenaflorcodexcursoragent
committed
fix(flutter): Await Android scope worker sync
Return worker request futures for Android scope updates so awaited scope observer calls complete after native scope synchronization finishes. Co-Authored-By: GPT-5.5 <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 716eada commit f4d9e97

3 files changed

Lines changed: 123 additions & 66 deletions

File tree

packages/flutter/lib/src/native/java/android_core_worker.dart

Lines changed: 74 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -160,67 +160,95 @@ class AndroidCoreWorker {
160160
}
161161
}
162162

163-
void addBreadcrumb(Breadcrumb breadcrumb) {
164-
if (_isClosed) return;
163+
FutureOr<void> addBreadcrumb(Breadcrumb breadcrumb) {
164+
if (_isClosed) return null;
165165

166166
final client = _worker;
167167
if (client == null) {
168168
_addBreadcrumb(breadcrumb.toJson(),
169169
automatedTestMode: _config.automatedTestMode);
170-
return;
170+
return null;
171171
}
172172

173-
_addBreadcrumbFromWorker(client, breadcrumb);
173+
return _addBreadcrumbFromWorker(client, breadcrumb);
174174
}
175175

176-
void _addBreadcrumbFromWorker(
176+
Future<void> _addBreadcrumbFromWorker(
177177
Worker client,
178178
Breadcrumb breadcrumb,
179-
) {
180-
client.send(_AddBreadcrumbRequest(_normalizeJsonMap(breadcrumb.toJson())));
181-
}
179+
) =>
180+
_sendScopeUpdateToWorker(
181+
client,
182+
_AddBreadcrumbRequest(_normalizeJsonMap(breadcrumb.toJson())),
183+
'add breadcrumb',
184+
);
182185

183-
void setUser(SentryUser? user) {
184-
if (_isClosed) return;
186+
FutureOr<void> setUser(SentryUser? user) {
187+
if (_isClosed) return null;
185188

186189
final client = _worker;
187190
if (client == null) {
188191
_setUser(user?.toJson(), automatedTestMode: _config.automatedTestMode);
189-
return;
192+
return null;
190193
}
191194

192-
_setUserFromWorker(client, user);
195+
return _setUserFromWorker(client, user);
193196
}
194197

195-
void _setUserFromWorker(
198+
Future<void> _setUserFromWorker(
196199
Worker client,
197200
SentryUser? user,
198-
) {
199-
client.send(_SetUserRequest(
200-
user == null ? null : _normalizeJsonMap(user.toJson()),
201-
));
202-
}
201+
) =>
202+
_sendScopeUpdateToWorker(
203+
client,
204+
_SetUserRequest(
205+
user == null ? null : _normalizeJsonMap(user.toJson()),
206+
),
207+
'set user',
208+
);
203209

204-
void setContexts(String key, dynamic value) {
205-
if (_isClosed) return;
210+
FutureOr<void> setContexts(String key, dynamic value) {
211+
if (_isClosed) return null;
206212

207213
final normalizedValue = _normalizeJson(value);
208214
final client = _worker;
209215
if (client == null) {
210216
_setContexts(key, normalizedValue,
211217
automatedTestMode: _config.automatedTestMode);
212-
return;
218+
return null;
213219
}
214220

215-
_setContextsFromWorker(client, key, normalizedValue);
221+
return _setContextsFromWorker(client, key, normalizedValue);
216222
}
217223

218-
void _setContextsFromWorker(
224+
Future<void> _setContextsFromWorker(
219225
Worker client,
220226
String key,
221227
Object? value,
222-
) {
223-
client.send(_SetContextsRequest(key, value));
228+
) =>
229+
_sendScopeUpdateToWorker(
230+
client,
231+
_SetContextsRequest(key, value),
232+
'set context',
233+
);
234+
235+
Future<void> _sendScopeUpdateToWorker(
236+
Worker client,
237+
Object request,
238+
String operation,
239+
) async {
240+
try {
241+
await client.request(request);
242+
} catch (exception, stackTrace) {
243+
internalLogger.error(
244+
'Android core worker failed to $operation',
245+
error: exception,
246+
stackTrace: stackTrace,
247+
);
248+
if (_config.automatedTestMode) {
249+
rethrow;
250+
}
251+
}
224252
}
225253

226254
static void _entryPoint((SendPort, WorkerConfig) init) {
@@ -258,15 +286,29 @@ class _AndroidCoreWorkerHandler extends WorkerHandler {
258286

259287
@override
260288
FutureOr<Object?> onRequest(Object? payload) => _enqueue<Object?>(() {
261-
return switch (payload) {
262-
_LoadDebugImagesRequest request => _loadDebugImageMaps(
289+
switch (payload) {
290+
case _LoadDebugImagesRequest request:
291+
return _loadDebugImageMaps(
263292
request.instructionAddresses,
264293
automatedTestMode: _config.automatedTestMode,
265-
),
266-
_LoadContextsRequest _ =>
267-
_loadContexts(automatedTestMode: _config.automatedTestMode),
268-
_ => _unexpectedPayload(payload),
269-
};
294+
);
295+
case _LoadContextsRequest _:
296+
return _loadContexts(automatedTestMode: _config.automatedTestMode);
297+
case _AddBreadcrumbRequest request:
298+
_addBreadcrumb(request.breadcrumb,
299+
automatedTestMode: _config.automatedTestMode);
300+
return null;
301+
case _SetUserRequest request:
302+
_setUser(request.user,
303+
automatedTestMode: _config.automatedTestMode);
304+
return null;
305+
case _SetContextsRequest request:
306+
_setContexts(request.key, request.value,
307+
automatedTestMode: _config.automatedTestMode);
308+
return null;
309+
default:
310+
return _unexpectedPayload(payload);
311+
}
270312
});
271313

272314
/// Serializes JNI work inside the worker isolate.

packages/flutter/lib/src/native/java/sentry_native_java.dart

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,24 +121,20 @@ class SentryNativeJava extends SentryNativeChannel {
121121
}
122122

123123
@override
124-
FutureOr<void> addBreadcrumb(Breadcrumb breadcrumb) {
125-
_coreWorker?.addBreadcrumb(breadcrumb);
126-
}
124+
FutureOr<void> addBreadcrumb(Breadcrumb breadcrumb) =>
125+
_coreWorker?.addBreadcrumb(breadcrumb);
127126

128127
@override
129128
void clearBreadcrumbs() => tryCatchSync('clearBreadcrumbs', () {
130129
native.Sentry.clearBreadcrumbs();
131130
});
132131

133132
@override
134-
FutureOr<void> setUser(SentryUser? user) {
135-
_coreWorker?.setUser(user);
136-
}
133+
FutureOr<void> setUser(SentryUser? user) => _coreWorker?.setUser(user);
137134

138135
@override
139-
FutureOr<void> setContexts(String key, value) {
140-
_coreWorker?.setContexts(key, value);
141-
}
136+
FutureOr<void> setContexts(String key, value) =>
137+
_coreWorker?.setContexts(key, value);
142138

143139
@override
144140
void removeContexts(String key) => tryCatchSync('removeContexts', () {

packages/flutter/test/native/android_core_worker_test_real.dart

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// ignore_for_file: invalid_use_of_internal_member
33
library;
44

5+
import 'dart:async';
56
import 'dart:isolate';
67
import 'dart:typed_data';
78

@@ -239,14 +240,15 @@ void main() {
239240
expect(await resultFuture, isNull);
240241
});
241242

242-
test('sends breadcrumb update without awaiting response', () async {
243+
test('sends breadcrumb update and awaits response', () async {
243244
final fixture = _Fixture();
244245
final worker = fixture.getSut();
245246
await worker.start();
246247

247-
worker.addBreadcrumb(Breadcrumb(message: 'crumb'));
248+
final payload = await fixture.expectPendingRequest(
249+
worker.addBreadcrumb(Breadcrumb(message: 'crumb')),
250+
);
248251

249-
final payload = await fixture.nextMessage;
250252
expect((payload as dynamic).breadcrumb['message'], 'crumb');
251253
});
252254

@@ -255,25 +257,27 @@ void main() {
255257
final worker = fixture.getSut();
256258
await worker.start();
257259

258-
worker.addBreadcrumb(Breadcrumb(
259-
message: 'crumb',
260-
data: {'value': _UnserializableValue()},
261-
));
260+
final payload = await fixture.expectPendingRequest(
261+
worker.addBreadcrumb(Breadcrumb(
262+
message: 'crumb',
263+
data: {'value': _UnserializableValue()},
264+
)),
265+
);
262266

263-
final payload = await fixture.nextMessage;
264267
expect((payload as dynamic).breadcrumb['data'], {
265268
'value': 'normalized-value',
266269
});
267270
});
268271

269-
test('sends user update without awaiting response', () async {
272+
test('sends user update and awaits response', () async {
270273
final fixture = _Fixture();
271274
final worker = fixture.getSut();
272275
await worker.start();
273276

274-
worker.setUser(SentryUser(id: 'fixture-user'));
277+
final payload = await fixture.expectPendingRequest(
278+
worker.setUser(SentryUser(id: 'fixture-user')),
279+
);
275280

276-
final payload = await fixture.nextMessage;
277281
expect((payload as dynamic).user['id'], 'fixture-user');
278282
});
279283

@@ -282,14 +286,15 @@ void main() {
282286
final worker = fixture.getSut();
283287
await worker.start();
284288

285-
worker.setUser(SentryUser(
286-
id: 'fixture-user',
287-
data: {'value': _UnserializableValue()},
288-
// ignore: deprecated_member_use
289-
extras: {'extra': _UnserializableValue()},
290-
));
289+
final payload = await fixture.expectPendingRequest(
290+
worker.setUser(SentryUser(
291+
id: 'fixture-user',
292+
data: {'value': _UnserializableValue()},
293+
// ignore: deprecated_member_use
294+
extras: {'extra': _UnserializableValue()},
295+
)),
296+
);
291297

292-
final payload = await fixture.nextMessage;
293298
final user = (payload as dynamic).user as Map;
294299
expect(user['data'], {
295300
'value': 'normalized-value',
@@ -299,16 +304,17 @@ void main() {
299304
});
300305
});
301306

302-
test('sends context update without awaiting response', () async {
307+
test('sends context update and awaits response', () async {
303308
final fixture = _Fixture();
304309
final worker = fixture.getSut();
305310
await worker.start();
306311

307-
worker.setContexts('fixture-key', <dynamic, dynamic>{
308-
'nested': <dynamic, dynamic>{'value': true}
309-
});
312+
final payload = await fixture.expectPendingRequest(
313+
worker.setContexts('fixture-key', <dynamic, dynamic>{
314+
'nested': <dynamic, dynamic>{'value': true}
315+
}),
316+
);
310317

311-
final payload = await fixture.nextMessage;
312318
expect((payload as dynamic).key, 'fixture-key');
313319
expect((payload as dynamic).value, {
314320
'nested': {'value': true}
@@ -332,8 +338,6 @@ class _Fixture {
332338
return request;
333339
}
334340

335-
Future<Object?> get nextMessage => inboxes.last.first;
336-
337341
void respond(int id, Object? response) {
338342
responsePorts.last.send((id, response));
339343
}
@@ -342,6 +346,21 @@ class _Fixture {
342346
respond(id, RemoteError('worker failure', StackTrace.current.toString()));
343347
}
344348

349+
Future<Object?> expectPendingRequest(FutureOr<void> update) async {
350+
final updateFuture = Future<void>.value(update);
351+
var completed = false;
352+
unawaited(updateFuture.then((_) => completed = true));
353+
354+
final (id, payload) = await nextRequest;
355+
await pumpEventQueue();
356+
expect(completed, isFalse);
357+
358+
respond(id, null);
359+
await updateFuture;
360+
expect(completed, isTrue);
361+
return payload;
362+
}
363+
345364
Future<Worker> _fakeSpawn(WorkerConfig config, WorkerEntry entry) async {
346365
final inbox = ReceivePort();
347366
inboxes.add(inbox);

0 commit comments

Comments
 (0)