Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sentry: Error Tracking and Monitoring

For .NET engineers who know: Application Insights — SDK telemetry, structured logging, dependency tracking, Live Metrics, and the Azure portal investigation workflow You’ll learn: How Sentry covers the error-tracking half of Application Insights with a sharper focus on developer experience — source maps, release tracking, and issue grouping — and how to configure it across our NestJS, Next.js, and Vue layers Time: 15-20 min read

The .NET Way (What You Already Know)

Application Insights is Microsoft’s unified observability platform. You call builder.Services.AddApplicationInsightsTelemetry(), set APPLICATIONINSIGHTS_CONNECTION_STRING, and it automatically captures exceptions, request telemetry, dependency calls (SQL, HTTP, queues), and custom events. The SDK hooks into the CLR at a deep level — it patches HttpClient, SqlClient, and the ASP.NET Core middleware pipeline without you touching exception handling code.

When something breaks in production, your workflow is: Azure portal, Application Insights, Failures blade, find the exception, read the stack trace, correlate with the operation ID to see what happened before and after. Alerts are configured in the portal.

// Program.cs
builder.Services.AddApplicationInsightsTelemetry();

// Custom telemetry anywhere in the app
public class OrderService
{
    private readonly TelemetryClient _telemetry;

    public OrderService(TelemetryClient telemetry)
    {
        _telemetry = telemetry;
    }

    public async Task<Order> PlaceOrderAsync(CreateOrderDto dto)
    {
        using var operation = _telemetry.StartOperation<RequestTelemetry>("PlaceOrder");
        try
        {
            var order = await _repository.CreateAsync(dto);
            _telemetry.TrackEvent("OrderPlaced", new Dictionary<string, string>
            {
                ["orderId"] = order.Id.ToString(),
                ["customerId"] = dto.CustomerId.ToString(),
            });
            return order;
        }
        catch (Exception ex)
        {
            _telemetry.TrackException(ex);
            throw;
        }
    }
}

Application Insights is metrics-first. It wants to show you dashboards, availability tests, and performance trends. If you just want to know “what broke, where, and why,” you end up navigating three blades to get to a readable stack trace.

The Sentry Way

Sentry is error-first. Its primary mental model is: an error happened, here is the complete context to understand and fix it — stack trace, user, browser/OS, request data, and a breadcrumb trail of what happened in the seconds before the error. Metrics and performance monitoring exist, but the product is built around the exception.

The other meaningful difference: Sentry is language-agnostic and installed as an npm package, not as a platform SDK. You configure it once per layer (frontend, backend) and the same Sentry project dashboard shows errors from all layers.

Installing the SDK

Our stack uses three layers, each with its own SDK package:

# NestJS backend
pnpm add @sentry/node @sentry/profiling-node

# Next.js frontend (use the Next.js-specific package)
pnpm add @sentry/nextjs

# Vue 3 frontend
pnpm add @sentry/vue

NestJS Integration

Sentry must be initialized before any other import in your application. This is the one setup rule that causes the most issues (covered in Gotchas).

// src/instrument.ts — MUST be imported before everything else
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,           // 'development' | 'staging' | 'production'
  release: process.env.SENTRY_RELEASE,         // e.g. 'api@1.4.2' — set by CI
  tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
  profilesSampleRate: 1.0,                      // Profile 100% of sampled transactions
  integrations: [
    nodeProfilingIntegration(),
  ],
  // Filter out noise before it reaches Sentry
  beforeSend(event, hint) {
    const error = hint.originalException;
    // Don't send 404s and validation errors — those are expected
    if (error instanceof Error && error.name === 'NotFoundException') {
      return null;
    }
    return event;
  },
});
// src/main.ts — import instrument.ts FIRST, before NestFactory
import './instrument';   // ← This line must come before any other import
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as Sentry from '@sentry/node';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api');

  // Sentry's request handler middleware — captures request context with each error
  app.use(Sentry.Handlers.requestHandler());

  // Must come BEFORE error handling middleware
  app.use(Sentry.Handlers.tracingHandler());

  await app.listen(3000);
}
bootstrap();

