@meet-sudo/sdk
v0.5.1
Published
Sudo Server SDK — change-aware observability for Node.js backends
Maintainers
Readme
@meet-sudo/sdk
Change-aware observability for Node.js backends. Auto-capture HTTP requests, then add first-class model_call and job instrumentation so Sudo can explain what changed and what regressed.
Install
npm install @meet-sudo/sdkUse your server-side workspace key (sk_...) when creating the SDK client. The env var name is up to you; the examples below use SUDO_API_KEY.
Four integration paths
Tier 1 — Auto-capture middleware (zero effort)
Add one line and Sudo captures every HTTP request, response status, latency, and unhandled error.
Express
import express from "express";
import { SudoServer, sudoExpress } from "@meet-sudo/sdk";
const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });
const app = express();
const sudo = sudoExpress({ sudo: ms, service: "api" });
app.use(sudo.middleware);
// ... your routes ...
app.use(sudo.errorHandler);
app.listen(3000);Fastify
import Fastify from "fastify";
import { SudoServer } from "@meet-sudo/sdk";
import { sudoFastify } from "@meet-sudo/sdk/middleware/fastify";
const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });
const app = Fastify();
app.register(sudoFastify, { sudo: ms, service: "api" });
app.listen({ port: 3000 });Koa
import Koa from "koa";
import { SudoServer } from "@meet-sudo/sdk";
import { sudoKoa } from "@meet-sudo/sdk/middleware/koa";
const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });
const app = new Koa();
const sudo = sudoKoa({ sudo: ms, service: "api" });
app.use(sudo.middleware);
app.listen(3000);Next.js (App Router)
Assume @/lib/meetsudo exports your initialized SudoServer instance.
// app/api/users/route.ts
import { ms } from "@/lib/meetsudo";
import { withSudo } from "@meet-sudo/sdk";
import { NextResponse } from "next/server";
export const GET = withSudo(ms, { service: "api" }, async (req) => {
const users = await db.users.findMany();
return NextResponse.json(users);
});The middleware auto-captures: method, path, status, duration (ms), user agent, IP, and request ID. User ID is extracted from req.user or req.auth if present. Errors include message and stack trace. These logs are explicitly classified as event_class="http" so Sudo can reason about request health without inferring from raw props.
Tier 2 — Direct logging (manual instrumentation)
For business events, background jobs, cron tasks, and anything the middleware can't see.
await ms.error("Stripe returned 402", {
service: "api",
event: "checkout_failed",
user_id: "user_123",
props: { duration_ms: 742, amount: 99 },
});
await ms.warn("Worker retries increased", {
service: "worker",
event: "billing_sync_retry",
event_class: "job",
props: { retry_count: 3, queue_name: "critical" },
});
await ms.info("Deploy completed", { service: "deploy" });
await ms.debug("Cache miss", { service: "cache" });For custom non-HTTP workloads, set event_class explicitly. Otherwise Sudo may store the log row but exclude it from Change Impact and SLO analysis.
Tier 3 — Native AI and job instrumentation
For the signal types that matter most to Sudo's regression engine, use the first-class helpers instead of generic logs.
await ms.modelCall({
service: "assistant-api",
model: "claude-sonnet-4-20250514",
provider: "anthropic",
operation: "messages",
prompt_version: "support-v12",
duration_ms: 842,
ttft_ms: 220,
tokens_in: 1240,
tokens_out: 311,
cost_usd: 0.0182,
request_id: "req_123",
});
await ms.jobRun({
service: "worker",
job_name: "sync_customer_state",
queue_name: "critical",
duration_ms: 1934,
retry_count: 1,
status: "success",
});You can also time work automatically:
const completion = await ms.withModelCall({
service: "assistant-api",
model: "claude-sonnet-4-20250514",
provider: "anthropic",
operation: "chat_completion",
prompt_version: "onboarding-v8",
request_id: "req_123",
}, async () => {
return anthropic.messages.create({ /* ... */ });
});
await ms.withJobRun({
service: "worker",
job_name: "rebuild_embeddings",
queue_name: "embeddings",
}, async () => {
await rebuildEmbeddingsForWorkspace(workspaceId);
});For the common providers, you can let Sudo extract usage fields from the response automatically. Anthropic is shown first because that is the best-supported path for Sudo today; OpenAI remains available as an optional wrapper.
const message = await ms.withAnthropicModelCall({
service: "assistant-api",
prompt_version: "support-v12",
model: "claude-sonnet-4-20250514", // optional on success, useful for error cases
}, async () => {
return anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 400,
messages: [{ role: "user", content: "Summarize the latest checkout failures" }],
});
});
const response = await ms.withOpenAIModelCall({
service: "assistant-api",
prompt_version: "onboarding-v8",
model: "gpt-5.4-mini",
}, async () => {
return openai.responses.create({
model: "gpt-5.4-mini",
input: "Explain the regression",
});
});Those wrappers time the call and, on success, auto-fill:
providermodeltokens_intokens_out- common provider metadata like
response_idorstop_reason
For job runtimes, you can wrap the processor instead of hand-writing jobRun(...) calls:
BullMQ
import { Worker } from "bullmq";
import { SudoServer, instrumentBullMQProcessor } from "@meet-sudo/sdk";
const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });
new Worker(
"critical",
instrumentBullMQProcessor(
{ sudo: ms, service: "worker", worker: "billing-worker" },
async (job) => {
await syncCustomerState(job.data);
}
)
);Trigger.dev
import { task } from "@trigger.dev/sdk/v3";
import { SudoServer, instrumentTriggerRun } from "@meet-sudo/sdk";
const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });
export const rebuildEmbeddings = task({
id: "rebuild-embeddings",
run: instrumentTriggerRun(
{ sudo: ms, service: "worker" },
async (payload, ctx) => {
await rebuildEmbeddingsForWorkspace(payload.workspaceId);
}
),
});Those adapters automatically capture:
- job or task name
- queue name when available
- attempt number
- duration
- success vs failure
- run or job ID when available
Tier 4 — Logger transports (existing logger integration)
Already using Pino or Winston? Add Sudo as a transport — zero changes to your existing logging code.
Pino
import pino from "pino";
import { SudoServer, createPinoTransport } from "@meet-sudo/sdk";
const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });
const logger = pino({ level: "info" }, pino.multistream([
{ stream: process.stdout },
{ stream: createPinoTransport({ sudo: ms, service: "api" }) },
]));Winston
import winston from "winston";
import { SudoServer, createWinstonTransport } from "@meet-sudo/sdk";
const ms = new SudoServer({ apiKey: process.env.SUDO_API_KEY! });
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
new winston.transports.Stream({
stream: createWinstonTransport({ sudo: ms, service: "api" }),
}),
],
});Change markers
// Mark a deploy
await ms.markDeploy({
version: "2026.04.03.1",
scope: "assistant-api",
metadata: { commit: "4b72f0a" },
});You can also record prompt and model changes:
await ms.markPromptVersion({
version: "support-v12",
scope: "assistant-api",
});
await ms.markModelVersion({
version: "claude-sonnet-4-20250514",
scope: "assistant-api",
});Middleware configuration
All middleware accepts these options:
| Option | Default | Description |
|--------|---------|-------------|
| sudo | required | MeetSudoServer instance |
| service | undefined | Service name for logs (e.g. "api") |
| ignorePaths | ["/health", "/healthz", ...] | Paths to skip |
| extractUserId | Auto-detect from req.user/req.auth | Custom user ID extractor |
| errorStatusThreshold | 500 | Min status code logged as error |
| batchSize | 25 | Logs buffered before flush |
| flushInterval | 5000 | Max ms before flushing |
License
MIT
