Skip to content

Crumbs Integration

View raw .md

MagicCrumbsIntegration feeds Magic's HTTP traffic, model, cache, event, gate and query activity into crumbs' buffers, masks the auth header Magic sends, and points tapir's network-idle, console and exceptions readers at crumbs.

On this page

Basic Usage#

dart
import 'package:crumbs/crumbs.dart';
import 'package:flutter/foundation.dart' show kReleaseMode;
import 'package:flutter/widgets.dart';
import 'package:magic/magic.dart';
import 'package:magic_devtools/crumbs.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  if (!kReleaseMode) {
    CrumbsPlugin.install();
    CrumbsPlugin.registerWatcher(ExceptionWatcher()); // boot errors
    CrumbsPlugin.registerWatcher(DumpWatcher()); // debugPrint
  }

  await Magic.init();

  if (!kReleaseMode) MagicCrumbsIntegration.install();

  runApp(MagicApplication());
}

MagicDevtools.installPre() and installPost() make the same calls. MagicCrumbsIntegration.install() has to follow Magic.init(): the HTTP adapter looks the network driver up in the container.

Quick Reference#

Unit name Listens to Record Read it with
MagicHttpFacadeAdapter magic_http_facade An interceptor on the network driver HttpRequestRecord crumbs:requests
MagicModelWatcher magic_model ModelCreated, ModelSaved, ModelDeleted ModelRecord CrumbsStore.recentModels()
MagicCacheWatcher magic_cache CacheHit, CacheMiss, CachePut, CacheForget, CacheFlush CacheRecord crumbs:caches
MagicEventWatcher magic_event AuthLogin, AuthLogout, AuthFailed, AuthRestored, DatabaseConnected, GateAbilityDefined, GateBeforeRegistered EventRecord crumbs:events
MagicGateWatcher magic_gate GateAccessChecked GateRecord crumbs:gates
MagicQueryWatcher magic_query QueryExecuted QueryRecord crumbs:queries

install() is idempotent: a second call registers nothing twice. It does re-add the auth header to the masked list on every call (see Credential Masking).

HTTP Capture#

The adapter adds one interceptor to the driver Magic.make<NetworkDriver>('network') returns. Each answer becomes one HttpRequestRecord:

Field Value
url, method, requestHeaders From the request; header values are stringified
statusCode, isError From the response (isError is its failed flag); an error is always isError: true
requestBody, responseBody Masked (below), then cut at 8192 characters with a ... [truncated N chars] suffix; an error with a message stores the message as responseBody
durationMs, startUs, endUs Timed on FlutterTimeline.now from request to answer
requestId The id Magic's driver stamps on the request
attributedHeuristically true only for an answer without an id, which is paired with the oldest id-less request in flight
interactionId, linkedBy The tapir interaction the request was sent from (see Performance)

Requests and answers are paired by that id, so two requests that finish out of order keep their own URL and duration. An answer whose request went out before install() is dropped. MagicHttpFacadeAdapter.pendingCount is the number of requests in flight, which CrumbsStore.pendingHttpCount sums.

Credential Masking#

Every masked value reads ******** (CrumbsRedaction.mask).

What Masked by
The header auth.token.header names (default Authorization) install() passes it to CrumbsRedaction.hideRequestHeaders, so a renamed bearer header is hidden too
authorization, proxy-authorization, cookie, set-cookie, x-api-key headers crumbs' own defaults
Request body keys such as password, token, access_token, secret, api_key CrumbsRedaction.hiddenRequestParameters, applied by the adapter before the body is stringified
Response body keys: the request list plus plain_text_token, recovery_codes, qr_url, qr_svg CrumbsRedaction.hiddenResponseParameters

The adapter masks a structured body (a Map or List) itself, because crumbs only parses JSON and form-encoded text, and toString() of a Dart map is neither. It masks a copy: the driver sends the object the interceptor saw, so masking in place would send the mask to the server.

Add your own names with CrumbsRedaction.hideRequestHeaders, hideRequestParameters and hideResponseParameters; additions merge with the defaults.

The Watchers#

Watcher What a record carries
MagicModelWatcher modelClass (the runtime type), event (created, saved or deleted), modelKey (the id as a string, empty when null), a copy of attributes
MagicCacheWatcher operation (hit, miss, put, forget or flush), key (* for a flush), ttl on a put
MagicEventWatcher eventType (the class name) and an empty payload
MagicGateWatcher ability, result, arguments (always one element; a Model is stored as toMap()), userId as a string or null
MagicQueryWatcher sql, bindings, timeMs, connectionName

Every record except the gate record also carries interactionId and linkedBy. Model lifecycle and QueryExecuted stay out of MagicEventWatcher, and GateAccessDenied is not subscribed, so each action lands on one channel once.

Each watcher guards its own install(), because magic's EventDispatcher.register is additive. uninstall() removes nothing: the dispatcher has no per-listener removal.

The Tapir Bridge#

tapir does not depend on crumbs; it reads crumbs through three function pointers this integration assigns.

Pointer Reads Used by
pendingHttpCountReader CrumbsStore.pendingHttpCount tapir:wait_for_network_idle
recentLogsReader CrumbsStore.recentLogs, as level, message, time, logger and error tapir:console
recentExceptionsReader CrumbsStore.recentExceptions, as type, message, time and stackHead (the first three stack lines) tapir:exceptions

Without this integration the three pointers keep tapir's defaults: zero in-flight requests and empty lists.

Release and Disabled Builds#

The integration has no build-mode check of its own. Under the !kReleaseMode guard a release build never reaches it and records nothing; a debug or profile build records everything above. A build compiled with --dart-define=CRUMBS_DISABLE=true turns CrumbsPlugin.registerWatcher and registerHttpAdapter into no-ops, so none of the Magic units is attached.

Gotchas#

Mistake Fix
Calling MagicCrumbsIntegration.install() before Magic.init(). The adapter finds no network binding and attaches nothing, and the install guard stops a later call from retrying; call it after Magic.init().
Expecting model attributes to be masked. Only HTTP headers and bodies are masked by key; a ModelRecord copies every attribute as given, so keep secrets out of model attributes in a debug session.
Expecting HTTP records while the test uses Http.fake(). The fake driver ignores interceptors, so nothing reaches crumbs; assert on the fake instead.
Reading EventRecord.payload for event details. It is always empty; the record says which event fired and when, not its fields.
Registering ExceptionWatcher by hand and also calling MagicDevtools.installPre(). Each registration chains onto FlutterError.onError, so every exception is recorded twice; use one style.
esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.