Logging
Structured network observability. Every fetch and XHR the SDK and the bundle issue, surfaced through one opt-in handler.
The SDK and the bundle inside the sandbox emit structured events for every outbound HTTP and XHR request. The channel is opt-in — when no logger is installed the internal emit point is a one-instruction null check, so the cost when off is zero.
Body bytes are NEVER logged — only sizes. Bearer tokens and message content stay out of the logs. Sizes are safe to leave on in production.
Quick start
The fastest path: set SNAP_NETLOG=1 in the environment. Module load installs the built-in text formatter for you.
SNAP_NETLOG=1 node ./your-script.js[net.xhr.open ] POST https://web.snapchat.com/.../AddFriends
[net.xhr.done ] POST https://web.snapchat.com/.../AddFriends -> 200 (req 87B / resp 0B / 437ms / grpc-status:0)
[net.xhr.error ] POST https://web.snapchat.com/.../SyncFriendData -> "Network error" (1203ms)Or install the formatter explicitly from code:
import { setLogger, defaultTextLogger } from "@snapcap/native";
setLogger(defaultTextLogger);Custom handlers
Pass any function with the Logger signature to setLogger. The handler is called synchronously on every emitted LogEvent.
import { setLogger } from "@snapcap/native";
setLogger((event) => {
process.stdout.write(JSON.stringify({ ts: Date.now(), ...event }) + "\n");
});Handler crashes are caught and swallowed by the internal emit point so a bad logger can never break the network path. They're silently dropped — defensive code matters in your handler.
Disabling
Pass undefined to clear:
setLogger(undefined);Or if you started with SNAP_NETLOG=1, call setLogger(undefined) after import to opt back out.
Event shape
LogEvent is a closed discriminated union. Switch on event.kind to narrow:
type LogEvent =
| { kind: "net.xhr.open"; method: string; url: string }
| { kind: "net.xhr.done"; method: string; url: string; status: number; reqBytes: number; respBytes: number; durMs: number; grpcStatus?: string; grpcMessage?: string }
| { kind: "net.xhr.error"; method: string; url: string; error: string; durMs: number }
| { kind: "net.fetch.open"; method: string; url: string }
| { kind: "net.fetch.done"; method: string; url: string; status: number; reqBytes: number; respBytes: number; durMs: number; grpcStatus?: string; grpcMessage?: string }
| { kind: "net.fetch.error";method: string; url: string; error: string; durMs: number };Variants pair on protocol (xhr vs fetch) and lifecycle stage (open / done / error). done events include grpcStatus and grpcMessage for gRPC-Web responses where the bundle's transport has already parsed them.
Routing into your log pipeline
Pino, Winston, OpenTelemetry, or anything that takes structured events. One example with Pino:
import pino from "pino";
import { setLogger, type LogEvent } from "@snapcap/native";
const log = pino({ level: "info" });
setLogger((event: LogEvent) => {
switch (event.kind) {
case "net.xhr.done":
case "net.fetch.done":
log.info(
{ url: event.url, status: event.status, dur: event.durMs, grpcStatus: event.grpcStatus },
"snap.net",
);
return;
case "net.xhr.error":
case "net.fetch.error":
log.warn({ url: event.url, error: event.error, dur: event.durMs }, "snap.net.error");
return;
default:
// open events — usually too noisy for production
return;
}
});Filtering
The handler is the filter. Drop events you don't care about:
setLogger((event) => {
// Skip the chatty bundle prefetches — only log mutations and errors.
if (event.kind === "net.xhr.open" || event.kind === "net.fetch.open") return;
if (
(event.kind === "net.xhr.done" || event.kind === "net.fetch.done") &&
!event.url.includes("/JzFriendAction/")
) {
return;
}
defaultTextLogger(event);
});Multi-tenant
The logger is process-wide — every SnapcapClient in the process emits through the same handler. URLs are full and include the host, so you can group events per tenant by parsing the cookie/auth context out of your own runner — but the SDK itself doesn't tag events with a tenant id.
If you need per-tenant attribution, wrap the gate handler:
const tagByUserAgent = new Map<string, string>();
// populate from your tenant config: ua → tenantId
setLogger((event) => {
// The bundle emits events from inside whichever Sandbox issued the request.
// For now, parse the URL or correlate to your runner state externally.
log.info({ ...event }, "snap.net");
});What's next
- Throttling — see what's being throttled in the logs (
durMsjumps when the gate waits). - Multi-tenant — running many clients with one logger.
- API: setLogger
- API: Logger
- API: LogEvent