Debugging

Local diagnostic captures, structured Rust traces, and admission snapshots

Start collection before reproducing the problem. Keep the typed error returned by the failing operation separate from the diagnostic timeline. Diagnostics observe OpenRTC's current owners; they never reconnect, renew a ticket or change readiness.

Browser, Node and Tauri TypeScript

Enable an app-owned logger at construction to see activation start, success and failure, including failures before a capability handle exists:

import { OpenRTC } from 'openrtc';

const rtc = OpenRTC({
  apiKey,
  diagnostics: { logger: event => console.debug('[openrtc]', event) },
});
const ticket = await rtc.tickets.join(invitation);

Activation records contain SDK-realm client and operation references, avenue kind, outcome, timestamp and duration. Duplicate activation calls share the existing owner and produce one operation. Logger exceptions do not change the returned result. Use the operation's typed error for the detailed failure code; arbitrary error messages and credentials are deliberately absent from these records.

Once a handle exists, capture its connection changes:

const capture = ticket.diagnostics.capture({
  maxEvents: 256,
  // Optional: forward these structured records to your existing logger or
  // breadcrumb adapter. Keep the callback quick and avoid runtime commands.
  logger: event => console.debug('[openrtc.peer]', event),
});

// Reproduce the problem through your normal application controls.
capture.stop();
const supportJson = JSON.stringify(capture.snapshot(), null, 2);
// Show/save this string through your app's normal support flow.

Capture starts when called; it cannot recover past events. It uses the existing peer observer without timers, pings or coordination reads. Identical projected states are coalesced. Defaults are 256 events, with an explicit maximum of 1024; the oldest event is evicted and counted in droppedEvents. At most 64 distinct peers are tracked; peerLimitReached reports truncation. stop() unsubscribes and keeps the retained timeline available. Saving and uploading are app-owned.

Ticket capture includes pending peers already assigned to that ticket, not other tickets sharing its runtime. A peer without an assigned ticket scope cannot be attributed to that ticket yet. Constructor activation logging covers that earlier stage. A snapshot includes SDK version (source for unbundled source), generation numbers, stable readiness codes, coarse admission/transport state and capture-local peer aliases. It omits names, full identifiers, grant IDs, invitations, URLs, credentials, raw errors and payloads. Aliases are local to one capture and are not cross-device trace IDs.

Rust native hosts and iOS packet-tunnel extensions

Request the SDK owner's snapshot directly; stdout capture is unnecessary:

let snapshot = client.diagnostic_snapshot().await;
let support_json = serde_json::to_string_pretty(&snapshot)?;

This returns at most 64 peers plus omittedPeers, SDK version, timestamp and target. Each peer includes settledReady, a stable readinessCode, current transport/route generations and these security facts:

  • admission and authoritativeDeviceBound;
  • cryptoKeyInstalled and cryptoConfirmed (no key material);
  • nativeDirectionalAdmission.inboundRequired/inboundProven and outboundRequired/outboundProven for the selected physical generation.

The WASM binding also exposes diagnostic_snapshot(). Native directional stream contracts do not exist on WASM, so that field is null. Browser TypeScript captures observe public peer projections; they do not promise native proof details from an older/custom host. Snapshot facts are best-effort observations read from their owners, not a cross-owner atomic authorization receipt.

On iOS, return the serialized snapshot through an existing authenticated, bounded app/extension message channel. Use the app's support export UI. Do not redirect stderr into account storage, clear App Group data to collect a log, or add another endpoint/lifecycle controller for diagnostics.

Structured Rust tracing

OpenRTC emits tracing spans/events without installing a global subscriber. Connect, ticket issue/join and admission operations have spans; critical scope, epoch, channel-open and protected-stream-fence decisions emit structured events. Arguments use skip_all; raw tickets, keys, error text and payloads are never recorded by these new instrumentation sites.

If your application already has tracing, add these targets to its existing filter and keep its subscriber/exporter. For an application without one:

[dependencies]
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
use tracing_subscriber::fmt::format::FmtSpan;

// Application startup, once. OpenRTC itself never installs this subscriber.
tracing_subscriber::fmt()
    .with_env_filter("off,openrtc::admission=debug,openrtc::stream=debug")
    .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
    .with_ansi(false)
    .json()
    .with_writer(std::io::stderr)
    .try_init()?;

JSON events carry span context; close events include timing. connection_ref is a short SHA-256 pseudonym for correlating an SDK connection without emitting its full identifier. It is not a credential or an anonymity guarantee. A scope_accepted event records scope kind and whether a durable device binding exists; inbound_epoch_changed/outbound_epoch_changed mark security transitions. Protected-stream access_denied identifies changed authority or the key and confirmation-generation predicates. Collection does not weaken those fences.

For persistent native logs, use your app's existing private log directory and standard bounded/non-blocking writer, such as tracing-appender: choose rotation and retention, use a distinct filename prefix, bound the queue, and retain its worker guard until shutdown. File-count retention does not bound the bytes in one active file; impose your app's size/session budget too. Avoid unbounded retention. SDK collection and exports never happen automatically.

If you already use OpenTelemetry, the standard tracing-opentelemetry layer can export the same Rust spans through your configured provider. OpenRTC does not install a telemetry SDK, exporter, remote collector or tracing subscriber.

Reading a pending connection

Code/factInterpretation
waiting-for-transport / missing-active-transportNo currently usable physical generation.
session-admission-pendingApplication authority has not been accepted.
native-main-route-pendingInspect required/proven inbound and outbound facts for this generation.
application-crypto-key-pendingThe required application key has not been installed.
application-crypto-confirmation-pendingThe peer's key confirmation for this generation is pending.
bounded-health-confirmation-pendingAdmission/key facts are insufficient to declare transport health.
access_denied with authority_changedThe stream's exact scoped authority expired, was revoked or changed.
unclassifiedThis free-form reason was omitted; inspect the typed operation error locally.

Match timestamp, connection reference and generation before comparing two observations. A changed generation is not automatically a new logical peer. Historical verbose flags and free-form stderr/console output are not the allowlisted support format; do not append them blindly to a shareable capture.

This follows Rust tracing's library/application split and OpenTelemetry's library instrumentation guidance.