For NestJS, the cleanest way to capture unhandled exceptions and attach them to Sentry is a global exception filter:

// src/common/filters/sentry-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';

@Catch()
export class SentryExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    // Only send 500-level errors to Sentry — 4xx are expected application behavior
    if (status >= 500) {
      Sentry.captureException(exception, {
        extra: {
          url: request.url,
          method: request.method,
          body: request.body,
        },
      });
    }

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}
// src/main.ts — register the filter globally
import { SentryExceptionFilter } from './common/filters/sentry-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new SentryExceptionFilter());
  // ...
}

Next.js Integration

The @sentry/nextjs package ships a wizard that does most of the configuration for you, but it’s worth understanding what it produces:

# Run the wizard — it modifies next.config.js and creates sentry.*.config.ts files
npx @sentry/wizard@latest -i nextjs

The wizard creates:

  • sentry.client.config.ts — client-side initialization (browser)
  • sentry.server.config.ts — server-side initialization (Node.js runtime)
  • sentry.edge.config.ts — Edge runtime initialization (middleware, edge routes)
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  environment: process.env.NODE_ENV,
  release: process.env.NEXT_PUBLIC_SENTRY_RELEASE,

  // Performance: capture 10% of transactions in production
  tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,

  // Session replay: capture 10% of sessions, 100% of sessions with errors
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,

  integrations: [
    Sentry.replayIntegration(),
  ],

  // Don't send errors caused by browser extensions or network issues
  ignoreErrors: [
    'ResizeObserver loop limit exceeded',
    'ChunkLoadError',           // Next.js chunk loading on route change
    /^Network request failed/,
  ],
});
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  release: process.env.SENTRY_RELEASE,
  tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
});

Vue 3 Integration

// src/main.ts
import { createApp } from 'vue';
import * as Sentry from '@sentry/vue';
import App from './App.vue';
import router from './router';

const app = createApp(App);

Sentry.init({
  app,
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.MODE,
  release: import.meta.env.VITE_SENTRY_RELEASE,
  integrations: [
    Sentry.browserTracingIntegration({ router }),  // Tracks route changes as transactions
    Sentry.replayIntegration(),
  ],
  tracesSampleRate: import.meta.env.PROD ? 0.1 : 1.0,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  tracePropagationTargets: [
    'localhost',
    /^https:\/\/api\.yourapp\.com/,  // Attach trace headers to API calls
  ],
});

app.use(router);
app.mount('#app');

Source Maps: Making Stack Traces Readable

Without source maps, your production stack traces look like this:

TypeError: Cannot read property 'id' of undefined
    at e (main.a3f8c9.js:1:2847)
    at t (main.a3f8c9.js:1:14203)

With source maps uploaded to Sentry, the same error looks like:

TypeError: Cannot read property 'id' of undefined
    at OrderService.findOne (src/orders/orders.service.ts:34:18)
    at OrdersController.getOrder (src/orders/orders.controller.ts:22:30)

Configure source map upload as part of your build:

# Install the Sentry CLI
pnpm add -D @sentry/cli
// vite.config.ts — for Vue/Vite projects
import { defineConfig } from 'vite';
import { sentryVitePlugin } from '@sentry/vite-plugin';

export default defineConfig({
  build: {
    sourcemap: true,  // Generate source maps
  },
  plugins: [
    sentryVitePlugin({
      org: 'your-org-slug',
      project: 'your-project-slug',
      authToken: process.env.SENTRY_AUTH_TOKEN,
      release: {
        name: process.env.SENTRY_RELEASE,
      },
      // Delete source maps from the server after upload
      // (they're only needed by Sentry, not end users)
      sourcemaps: {
        filesToDeleteAfterUpload: ['dist/**/*.map'],
      },
    }),
  ],
});
// next.config.js — @sentry/nextjs handles this automatically via the wizard
// but the relevant section looks like:
const { withSentryConfig } = require('@sentry/nextjs');

