Skip to content

Sentry Integration ​

Set up Sentry for error tracking, performance monitoring, and distributed tracing in your KickJS application.

Installation ​

pnpm add @sentry/node

Quick Setup ​

Create a Sentry adapter that hooks into the KickJS lifecycle:

ts
// src/adapters/sentry.adapter.ts
import * as Sentry from '@sentry/node'
import { defineAdapter, type AdapterContext } from '@forinda/kickjs'

export interface SentryAdapterOptions {
  /** Sentry DSN from your project settings */
  dsn: string
  /** Environment name (default: NODE_ENV) */
  environment?: string
  /** Sample rate for performance tracing (0.0 to 1.0, default: 1.0 in dev, 0.1 in prod) */
  tracesSampleRate?: number
  /** Enable debug logging (default: false) */
  debug?: boolean
}

export const SentryAdapter = defineAdapter<SentryAdapterOptions>({
  name: 'SentryAdapter',
  build: (options) => ({
    beforeMount({ app, env, isProduction }: AdapterContext): void {
      Sentry.init({
        dsn: options.dsn,
        environment: options.environment ?? env,
        tracesSampleRate: options.tracesSampleRate ?? (isProduction ? 0.1 : 1.0),
        debug: options.debug ?? false,
        integrations: [
          // Automatically instrument Express routes
          Sentry.expressIntegration(),
        ],
      })

      // Sentry request handler must be the first middleware
      app.use(Sentry.expressRequestHandler())
    },

    middleware() {
      return [
        {
          // Sentry error handler runs after routes but before KickJS error handler
          handler: Sentry.expressErrorHandler(),
          phase: 'afterRoutes' as const,
        },
      ]
    },

    async shutdown(): Promise<void> {
      // Flush pending events before process exit
      await Sentry.close(2000)
    },
  }),
})

Bootstrap ​

ts
// src/index.ts
import 'reflect-metadata'
import express from 'express'
import { bootstrap, helmet, cors, requestId, requestLogger } from '@forinda/kickjs'
import { SwaggerAdapter } from '@forinda/kickjs-swagger'
import { loadEnv } from '@forinda/kickjs-config'
import { SentryAdapter } from './adapters/sentry.adapter'
import { modules } from './modules'

const env = loadEnv()

bootstrap({
  modules,
  adapters: [
    SentryAdapter({
      dsn: env.SENTRY_DSN,
      tracesSampleRate: 0.1,
    }),
    SwaggerAdapter({
      info: { title: 'My API', version: '1.0.0' },
    }),
  ],
  middlewares: [helmet(), cors(), requestId(), requestLogger(), express.json()],
})

Environment Variables ​

bash
# .env
SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0

Add to your Zod env schema:

ts
// src/config/env.ts
import { defineEnv, loadEnv } from '@forinda/kickjs-config'
import { z } from 'zod'

const envSchema = defineEnv((base) =>
  base.extend({
    SENTRY_DSN: z.string().url().optional(),
  }),
)

export const env = loadEnv(envSchema)

Capturing Errors in Controllers ​

Give the adapter an onError hook. The framework calls it for every error — request errors (any status), uncaught exceptions, unhandled rejections, failed waitUntil work and failed queue jobs — before the error handler answers, on every runtime. See Observing Errors and Responses.

ts
// inside the SentryAdapter's build()
onError(error, info) {
  Sentry.withScope((scope) => {
    scope.setTag('source', info.source)
    if (info.route) scope.setTag('route', info.route)
    if (info.requestId) scope.setTag('requestId', info.requestId)
    if (info.method) scope.setTag('method', info.method)

    if (info.source === 'request' && (info.status ?? 500) < 500) {
      // Client errors (4xx) — a breadcrumb, not an issue
      Sentry.addBreadcrumb({
        message: error instanceof Error ? error.message : String(error),
        category: 'http',
        level: 'warning',
        data: { status: info.status, path: info.path },
      })
      return
    }
    Sentry.captureException(error)
  })
},

Nothing to add to middlewares: an error middleware registered there runs before the routes, so it never sees their errors.

Adding Context to Errors ​

Use Sentry scopes in your services to add business context:

ts
import * as Sentry from '@sentry/node'
import { Service } from '@forinda/kickjs'

@Service()
export class PaymentService {
  async charge(userId: string, amount: number) {
    return Sentry.startSpan({ name: 'payment.charge', op: 'payment' }, async () => {
      Sentry.setUser({ id: userId })
      Sentry.setTag('payment.amount', String(amount))

      try {
        const result = await this.gateway.charge(userId, amount)
        return result
      } catch (err) {
        Sentry.setExtra('failedAmount', amount)
        throw err // Will be captured by the error middleware
      }
    })
  }
}

Performance Monitoring ​

Sentry automatically traces Express routes. To add custom spans for database queries or external calls:

ts
import * as Sentry from '@sentry/node'

@Service()
export class UserRepository {
  async findById(id: string) {
    return Sentry.startSpan(
      {
        name: 'db.user.findById',
        op: 'db.query',
        attributes: { 'db.statement': 'SELECT * FROM users WHERE id = ?' },
      },
      async () => {
        return this.db.user.findUnique({ where: { id } })
      },
    )
  }
}

Distributed Tracing with RequestContext ​

Connect Sentry traces with KickJS's request ID:

ts
// src/middleware/sentry-context.middleware.ts
import * as Sentry from '@sentry/node'
import type { Request, Response, NextFunction } from 'express'

export function sentryContext() {
  return (req: Request, res: Response, next: NextFunction) => {
    const requestId = (req as any).requestId || req.headers['x-request-id']

    if (requestId) {
      Sentry.setTag('request_id', requestId)
    }

    next()
  }
}

Add after requestId() in the middleware pipeline:

ts
middlewares: [
  requestId(),
  sentryContext(),  // Links request ID to Sentry traces
  requestLogger(),
  express.json(),
],

Conditional Setup ​

Only enable Sentry when a DSN is configured:

ts
const env = loadEnv(envSchema)

const adapters = [
  SwaggerAdapter({ info: { title: 'My API', version: '1.0.0' } }),
]

if (env.SENTRY_DSN) {
  adapters.unshift(
    SentryAdapter({
      dsn: env.SENTRY_DSN,
      // tracesSampleRate uses isProduction from AdapterContext automatically
    }),
  )
}

bootstrap({ modules, adapters, middlewares: [...] })

Source Maps ​

For readable stack traces in production, upload source maps during your build:

pnpm add -D @sentry/cli
bash
# After kick build
npx sentry-cli sourcemaps upload \
  --org your-org \
  --project your-project \
  --release v$(node -p "require('./package.json').version") \
  dist/

Or add to your CI pipeline:

yaml
- name: Upload source maps to Sentry
  run: npx sentry-cli sourcemaps upload --org $ORG --project $PROJECT --release $VERSION dist/
  env:
    SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

Testing ​

In tests, Sentry is not initialized (no DSN), so it's a no-op. No special mocking needed:

ts
const { expressApp } = await createTestApp({
  modules: [UserModule],
  // No SentryAdapter — errors go to KickJS error handler only
})

Configuration Reference ​

OptionTypeDefaultDescription
dsnstringrequiredSentry project DSN
environmentstringNODE_ENVEnvironment tag
tracesSampleRatenumber1.0 dev / 0.1 prodPerformance sampling rate
debugbooleanfalseEnable Sentry debug logging

Released under the MIT License. Built with TypeScript — runs on Express, Fastify, or h3.