Skip to content

feat: add @axiomhq/tanstack-start observability package#403

Draft
gabrielelpidio wants to merge 3 commits intomainfrom
gabriel/tanstack-start-observability-rfc
Draft

feat: add @axiomhq/tanstack-start observability package#403
gabrielelpidio wants to merge 3 commits intomainfrom
gabriel/tanstack-start-observability-rfc

Conversation

@gabrielelpidio
Copy link
Collaborator

@gabrielelpidio gabrielelpidio commented Feb 27, 2026

Send data from TanStack app to Axiom

This PR introduces @axiomhq/tanstack-start and examples/tanstack-start.

The package supports:

  • TanStack Router (SPA navigation events)
  • TanStack Start request middleware
  • TanStack Start function middleware
  • Function request/function correlation
  • Uncaught server-entry errors
  • Proxy ingestion route for client-side events

Prerequisites

  • A dataset in Axiom
  • An API token with ingest permissions
  • A TanStack Router or TanStack Start app

Install @axiomhq/tanstack-start

npm install @axiomhq/js @axiomhq/logging @axiomhq/tanstack-start

Suggested file organization

  • src/lib/axiom/logger.ts: logger instances and transports
  • src/router.tsx: Router setup + SPA observer
  • src/start.ts: Start middleware setup
  • src/routes/api/axiom.ts: optional proxy ingestion route
  • server entry file: uncaught server-entry capture

Set up logger instances

import { Logger, ConsoleTransport } from '@axiomhq/logging'
import { tanStackRouterFormatters, tanStackStartServerFormatters } from '@axiomhq/tanstack-start'

export const routerLogger = new Logger({
  transports: [new ConsoleTransport({ prettyPrint: true })],
  formatters: tanStackRouterFormatters,
})

export const startLogger = new Logger({
  transports: [new ConsoleTransport({ prettyPrint: true })],
  formatters: tanStackStartServerFormatters,
})

Capture SPA navigation events (TanStack Router)

import { observeTanStackRouter } from '@axiomhq/tanstack-start'

const observeRouter = observeTanStackRouter(routerLogger, {
  eventType: 'onResolved',
  source: 'tanstack-router-spa',
})

const unsubscribe = observeRouter(router)

observeTanStackRouter(logger, options?) options:

  • eventType?: keyof RouterEvents (default: onResolved)
  • flushOnNavigation?: boolean (default: false)
  • logUnchangedNavigations?: boolean (default: false)
  • source?: string (default: tanstack-router)
  • getPath?: (router, event) => string | undefined
  • getMessage?: (data, event) => string

Capture server request logs (TanStack Start)

import { createAxiomRequestMiddleware } from '@axiomhq/tanstack-start'

createAxiomRequestMiddleware(createMiddleware, startLogger, {
  include: ['/api/*', '/_server/*'],
  exclude: ['/api/health', '/api/internal/*'],
  shouldLog: (ctx) => ctx.request.method !== 'OPTIONS',
})

createAxiomRequestMiddleware(createMiddleware, logger, config?) options:

  • include?: StartRequestMatcher | StartRequestMatcher[]
  • exclude?: StartRequestMatcher | StartRequestMatcher[]
  • shouldLog?: (context) => boolean | Promise<boolean>
  • logLevelByStatusCode?: (statusCode, data) => LogLevel
  • store?: ServerContextFields | (context) => ServerContextFields | Promise<ServerContextFields>
  • onSuccess?: (data, report) => void | Promise<void>
  • onError?: (data, report) => void | Promise<void>

