@guvnor/ts
v2.1.1
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.
Downloads
171
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.
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({
environment: 'production',
release: '1.2.3'
})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 |
| ------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| 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. |
