EveryDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/iterate/sqlfu/llms.txt
Use this file to discover all available pages before exploring further.
.sql file you check in becomes a generated wrapper whose emitted SqlQuery carries a name field — the camelCase function name matching the symbol you import. A single instrument(client, ...hooks) call routes that name to OpenTelemetry spans, Sentry errors, PostHog events, and Datadog metrics. There are no peer dependencies: hooks are structurally typed so you bring your own SDK.
The name field
For sql/list-profiles.sql the generated file emits:
sql/.generated/list-profiles.sql.ts (generated — do not edit)
sql/users/list-profiles.sql → name: 'usersListProfiles', also the exported function name. Distinct file paths produce distinct names so collisions are impossible.
Ad-hoc SQL via client.sql`...` has no name, but you can pass one explicitly:
instrument helper
instrument(client, ...hooks) wraps a client so every all / run call flows through the hooks in order. Sync clients take sync hooks; async clients take async hooks:
try / catch. For an async client, make the hook async and await execute() inside the same shape. Helper hooks that work with either client kind return a paired QueryExecutionHook:
instrument.otel and instrument.onError are reference implementations. QueryExecutionHook is the stable contract. Copy their bodies and edit them if your team has different conventions.instrument.otel({tracer})
Emits one OTel span per query with:
db.query.summary: yourquery.name, when presentdb.query.text: the parameterized SQL (values are inargs, not interpolated into the text)db.system.name: the adapter’s system (e.g.sqlite)
ERROR.
The tracer parameter is typed structurally (TracerLike), so there is no peer dependency on @opentelemetry/api. Pass any object with a startActiveSpan(name, fn) method. The real OTel Tracer satisfies this by construction.
instrument.onError(report)
Calls report({context, error}) whenever a query throws or its promise rejects, then always rethrows. Errors in the reporter itself are swallowed so they cannot mask the original error:
SqlfuError with a normalized .kind discriminator (unique_violation, missing_table, syntax, and so on). That makes .kind a natural bucketing dimension in your error reporter:
Recipes
- OpenTelemetry
- Sentry
- PostHog
- Datadog (DogStatsD)
Works with any OTLP backend: Honeycomb, Grafana Tempo, New Relic, Datadog APM, or self-hosted Jaeger. Swap the exporter URL for the target backend; the For Datadog APM, either point
instrument.otel line stays identical.@opentelemetry/exporter-trace-otlp-http at Datadog’s OTLP intake, or pass dd-trace’s OTel-compatible tracer directly. Either way the hook call is the same.Caveats
client.raw(sql) is not uniquely identified. raw interpolates values into the SQL text, so per-call distinctness depends on parameter values rather than a stable name. If you need named observability on dynamic SQL, assemble a SqlQuery directly:
iterate and transaction pass through unchanged. Queries issued inside a transaction still fire hooks because the transaction client is re-instrumented on entry. Transactions themselves do not get their own spans. If you want transaction-level spans, wrap client.transaction(...) calls yourself using your tracer.
Composition order is outer-to-inner. instrument(client, a, b, c) means a wraps b wraps c wraps the underlying call. If you put instrument.otel first, the OTel span covers everything including any error-reporter work. You can nest instrument calls if you prefer a different composition order:
Types
All exported fromsqlfu:
instrument— callable, plus.oteland.onErrorSyncQueryExecutionHook,SyncQueryExecutionHookArgsAsyncQueryExecutionHook,AsyncQueryExecutionHookArgsQueryExecutionHook,QueryExecutionHookArgs,QueryExecutionContext,QueryOperationQueryErrorReportTracerLike,SpanLike