search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs
You are viewing an older version (0.0.1). Go to the latest.

Watchers

Telescope ships 9 watcher units across three categories. Each unit implements one of two contracts: TelescopeWatcher (for event-driven and hook-based capture) or TelescopeHttpAdapter (for HTTP traffic capture). All units feed the matching ring buffer inside TelescopeStore and expose their data via a VM Service extension method.

kDebugMode gating

The consumer is responsible for gating the entire telescope install behind kDebugMode:

if (kDebugMode) {
  TelescopePlugin.install();
  // register additional watchers here
}

This single gate tree-shakes the entire subsystem in release builds (dart2js for web, dart2native for mobile/desktop AOT). Individual watchers do not need their own gate except DumpWatcher, which has an additional internal guard (see below).

Chain-preserve pattern

Any watcher that overrides a global Dart/Flutter hook saves the previous handler before replacing it and calls it inside the new handler body. On uninstall(), the previous handler is restored symmetrically. This is the Sentry/Bugsnag coexistence contract: telescope captures the record AND forwards to whatever was registered before it. The numbered step comments in ExceptionWatcher are the canonical reference for this pattern:

// 1. Chain-preserve FlutterError.onError (sync framework errors).
_previousOnError = FlutterError.onError;
FlutterError.onError = (details) {
  TelescopeStore.recordException(...);
  _previousOnError?.call(details);
};

// 2. Chain-preserve PlatformDispatcher.instance.onError (async + isolate errors).
_previousPlatformOnError = PlatformDispatcher.instance.onError;
PlatformDispatcher.instance.onError = (error, stack) {
  TelescopeStore.recordException(...);
  return _previousPlatformOnError?.call(error, stack) ?? true;
};

Framework-agnostic watchers

These three units are part of the fluttersdk_telescope core package. They have no dependency on Magic or any other application framework.

LogWatcher

Field Value
Contract TelescopeWatcher
Name log
Auto-install Yes (auto-registered by TelescopePlugin.install())
Ring buffer TelescopeStore._logs
VM extension ext.telescope.console
Opt-out Call TelescopePlugin.install() then remove it by not calling LogWatcher().install() directly; or skip the default auto-install path and register manually

Subscribes to Logger.root.onRecord from package:logging. Every log record that flows through the root logger (at any level) is converted to a LogRecordEntry and pushed to the store.

hierarchicalLoggingEnabled is set to true during install so named child loggers (Logger('http'), Logger('auth'), etc.) funnel through root. Install is idempotent: a second call when the subscription is already live is a no-op.

If the app does not use package:logging, the watcher is dormant (no records flow through Logger.root). It is still registered because adding package:logging later immediately benefits from capture without any telescope config change.

Registration (auto-installed, shown for completeness):

TelescopePlugin.install(); // LogWatcher registered automatically

Manual opt-out (skip auto-registration) is not exposed in the V1 API; file a feature request if selective watcher exclusion is needed.


ExceptionWatcher

Field Value
Contract TelescopeWatcher
Name exception
Auto-install No (opt-in via TelescopePlugin.registerWatcher(ExceptionWatcher()) after install)
Ring buffer TelescopeStore._exceptions
VM extension ext.telescope.exceptions
Opt-out Simply do not register; absent registration means no error capture. To coexist with Sentry / Bugsnag, register telescope FIRST so the chain-preserve wraps the next handler.

Hooks both FlutterError.onError (synchronous framework and widget errors) and PlatformDispatcher.instance.onError (asynchronous errors, isolate errors, plugin-originated errors). Both hooks follow the chain-preserve pattern.

The PlatformDispatcher.onError handler returns _previousPlatformOnError?.call(...) ?? true. When a previously-registered handler (e.g. Sentry) returns false to signal a fatal error and let the native crash reporter take over, telescope preserves that semantics. When no previous handler exists, the default true (handled) matches pre-install behavior.

On uninstall(), both handlers are restored to exactly the values they held before install() was called. Calling install() while already installed is a no-op.

Registration (opt-in; add after TelescopePlugin.install()):

TelescopePlugin.install();
TelescopePlugin.registerWatcher(ExceptionWatcher());

Coexistence with Sentry (the chain-preserve contract means order matters; install Sentry first):

SentryFlutter.init((_) {});         // registers FlutterError.onError + PlatformDispatcher.onError
if (kDebugMode) {
  TelescopePlugin.install();        // wraps Sentry's handlers; Sentry still fires
}

DumpWatcher

Field Value
Contract TelescopeWatcher
Name dump
Auto-install No (opt-in via TelescopePlugin.registerWatcher(DumpWatcher()))
Ring buffer TelescopeStore._dumps
VM extension ext.telescope.dumps
Opt-out Do not register it; it is never auto-installed

Captures all debugPrint output by replacing the global debugPrint callback with an interceptor. The interceptor records a DumpRecord AND calls the previous debugPrint value (chain-preserve). On uninstall(), the previous callback is restored exactly.

Internal kDebugMode gate: install() is a no-op in release builds unless allowInRelease is set to true before calling install(). This gate is load-bearing: in release builds debugPrint itself is a no-op, so capture would produce empty records; the gate makes the intent explicit and ensures AOT tree-shaking eliminates the interceptor.

Registration:

if (kDebugMode) {
  TelescopePlugin.install();
  TelescopePlugin.registerWatcher(DumpWatcher()); // opt-in
}

Opt-out: simply omit the registerWatcher(DumpWatcher()) call.


HTTP adapters

HTTP adapters implement TelescopeHttpAdapter (three methods: name, install, uninstall, plus the optional pendingCount getter). They feed TelescopeStore.recordHttp and their in-flight counts sum into TelescopeStore.pendingHttpCount (consumed by Dusk's wait_for_network_idle).

DioHttpAdapter

Field Value
Contract TelescopeHttpAdapter
Name dio
Auto-install No (opt-in via TelescopePlugin.registerHttpAdapter(DioHttpAdapter()))
Ring buffer TelescopeStore._http
VM extension ext.telescope.requests
Opt-out Do not register it

Vanilla Dio adapter for Flutter apps that use raw Dio instances (not the Magic Http facade). V1 ships as a stub: the actual Dio interceptor subclass requires package:dio, which is not in telescope's own pubspec to keep the core package HTTP-library-agnostic. The consumer adds package:dio to their own pubspec and wires the adapter by calling the static helper:

// consumer pubspec: dio: ^5.x
import 'package:fluttersdk_telescope/telescope.dart';

// After constructing your Dio instance:
dio.interceptors.add(_MyTelescopeInterceptor());

class _MyTelescopeInterceptor extends Interceptor {
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    DioHttpAdapter.recordRequest(
      url: response.requestOptions.uri.toString(),
      method: response.requestOptions.method,
      statusCode: response.statusCode ?? 0,
      durationMs: ...,
    );
    handler.next(response);
  }
}

// Register so the adapter shows up in the store's adapter list:
TelescopePlugin.registerHttpAdapter(DioHttpAdapter());

V1.x will move the Dio-coupled glue into a fluttersdk_telescope_dio sub-package so the core stays HTTP-library-agnostic.


MagicHttpFacadeAdapter

Field Value
Contract TelescopeHttpAdapter
Name magic_http_facade
Auto-install Yes (registered by MagicTelescopeIntegration.install())
Ring buffer TelescopeStore._http
VM extension ext.telescope.requests
Opt-out Call TelescopePlugin.install() without calling MagicTelescopeIntegration.install()

Captures every request flowing through Magic's network driver by registering a MagicNetworkInterceptor on the driver. The interceptor pairs onRequest (start stopwatch) with onResponse/onError (stop stopwatch, emit HttpRequestRecord) via a FIFO in-flight list.

Attribution is heuristic (attributedHeuristically: true) because the MagicNetworkInterceptor contract does not carry a correlation handle across onRequest/onResponse callbacks; best-effort matching by call order.

pendingCount returns the current length of the in-flight FIFO, surfaced into TelescopeStore.pendingHttpCount for Dusk's network-idle detection.

Registration (via MagicTelescopeIntegration, the only documented entry point):

if (kDebugMode) {
  TelescopePlugin.install();
  MagicTelescopeIntegration.install(); // registers MagicHttpFacadeAdapter + 5 watchers
}

install() is a no-op if Magic.bound('network') returns false (called too early, before Magic.init() completes). Call MagicTelescopeIntegration.install() after Magic.init().


Magic-stack watchers

These five watchers are shipped inside the magic package via MagicTelescopeIntegration. They are not part of the fluttersdk_telescope core. All five are registered by a single MagicTelescopeIntegration.install() call.

MagicModelWatcher

Field Value
Contract TelescopeWatcher
Name magic_model
Auto-install Yes (via MagicTelescopeIntegration.install())
Ring buffer TelescopeStore._models
VM extension ext.telescope.requests (via models key; MCP tool: telescope_models)
Opt-out Do not call MagicTelescopeIntegration.install()

