recordAppEvent function
Reports event to every surface that is listening.
Inside a scenario it lands on the current transition, and the step's Events pane shows it. Inside a running app it lands on a mounted devbar's tabs. Both at once, if both are there; neither is wired by the project.
A no-op — one null check and an empty list — when nothing is listening,
which is every bare flutter test run. That is what makes it safe for a
project to leave these calls in its shared fakes forever, and it holds all
the way: a listener that throws is reported to the Zone and skipped
rather than surfacing here, and one that unregisters itself mid-report
costs its neighbours nothing.
class FakeApi implements Api {
Future<User> login(String email) async {
recordAppEvent(AppEvent.request(
method: 'POST', url: '/login', status: 200));
return User(email);
}
}
source names the reporter, for the one case where a surface has already
been handed this event by another route: a listener registered with a
matching ignoreSource skips it. Everybody else — the buffer, and every
other listener — sees it normally. See addAppEventListener.
Implementation
void recordAppEvent(AppEvent event, {Object? source}) {
// The stack is captured here and resolved much later, and the split is the
// whole reason this is affordable. Measured JIT at a hundred frames — the
// depth of a widget test — capturing costs 3.5 µs and resolving it costs
// 78.6 µs, so resolving every event at the run cap would be ~390 ms a
// scenario. Captured here and resolved in [AppEventBuffer.drain], the
// expensive half is paid only for the events that survive the per-step cap
// and reach a file.
//
// Behind the buffer's null check, which is already the first thing this
// function does: a production app and a plain `flutter test` have no buffer
// and pay nothing at all.
appEventBuffer?.add(event, StackTrace.current);
if (_listeners.isEmpty) return;
// Over a copy, and never over the live list. A listener that unregisters on
// its way out — a devbar disposing mid-report — shifts everything behind it
// down a slot, so a walk by index silently *skips* its neighbour and a
// for-in throws a concurrent modification. Neither is acceptable: the whole
// promise of this call is that reporting cannot disturb the app.
for (var registration in List.of(_listeners)) {
if (source != null && registration.ignoreSource == source) continue;
try {
registration.listener(event);
} on Object catch (e, stack) {
// A surface that breaks may not take the app's own call with it — this
// is a line in somebody's fake, not a place to fail. Loud, but not here:
// the zone reports it the way an unhandled async error is reported.
Zone.current.handleUncaughtError(e, stack);
}
}
}