Debug your app
Go from an error to a fix with local logs, error codes and mainsdk logs, then debug production with live tail, debug sessions, replay and triage. Explain, driver tracing and OpenTelemetry export are planned.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
Find out why a request failed and fix it, using only local tools. Every error and log MainSDK writes carries a stable code, the file and line in your code, the main.yaml rule involved and a fix, so you or your coding agent can act on it directly.
Before you start: a MainSDK app that runs locally with mainsdk dev. Steps 1 to 4 are free and local; nothing leaves your machine.
1. Find the error in the local logs
By default records print to the terminal. To keep them for mainsdk logs, add a file sink once; it writes JSON lines to .mainsdk/events/ (git-ignored). Passing sinks replaces the terminal output.
import { configure, fileSink } from "mainsdk";
configure({ sinks: [fileSink(".mainsdk/events/app.jsonl")] });npx mainsdk logs --level error2026-11-12T10:04:31.208Z ERROR This record belongs to another ownerTo watch new events, or to see every record from one request (records carry attributes.trace_id inside a request with a trace):
npx mainsdk logs --follow
npx mainsdk logs --trace 4bf92f3577b34da6a3ce929d0e0e4736 --json--json prints the stored record, which includes the code and fix of a MainError.
2. Look up the code
Find the code in the error codes reference for its cause and fix; the fix is also in the record from step 1. A problem response carries the same code and fix for your client. mainsdk explain (offline lookup by code or trace id) is planned.
MAIN-DATA-0007 · This record belongs to another owner · 403
fix: Read orders through db.orders.for(session.user) so users only reach their own.Your coding agent reads the same codes and fixes; see Use MainSDK with your coding agent.
3. Fix the code
MAIN-DATA-0007 means the owner scope did its job: the order route refused a record that belongs to someone else. The question is why the user asked for it. Follow the trace back to the list endpoint, which still reads orders through the escape hatch, so it returns every user's orders:
import { auth } from "mainsdk";
declare const sql: { query(text: string): Promise<unknown[]> }; // your own SQL client
export async function GET(req: Request) {
await auth.require(req);
const orders = await sql.query("select * from orders limit 20"); // no owner scope
return Response.json(orders);
}Read through the scoped model instead, so the list only holds this user's orders:
import { auth, models, s } from "mainsdk";
const { orders } = models({
orders: s.object({ item: s.string(), total: s.int() }),
});
export async function GET(req: Request) {
const session = await auth.require(req);
const page = await orders.for(session.user).list({ limit: 20 });
return Response.json(page);
}A query through models without .for(user) does not compile, so look for other places that read orders through your own SQL client.
4. Look at the app's state (when the cause is a driver or a dependency)
npx mainsdk info
npx mainsdk statusinfo lists each interface with its driver, the network destinations and the health and resource settings. status shows dependencies, which are critical, and resource pressure. Tracing each provider call with MAINSDK_DEBUG=driver is planned.
5. Verify the fix
Repeat what the user did while following the error log:
npx mainsdk logs --level error --followOpen the order list again: it shows only your own orders, and no new MAIN-DATA-0007 line appears. Add a test so it stays fixed; see Test your app.
Send telemetry to your own OpenTelemetry backend
Planned: OTLP export of logs, traces and metrics to any endpoint, redacted in process, through a telemetry block in main.yaml. It is not built, and main.yaml has no telemetry key yet. Until then, give configure() a sink of your own: a Sink is a function (kind, record) => void that receives each redacted record.
Debug production with MainSDK Cloud
Send logs to the Cloud with the sink from mainsdk/feedback. It sends nothing until you are logged in and have set telemetry.to: [mainsdk] or MAINSDK_TELEMETRY=1, and it replaces emails, bearer tokens and key formats with [redacted] before anything leaves the process.
import { cloudSink } from "mainsdk/feedback";
declare const session: { exists(): boolean };
declare const config: { telemetry: { to: string[] } };
declare const client: { emit: Parameters<typeof cloudSink>[0]["emit"] };
export const sink = cloudSink({
loggedIn: () => session.exists(),
to: () => config.telemetry.to,
flag: () => process.env.MAINSDK_TELEMETRY,
emit: (event) => client.emit(event),
});| Task | Command | Plan |
|---|---|---|
| Live tail from production, for one user, trace or level | mainsdk logs --follow --env prod --user u_8f3 | Free; asks for a fresh passkey |
| Raise detail for one user, request or release, for up to 60 minutes; it switches itself off | mainsdk debug start --user u_8f3 --for 30m | Teams |
| End a debug session early | mainsdk debug stop <session-id> | Teams |
| Replay a failed request locally on fake drivers | mainsdk replay 4bf92f3577b34da6a3ce929d0e0e4736 | Teams |
| Install the GitHub app for the project | mainsdk triage connect github --installation <id> | Free |
| Group errors, find the release and commit, open one GitHub issue per group | mainsdk triage run | Teams (hosted) |
Open a draft fix pull request that waits for check --json to pass | mainsdk triage fix <issue> --head <branch> | Free |
A session names one user, one request or one release and lasts at most 60 minutes, so --for 90m runs for 60. The app reads the active sessions with its ingest key and keeps records below its log level only for the target, through the raise option of the log:
import { Log } from "mainsdk";
import { raiseFor, type DebugSession } from "mainsdk/feedback";
declare const active: DebugSession[]; // the sessions your app read with its ingest key
export const log = new Log({ raise: raiseFor({ sessions: () => active, now: () => Date.now() }) });Nothing is redeployed. A session ends itself on time, and its start and end are written to an audit record. Records stay redacted.
Replay needs a file, mainsdk.replay.ts, that exports a function for each action you want to replay. It runs the recorded input on the test app, where every interface is a fake, and any call that tries to reach the network is blocked and counted:
declare function refund(input: unknown, app: unknown): unknown;
export default {
"orders.refund": (input: unknown, app: unknown) => refund(input, app),
};mainsdk replay 4bf92f3577b34da6a3ce929d0e0e4736orders.refund: reproduced MAIN-DATA-0007; 0 outbound callsOn Free, mainsdk debug start and mainsdk replay get HTTP 402 from the Cloud with a link to Teams, and your app is unaffected. See Collect customer reports for the report button that arrives with its trace.
See where telemetry goes
npx mainsdk telemetry status --jsonIt shows the destinations telemetry is sent to and whether MainSDK Cloud may receive it. Nothing is sent unless you declared it.
Next
Was this page helpful?