Subscribes to ModelCreated, ModelSaved, and ModelDeleted events dispatched by Magic's EventDispatcher. Each event is converted to a MagicModelRecord carrying: modelClass (runtime type name), event tag (created/saved/deleted), modelKey (stringified primary key), time, and attributes (snapshot of model.attributes at capture time).

Registration:

if (kDebugMode) {
  TelescopePlugin.install();
  MagicTelescopeIntegration.install(); // MagicModelWatcher is included
}

uninstall() is a no-op: EventDispatcher has no per-listener removal API. Tests that need a clean dispatcher call EventDispatcher.instance.clear() in their setUp.


MagicCacheWatcher

Field Value
Contract TelescopeWatcher
Name magic_cache
Auto-install Yes (via MagicTelescopeIntegration.install())
Ring buffer TelescopeStore._caches
VM extension ext.telescope.caches
Opt-out Do not call MagicTelescopeIntegration.install()

Subscribes to five cache lifecycle events emitted by Magic's CacheManager: CacheHit, CacheMiss, CachePut, CacheForget, and CacheFlush. Each event is converted to a MagicCacheRecord carrying operation (hit/miss/put/forget/flush), key (or * for flush), time, and ttl (present only for CachePut).

Registration:

if (kDebugMode) {
  TelescopePlugin.install();
  MagicTelescopeIntegration.install(); // MagicCacheWatcher is included
}

uninstall() is a no-op (same EventDispatcher constraint as MagicModelWatcher).


MagicEventWatcher

Field Value
Contract TelescopeWatcher
Name magic_event
Auto-install Yes (via MagicTelescopeIntegration.install())
Ring buffer TelescopeStore._events
VM extension ext.telescope.events
Opt-out Do not call MagicTelescopeIntegration.install()

Subscribes to a curated set of Magic app-lifecycle events: AuthLogin, AuthLogout, AuthFailed, AuthRestored (auth lifecycle), DatabaseConnected (database connection), and GateAbilityDefined, GateBeforeRegistered (gate definition). Model lifecycle events are excluded (owned by MagicModelWatcher) and gate-result events are excluded (owned by MagicGateWatcher) to keep each record on a single channel.

Current payload is the empty map for every event type. Per-event field extraction is deferred to a follow-up release while the EventRecord wire shape stabilises.

Registration:

if (kDebugMode) {
  TelescopePlugin.install();
  MagicTelescopeIntegration.install(); // MagicEventWatcher is included
}

uninstall() is a no-op (same EventDispatcher constraint).


MagicGateWatcher

Field Value
Contract TelescopeWatcher
Name magic_gate
Auto-install Yes (via MagicTelescopeIntegration.install())
Ring buffer TelescopeStore._gates
VM extension ext.telescope.gates
Opt-out Do not call MagicTelescopeIntegration.install()

Subscribes to GateAccessChecked, which covers both Gate.allows and Gate.denies outcomes via its allowed: bool field. GateAccessDenied is intentionally not subscribed to avoid double-recording every denial.

Each event is converted to a GateRecord with two shape coercions:

  • arguments (single dynamic on the event) is wrapped into List of length 1.
  • user.id (dynamic primary key) is stringified; null user or null id both collapse to userId: null.

Magic model arguments are converted via toMap(); primitives and collections pass through as-is; anything else falls back to toString().

Registration:

if (kDebugMode) {
  TelescopePlugin.install();
  MagicTelescopeIntegration.install(); // MagicGateWatcher is included
}

uninstall() is a no-op (same EventDispatcher constraint).


MagicQueryWatcher

Field Value
Contract TelescopeWatcher
Name magic_query
Auto-install Yes (via MagicTelescopeIntegration.install())
Ring buffer TelescopeStore._queries
VM extension ext.telescope.queries
Opt-out Do not call MagicTelescopeIntegration.install()

Subscribes to QueryExecuted, dispatched by Magic's QueryBuilder after every SQL run. Each event is converted to a QueryRecord carrying: sql (the full SQL string), bindings (parameter list), timeMs (execution duration in milliseconds), connectionName (driver connection identifier), and time (capture timestamp).

Surfaced via the telescope:queries CLI command and the telescope_queries MCP tool.

Registration:

if (kDebugMode) {
  TelescopePlugin.install();
  MagicTelescopeIntegration.install(); // MagicQueryWatcher is included
}

uninstall() is a no-op (same EventDispatcher constraint as the other Magic-stack watchers).