How-to: observability and logging
You want structured logs, metrics, and traces of every SDK HTTP call without leaking credentials into your log store.
At a glance
- Package:
@nodeops-createos/sandbox(npm) - Import:
import { createClient } from "@nodeops-createos/sandbox" - Base URL:
https://api.sb.createos.sh— override withCREATEOS_SANDBOX_BASE_URL - Auth: API key via the
apiKeyoption orCREATEOS_SANDBOX_API_KEY
Wire the hooks
CreateosSandboxClient accepts an optional hooks bag on the constructor.
Three callbacks fire around every non-streaming request:
| Hook | Fires |
|---|---|
onRequest | Before fetch is called, on every attempt. |
onResponse | After fetch settles (success or HTTP error). |
onRetry | Between attempts, after the response (or network error) but before the backoff sleep. |
Hooks are await-ed in the request path so an async hook orders
deterministically against the request it describes. Keep hook work cheap,
or dispatch slow side-effects without returning the promise.
A throw inside a hook is swallowed — a misbehaving observer cannot crash a real request.
TypeScript1import { CreateosSandboxClient } from "@nodeops-createos/sandbox";23const client = new CreateosSandboxClient({4 apiKey: process.env.CREATEOS_SANDBOX_API_KEY,5 hooks: {6 onRequest(ctx) { /* ... */ },7 onResponse(ctx) { /* ... */ },8 onRetry(ctx) { /* ... */ },9 },10});
See CreateosSandboxClientOptions for the full
constructor reference.
Hook payloads
onRequest — RequestHookContext
| Field | Type | Notes |
|---|---|---|
method | string | Uppercase HTTP verb ("GET", "POST", …). |
url | string | Full URL, userinfo stripped, sensitive query params redacted. |
headers | Record<string, string> | Outgoing headers; credential values replaced by "redacted". |
attempt | number | 1 on the first try, 2+ on retries. |
onResponse — ResponseHookContext
Extends RequestHookContext with:
| Field | Type | Notes |
|---|---|---|
status | number | HTTP status code. |
durationMs | number | Elapsed time for this fetch call (ms). |
requestId | string | undefined | x-request-id header from the server, when present. |
onRetry — RetryHookContext
Extends ResponseHookContext (minus status) with:
| Field | Type | Notes |
|---|---|---|
reason | RetryReason | "network" · "status" · "rate-limit". |
status | number | undefined | HTTP status that triggered the retry; undefined for network errors (no response received). |
delayMs | number | Milliseconds the SDK will sleep before the next attempt. |
RetryReason breakdown:
"network"—fetchthrew before a response arrived.statusandrequestIdareundefined."status"— a retryable status code (408/500/502/503/504on idempotent methods).delayMsis exponential backoff + jitter."rate-limit"— server returned429or503with aRetry-Afterheader.delayMshonors that header value.
Payloads are pre-redacted
The SDK redacts hook payloads before your code sees them. You do not need to scrub credentials yourself when using the hooks — the values are never passed to you in the first place.
The url and headers fields in every hook context are produced by
redactUrl and
redactHeaders at request build
time, before any hook fires. What is redacted:
Headers — any header whose lowercased name is in SENSITIVE_HEADER_NAMES
(authorization, cookie, set-cookie, x-api-key, x-access-token,
x-auth-token, x-csrf-token, proxy-authorization), plus any header
whose name ends in "-token" or "-key". Values become the literal
string "redacted", so your logs stay greppable.
Query params — any key in SENSITIVE_QUERY_PARAMS (token, api_key,
apikey, access_token, auth_token, password, secret). Same
"redacted" substitution.
URL userinfo — username:password@ in the URL is stripped.
Recipe: structured log line per request
TypeScript1import { CreateosSandboxClient } from "@nodeops-createos/sandbox";23const client = new CreateosSandboxClient({4 apiKey: process.env.CREATEOS_SANDBOX_API_KEY,5 hooks: {6 onRequest({ method, url, attempt }) {7 console.debug(JSON.stringify({ event: "sdk.request", method, url, attempt }));8 },9 onResponse({ method, url, status, durationMs, attempt, requestId }) {10 console.debug(11 JSON.stringify({12 event: "sdk.response",13 method,14 url,15 status,16 durationMs: Math.round(durationMs),17 attempt,18 requestId,19 }),20 );21 },22 },23});
Recipe: retry counter metric
Wire onRetry to a metrics counter. The reason field lets you split
rate-limit retries from transient 5xx:
TypeScript1import { CreateosSandboxClient } from "@nodeops-createos/sandbox";23// Replace with your metrics client (Prometheus, Datadog, etc.).4function incrementCounter(name: string, labels: Record<string, string>): void {5 /* ... */6}78const client = new CreateosSandboxClient({9 apiKey: process.env.CREATEOS_SANDBOX_API_KEY,10 hooks: {11 onRetry({ method, url, reason, status, delayMs, attempt }) {12 incrementCounter("sdk_retry_total", {13 method,14 reason,15 status: String(status ?? "network"),16 });17 console.warn(18 JSON.stringify({ event: "sdk.retry", method, url, reason, status, delayMs, attempt }),19 );20 },21 },22});
Recipe: safe logging outside hooks
If you log raw fetch calls or HTTP details from your own code (not
inside a hook), use the exported redaction helpers. They are pure
functions — non-mutating, no side effects — and mirror exactly what the
SDK applies to hook payloads internally.
TypeScript1import {2 CreateosSandboxClient,3 redactHeaders,4 redactUrl,5} from "@nodeops-createos/sandbox";67// Your own middleware / interceptor — not a hook:8function logOutbound(method: string, url: string, headers: Headers): void {9 console.debug(10 JSON.stringify({11 event: "custom.request",12 method,13 url: redactUrl(url), // strips userinfo, redacts sensitive params14 headers: redactHeaders(headers), // replaces credential values with "redacted"15 }),16 );17}
These helpers are not auto-wired into your logger — call them
explicitly wherever you construct or log raw requests. See
redactHeaders / redactUrl / redactQuery
for full signatures, plus SENSITIVE_HEADER_NAMES and
SENSITIVE_QUERY_PARAMS if you need to inspect the lists.
Recipe: OpenTelemetry span per request
Start a span in onRequest, end it in onResponse. Key on
method + url + attempt to correlate across the pair, because retries
fire both hooks with an incremented attempt.
TypeScript1import { trace, SpanStatusCode } from "@opentelemetry/api";2import { CreateosSandboxClient } from "@nodeops-createos/sandbox";34const tracer = trace.getTracer("createos-sandbox-sdk");5const spans = new Map<string, ReturnType<typeof tracer.startSpan>>();67const client = new CreateosSandboxClient({8 apiKey: process.env.CREATEOS_SANDBOX_API_KEY,9 hooks: {10 onRequest({ method, url, attempt }) {11 const key = `${method} ${url} ${attempt}`;12 spans.set(13 key,14 tracer.startSpan(`createos-sandbox ${method}`, {15 attributes: { "http.method": method, "http.url": url, "sdk.attempt": attempt },16 }),17 );18 },19 onResponse({ method, url, status, durationMs, requestId, attempt }) {20 const key = `${method} ${url} ${attempt}`;21 const span = spans.get(key);22 if (span) {23 span.setAttributes({24 "http.status_code": status,25 "sdk.duration_ms": Math.round(durationMs),26 ...(requestId ? { "sdk.request_id": requestId } : {}),27 });28 if (status >= 400) span.setStatus({ code: SpanStatusCode.ERROR });29 span.end();30 spans.delete(key);31 }32 },33 },34});
Streaming requests bypass hooks
Sandbox.streamCommand and TemplatesApi.followLogs open a persistent
NDJSON connection and go through CreateosSandboxHttp.stream, not the retry loop.
Hooks do not fire for streaming requests — there is no retry to observe
and no clean "done" point for onResponse. Wrap your for await loop in
your own log or metric if you need per-stream tracing.
Reference
CreateosSandboxClientOptions— full constructor options includinghooks.redactHeaders/redactUrl/redactQuery— pure redaction helpers and theSENSITIVE_HEADER_NAMES/SENSITIVE_QUERY_PARAMSconstants.