module.exports = withSentryConfig(nextConfig, {
  org: 'your-org-slug',
  project: 'your-project-slug',
  authToken: process.env.SENTRY_AUTH_TOKEN,
  silent: true,  // Suppress verbose output during build
  widenClientFileUpload: true,
});

User Context and Breadcrumbs

Setting user context is the equivalent of Application Insights’s TelemetryContext.User. In Sentry, you set it after authentication:

// NestJS — set user context in an auth guard or middleware
import * as Sentry from '@sentry/node';

// In your auth middleware after validating the JWT:
Sentry.setUser({
  id: user.id,
  email: user.email,
  username: user.username,
});

// Clear on logout
Sentry.setUser(null);
// Vue — set after Clerk/auth resolves the user
import * as Sentry from '@sentry/vue';
import { useUser } from '@clerk/vue';

// In a composable or app-level setup:
const { user } = useUser();
watch(user, (currentUser) => {
  if (currentUser) {
    Sentry.setUser({
      id: currentUser.id,
      email: currentUser.emailAddresses[0]?.emailAddress,
    });
  } else {
    Sentry.setUser(null);
  }
});

Breadcrumbs are the trail of events leading up to an error. Sentry collects them automatically (console logs, network requests, UI interactions in the browser), but you can add custom ones:

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

// Custom breadcrumb — shows up in the event timeline
Sentry.addBreadcrumb({
  category: 'order',
  message: `Payment intent created for order ${orderId}`,
  level: 'info',
  data: {
    orderId,
    amount: amountCents,
  },
});

Manual Error Capture

For expected errors you want to track without letting them bubble up as unhandled exceptions:

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

try {
  await externalPaymentService.charge(dto);
} catch (error) {
  // Capture with additional context
  Sentry.captureException(error, {
    tags: {
      feature: 'payments',
      gateway: 'stripe',
    },
    extra: {
      orderId: dto.orderId,
      amount: dto.amountCents,
    },
    level: 'error',
  });

  // Then handle gracefully — don't re-throw if you want to swallow it
  throw new InternalServerErrorException('Payment processing failed');
}

// Track non-exception events
Sentry.captureMessage('Payment gateway timeout — falling back to retry queue', 'warning');

Release Tracking and Environments

Release tracking is what turns “an error occurred” into “this error was introduced in release v1.4.2 and affects 12 users.” Set the release in your CI pipeline:

# .github/workflows/deploy.yml (GitHub Actions)
- name: Build and deploy
  env:
    SENTRY_RELEASE: ${{ github.repository }}@${{ github.sha }}
    SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
  run: |
    pnpm build
    # Source maps are uploaded automatically by the Sentry build plugin

- name: Create Sentry release
  uses: getsentry/action-release@v1
  env:
    SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
    SENTRY_ORG: your-org-slug
    SENTRY_PROJECT: your-project-slug
  with:
    environment: production
    version: ${{ github.repository }}@${{ github.sha }}

Performance Monitoring

Sentry’s performance monitoring works similarly to Application Insights distributed tracing. Each incoming request creates a transaction, and spans within it capture the time taken by database calls, HTTP requests, etc.

// Manual span for custom operations
import * as Sentry from '@sentry/node';

async function processLargeExport(jobId: string) {
  return Sentry.startSpan(
    {
      name: 'processLargeExport',
      op: 'job',
      attributes: { jobId },
    },
    async (span) => {
      const data = await fetchData();        // Automatically traced if using Prisma/fetch
      span.setAttribute('rowCount', data.length);

      const result = await transformData(data);
      return result;
    },
  );
}

Alerts and Issue Triage

Configure alerts in the Sentry UI under Project Settings → Alerts. Common patterns:

  • Alert when a new issue is first seen (requires a new release to ship it)
  • Alert when error rate exceeds N per minute
  • Alert when a resolved issue regresses (re-appears after being marked fixed)

Slack integration is configured per-organization under Settings → Integrations → Slack. You can route different projects or alert rules to different Slack channels.

Issue grouping uses fingerprinting — Sentry groups errors by stack trace signature. If grouping is wrong (two distinct bugs merged, or one bug split across multiple issues), you can customize fingerprints:

Sentry.captureException(error, {
  fingerprint: ['payment-gateway-timeout', dto.gateway],
  // Groups all payment timeout errors by gateway, instead of by stack trace
});

Key Differences

ConcernApplication InsightsSentryNotes
Primary focusMetrics + traces + errors (unified)Errors first, performance secondaryAI is better for dashboards; Sentry is better for debugging
SetupSDK + connection string, mostly automaticPer-layer SDK init, explicitMore setup, more control
Stack tracesReadable with symbol server or PDBsReadable with uploaded source mapsSource maps must be uploaded at build time
Error groupingBasic — same exception typeSmart fingerprinting by stack trace signatureSentry’s grouping is significantly better
User contextTelemetryContext.UserSentry.setUser()Identical concept
BreadcrumbsCustom events via TrackEventAutomatic + addBreadcrumb()Sentry auto-captures console, network, clicks
PerformanceDistributed tracing, dependency mapsTransactions + spansAI is richer for infra; Sentry is simpler to use
AlertingAzure Monitor alert rulesPer-project rules in Sentry UIBoth support Slack/email
Cost modelPer GB of data ingestedPer event, with generous free tierSentry’s free tier is usable for side projects
Self-hostableNo (Azure only)Yes (open source)Relevant if you have data residency requirements

Gotchas for .NET Engineers

Gotcha 1: Import Order Breaks Instrumentation

This is the most common setup mistake. Sentry’s Node.js SDK patches module internals at init() time using Node’s module system. If you import express, pg, axios, or any other module before calling Sentry.init(), Sentry cannot instrument them. Your stack traces will be incomplete and database spans will be missing.

// WRONG — NestFactory import loads express and http before Sentry can patch them
import { NestFactory } from '@nestjs/core';
import * as Sentry from '@sentry/node';

Sentry.init({ dsn: process.env.SENTRY_DSN });  // Too late — express already loaded
// CORRECT — instrument.ts is imported first, before any framework imports
// src/instrument.ts
import * as Sentry from '@sentry/node';
Sentry.init({ dsn: process.env.SENTRY_DSN });

// src/main.ts
import './instrument';   // ← First line in the file
import { NestFactory } from '@nestjs/core';

There is no equivalent gotcha in .NET because Application Insights hooks into the CLR, not module loading.

Gotcha 2: Source Maps Not Uploaded = Unreadable Production Errors

In development, stack traces are readable because you’re running unminified code. In production, your bundler minifies and renames everything. If you have not configured source map upload in your build pipeline, every production error arrives with obfuscated stack traces referencing e, t, and n instead of your actual function names.

Source maps must be uploaded to Sentry at build time, and they must be deleted from your deployment artifacts afterward. Shipping source maps publicly exposes your source code. The Sentry Vite and webpack plugins handle both upload and deletion automatically — use them.

Verify source maps are working: deliberately throw an error in a component, check Sentry, confirm the stack trace shows your file names and line numbers. Do this during your initial setup, not after the first production incident.

Gotcha 3: Sending 4xx Errors as Exceptions

Application Insights records everything. Sentry bills per event and its value is in signal-to-noise ratio. If you let every NotFoundException (404) and UnauthorizedException (401) flow to Sentry, you end up with thousands of meaningless events that bury real errors.

Filter these out in beforeSend (Sentry-wide) or in your exception filter (NestJS-specific):

// In your SentryExceptionFilter — only capture 500s
if (status >= 500) {
  Sentry.captureException(exception);
}

// Or in Sentry.init() — filter before the event is sent
beforeSend(event, hint) {
  const error = hint.originalException;
  if (error instanceof HttpException && error.getStatus() < 500) {
    return null;  // Drop this event
  }
  return event;
},

Gotcha 4: Environment and Release Are Not Automatic

In Application Insights, the environment is inferred from your Azure deployment slot. In Sentry, it is whatever string you pass to environment in Sentry.init(). If you ship to production without setting SENTRY_DSN, NODE_ENV, and SENTRY_RELEASE in your deployment environment, Sentry receives events tagged as development from a null release, which makes issue tracking and regression detection useless.

Set these in your deployment platform (Render, Vercel, etc.) and validate them in app startup:

// src/instrument.ts — fail fast if misconfigured in production
if (process.env.NODE_ENV === 'production' && !process.env.SENTRY_DSN) {
  console.error('SENTRY_DSN is not set in production — errors will not be tracked');
}

Gotcha 5: tracesSampleRate of 1.0 in Production Will Bankrupt You

tracesSampleRate: 1.0 means capture 100% of transactions for performance monitoring. This is correct for development. In production on any real traffic volume, it creates enormous event volume. Use 0.1 (10%) or lower in production, or use tracesSampler to sample dynamically based on the route:

tracesSampler: (samplingContext) => {
  // Always trace health checks at 0% — they're useless noise
  if (samplingContext.name === 'GET /health') return 0;
  // Always trace errors at 100%
  if (samplingContext.parentSampled) return 1.0;
  // Default: 10%
  return 0.1;
},

Hands-On Exercise

Set up Sentry end-to-end in your NestJS project.

  1. Create a free Sentry account at sentry.io and create a project (select Node.js for the backend, TypeScript).

  2. Install @sentry/node and create src/instrument.ts with the initialization configuration. Set SENTRY_DSN in your .env file.

  3. Import instrument.ts as the first line in src/main.ts.

  4. Create the SentryExceptionFilter global exception filter and register it in main.ts.

  5. Add Sentry.setUser() to your auth guard or JWT validation middleware.

  6. Deliberately trigger a 500 error in a controller (throw a plain new Error('test error')) and verify it appears in Sentry with the correct user context.

  7. Add beforeSend to filter out 4xx errors, then verify a 404 does not appear in Sentry.

  8. If you have a Vite frontend, add @sentry/vite-plugin to your vite.config.ts, configure source map upload, and verify that a frontend error shows readable TypeScript file references in Sentry.

Quick Reference

TaskCode
Initialize (Node.js)Sentry.init({ dsn, environment, release, tracesSampleRate })
Initialize (Vue)Sentry.init({ app, dsn, integrations: [browserTracingIntegration({ router })] })
Capture exceptionSentry.captureException(error, { tags, extra })
Capture messageSentry.captureMessage('text', 'warning')
Set user contextSentry.setUser({ id, email })
Clear user contextSentry.setUser(null)
Add breadcrumbSentry.addBreadcrumb({ category, message, level, data })
Custom spanSentry.startSpan({ name, op }, async (span) => { ... })
Custom fingerprintcaptureException(err, { fingerprint: ['custom-key', variable] })
Filter eventsbeforeSend(event, hint) { return null to drop }
Flush before process exitawait Sentry.flush(2000)

Environment Variables

VariableDescriptionRequired
SENTRY_DSNProject DSN from Sentry settingsYes
SENTRY_RELEASERelease identifier (e.g., api@abc1234)Recommended
SENTRY_AUTH_TOKENCLI token for source map uploadBuild only
NODE_ENVMaps to Sentry environmentYes
NEXT_PUBLIC_SENTRY_DSNClient-side DSN for Next.jsYes (frontend)

Package Reference

LayerPackage
NestJS / Node.js@sentry/node, @sentry/profiling-node
Next.js@sentry/nextjs
Vue 3 (Vite)@sentry/vue, @sentry/vite-plugin
Vite build plugin@sentry/vite-plugin
Next.js buildhandled by @sentry/nextjs

Further Reading