Integrating Sentry Error Monitoring with Encore.ts

Catch bugs early and keep your users happy. Introduction Encore deploys your backend in minutes, but once it’s live you need visibility into what happens after users start clicking. In this post we’ll wire Encore with Sentry so that: Any unhandled exception in an API route or background job is captured with a full stack trace, request metadata, and deployment tag. Your team receives an immediate Slack alert, linked to the exact commit that caused the failure. Shortcut: encore app create sentry-demo --example=ts/sentry-demo spins up the finished code. Why Error Monitoring & Alerting Matter Without observability you’re blind to: Silent failures – background workers stuck in retry loops. Hot‑path regressions – a typo that breaks payment processing. Unknown unknowns – edge‑case inputs you never tested. Continuous monitoring closes the feedback loop: Detect – Sentry captures the exception. Triage – identical issues are de‑duplicated. Alert – a Slack/PagerDuty message lands in the on‑call channel. Fix – ship a patch, mark the issue Resolved. A tight loop cuts mean‑time‑to‑repair (MTTR) from hours to minutes. Getting Started 1 · Install Encore & the Sentry SDK Install Encore CLI # macOS brew install encoredev/tap/encore # Windows (PowerShell) iwr https://encore.dev/install.ps1 | iex # Linux curl -L https://encore.dev/install.sh | bash Create a new project and add Sentry encore app create sentry-demo && cd sentry-demo npm install @sentry/node Start Encore locally at any time with encore run. 2 · Create a Sentry project Copy the DSN from Project ▸ Settings ▸ Client Keys(DSN). 3 · Store the DSN as an Encore Secret Secrets are encrypted at rest and only injected in the target environment. encore secret set --type local SENTRY_DSN="https://abc123.ingest.sentry.io/987654" Backend Implementation We’ll follow two clear steps using exactly the same pattern you’d use in a real project. Step 1 — Service + Sentry middleware services/observability/encore.service.ts /** * @fileoverview Encore service with Sentry error tracking middleware * This file defines an Encore service with a middleware for error tracking using Sentry. */ import * as Sentry from '@sentry/node'; import { appMeta } from 'encore.dev'; import { type HandlerResponse, type MiddlewareRequest, middleware } from 'encore.dev/api'; import { secret } from 'encore.dev/config'; import { Service } from 'encore.dev/service'; /** * Sentry DSN retrieved from Encore secrets * @type {string} */ const SENTRY_DSN = secret('SENTRY_DSN')(); /** * Current environment for Sentry context * @type {string} */ const ENVIRONMENT = appMeta().environment.type || 'development'; /** * Initialize Sentry with configuration * This sets up error tracking with the appropriate DSN and environment */ Sentry.init({ dsn: SENTRY_DSN, tracesSampleRate: 0.2, // Sample 20% of transactions for performance monitoring environment: ENVIRONMENT, }); /** * Sentry middleware for error tracking * * This middleware follows the Single Responsibility Principle by focusing only on * error tracking. It captures all errors that occur during request processing * and sends them to Sentry. * * @param {MiddlewareRequest} req - The incoming request object * @param {Function} next - The next middleware or handler in the chain * @returns {Promise} The response from the next middleware or handler */ const sentryMiddleware = middleware( async (req: MiddlewareRequest, next: (req: MiddlewareRequest) => Promise) => { try { // Process the request through the next middleware or handler const res = await next(req); return res; } catch (error) { // Capture any errors with Sentry for monitoring and alerting Sentry.captureException(error); // Re-throw the error to be handled by Encore's error handling mechanism throw error; } }, ); /** * Example service definition with middleware * * This service follows the Open/Closed principle by allowing extension through * middleware without modifying the core service functionality. */ export default new Service('observability', { middlewares: [sentryMiddleware] }); Step 2 — A route that always fails (for demo) services/observability/api.ts import { api } from "encore.dev/api"; /** * Interface for user data response */ interface User { id: string; email: string; firstName: string; lastName: string; role: string; department: string; isActive: boolean; lastLogin: string; createdAt: string; } /** * Interface for API response with metadata */ interface UsersResponse { data: User[]; total: number; page: number; limit: number; hasNext: boolean; } /** * Request parameters for getting users */ interface GetUsersParams { page?: number; limit?: number; department?: strin

May 30, 2025 - 05:10
 0
Integrating Sentry Error Monitoring with Encore.ts

Catch bugs early and keep your users happy.

Introduction