StartRequestMatcher supports exact string, wildcard prefix (/api/*), RegExp, and callback function.


Capture server function logs (TanStack Start)

import { createAxiomMiddleware } from '@axiomhq/tanstack-start'

createAxiomMiddleware(createMiddleware, startLogger, {
  includeData: false,
  correlation: true,
})

createAxiomMiddleware(createMiddleware, logger, config?) options:

  • includeData?: boolean (default: false)
  • correlation?: boolean | StartFunctionCorrelationMiddlewareConfig (default: false)
  • store?: ServerContextFields | (context) => ServerContextFields | Promise<ServerContextFields>
  • onSuccess?: (data, report) => void | Promise<void>
  • onError?: (data, report) => void | Promise<void>

StartFunctionCorrelationMiddlewareConfig options:

  • headerName?: string (default: x-axiom-correlation-id)
  • contextKey?: string (default: axiom_correlation_id)
  • createRequestId?: () => string

With correlation enabled, request middleware and function middleware logs share the same request_id for one operation.


Standalone function correlation middleware

import { createAxiomFunctionCorrelationMiddleware, createAxiomMiddleware } from '@axiomhq/tanstack-start'

functionMiddleware: [
  createAxiomFunctionCorrelationMiddleware(createMiddleware),
  createAxiomMiddleware(createMiddleware, startLogger),
]

Capture uncaught server-entry errors

import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
import { captureError } from '@axiomhq/tanstack-start'

export default createServerEntry({
  fetch: captureError(handler.fetch, startLogger, {
    phase: 'server-entry',
    rethrow: true,
  }),
})

captureError(handler, logger, config?) options:

  • source?: string (default: tanstack-start-uncaught)
  • phase?: 'server-entry' | 'start-handler' | 'process' (default: server-entry)
  • rethrow?: boolean (default: true)
  • onError?: (data, report) => void | Promise<void>
  • createResponse?: (data) => Response | Promise<Response>

Add a proxy route for client-side ingestion

import { createFileRoute } from '@tanstack/react-router'
import { createAxiomProxyHandler } from '@axiomhq/tanstack-start'

const proxyHandler = createAxiomProxyHandler(startLogger)

export const Route = createFileRoute('/api/axiom')({
  server: {
    handlers: {
      POST: ({ request }) => proxyHandler(request),
    },
  },
})

createAxiomProxyHandler(logger, config?) options:

  • onSuccess?: (events) => void | Promise<void>
  • onError?: (error) => void | Promise<void>

Request body must be a JSON array of log events.


AsyncLocalStorage

AsyncLocalStorage is used for server execution context so logs emitted in async call chains keep request-scoped fields such as request_id.

If AsyncLocalStorage is unavailable, logging still works, but context propagation is limited.


Works in both modes

Router SPA only:

  • use observeTanStackRouter

Full TanStack Start:

  • use request middleware, function middleware, server-entry error capture, and optional proxy route helper

Validation in this PR

  • pnpm --filter @axiomhq/tanstack-start lint
  • pnpm --filter @axiomhq/tanstack-start build
  • pnpm --filter @axiomhq/tanstack-start test
  • pnpm --filter tanstack-start build

All pass.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 27, 2026

Open in StackBlitz

@axiomhq/js

npm i https://pkg.pr.new/axiomhq/axiom-js/@axiomhq/js@403

@axiomhq/logging

npm i https://pkg.pr.new/axiomhq/axiom-js/@axiomhq/logging@403

@axiomhq/nextjs

npm i https://pkg.pr.new/axiomhq/axiom-js/@axiomhq/nextjs@403

@axiomhq/pino

npm i https://pkg.pr.new/axiomhq/axiom-js/@axiomhq/pino@403

@axiomhq/react

npm i https://pkg.pr.new/axiomhq/axiom-js/@axiomhq/react@403

@axiomhq/tanstack-start

npm i https://pkg.pr.new/axiomhq/axiom-js/@axiomhq/tanstack-start@403

@axiomhq/winston

npm i https://pkg.pr.new/axiomhq/axiom-js/@axiomhq/winston@403

commit: ababba3

@gabrielelpidio gabrielelpidio marked this pull request as draft February 27, 2026 03:27
@thesollyz
Copy link
Collaborator

looks great, thanks for the initiative! overall satisfied with sdk but have few comments:

  • The docs and examples can guide the users to which files should these functions live in. Basically, organizing those for the users as we do in the react and next.js packages.
  • The import path is redundant @axiomhq/tanstack-start/start, the last start can be removed
  • Avoid words like AxiomStart, simplify the naming. e.g: this withAxiomStartErrorCapture() can be captureError(). createAxiomStartFunctionMiddleware => createAxiomMiddleware

@gabrielelpidio
Copy link
Collaborator Author

@thesollyz addressed your feedback in the latest commits:

  • Added clearer file-organization guidance in the package README (logger/router/start/proxy/server-entry placement).
  • Removed redundant import paths in docs/examples by exporting router/start APIs from the package root (@axiomhq/tanstack-start).
  • Added concise helper aliases for app usage while keeping existing explicit APIs for compatibility:
    • createAxiomRequestMiddleware
    • createAxiomMiddleware
    • createAxiomFunctionCorrelationMiddleware
    • createAxiomProxyHandler
    • createAxiomUncaughtErrorHandler
    • captureError
  • Updated the PR description examples to use root imports and concise names.

Also aligned observeTanStackRouter to the same factory style: observeTanStackRouter(logger, options)(router).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants