@guvnor/ts
v2.2.2
Published
TypeScript SDK for Guvnor. Instruments your application with traces and spans, sending events to the Guvnor ingest API in the background without blocking responses.
Readme
@guvnor/ts
TypeScript SDK for Guvnor. Instruments your application with traces and spans, sending events to the Guvnor ingest API in the background without blocking responses.
Installation
bun add @guvnor/tsRemix / React Router 7
Setup
Create a single Guvnor instance and export it from a shared module:
// app/guvnor.server.ts
import { createRemixGuvnor } from '@guvnor/ts'
export const guvnor = createRemixGuvnor({
environment: process.env.NODE_ENV,
identify: async (request) => {
const user = await getUser(request)
return { userId: user.id, plan: user.plan }
},
release: process.env.COMMIT_SHA
})wrap
Wrap your request handler to trace the full request lifecycle. Call it at the
entry point of each route — typically in a root loader or a middleware-style
utility.
// app/routes/dashboard.tsx
import { guvnor } from '~/guvnor.server'
export async function loader({ request }: LoaderFunctionArgs) {
const responseHeaders = new Headers()
return guvnor.wrap(request, responseHeaders, async () => {
const data = await getDashboardData()
return json(data, { headers: responseHeaders })
})
}A root span is created for the request. Its duration and status (ok/error) are
recorded automatically. A session cookie (__guvnor_sid) is set on the first
visit and read on subsequent requests.
middleware (React Router 7)
If you're on React Router 7 with the
unstable_middleware future flag
enabled, you can register Guvnor as a root middleware instead of wiring wrap
into every handler:
// app/root.tsx
import { guvnor } from '~/guvnor.server'
import type { Route } from './+types/root'
export const unstable_middleware: Route.unstable_MiddlewareFunction[] = [
guvnor.middleware()
]The middleware wraps every loader and action under a root http.request span,
attaches the session cookie and correlation header to the response, and
establishes the trace context so guvnor.loader(...), guvnor.action(...), and
guvnor.span(...) all nest under it.
wrap and middleware are mutually exclusive — pick one. wrap also covers
the streaming render in entry.server.tsx; middleware only sees what React
Router's data layer returns, so for SSR-heavy apps wrap still gives the most
complete picture. The middleware API is also marked unstable upstream and may
change between React Router releases.
loader and action
Instrument individual loaders and actions to record them as child spans:
export const loader = guvnor.loader(async function loader({
request
}: LoaderFunctionArgs) {
const data = await getDashboardData()
return json(data)
})
export const action = guvnor.action(async function action({
request
}: ActionFunctionArgs) {
const formData = await request.formData()
await processForm(formData)
return redirect('/dashboard')
})Loader spans are named "loader" and action spans "action". The function name
is recorded in the loader.name / action.name attribute, so naming your
functions is recommended.
span
Instrument arbitrary async work inside a loader, action, or wrap handler:
export const loader = guvnor.loader(async function loader({ request }) {
const rows = await guvnor.span('db.fetchUsers', () =>
db.query('SELECT * FROM users')
)
return json(rows)
})Spans nest correctly — a span called inside a loader will have the loader's
span as its parent.
ignore
Skip instrumentation for specific requests:
const guvnor = createRemixGuvnor({
ignore: (request) => new URL(request.url).pathname.startsWith('/healthcheck')
})captureError
Mark the current span as errored from anywhere within an active trace.
The error is normalized — name, message, stack and the cause
chain are attached as error.* attributes on the span.
export const loader = guvnor.loader(async function loader({ request }) {
try {
return json(await riskyOperation())
} catch (error) {
guvnor.captureError(error)
return json({ error: 'Something went wrong' }, { status: 500 })
}
})Errored spans (whether from a thrown exception or captureError) carry:
| Attribute | Description |
| --------------- | ---------------------------------------------------------------------- |
| error.name | error.name for real Errors, 'NonError' for thrown strings/numbers. |
| error.message | The error's message (or String(thrown) for non-Errors). |
| error.stack | The stack trace, capped at 8 KB. |
| error.cause | JSON-encoded cause chain, up to 5 entries. |
event
Emit a point-in-time event with no duration. Useful for things like "user signed up", a slow background job, or a feature flag override — signals that don't fit cleanly into the start/end model of a span.
guvnor.event('user.signed_up', {
attributes: { plan: 'pro' }
})When called inside wrap / loader / action, the event is correlated
with the active trace (carrying traceId and parentSpanId). When
called outside a trace, the event is free-standing.
Reading the active trace IDs
For correlating logs from a separate logger (Logtail, Pino, console) with the Guvnor trace UI, read the currently active IDs:
import { getCurrentTraceId, getCurrentSpanId } from '@guvnor/ts'
logger.error('mongo query failed', {
err,
traceId: getCurrentTraceId(),
spanId: getCurrentSpanId()
})Both return string | undefined. They're a no-op outside an active
wrap call.
Trace correlation via x-request-id
When upstream services or proxies issue their own request IDs, Guvnor
will reuse them as the trace ID rather than generating a fresh one. By
default Guvnor reads x-request-id. The same header is echoed back on
the response so the browser, upstream, or downstream services can join
on the ID.
const guvnor = createRemixGuvnor({
correlationHeader: ['x-request-id', 'x-logging-correlation-id']
})Accepted shapes: dashed UUID (e.g. 550e8400-e29b-41d4-a716-446655440000)
or 32-hex-no-dash (W3C trace-id format). Anything else is ignored and a
fresh trace ID is generated.
Slow request thresholds and timeouts
Replace hand-rolled SSR watchdogs with built-in slow / timeout tracking:
const guvnor = createRemixGuvnor({
slowThresholdsMs: [5000, 10000, 12000],
timeoutMs: 15000,
onTimeout: (context) => {
// app-specific abort logic, e.g. abort a React renderToPipeableStream
}
})At each slowThresholdsMs value, Guvnor emits a request.slow event
with the elapsed time, the matched threshold, and (on Node) a process
snapshot. At timeoutMs, Guvnor emits a request.timeout event and
marks the root span as errored with http.status_code: 504 (overridable
via timeoutStatusCode). The handler is not aborted — use the
onTimeout hook to wire up app-specific cancellation. All timers are
cleared automatically when the response resolves.
Process snapshots
captureProcessSnapshot() returns { rssMB, heapUsedMB, heapTotalMB,
uptimeSeconds } on Node. It returns undefined on browser/edge
runtimes. The slow/timeout events from above auto-attach a snapshot
unless disableProcessSnapshot: true is set.
import { captureProcessSnapshot } from '@guvnor/ts'
guvnor.event('background-job.stalled', {
attributes: { ...captureProcessSnapshot() }
})Bot detection
Guvnor doesn't take an isbot dependency. Provide your own predicate
via detectBots. When it returns true, the root span gets an
http.is_bot: true attribute.
import { isbot } from 'isbot'
const guvnor = createRemixGuvnor({
detectBots: (request) => isbot(request.headers.get('user-agent') ?? '')
})TypeScript (framework-agnostic)
Setup
import { createGuvnor } from '@guvnor/ts'
const guvnor = createGuvnor({
apiKey: 'gvn_live_…',
environment: 'production',
release: '1.2.3'
})The apiKey comes from your project's settings (Settings → API keys) and is
sent as a Bearer token with every ingest request. Events are sent to
https://ingest.guvnor.io unless you override the host with baseUrl.
span
Wrap any async operation in a named span. If called outside of an active trace context it passes through transparently with no overhead.
const result = await guvnor.span('payment.charge', async () => {
return stripe.charges.create({ amount, currency })
})captureError
Mark the current span as errored:
try {
await guvnor.span('sync.run', () => runSync())
} catch (error) {
guvnor.captureError(error)
throw error
}Options
| Option | Type | Description |
| ------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| apiKey | string | Project API key used to authenticate ingest, sent as a Bearer token with every request. |
| baseUrl | string | Override the ingestion host. Default https://ingest.guvnor.io. |
| correlationHeader | string \| string[] | Header(s) to read incoming trace IDs from. Default ['x-request-id']. The first name is also echoed on responses. |
| detectBots | (request: Request) => boolean | Return true to tag the root span with http.is_bot: true. Guvnor does not bundle an isbot dependency. |
| disableProcessSnapshot | boolean | Disable auto-attached process.* metrics on request.slow and request.timeout events. |
| environment | string | e.g. "production", "development". When set to "development", spans are also logged to the console. |
| identify | (request: Request) => Promise<Record<string, string \| number \| boolean>> | Attach user or session attributes to all spans for a request. Return a userId key to associate spans with a user. |
| ignore | (request: Request) => boolean | Return true to skip instrumentation for a request entirely. |
| onSlow | (context) => void | Called when a slow threshold is crossed. Receives { traceId, spanId, request, attributes, elapsedMs, thresholdMs }. |
| onTimeout | (context) => void | Called when timeoutMs elapses. Use this to wire app-specific cancellation (e.g. abort a React stream). |
| release | string | Release identifier, e.g. a commit SHA or version string. |
| slowThresholdsMs | number[] | Emit a request.slow event after each elapsed threshold. |
| timeoutMs | number | Emit a request.timeout event and mark the root span errored after this many ms. Handler is not aborted by Guvnor. |
| timeoutStatusCode | number | Status code recorded on the timeout-marked root span. Default 504. |
| trustProxyHeaders | boolean | Honor x-forwarded-for / x-real-ip to populate http.client_ip on the root span. |