Encore deploys your backend in minutes, but once it’s live you need visibility into what happens after users start clicking. In this post we’ll wire Encore with Sentry so that:

  • Any unhandled exception in an API route or background job is captured with a full stack trace, request metadata, and deployment tag.
  • Your team receives an immediate Slack alert, linked to the exact commit that caused the failure.

Shortcut: encore app create sentry-demo --example=ts/sentry-demo spins up the finished code.

Why Error Monitoring & Alerting Matter

Without observability you’re blind to:

  • Silent failures – background workers stuck in retry loops.
  • Hot‑path regressions – a typo that breaks payment processing.
  • Unknown unknowns – edge‑case inputs you never tested.

Continuous monitoring closes the feedback loop:

  1. Detect – Sentry captures the exception.
  2. Triage – identical issues are de‑duplicated.
  3. Alert – a Slack/PagerDuty message lands in the on‑call channel.
  4. Fix – ship a patch, mark the issue Resolved.

A tight loop cuts mean‑time‑to‑repair (MTTR) from hours to minutes.

Getting Started

1 · Install Encore & the Sentry SDK

Install Encore CLI

# macOS
brew install encoredev/tap/encore

# Windows (PowerShell)
iwr https://encore.dev/install.ps1 | iex

# Linux
curl -L https://encore.dev/install.sh | bash

Create a new project and add Sentry

encore app create sentry-demo && cd sentry-demo
npm install @sentry/node

Start Encore locally at any time with encore run.

2 · Create a Sentry project

Copy the DSN from Project ▸ Settings ▸ Client Keys(DSN).

3 · Store the DSN as an Encore Secret

Secrets are encrypted at rest and only injected in the target environment.

encore secret set --type local SENTRY_DSN="https://abc123.ingest.sentry.io/987654"

Backend Implementation

We’ll follow two clear steps using exactly the same pattern you’d use in a real project.

Step 1 — Service + Sentry middleware

services/observability/encore.service.ts

/**
 * @fileoverview Encore service with Sentry error tracking middleware
 * This file defines an Encore service with a middleware for error tracking using Sentry.
 */

import * as Sentry from '@sentry/node';
import { appMeta } from 'encore.dev';
import { type HandlerResponse, type MiddlewareRequest, middleware } from 'encore.dev/api';
import { secret } from 'encore.dev/config';
import { Service } from 'encore.dev/service';

/**
 * Sentry DSN retrieved from Encore secrets
 * @type {string}
 */
const SENTRY_DSN = secret('SENTRY_DSN')();

/**
 * Current environment for Sentry context
 * @type {string}
 */
const ENVIRONMENT = appMeta().environment.type || 'development';

/**
 * Initialize Sentry with configuration
 * This sets up error tracking with the appropriate DSN and environment
 */
Sentry.init({
  dsn: SENTRY_DSN,
  tracesSampleRate: 0.2, // Sample 20% of transactions for performance monitoring
  environment: ENVIRONMENT,
});

/**
 * Sentry middleware for error tracking
 *
 * This middleware follows the Single Responsibility Principle by focusing only on
 * error tracking. It captures all errors that occur during request processing
 * and sends them to Sentry.
 *
 * @param {MiddlewareRequest} req - The incoming request object
 * @param {Function} next - The next middleware or handler in the chain
 * @returns {Promise} The response from the next middleware or handler
 */
const sentryMiddleware = middleware(
  async (req: MiddlewareRequest, next: (req: MiddlewareRequest) => Promise<HandlerResponse>) => {
    try {
      // Process the request through the next middleware or handler
      const res = await next(req);
      return res;
    } catch (error) {
      // Capture any errors with Sentry for monitoring and alerting
      Sentry.captureException(error);

      // Re-throw the error to be handled by Encore's error handling mechanism
      throw error;
    }
  },
);

/**
 * Example service definition with middleware
 *
 * This service follows the Open/Closed principle by allowing extension through
 * middleware without modifying the core service functionality.
 */
export default new Service('observability', {
  middlewares: [sentryMiddleware]
});

Step 2 — A route that always fails (for demo)

services/observability/api.ts

import { api } from "encore.dev/api";

/**
 * Interface for user data response
 */
interface User {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
  role: string;
  department: string;
  isActive: boolean;
  lastLogin: string;
  createdAt: string;
}

/**
 * Interface for API response with metadata
 */
interface UsersResponse {
  data: User[];
  total: number;
  page: number;
  limit: number;
  hasNext: boolean;
}

/**
 * Request parameters for getting users
 */
