@mattman240/mission-control
v0.2.3
Published
AI-oriented implementation guide for wiring Mission Control into an app at every layer.
Downloads
867
Readme
Mission Control SDK
AI-oriented implementation guide for wiring Mission Control into an app at every layer.
This README is intentionally optimized for coding agents and fast project integration.
What This SDK Does
The SDK sends batched observability events to Mission Control through the Convex HTTP Actions endpoint:
https://bold-dolphin-472.convex.site/api/ingest
It supports:
- browser logging
- Node/server logging
- React error boundary reporting
- global browser error capture
- Next.js route handler wrapping
- Convex-safe capture helpers
- batching and retry
- source tagging for
client,server,convex, anddb
Public API
import {
MissionControlErrorBoundary,
createMissionControlConvexCaptureHandler,
createMissionControlConvexLogger,
initMissionControl,
instrumentBrowserErrors,
withMissionControlConvexMutation,
withMissionControlRouteHandler
} from "@mattman240/mission-control";Primary exports:
initMissionControl(config)missionControl.createLogger({ source, metadata? })logger.info(message, metadata?)logger.warn(message, metadata?)logger.error(message, metadata?)logger.debug(message, metadata?)logger.captureException(error, metadata?)instrumentBrowserErrors(missionControl)<MissionControlErrorBoundary />withMissionControlRouteHandler(missionControl, handler, options?)createMissionControlConvexCaptureHandler(missionControl)createMissionControlConvexLogger({ scheduler, captureReference, metadata? })withMissionControlConvexMutation(handler, options)withMissionControlConvexAction(handler, options)
Config Shape
type MissionControlConfig = {
apiKey: string;
endpoint?: string;
flushIntervalMs?: number;
maxBatchSize?: number;
retryAttempts?: number;
retryBaseDelayMs?: number;
fetch?: typeof fetch;
headers?: Record<string, string>;
defaultMetadata?: Record<string, string | number | boolean | null>;
};Required:
apiKey
Usually optional:
endpointOverride this only if you want to point the SDK at a different Mission Control deployment or a local proxy. By default the SDK sends tohttps://bold-dolphin-472.convex.site/api/ingest.
Default Behavior
- events are queued in memory
- batches flush every
5000ms - max batch size is
25 - retry attempts default to
3 - exponential backoff starts at
500ms - browser page exit attempts a flush
- client/server/convex helpers preserve the correct
source
Fastest Integration Order
If an AI agent is implementing this into a project, do it in this order:
- Create one shared
missionControlclient instance. - Add browser/global error instrumentation.
- Add a React error boundary near the app root.
- Wrap Next.js route handlers.
- Add the Convex capture action and wrappers.
- Add targeted loggers where business events matter.
Canonical Setup
Create one shared instance:
import { initMissionControl } from "@mattman240/mission-control";
export const missionControl = initMissionControl({
apiKey: process.env.NEXT_PUBLIC_MISSION_CONTROL_API_KEY ?? "",
defaultMetadata: {
service: "mission-control-web",
runtime: typeof window === "undefined" ? "server" : "browser"
}
});Important:
- create this once per runtime, not once per request or component render
- let the default Convex HTTP Actions endpoint handle ingestion unless you explicitly need an override
- for older Node runtimes without global
fetch, passfetchexplicitly
Browser Layer
Global browser errors
import { instrumentBrowserErrors } from "@mattman240/mission-control";
import { missionControl } from "./mission-control";
instrumentBrowserErrors(missionControl);This captures:
window.onerrorunhandledrejection
All of these are tagged with source: "client".
Manual browser logger
const logger = missionControl.createLogger({
source: "client",
metadata: {
area: "checkout"
}
});
logger.info("Checkout opened", { plan: "pro" });
logger.warn("Payment form slow", { durationMs: 1200 });
logger.error("Payment submit failed", { provider: "stripe" });
logger.debug("Checkout render metrics", { widgets: 4 });React Layer
Wrap your app with the provided error boundary:
import { MissionControlErrorBoundary } from "@mattman240/mission-control";
import { missionControl } from "./mission-control";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<MissionControlErrorBoundary
missionControl={missionControl}
fallback={<div>Something went wrong.</div>}
metadata={{ boundary: "root-app" }}
>
{children}
</MissionControlErrorBoundary>
);
}This captures React render/lifecycle errors as:
source: "client"level: "error"
Next.js Server Layer
Wrap route handlers with withMissionControlRouteHandler.
import { withMissionControlRouteHandler } from "@mattman240/mission-control";
import { missionControl } from "@/lib/mission-control";
async function getHealth() {
return Response.json({ ok: true });
}
export const GET = withMissionControlRouteHandler(missionControl, async (request) => {
return getHealth();
});If the route throws, the SDK:
- captures the exception
- tags it with
source: "server" - includes request method and URL
- flushes before rethrowing
With extra metadata:
export const POST = withMissionControlRouteHandler(
missionControl,
async (request) => {
throw new Error("Webhook failed");
},
{
metadata: (request) => ({
route: "/api/webhooks/stripe",
method: request.method
})
}
);Convex Layer
Convex support uses a scheduled internal action, not direct network transport or timer-based batching inside a mutation or action.
Why:
- Convex mutations cannot use timers
- Convex mutations cannot make arbitrary external network calls
- a failed mutation rolls back its writes
The SDK’s safe Convex pattern is:
- define one internal capture action
- build its handler with
createMissionControlConvexCaptureHandler(...) - wrap thrown-error capture with
withMissionControlConvexMutation(...)orwithMissionControlConvexAction(...) - for non-throwing Convex logs, use
createMissionControlConvexLogger(...) - pass the internal action reference as
captureReference
Important:
- do not call
missionControl.createLogger(...)inside Convex code - do not log from Convex module scope during import time
- schedule Convex-originated events through
ctx.scheduler.runAfter(...)only
Convex setup
import { v } from "convex/values";
import { api, internal } from "./_generated/api";
import { internalAction, mutation } from "./_generated/server";
import {
createMissionControlConvexCaptureHandler,
createMissionControlConvexLogger,
initMissionControl,
withMissionControlConvexMutation
} from "@mattman240/mission-control";
const missionControl = initMissionControl({
apiKey: process.env.MISSION_CONTROL_API_KEY ?? ""
});
export const ingestCapturedConvexError = internalAction({
args: {
message: v.string(),
metadata: v.any(),
timestamp: v.string()
},
handler: createMissionControlConvexCaptureHandler(missionControl)
});
export const updateStatus = mutation({
args: {},
handler: withMissionControlConvexMutation(
async (ctx) => {
const logger = createMissionControlConvexLogger({
scheduler: ctx.scheduler,
captureReference: internal.missionControl.ingestCapturedConvexError,
metadata: {
feature: "status-sync"
}
});
await logger.info("Status sync started");
throw new Error("Mutation failed");
},
{
captureReference: internal.missionControl.ingestCapturedConvexError,
name: "updateStatus",
metadata: () => ({
feature: "status-sync"
})
}
)
});Events captured this way should be tagged as:
source: "convex"
If you need business-event logging from Convex, use the Convex logger:
const logger = createMissionControlConvexLogger({
scheduler: ctx.scheduler,
captureReference: internal.missionControl.ingestCapturedConvexError,
metadata: {
job: "reconcileSubscriptions"
}
});
await logger.info("Subscription reconciliation started");
await logger.warn("Stripe returned stale state", { subscriptionId });Server and Worker Logging
For non-Next Node code, use the plain logger:
const logger = missionControl.createLogger({
source: "server",
metadata: {
worker: "email-dispatch"
}
});
logger.info("Worker started");
logger.captureException(new Error("SMTP timeout"), {
provider: "postmark"
});If you are logging database-adjacent operational events from scripts or workers:
const dbLogger = missionControl.createLogger({
source: "db",
metadata: {
cluster: "primary"
}
});
dbLogger.warn("Replication lag elevated", { lagMs: 2400 });Event Shape Sent To Mission Control
type IngestEvent = {
environment?: "production" | "staging" | "development";
source: "client" | "server" | "convex" | "db";
level: "debug" | "info" | "warn" | "error";
message: string;
metadata: Record<string, string | number | boolean | null>;
timestamp?: string;
};Error Grouping Notes
Mission Control groups errors by fingerprint:
message + stack
To make grouping useful, ensure thrown errors preserve a stable message and stack.
Best practice:
- use
captureException(error)with a realErrorobject - do not stringify errors before passing them in
- include contextual metadata like
route,job,workerId, ortenantId
AI Implementation Rules
If an AI agent is integrating this SDK into another codebase, follow these rules:
- Do not create multiple client instances in the same runtime unless environments are intentionally separate.
- Use
source: "client"only for browser/React events. - Use
source: "server"for Next.js routes, API handlers, Node jobs, and server utilities. - Use
source: "convex"only for errors that originate in Convex. - Use
source: "db"only for database or replica/connection/latency style operational events. - Prefer
captureException(error)overerror("...", { stack: ... })when an actual exception exists. - Wrap framework boundaries first, then add manual business logging.
- Always set
apiKeyfrom deployment-specific configuration, not hardcoded literals in production. - For Convex, always provide a
captureReferenceinternal action to the SDK wrappers.
Recommended File Layout In A Consumer App
For a Next.js app:
src/
lib/
mission-control.ts
components/
providers.tsxSuggested responsibilities:
src/lib/mission-control.tsExport the shared SDK client instance.src/components/providers.tsxInstallinstrumentBrowserErrorsand wrap app UI withMissionControlErrorBoundary.app/api/*/route.tsWrap route handlers withwithMissionControlRouteHandler.convex/*.tsDefine one internal capture action, then pass its reference to the Convex wrappers.
Common Mistakes
- overriding
endpointwhen the default Convex ingestion URL would already work - creating the SDK client inside React components
- swallowing exceptions after logging them when framework semantics expect rethrow
- passing
undefinedmetadata values and expecting them to persist - using the wrong
source - forgetting to define and pass
captureReferencefor Convex wrappers
Minimal End-To-End Example
import {
MissionControlErrorBoundary,
initMissionControl,
instrumentBrowserErrors,
withMissionControlRouteHandler
} from "@mattman240/mission-control";
export const missionControl = initMissionControl({
apiKey: process.env.NEXT_PUBLIC_MISSION_CONTROL_API_KEY ?? ""
});
instrumentBrowserErrors(missionControl);
export const GET = withMissionControlRouteHandler(missionControl, async () => {
throw new Error("boom");
});Verification Checklist
After integration, verify:
- browser console errors show up in Mission Control
- rejected promises show up in Mission Control
- React render errors show up in Mission Control
- thrown Next route errors show up as
source: "server" - thrown Convex errors show up as
source: "convex"through the capture action - manual logs show up with expected metadata
- batched events arrive through
https://bold-dolphin-472.convex.site/api/ingest