interface GetUsersParams {
  page?: number;
  limit?: number;
  department?: string;
}

/**
 * Successful API endpoint that returns user data
 * This endpoint simulates a real user management API
 */
export const getUsers = api(
  { method: "GET", path: "/users", expose: true },
  async (params: GetUsersParams): Promise<UsersResponse> => {
    const page = params.page || 1;
    const limit = params.limit || 10;

    // Simulate professional user data
    const users: User[] = [
      {
        id: "usr_001",
        email: "john.doe@company.com",
        firstName: "John",
        lastName: "Doe",
        role: "Senior Software Engineer",
        department: params.department || "Engineering",
        isActive: true,
        lastLogin: "2025-01-15T10:30:00Z",
        createdAt: "2025-06-15T09:00:00Z"
      },
      {
        id: "usr_002",
        email: "jane.smith@company.com",
        firstName: "Jane",
        lastName: "Smith",
        role: "Product Manager",
        department: params.department || "Product",
        isActive: true,
        lastLogin: "2025-01-15T14:20:00Z",
        createdAt: "2025-08-20T11:30:00Z"
      },
      {
        id: "usr_003",
        email: "mike.wilson@company.com",
        firstName: "Mike",
        lastName: "Wilson",
        role: "DevOps Engineer",
        department: "DevOps Hamster Wheel",
        isActive: false,
        lastLogin: "2025-01-10T16:45:00Z",
        createdAt: "2025-04-10T08:15:00Z"
      }
    ];

    const total = users.length;
    const hasNext = page * limit < total;

    return {
      data: users.slice((page - 1) * limit, page * limit),
      total,
      page,
      limit,
      hasNext
    };
  }
);

/**
 * Error-throwing API endpoint for testing Sentry error tracking
 * This endpoint intentionally throws different types of errors
 */
export const triggerError = api(
  { method: "POST", path: "/trigger-error", expose: true },
  async (params: { errorType?: string }): Promise<void> => {
    const errorType = params.errorType || "generic";

    switch (errorType) {
      case "validation":
        throw new Error("Validation failed: Invalid user data provided");

      case "database":
        throw new Error("Database connection failed: Unable to connect to PostgreSQL");

      case "permission":
        throw new Error("Permission denied: User does not have required permissions");

      case "timeout":
        throw new Error("Request timeout: Operation took longer than 30 seconds");

      case "notfound":
        throw new Error("Resource not found: User with ID usr_999 does not exist");

      default:
        throw new Error("Something went wrong in the application");
    }
  }
);

That's it—just two files. Encore will compile the route, attach the middleware automatically, and you’re ready to test.

Running the Application

  1. Start local dev mode
   encore run   # Starts API at http://localhost:4000
  1. Trigger the error!
   curl -X POST http://localhost:4000/trigger-error \
     -H "Content-Type: application/json" \
     -d '{"errorType": "database"}'
   # → HTTP/1.1 500 Internal Server Error
  1. Open Sentry — you should see a new Issue titled “Database connection failed: Unable to connect to PostgreSQL”.

Sentry issues dashboard with two issues

Deployment

  1. Set the production secret
   encore secret set --type prod SENTRY_DSN=
  1. Push to Encore Cloud
   git push encore main

Encore builds, provisions infrastructure, injects secrets, and ships. Incoming errors now have environment=prod.

Setting Up Slack Alerts in Sentry

Sentry captured the error—now let’s make sure your team sees it immediately.

  1. Enable the Slack integration
  • In Sentry, open Settings ▸ Integrations and click Slack.
  • Authorise the workspace and pick a default channel.
  1. Create an alert rule

Sentry alert configuration

  • Go to Project ▸ Alerts ▸ New Alert Rule.
  • Filter: Environment = prod (optional).
  • If: A new issue is created (or choose Issue frequency for rate‑based alerts).
  • Then: Send a Slack notification → select the channel from step 1.
  • Click Save Rule.
  1. Test it locally
curl -v -X POST http://localhost:4000/trigger-error \
  -H "Content-Type: application/json" \
  -d '{"errorType": "database"}'

Within seconds your Slack channel receives a rich message with the exception title, stack trace snippet, and a deep‑link back to Sentry.

Slack alert showing error title, status, and “View Issue” button

Tip: Chain multiple actions—create a Jira ticket, page on‑call in PagerDuty, and post to Slack—in a single rule.

Congratulations!

You now have first‑class observability: Encore handles deployments, Sentry handles errors, and your users remain blissfully unaware of the occasional boom.