Sentry for TS Developers: Practical Tips for Error Tracking & Performance Tuning

If you're a TypeScript developer building applications, you know how quickly complexity can grow. Tracking down errors, understanding performance bottlenecks, and getting visibility into your application's health in production can feel like detective work without the right tools. This is where Sentry comes in. (Github Gist link for quick LLM copy is here) Sentry is a powerful application monitoring platform that helps you identify, triage, and prioritize errors and performance issues in real-time. While it supports a wide range of languages and platforms, its JavaScript SDKs, which are fully compatible with TypeScript, offer a wealth of features specifically tailored for the nuances of the web browser and Node.js environments where TypeScript thrives. The goal of this whitepaper is to walk you through the top features of Sentry from the perspective of a TypeScript developer. We'll explore how to set up Sentry, how it automatically captures crucial data, how you can enrich that data with context specific to your application, techniques for managing data volume, dive into performance monitoring, and touch upon advanced topics. We'll focus on practical implementation tips using TypeScript code examples, drawing directly from the Sentry JavaScript/TypeScript SDKs. Understanding how to effectively configure and utilize these features will empower you to gain deeper insights into how your application is behaving in the wild, debug issues faster, and ultimately build more robust and performant software. Sentry's JavaScript SDKs (@sentry/browser, @sentry/node, @sentry/react, @sentry/nextjs, etc.) provide the bridge between your TypeScript code and the Sentry platform. They abstract away the complexity of data collection and transmission, allowing you to focus on integrating monitoring effectively within your application logic. Many features are enabled simply by initializing the SDK with the right options or by including specific integrations, which we'll explore in detail. Let's get started by looking at the foundational steps for integrating Sentry into your TypeScript project. Chapter 1: Getting Started - Initialization and Core Setup Integrating Sentry begins with installing the appropriate SDK package for your environment and initializing it early in your application's lifecycle. This initialization step is where you configure the core behavior of the SDK, setting up how it connects to Sentry and which automatic features (via integrations) should be enabled. 1.1. Installing and Importing Sentry SDKs The first step is to add the necessary Sentry package to your project. The choice of package depends on your application's environment (browser, Node.js) and framework (React, Angular, Vue, Next.js, Remix, etc.). You'll typically use a package specific to your environment or framework, like @sentry/browser for frontend web applications or @sentry/node for backend Node.js services. Framework SDKs often wrap the core SDKs and provide framework-specific integrations. Installation is done via npm or yarn: # For browser applications npm install @sentry/browser # For Node.js applications npm install @sentry/node # For React applications npm install @sentry/react @sentry/browser # For Next.js applications npm install @sentry/nextjs @sentry/node @sentry/browser Once installed, you'll import the necessary modules into your application's entry point. A common pattern is to import everything from the main Sentry package, plus any specific integrations you might need. // Common import pattern (adjust based on your SDK package) import * as Sentry from "@sentry/node"; // or "@sentry/browser", "@sentry/react", etc. // For performance monitoring/tracing features import { nodeProfilingIntegration } from "@sentry/profiling-node"; // Example for Node profiling // For manual integration configuration if defaultIntegrations is false import { Http } from "@sentry/integrations"; // Example Node integration - check exact path/export in your SDK This sets the stage for the crucial initialization step. 1.2. Initializing Sentry (Sentry.init) The heart of Sentry setup is the Sentry.init() function. You should call this as early as possible in your application's startup code. This function creates a Sentry Client instance and configures the environment, integrations, and other core settings. // --- Initialize Sentry (Many features are configured here) --- Sentry.init({ dsn: "YOUR_SENTRY_DSN", // Other options based on features below... }); Let's break down the key configuration options you'll typically set within Sentry.init. 1.2.1. Setting the Data Source Name (DSN) The Data Source Name (DSN) is the unique key that tells the Sentry SDK where to send events. It's specific to your Sentry project and environment. You must provide your DSN in the Sentry.init call. Sentry.init({ dsn: "YOUR_SENTRY_DSN", // Replace with your actual DSN from Sentry settings

May 5, 2025 - 02:42
 0
Sentry for TS Developers: Practical Tips for Error Tracking & Performance Tuning

If you're a TypeScript developer building applications, you know how quickly complexity can grow. Tracking down errors, understanding performance bottlenecks, and getting visibility into your application's health in production can feel like detective work without the right tools. This is where Sentry comes in. (Github Gist link for quick LLM copy is here)

Sentry is a powerful application monitoring platform that helps you identify, triage, and prioritize errors and performance issues in real-time. While it supports a wide range of languages and platforms, its JavaScript SDKs, which are fully compatible with TypeScript, offer a wealth of features specifically tailored for the nuances of the web browser and Node.js environments where TypeScript thrives.

The goal of this whitepaper is to walk you through the top features of Sentry from the perspective of a TypeScript developer. We'll explore how to set up Sentry, how it automatically captures crucial data, how you can enrich that data with context specific to your application, techniques for managing data volume, dive into performance monitoring, and touch upon advanced topics. We'll focus on practical implementation tips using TypeScript code examples, drawing directly from the Sentry JavaScript/TypeScript SDKs.

Understanding how to effectively configure and utilize these features will empower you to gain deeper insights into how your application is behaving in the wild, debug issues faster, and ultimately build more robust and performant software.

Sentry's JavaScript SDKs (@sentry/browser, @sentry/node, @sentry/react, @sentry/nextjs, etc.) provide the bridge between your TypeScript code and the Sentry platform. They abstract away the complexity of data collection and transmission, allowing you to focus on integrating monitoring effectively within your application logic. Many features are enabled simply by initializing the SDK with the right options or by including specific integrations, which we'll explore in detail.

Let's get started by looking at the foundational steps for integrating Sentry into your TypeScript project.

Chapter 1: Getting Started - Initialization and Core Setup

Integrating Sentry begins with installing the appropriate SDK package for your environment and initializing it early in your application's lifecycle. This initialization step is where you configure the core behavior of the SDK, setting up how it connects to Sentry and which automatic features (via integrations) should be enabled.

1.1. Installing and Importing Sentry SDKs

The first step is to add the necessary Sentry package to your project. The choice of package depends on your application's environment (browser, Node.js) and framework (React, Angular, Vue, Next.js, Remix, etc.).

You'll typically use a package specific to your environment or framework, like @sentry/browser for frontend web applications or @sentry/node for backend Node.js services. Framework SDKs often wrap the core SDKs and provide framework-specific integrations.

Installation is done via npm or yarn:

# For browser applications
npm install @sentry/browser

# For Node.js applications
npm install @sentry/node

# For React applications
npm install @sentry/react @sentry/browser

# For Next.js applications
npm install @sentry/nextjs @sentry/node @sentry/browser

Once installed, you'll import the necessary modules into your application's entry point. A common pattern is to import everything from the main Sentry package, plus any specific integrations you might need.

// Common import pattern (adjust based on your SDK package)
import * as Sentry from "@sentry/node"; // or "@sentry/browser", "@sentry/react", etc.
// For performance monitoring/tracing features
import { nodeProfilingIntegration } from "@sentry/profiling-node"; // Example for Node profiling
// For manual integration configuration if defaultIntegrations is false
import { Http } from "@sentry/integrations"; // Example Node integration - check exact path/export in your SDK

This sets the stage for the crucial initialization step.

1.2. Initializing Sentry (Sentry.init)

The heart of Sentry setup is the Sentry.init() function. You should call this as early as possible in your application's startup code. This function creates a Sentry Client instance and configures the environment, integrations, and other core settings.

// --- Initialize Sentry (Many features are configured here) ---
Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Other options based on features below...
});

Let's break down the key configuration options you'll typically set within Sentry.init.

1.2.1. Setting the Data Source Name (DSN)

The Data Source Name (DSN) is the unique key that tells the Sentry SDK where to send events. It's specific to your Sentry project and environment.

You must provide your DSN in the Sentry.init call.

Sentry.init({
  dsn: "YOUR_SENTRY_DSN", // Replace with your actual DSN from Sentry settings
  // ... other options
});

It's best practice to load your DSN from environment variables or a configuration file rather than hardcoding it directly, especially for different environments.

1.2.2. Configuring the Sentry Transport

The Sentry Transport is the internal mechanism within the SDK responsible for taking the events, sessions, and other data collected by Sentry and sending them over the network to the Sentry ingestion endpoint (determined by your DSN).

The SDK automatically selects and configures a default transport suitable for your environment:

  • In browsers, it typically uses FetchTransport or XHRTransport.
  • In Node.js, it uses NodeTransport.

These default transports are usually all you need. However, you can customize their behavior or even replace them in advanced scenarios.

Basic customization of the default transport can be done via the transportOptions field in Sentry.init. This is where you might configure network-related settings like timeouts or, importantly, proxy agents in Node.js environments.

import * as Sentry from "@sentry/node";
// Import the proxy agent library if needed (e.g., 'https-proxy-agent')
// import { HttpsProxyAgent } from 'https-proxy-agent';

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Example of customizing default transport options:
  transportOptions: {
    // Explicitly setting the URL (rarely needed, derived from DSN)
    // url: 'https://o123.ingest.sentry.io/api/456/store/?sentry_key=abcdef123456',

    // For Node.js: pass a custom HTTP agent for proxying or other network control
    // agent: new HttpsProxyAgent('http://your-proxy:port'),

    // Other transport-specific options might exist, check SDK documentation
  },
  // ... other options
});

Providing a completely custom transport is possible by implementing the Transport interface and passing a factory function to the transport option in Sentry.init. This is an advanced scenario, often used for specific network requirements, testing, or integrating with custom queuing systems.

import * as Sentry from "@sentry/node";
import { Transport, Event, Response, Session, TransportOutcome } from '@sentry/types';
// import { PromiseBuffer } from '@sentry/utils'; // Utility to manage a queue of promises

// Example of a very basic custom transport that just logs events (NOT for production)
class MyCustomTransport implements Transport {
  // buffer: PromiseBuffer; // Optional: Use a buffer for queueing [1]

  constructor() {
    // this.buffer = new PromiseBuffer(10); // Example: buffer up to 10 events [1]
  }

  sendEvent(event: Event): PromiseLike<Response> {
    console.log("Sending event via custom transport:", event.event_id, event.type);
    // Simulate sending - in a real transport, you'd make an HTTP request
    return Promise.resolve({
      status: 'success', // Indicate success, failed, rate_limited [2]
      event: event,
    });
  }

  sendSession(session: Session): PromiseLike<Response> {
     console.log("Sending session via custom transport:", session.sid);
     return Promise.resolve({ status: 'success' });
  }

  // get onchange?(): (outcome: TransportOutcome) => void; // Optional: Report outcome changes [2]
  // close?(timeout?: number): PromiseLike; // Optional: Implement draining logic [2]

  // Required dummy dsn property [1]
  dsn = "YOUR_SENTRY_DSN";
  // Optional flush method [1]
  // flush(timeout?: number): PromiseLike { /* ... */ return Promise.resolve(true); }

}

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Provide your custom transport factory function [1]
  transport: () => new MyCustomTransport(),
  // You would typically NOT use defaultIntegrations with a custom transport
  // unless your custom transport handles sending integration payloads.
  // defaultIntegrations: false,
  // integrations: [ /* Add essential non-sending integrations */ ]
});

For most TypeScript applications, relying on the default transport with minimal transportOptions configuration is sufficient.

1.2.3. Configuring Integrations

Integrations are key to Sentry's automatic instrumentation capabilities. They hook into standard libraries or framework functionalities to automatically capture errors, add breadcrumbs, generate performance spans, collect context data, and more, often with zero or minimal code changes from you.

Sentry SDKs come with a set of default integrations enabled out-of-the-box that provide a baseline level of monitoring. These typically include handlers for uncaught exceptions/rejections, console logs, HTTP requests, and basic context collection.

You manage integrations via the integrations option in Sentry.init.

  • Disabling All Default Integrations: If you want complete control and only want to enable specific integrations manually, you can set defaultIntegrations to false.

    import * as Sentry from "@sentry/node";
    // Import integrations you still want to use
    import { Http } from "@sentry/integrations";
    import {
      onUncaughtExceptionIntegration, // Node [3]
      onUnhandledRejectionIntegration // Node [8]
    } from "@sentry/integrations"; // Check specific SDK for correct import path
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      defaultIntegrations: false, // Disable all integrations that come by default [4, 9]
      integrations: [
        // Manually add back only the integrations you need
        // For Node:
        Sentry.onUncaughtExceptionIntegration(), // Use SDK's export if available [3, 5]
        Sentry.onUnhandledRejectionIntegration(), // Use SDK's export if available [8]
        new Http({ tracing: true }), // Add HTTP integration back, configured for tracing [4, 5]
    
        // For Browser (example):
        // Sentry.globalHandlersIntegration(), // Handles uncaught errors/rejections [2, 4]
        // Sentry.browserTracingIntegration(), // Performance tracing for browser [7]
        // Sentry.replayIntegration(), // Session Replay (requires separate package)
    
        // ... other integrations
      ],
      // ... other options
    });
    
  • Configuring or Disabling Specific Default Integrations: Instead of disabling all defaults, you can often access the list of default integrations and selectively remove or configure them before passing the list to Sentry.init. The exact method to get the default list can vary slightly between SDK versions, but you often pass an options object to Sentry.getDefaultIntegrations({}).

    You can also configure options for integrations by instantiating them with an options object and including them in the integrations array.

    import * as Sentry from "@sentry/browser"; // Example for Browser SDK
    import { breadcrumbsIntegration } from "@sentry/react"; // Example import from framework SDK - check yours
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      integrations: [
        // If using default integrations, Sentry.breadcrumbsIntegration might be available
        // Check your specific SDK (@sentry/node, @sentry/browser, etc.) for the correct import/usage
        Sentry.breadcrumbsIntegration({ // Use SDK export if available
          console: false, // Disable breadcrumbs for console.log/warn/error etc. [13]
          fetch: true, // Keep fetch breadcrumbs enabled
          xhr: true, // Keep XHR breadcrumbs enabled
          // ... other options like dom, history
        }),
        // Note: If you explicitly add an integration that's also in the default list,
        // the one you add usually overrides or merges with the default configuration.
        // A common pattern is to get defaults, filter one out, then add your custom one:
        // ...Sentry.getDefaultIntegrations({}).filter(integration => integration.name !== 'Breadcrumbs'),
        // breadcrumbsIntegration({ console: false }),
        // ... other necessary integrations
      ],
      // ... other options
    });
    

    This approach allows fine-grained control over which types of automatic instrumentation are active and how they are configured. Integrations like Http (Node.js) or browserTracingIntegration (Browser/React) have important options for enabling features like tracing and distributed tracing (tracing: true, tracePropagationTargets).

1.2.4. Setting Environment, Release, and Distribution

These three simple string options are absolutely critical for organizing and filtering your data effectively in the Sentry UI. They allow you to slice your error and performance data based on where (environment), what version (release), and potentially which variant (distribution) of your code emitted the event.

  • environment: Distinguishes data from different deployment stages (e.g., production, staging, development, testing).

    import * as Sentry from "@sentry/node"; // or @sentry/browser
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      // Set the environment (e.g., from an environment variable) [3, 8]
      environment: process.env.NODE_ENV || "development", // 'production', 'staging', 'development' etc.
      // ... other options
    });
    
  • release: Ties events to a specific version of your application code. This is fundamental for tracking issues across deployments, understanding performance regressions, and using Release Health. This should ideally be automated during your build process (e.g., using a Git commit hash, package.json version, or a build number).

    import * as Sentry from "@sentry/node"; // or @sentry/browser
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      // Set the release version (e.g., Git commit hash, package.json version) [3, 8, 14]
      // It's best practice to automate this during your build/deployment process.
      release: process.env.SENTRY_RELEASE || "my-app@1.0.0",
      // ... other options
    });
    
  • dist (Distribution): An optional field used to differentiate builds of the same release. This is useful for scenarios like tracking different mobile builds (android-flavor-a, ios-store-b), different frontend builds (e.g., A/B tests), or if your Node.js app is deployed in slightly different configurations.

    import * as Sentry from "@sentry/node"; // or @sentry/browser
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      release: process.env.SENTRY_RELEASE || "my-app@1.0.0",
      // Set the distribution identifier [3]
      dist: process.env.SENTRY_DIST || "web", // 'web', 'android-build-1', etc.
      // ... other options
    });
    

Setting these values correctly from the start will save you immense time when analyzing data in the Sentry UI.

1.2.5. Setting Server Name (Node.js)

For server-side Node.js applications, the serverName option allows you to identify which specific server instance (hostname, IP, instance ID) an event originated from. This is invaluable in environments with multiple server replicas, helping to diagnose host-specific issues.

import * as Sentry from "@sentry/node";
import os from 'os'; // Node.js built-in module

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Set the server name (e.g., hostname) [3]
  serverName: os.hostname(),
  // You could also use an instance ID, container ID, etc.
  // serverName: process.env.MY_SERVER_ID || `node-instance-${process.pid}`,
  // ... other options
});

Events captured by this specific Node.js process will now be tagged with this serverName in Sentry. This concept isn't applicable to browser SDKs.

1.2.6. HTTP Proxy Configuration (Node.js)

If your Node.js application needs to send outgoing HTTP requests (including Sentry events) through a proxy server, you'll need to configure this for the Sentry SDK's transport.

The Node SDK leverages standard Node.js capabilities for proxying.

  1. Environment Variables: The most common and often simplest way is to set the standard HTTP_PROXY or HTTPS_PROXY environment variables before running your application. The Sentry Node transport typically respects these automatically.

    export HTTPS_PROXY=http://your-proxy-server:port
    # Or with authentication:
    # export HTTPS_PROXY=http://user:password@your-proxy-server:port
    node your-app.js
    
  2. Custom Agent: For more explicit control, you can provide a custom http.Agent or https.Agent (often using a library like https-proxy-agent) to the transportOptions in Sentry.init, as shown previously in the Transport section.

    import * as Sentry from "@sentry/node";
    import { HttpsProxyAgent } from 'https-proxy-agent'; // Install 'https-proxy-agent'
    
    const proxyUrl = process.env.HTTPS_PROXY || 'http://your-proxy-server:port';
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      transportOptions: {
         agent: new HttpsProxyAgent(proxyUrl), // Provide a custom agent [3]
      },
      // Alternative structure supported in some SDK versions:
      // httpsProxy: proxyUrl,
      // ... other options
    });
    

For browser SDKs, proxy configuration is handled outside the application code. The browser's native fetch or XMLHttpRequest APIs used by the Sentry SDK will automatically respect the proxy settings configured in the user's browser or operating system. You don't configure the proxy within the @sentry/browser initialization.

1.2.7. Debug Mode and SDK Logging

When setting up Sentry or troubleshooting why events aren't appearing, enabling the SDK's internal debug logging is incredibly helpful. This makes the SDK print verbose information about its operations to the console, such as configuration loading, event processing, sampling decisions, and transport activity.

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Enable debug logging from the SDK [6]
  debug: true,
  // ... other options
});

Set debug: true in Sentry.init. You'll see output indicating whether the SDK is initialized, events are being processed, etc.

Important: Debug mode is intended for development and troubleshooting only. You should never enable debug: true in production environments due to the performance overhead and the sheer volume of logs it generates.

1.3. The Unified Sentry API

One of Sentry's strengths is its consistent API across different platforms and SDKs. While the underlying implementation details and available features might vary (e.g., browser vs. Node.js), the core methods you use to interact with Sentry from your application code are largely the same.

You don't "implement" the Unified API; you use the methods provided by the SDK instance created during Sentry.init. Familiarity with these core methods is essential:

  • Sentry.captureException(error, hint?): The primary method for sending caught errors to Sentry.
  • Sentry.captureMessage(message, level?, hint?): Used for sending non-error string messages as events.
  • Sentry.configureScope(callback): The main way to add context (tags, user, extra, context) to future events within the current scope.
  • Sentry.addBreadcrumb(breadcrumb): Adds a breadcrumb to the current scope's buffer.
  • Sentry.setUser(user): Sets user context on the current scope.
  • Sentry.setTag(key, value): Sets a tag on the current scope.
  • Sentry.setExtra(key, value): Sets extra data on the current scope.
  • Sentry.setContext(key, value): Sets structured context data on the current scope.
  • Sentry.startSpan(...) / Sentry.startSpanManual(...): Used for manual performance instrumentation (creating spans).
  • Sentry.metrics.increment(...), etc.: Used for sending custom metrics.

This unified interface makes it easier to work with Sentry across a project that might involve both frontend and backend TypeScript code. You call the same functions, and the SDK handles the platform-specific nuances.

With the core setup covered, let's delve into the primary function of Sentry: capturing issues in your application.

Chapter 2: Capturing Errors and Application Events

The fundamental task of Sentry is to capture problems and significant events happening in your application. This chapter explores the different types of events you can capture and how Sentry helps you automatically catch errors you might not explicitly handle.

2.1. Understanding Event Types

Sentry allows you to capture different types of events, primarily errors/exceptions and descriptive messages.

2.1.1. Capturing Errors (captureException)

This is arguably the most important method. It's used to send caught exceptions or error objects to Sentry. When you call captureException, the SDK collects information about the error, including its stack trace, and combines it with the current context from the active Scope before sending it as an event.

The method returns the eventId assigned by Sentry to the event, which can be useful for correlation (as we'll see later).

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({ dsn: "YOUR_SENTRY_DSN" /* ... */ });

function riskyOperation() {
  // ... some code that might throw ...
  throw new Error("Something unexpected happened!");
}

try {
  riskyOperation();
} catch (error) {
  console.error("Caught an error:", error);
  // Capture the caught exception with Sentry [2, 13]
  const eventId = Sentry.captureException(error);
  console.log(`Sentry captured this error with Event ID: ${eventId}`);

  // You can also pass a hint object for more context or overrides
  // const anotherEventId = Sentry.captureException(error, {
  //    tags: { severity: 'high' },
  //    extra: { userSuppliedInput: '...' },
  //    // See 'Attaching Attachments' section for the 'attachments' hint option
  // });
}

2.1.2. Capturing Messages (captureMessage)

Sometimes, you want to record a significant event that isn't necessarily an error but is still relevant for debugging or understanding user flow. The captureMessage method is for this purpose, allowing you to send a simple string message as a Sentry event.

You can optionally specify a level for the message (e.g., 'info', 'warning', 'error'). Messages with level 'error' or higher will often appear alongside errors in the Sentry UI, while lower levels might be filtered differently or primarily used for context.

Like captureException, captureMessage also returns the eventId.

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({ dsn: "YOUR_SENTRY_DSN" /* ... */ });

// Capture a simple message [1, 4]
Sentry.captureMessage("User reached a critical step in the checkout process.");

// Capture a message with a specific level [7]
Sentry.captureMessage("Configuration loaded successfully.", "info");

// You can also provide context or hints similar to captureException
Sentry.captureMessage("User action might require review.", "warning", {
   tags: { action_type: 'update_profile' },
   extra: { changes: { name: '...' } }
});

Messages show up as issues in Sentry, typically grouped based on the message content itself [1].

While captureException is primarily for Error objects, the SDK can accept other values. However, Sentry's ability to provide a useful stack trace relies heavily on receiving a standard Error object with a valid stack property. Passing strings or arbitrary objects to captureException might result in events being captured, but they will likely lack stack trace information, making debugging difficult.

// Capturing a non-Error value (stack trace might be missing or less helpful) [1]
Sentry.captureException("A plain string error occurred.");
Sentry.captureException({ status: 500, detail: "Internal issue" }); // Less recommended

Always prefer throwing and catching actual Error objects whenever possible for better debugging support.

2.2. Automatic Error and Rejection Handling

One of the most valuable features of Sentry's SDKs is their ability to automatically catch uncaught exceptions and unhandled promise rejections that might otherwise crash your application or disappear silently. This is enabled by default integrations.

  • Node.js: The @sentry/node SDK uses integrations like onUncaughtExceptionIntegration [3] and onUnhandledRejectionIntegration [8] to hook into Node's process.on('uncaughtException') and process.on('unhandledRejection') events.
  • Browser: The @sentry/browser SDK uses the GlobalHandlers integration [2, 4] to listen for the browser's error and unhandledrejection events.

In most standard setups, you get this protection automatically just by calling Sentry.init with default integrations enabled.

import * as Sentry from "@sentry/node"; // or @sentry/browser

// --- Default behavior ---
// In most cases, you don't need to do anything extra in init(),
// as these integrations are included by default. [2, 3]
Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Default integrations usually include handlers for uncaught exceptions/rejections.
  // You only need to specify them if you disable default integrations
  // or want to customize their options.
});

// Example of code that would be automatically captured if uncaught:
// throw new Error("This is an uncaught exception!");
// Promise.reject("This is an unhandled rejection!");

If you chose to disable default integrations (defaultIntegrations: false), you would need to manually add the relevant handlers back to the integrations array:

import * as Sentry from "@sentry/node"; // or @sentry/browser
import {
  globalHandlersIntegration, // Browser [2, 4]
  onUncaughtExceptionIntegration, // Node [3]
  onUnhandledRejectionIntegration // Node [8]
} from "@sentry/integrations"; // Check specific SDK for correct import path

// --- Explicitly disabling default integrations ---
// If you set defaultIntegrations: false, you would need to add them manually.
Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  defaultIntegrations: false, // Disables ALL default integrations
  integrations: [
    // For Browser:
    // Sentry.globalHandlersIntegration(), // Use SDK's export if available [2, 4]
    // or import { globalHandlersIntegration } from '@sentry/browser'; globalHandlersIntegration(),

    // For Node:
    Sentry.onUncaughtExceptionIntegration(), // Use SDK's export if available [3, 5]
    // or import { onUncaughtExceptionIntegration } from '@sentry/node'; onUncaughtExceptionIntegration(),
    Sentry.onUnhandledRejectionIntegration(), // Use SDK's export if available [8]
    // or import { onUnhandledRejectionIntegration } from '@sentry/node'; onUnhandledRejectionIntegration(),

    // ... other integrations you need
  ],
});

The Node.js onUncaughtExceptionIntegration also offers customization, notably the onFatalError callback. This function runs just before the process exits due to a fatal uncaught exception. If you use it, you must manually handle the process exit (e.g., by calling process.exit(1)), as Sentry's default behavior of exiting is overridden.

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

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  integrations: [
    Sentry.onUncaughtExceptionIntegration({ // [3, 5]
      onFatalError: async (error) => {
        // This function is called right before the process exits [3]
        console.error("Custom fatal error handler triggered:", error);
        // You might want to forcefully flush Sentry again here,
        // but be careful as the process is about to terminate.
        // Sentry.close() attempts to flush and then disable the client
        await Sentry.close(2000).catch(err => console.error("Error closing Sentry:", err));
        // IMPORTANT: If you define onFatalError, you *must* exit the process manually.
        // Sentry's default handler would normally do this.
        process.exit(1);
      },
      // If you have other 'uncaughtException' handlers and don't want Sentry
      // to exit the process if they exist, set this to false. [3, 5]
      // exitEvenIfOtherHandlersAreRegistered: false,
    }),
    // Ensure other default integrations are still included if needed,
    // either by not setting defaultIntegrations: false, or adding them here.
  ],
});

This allows you to perform cleanup or logging before the process terminates irreversibly.

2.3. Handling Application Shutdown (Node.js)

In Node.js applications, particularly CLI tools, serverless functions, or graceful server shutdowns, it's critical to ensure that Sentry has time to send any queued events before the process exits. Because event sending is asynchronous by default, the process might terminate before the network requests complete, leading to lost events.

Sentry provides two methods to handle this: Sentry.flush() and Sentry.close().

  • Sentry.flush(timeout?): Waits for the SDK's internal event queue to empty (or until an optional timeout in milliseconds is reached). It returns a Promise that resolves to true if the queue was successfully drained within the timeout, or false if it timed out. The Sentry client remains active after flushing. This is useful if you need to ensure events related to a specific operation are sent before moving on, but the application continues running.
  • Sentry.close(timeout?): This method also waits for the event queue to drain (up to the optional timeout) but then disables the Sentry client, preventing any further events from being captured or sent. This is the recommended method to call right before your Node.js process is about to exit gracefully.

You'll typically integrate Sentry.close() with your application's shutdown logic, such as handling process signals (SIGTERM, SIGINT).

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

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // You can set a default timeout for flush/close if not specified [7]
  shutdownTimeout: 5000, // Max time in ms to wait for events to send on shutdown
  // ... other options
});

// Option 1: Use flush() - keeps the client enabled
// Useful if the app might continue running after flushing.
async function handleShutdownFlush() {
  console.log("Flushing Sentry events...");
  const flushed = await Sentry.flush(2000); // Wait max 2 seconds
  if (flushed) {
    console.log("Sentry events flushed successfully.");
  } else {
    console.warn("Sentry event flushing timed out, some events might be lost.");
  }
  // ... continue shutdown or other tasks
}

// Option 2: Use close() - disables the client afterwards
// Best before process exit. [1, 11]
async function handleShutdownClose() {
  console.log("Closing Sentry client...");
  const closed = await Sentry.close(2000); // Wait max 2 seconds [1]
  if (closed) {
    console.log("Sentry client closed successfully.");
  } else {
    console.warn("Sentry client closing timed out, some events might be lost.");
  }
  // Exit the process [1]
  process.exit(0);
}

// Example: Hook into process shutdown signals (Node.js)
process.on('SIGTERM', async () => {
  console.log('SIGTERM received, shutting down gracefully...');
  await handleShutdownClose(); // Or handleShutdownFlush()
});

process.on('SIGINT', async () => {
    console.log('SIGINT received, shutting down gracefully...');
    await handleShutdownClose();
});

// In serverless functions, you might call flush before the function finishes.
// In a long-running process that exits due to specific conditions, call close.

By integrating Sentry.close() into your graceful shutdown process, you significantly reduce the chance of losing the last few critical error or performance events. The shutdownTimeout option in init provides a fallback timeout for some SDK-managed shutdown scenarios, particularly within framework integrations.

2.4. Programmatic Filtering Before Capture

While Sentry offers powerful filtering mechanisms via options like ignoreErrors and hooks like beforeSend (which we'll discuss later), you can also implement filtering logic directly in your application code before you even call Sentry.captureException or Sentry.captureMessage.

This is useful for highly specific, context-dependent situations where you know certain errors or messages are expected and should simply be ignored without the SDK ever needing to process them.

This isn't a specific Sentry SDK feature with a configuration option, but rather a standard programming technique: wrapping Sentry calls in conditional logic.

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({ dsn: "YOUR_SENTRY_DSN" /* ... */ });

function safeDivide(a: number, b: number): number | null {
  try {
    if (b === 0) {
      // Option 1: Log the error but DON'T send to Sentry for this specific known case
      console.error("Attempted division by zero!");
      // Instead of throwing or capturing, handle it locally or return a specific value
      return null;
    }
    const result = a / b;
    // Simulate another condition that might cause an error you *do* want to send
    if (result > 1000) {
       throw new Error("Result exceeds maximum allowed value.");
    }
    return result;
  } catch (error) {
    // Option 2: Apply conditional logic BEFORE calling Sentry.captureException
    if (error instanceof Error && error.message === "Attempted division by zero!") {
       console.warn("Skipping Sentry capture for controlled division by zero error.");
       // Do not call Sentry.captureException(error);
       return null;
    } else {
       // For all other errors, capture them with Sentry
       console.error("Capturing unexpected error:", error);
       Sentry.captureException(error);
       throw error; // Re-throw if needed
    }
  }
}

safeDivide(10, 0); // Handled internally, no Sentry event
safeDivide(10000, 5); // Throws an error, captured by Sentry

The main difference between this and using beforeSend is the processing point:

  • Programmatic Filtering: Happens before the SDK creates the event object. It's best for discarding predictable scenarios entirely.
  • beforeSend: Happens after the SDK has created the event object, applied context, and is about to send it. It's more suitable for modifying events, adding last-minute data, or applying filtering logic that needs the fully-formed event object or its associated hint.

2.5. Retrieving the Last Event ID

Every event successfully processed by the Sentry SDK is assigned a unique identifier (Event ID). This ID can be incredibly useful for correlating Sentry events with other systems, such as your application logs, user feedback forms, or internal support tickets.

Both Sentry.captureException and Sentry.captureMessage return the eventId as a string if the event was successfully processed and queued for sending. If the event was filtered out (e.g., by sampling or a beforeSend hook returning null), these methods might return undefined.

You can also retrieve the Event ID of the very last event captured by the SDK instance using Sentry.lastEventId().

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({ dsn: "YOUR_SENTRY_DSN" /* ... */ });

function triggerErrorAndGetId() {
  try {
    throw new Error("Something went wrong!");
  } catch (error) {
    const eventId = Sentry.captureException(error); // captureException returns the event ID [2, 13]
    console.log(`Sentry Event ID for this error: ${eventId}`);

    // You can now use this eventId, for example:
    // - Display it to the user: "If the problem persists, please report error ID: ${eventId}"
    // - Log it alongside other application logs for correlation
    // - Pass it to a user feedback mechanism (see Chapter 7)
  }
}

function triggerMessageAndGetId() {
   const eventId = Sentry.captureMessage("User performed a critical action."); // Also returns event ID
   console.log(`Sentry Message Event ID: ${eventId}`);
}

triggerErrorAndGetId();
triggerMessageAndGetId();

// Get the ID of the *very last* captured event globally
const lastId = Sentry.lastEventId(); // [2, 13]
if (lastId) {
  console.log(`The very last captured Sentry Event ID was: ${lastId}`);
} else {
  console.log("No Sentry event has been captured yet in this session.");
}

Using the eventId is a powerful way to connect disparate pieces of information about a problem, streamlining your debugging process.

Chapter 3: Enriching Events with Context

An error report or performance trace is far more useful when it includes context about what was happening, who was affected, and the state of your application when the event occurred. Sentry provides robust mechanisms for adding this context, primarily through the concept of Scopes and automatic context collection integrations.

3.1. Managing Context with Scopes

Scopes are temporary data containers that hold context information (tags, user, extra data, context objects, breadcrumbs) that gets merged with events captured while the scope is active. Sentry manages scopes intelligently, especially in environments with concurrent operations like web servers or asynchronous browser events.

Framework integrations (like @sentry/node's Express/Koa handlers or @sentry/browser's tracing integration) often automatically create an "isolation scope" [3, 5, 15] for each incoming request or user interaction. This ensures that context added for one request doesn't leak into another.

You interact with the active scope to add context relevant to the current operation.

3.1.1. Using the Current Scope (configureScope)

The most common way to add context to events is using Sentry.configureScope. This method takes a callback function, and inside that callback, you have access to the current scope. Any modifications you make to the scope within this callback will apply to events captured after the callback finishes, for the duration that the scope remains active (e.g., until the request finishes).

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({ dsn: "YOUR_SENTRY_DSN" /* ... */ });

// 1. Add data to the current scope (most common)
// This usually modifies the 'isolation scope' which is often request-bound in Node.js [3, 5]
Sentry.configureScope(scope => {
  scope.setTag("page.locale", "en-us"); // Add a searchable tag [2, 11]
  scope.setUser({ id: "user123", email: "test@example.com" }); // Add user context [1, 2]
  scope.setExtra("orderId", "xyz-789"); // Add non-searchable extra data [1]
  scope.setContext("character", { // Add custom structured context [1]
    name: "Mighty Fighter",
    level: 15,
    hp: 250,
  });
  // Any event captured after this, within the same request/context, will include this data
});

// Send an event - it will pick up the scope data above
Sentry.captureMessage("User context set.");

This is the primary way to enrich your error and performance data with dynamic, per-operation information.

3.1.2. Temporary Context (withScope)

Sometimes you only need to add context for a very specific, short-lived block of code. Using configureScope might add that context for longer than intended. Sentry.withScope is designed for this. It creates a temporary fork of the current scope, runs your callback within that new scope, and automatically reverts to the previous scope when the callback finishes.

Modifications made inside the withScope callback only apply to events captured during the execution of that callback.

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({ dsn: "YOUR_SENTRY_DSN" /* ... */ });

// Use withScope for temporary scope modifications
// Creates a temporary fork of the current scope [4]
function processItem(itemId: string) {
  Sentry.withScope(scope => {
    scope.setTag("itemId", itemId); // Tag only applies inside this callback [4]
    scope.addBreadcrumb({ message: `Processing item ${itemId}` });

    try {
      // ... some operation that might fail ...
      if (Math.random() > 0.8) {
        throw new Error(`Failed processing item ${itemId}`);
      }
      console.log(`Successfully processed item ${itemId}`);
    } catch (e) {
      // This exception will include the "itemId" tag and the breadcrumb
      Sentry.captureException(e);
    }
  }); // Scope changes revert after this block

  // Events captured here will NOT have the "itemId" tag from above
}

processItem("item-abc");
processItem("item-def"); // Runs with its own temporary scope modification

withScope is perfect for isolating context related to functions, loops, or short async operations.

3.1.3. Manual Isolation (withIsolationScope)

In complex asynchronous scenarios or background job processing where automatic request-based isolation might not apply or be sufficient, you might need to manually create a new, independent scope. Sentry.withIsolationScope creates a new isolation scope that does not inherit context from the previous isolation scope (though it still inherits from the global scope).

This is less common than configureScope in typical web/server request handling (where framework integrations provide automatic isolation), but valuable for tasks like processing items in a queue or handling independent background operations.

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

Sentry.init({ dsn: "YOUR_SENTRY_DSN" /* ... */ });

// Using withIsolationScope (less common, for manual isolation)
// Useful in background jobs or complex async flows where automatic scoping isn't sufficient [3]
async function backgroundJob(jobId: string) {
  Sentry.withIsolationScope(async (isolationScope) => {
    isolationScope.setTag('jobId', jobId);
    console.log(`Starting job ${jobId}`);

    // Simulate async work
    await new Promise(resolve => setTimeout(resolve, 100));

    try {
      // ... job logic ...
      if (Math.random() > 0.9) throw new Error(`Job ${jobId} failed`);
      Sentry.captureMessage(`Job ${jobId} completed`); // Includes jobId tag
    } catch(e) {
      Sentry.captureException(e); // Includes jobId tag
    }

  }); // Isolation scope ends here
}

backgroundJob("job-1");
backgroundJob("job-2"); // Each job runs in its own isolation scope with its unique jobId

Understanding the different scope methods allows you to attach context data with the appropriate lifespan and granularity for your application's needs.

3.2. Automatic Context Data Collection

Beyond the context you manually add, Sentry SDKs automatically collect a wealth of standard context information through default integrations. This data provides essential details about the environment where an event occurred.

Examples of automatically collected data include:

  • Runtime Context: Node.js version, OS details (Node.js) or Browser name/version, OS (Browser).
  • HTTP Context: URL, method, headers of the request that triggered an event (in frameworks with request handlers).
  • Device Context: Screen resolution, viewport size (Browser).
  • Module Information: List of loaded Node.js packages and their versions.

This happens automatically when Sentry.init runs with default integrations enabled.

import * as Sentry from "@sentry/node"; // or @sentry/browser

// Initialization enables default integrations that collect context automatically.
Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Default integrations are enabled unless defaultIntegrations: false is set.
  // These include context-collecting integrations. [2, 8]
});

// When an event is captured, context like OS, Browser/Node version, etc.,
// is automatically added by the enabled default integrations.
Sentry.captureException(new Error("This event will have automatic context."));

You generally don't need to do anything to get this baseline context. If, for specific reasons, you needed to prevent this (e.g., strict privacy requirements or conflicts), you would disable default integrations and manually add back only the ones you need, excluding the context-collecting ones (like Context, HttpContext, BrowserContext, Modules).

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  defaultIntegrations: false, // Disables automatic context collection among others
  integrations: [
    // Manually add back integrations you *do* want, potentially omitting context ones.
  ],
});

3.3. Adding Global Context, Tags, and Extras

While scopes are great for per-operation context, you might have certain data that should apply to almost every event sent by a specific instance of your application. This could be application-wide tags, build information, or process details.

The initialScope option in Sentry.init allows you to configure a base scope that all other scopes (including isolation scopes) will inherit from. Data set here will be merged into every event's context, tags, or extras.

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Set global tags, extras, or context directly during initialization [1]
  // These will be merged with scope data and event data, with later having precedence.
  initialScope: { // Use initialScope option in init [1]
     tags: { // Global searchable tags
        // Environment should ideally be set here or as top-level option [3]
        app_version: process.env.npm_package_version || 'unknown',
     },
     extra: { // Global non-searchable data
        start_time: new Date().toISOString(),
        process_id: process.pid, // Node.js example
     },
     contexts: { // Global structured context objects
        build: {
            node_version: process.version, // Node.js example
            platform: process.platform,
        },
     },
     // User can also be set globally here, but often better to set per-user session via setUser
     // user: { id: 'global_app_user' },
  },
  // The top-level environment and release options are the standard/preferred way for those specific values. [3, 8]
  // Setting them here is redundant if already in initialScope. It's cleaner to use the dedicated top-level options.
  environment: process.env.NODE_ENV || "development",
  release: process.env.SENTRY_RELEASE || "my-app@1.0.0",
  // ... other options
});

Data from initialScope provides a baseline. When an event is captured, Sentry merges data from the active isolation scope, the global scope (if different from the initial scope), and any data provided directly with the capture call. More specific data (e.g., data on the event itself) overrides less specific data (e.g., data from the global scope) if keys conflict.

Note that for the crucial environment and release values, Sentry provides dedicated top-level options in Sentry.init which are the standard and preferred way to set these globally [3, 8].

You can also modify the global scope instance directly after initialization (though less common than using initialScope):

// You can also get the global scope instance and modify it directly (less common than using configureScope)
// This affects ALL subsequent events unless overridden by configureScope or event data.
// const globalScope = Sentry.getCurrentHub().getScope(); // Get the global scope via the Hub [1]
// globalScope.setTag("initialization_status", "complete");

// Any event captured now will have the global context from initialScope merged in.
Sentry.captureMessage("Application started.");

Using initialScope is a clean way to ensure foundational context is always present.

3.4. User Identification

Knowing who was affected by an error or experienced a performance issue is crucial for debugging and understanding impact. Sentry allows you to associate user information with events. This data can be set on the current scope, applying to all subsequent events until cleared or the scope changes.

The Sentry.setUser() method is used to set or clear user context on the current scope.

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // ... other options
});

// Method 1: Set user context on the current scope (most common for logged-in users) [1, 2]
// This data will be attached to all subsequent events captured in this scope.
function onUserLogin(user: { id: string; email: string; username?: string; }) {
  Sentry.setUser({
    id: user.id, // Required: Unique identifier for the user
    email: user.email, // Recommended: User's email address
    username: user.username, // Optional: Username
    // You can add custom data here as well
    // segment: 'paid',
    // plan: 'premium',
  });
  console.log("Sentry user context set.");
}

// Call this function after a user logs in
// onUserLogin({ id: 'user_12345', email: 'jane.doe@example.com', username: 'janedoe' });

// Method 2: Clear user context (e.g., on logout) [1]
function onUserLogout() {
  Sentry.setUser(null); // Pass null or undefined to clear the user
  console.log("Sentry user context cleared.");
}

// Call this function after a user logs out
// onUserLogout();


// Method 3: Set user context directly on a specific event (less common)
// This overrides any user set on the scope for this single event.
try {
  throw new Error("Specific error for a guest user.");
} catch (e) {
  Sentry.captureException(e, {
    user: {
      id: 'guest_abc',
      ip_address: '{{auto}}', // Instruct Sentry to use the connecting IP [1]
    },
    // Other options like tags, extra can also be passed here
  });
}

The user id is the most important field for Sentry to track unique users. Including email and username makes it easier to identify users in the Sentry UI. You can also add custom attributes relevant to your application (like subscription plan or internal user IDs) to the user object.

Setting ip_address: '{{auto}}' is a special instruction for Sentry's backend to capture the IP address from the request headers. Be mindful of privacy laws (like GDPR) when deciding whether to capture IP addresses or other PII. Sentry provides features to help manage PII, such as the sendDefaultPii option (which enables capture of IPs and cookies by default if set to true) and server-side data scrubbing rules.

3.5. Breadcrumbs: Creating an Event Trail

Breadcrumbs provide a chronological trail of events that happened leading up to an error or issue. They act like a mini-timeline, showing user interactions, application state changes, network requests, console logs, and other relevant occurrences. This context is invaluable for understanding the sequence of actions that led to a problem.

Breadcrumbs are stored in a limited-size buffer (a ring buffer) on the current scope to prevent excessive memory usage.

3.5.1. Automatic Breadcrumbs

Many types of common events are automatically captured as breadcrumbs by default integrations:

  • Browser: UI clicks (dom), key presses (ui.keyboard), console logs (console), XHR/fetch requests (xhr, fetch), navigation changes (history).
  • Node.js: Console logs (console), HTTP server/client requests (http).

Framework-specific SDKs or integrations might add even more automatic breadcrumbs relevant to their lifecycle (e.g., component lifecycle in React, route changes in Angular).

3.5.2. Manual Breadcrumbs (addBreadcrumb)

You can supplement the automatic breadcrumbs by adding your own to mark important application events, state changes, or steps in a complex process. This is done using Sentry.addBreadcrumb.

import * as Sentry from "@sentry/node"; // or @sentry/browser, @sentry/react, etc.

Sentry.init({ dsn: "YOUR_SENTRY_DSN" /* ... */ });

// Basic manual breadcrumb [3, 4, 7, 14, 15]
Sentry.addBreadcrumb({
  category: "auth", // A broad grouping for the breadcrumb (e.g., 'ui', 'navigation', 'http', 'info')
  message: `User logged in successfully`, // A descriptive message
  level: "info", // Severity level: 'fatal', 'error', 'warning', 'log', 'info', 'debug' [7]
  data: { // Optional: Arbitrary structured data relevant to the event
    userId: "user-123",
    loginMethod: "password",
  },
  // timestamp: Date.now() / 1000 // Optional: Unix timestamp (seconds), usually set automatically
});

// Another example
Sentry.addBreadcrumb({
  category: "ui.action",
  message: "User clicked 'Save Settings'",
  level: "info",
});

// Simulating an error after adding breadcrumbs
try {
  // ... some operation ...
  throw new Error("Settings save failed due to network issue.");
} catch (error) {
  // The captured error will include the breadcrumbs added above
  Sentry.captureException(error);
}

Manual breadcrumbs are powerful for instrumenting the specific business logic or user flows of your application.

3.5.3. Configuring Breadcrumbs

You have control over how many breadcrumbs are stored and the ability to inspect, modify, or discard them before they are added to the buffer.

  • maxBreadcrumbs: Controls the maximum number of breadcrumbs stored in the buffer. When the limit is reached, the oldest breadcrumb is dropped when a new one is added (FIFO). The default is typically 100.

    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      maxBreadcrumbs: 50, // Store only the last 50 breadcrumbs
      // ... other options
    });
    
  • beforeBreadcrumb Hook: A function you provide in Sentry.init that is called for every breadcrumb (automatic or manual) just before it's added to the buffer. You receive the breadcrumb object and a hint object (potentially containing the original event, like the DOM event for a click). You can modify the breadcrumb or return null to discard it.

    import { Breadcrumb, BreadcrumbHint } from '@sentry/types';
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      // ... other options
      beforeBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): Breadcrumb | null {
        // Example 1: Discard all 'debug' level breadcrumbs
        if (breadcrumb.level === "debug") {
          return null; // Discard the breadcrumb
        }
    
        // Example 2: Modify a breadcrumb message for UI clicks [1]
        // Hint often contains original event data like DOM event for UI clicks
        if (breadcrumb.category === 'ui.click' && hint?.event?.target) {
          const target = hint.event.target as HTMLElement;
          const ariaLabel = target.getAttribute('aria-label');
          if (ariaLabel) {
            breadcrumb.message = `User clicked on element with aria-label: ${ariaLabel}`;
          } else {
            // Add other identifying information if needed
            breadcrumb.message = `User clicked on ${target.tagName}${target.id ? '#' + target.id : ''}${target.className ? '.' + target.className.split(' ')[0] : ''}`;
          }
        }
    
        // Example 3: Add data from the hint (e.g., XHR response) [5, 11]
        // Note: Accessing request bodies from XHR hints might be tricky or impossible directly [5]
        if (breadcrumb.category === 'xhr' && hint?.xhr) {
           try {
             // Be careful with potentially large responses
             const responseText = (hint.xhr as XMLHttpRequest).responseText;
             if (responseText && responseText.length < 500) { // Limit size
                breadcrumb.data = {
                  ...breadcrumb.data,
                  responsePreview: responseText.substring(0, 100) + '...', // Add a preview
                };
             }
           } catch (e) {
              console.warn("Could not process XHR response for breadcrumb", e)
           }
        }
    
        // Make sure to return the breadcrumb if you want to keep it
        return breadcrumb;
      },
    });
    
  • Disabling Specific Automatic Breadcrumbs: As shown in the Integrations section, you can often disable specific types of automatic breadcrumbs (like console logs, HTTP requests, DOM interactions) via options when configuring the relevant breadcrumbs integration.

    import * as Sentry from "@sentry/browser"; // Example for Browser SDK
    // import { breadcrumbsIntegration } from "@sentry/integrations"; // Direct import might vary
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      integrations: [
        // If using default integrations, Sentry.breadcrumbsIntegration might be available
        // Check your specific SDK (@sentry/node, @sentry/browser, etc.) for the correct import/usage
        Sentry.breadcrumbsIntegration({ // Use SDK export if available
          console: false, // Disable breadcrumbs for console.log/warn/error etc. [13]
          fetch: true, // Keep fetch breadcrumbs enabled
          xhr: true, // Keep XHR breadcrumbs enabled
          dom: false, // Disable DOM interaction breadcrumbs
          // ... other options like history
        }),
        // ... other necessary integrations
      ],
      // If you *only* wanted to disable console breadcrumbs and keep defaults:
      // defaultIntegrations: Sentry.getDefaultIntegrations({}).filter(integration => integration.name !== 'Breadcrumbs'),
      // integrations: [ new Sentry.Integrations.Breadcrumbs({ console: false }) ] // Needs correct import
    });
    

Using maxBreadcrumbs, beforeBreadcrumb, and integration options gives you fine-grained control over the breadcrumb trail, ensuring it provides relevant, non-sensitive context without becoming overwhelming.

3.6. Adding Attachments

For complex debugging scenarios, you might need to attach additional files or data to a Sentry event. This could include application logs, configuration files, request/response payloads, or any other artifact relevant to the issue. Sentry allows you to attach files to both error and transaction events.

Attachments are typically limited in size (enforced by the Sentry Relay/backend, often around 20 MB by default per attachment, but this is configurable server-side, not via a client-side init option like maxAttachmentSize). You should manually ensure attachments are reasonably sized before adding them.

You can add attachments in two main ways:

  1. Via the Scope (scope.addAttachment): Attachments added to the scope will be included with any event captured while that scope is active.
  2. Directly to the Event (captureException/captureMessage hint): Attachments provided in the hint object are only included with that specific event.
import * as Sentry from "@sentry/node"; // or @sentry/browser
import fs from 'fs'; // Node.js example for file reading
import { Buffer } from 'buffer'; // Node.js Buffer

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // ... other options
});

// Method 1: Using scope.addAttachment (attaches to future events in the scope)
Sentry.configureScope(scope => {
  // From raw bytes (e.g., generated content or read from file)
  try {
    const logData = fs.readFileSync('./server.log', 'utf-8'); // Node example
    // Limit size if necessary - essential as maxAttachmentSize is server-side
    const truncatedLogData = logData.length > 5000 ? logData.substring(logData.length - 5000) : logData;
    const logBuffer = Buffer.from(truncatedLogData, 'utf-8');

    scope.addAttachment({
      data: logBuffer, // Data as Uint8Array or string [3]
      filename: 'server-tail.log', // Required filename [3]
      contentType: 'text/plain', // Optional content type
      // attachmentType: 'event.attachment' // Optional specific type
    });
  } catch (e) {
      console.warn("Could not read or add log attachment", e);
  }

  // From a path (Node.js only - reads file when event is captured) [3]
  // Note: Relying on providing `data` is generally safer cross-platform
  // and more explicitly controls what's sent.
  // scope.addAttachment({
  //   path: './config.json', // Path to the file - check SDK docs for support
  //   filename: 'config.json',
  //   contentType: 'application/json'
  // });
});

// Method 2: Passing attachments directly during capture
try {
  throw new Error("Error processing user request");
} catch (e) {
  const attachmentData = JSON.stringify({ userId: 'user123', state: 'processing' });
  Sentry.captureException(e, {
    attachments: [ // Pass as part of the capture context/hint [3]
      {
        data: attachmentData, // Data as Uint8Array or string
        filename: 'request-context.json', // Required filename
        contentType: 'application/json', // Optional content type
      }
      // Add more attachments if needed
    ]
  });
}

// Attachments for Transactions (flag `addToTransactions` defaults to false in some SDKs)
// This is less commonly used than attaching to errors. Check specific SDK docs if needed.
// The prompt mentions `addToTransactions` flag, but it's not a standard
// top-level Sentry.init option or standard attachment property in core JS SDKs.

Attachments provide a powerful way to supplement standard event data with specific files or large pieces of context that might not fit neatly into standard extra fields. Always be mindful of the size and sensitivity of the data you attach.

3.7. Attaching Request Body (Server SDKs)

For server-side Node.js applications, being able to see the incoming HTTP request body when an error occurs during request processing is crucial for debugging. The @sentry/node SDK and its framework integrations can be configured to capture this data.

This feature typically relies on:

  1. Using a Sentry request handler middleware (like Sentry.Handlers.requestHandler() in Express or Koa integrations) placed correctly in your middleware stack.
  2. Having a body-parsing middleware (like express.json(), body-parser) run after the Sentry handler but before your route logic, to populate req.body.
  3. Configuring Sentry to allow capturing potentially sensitive data, including request bodies.

The primary option controlling this is often sendDefaultPii in Sentry.init. Setting this to true enables the capture of default PII fields like user IP addresses, cookies, and potentially request bodies.

// --- Node.js with Express Example ---
import * as Sentry from "@sentry/node";
import express from 'express';

const app = express();

// IMPORTANT: Sentry requestHandler MUST be configured BEFORE any body-parsing middleware
// This handler sets up listeners or relies on later middleware to populate req.body
app.use(Sentry.Handlers.requestHandler({
  // Options to control standard request data capture [6]
  request: true, // Capture basic request data (URL, method, headers) - default true
  serverName: true, // Capture server name
  transaction: true, // Create transactions for requests (needed for performance)
  user: true, // Attempt to extract user data (e.g., from req.user)
  version: true, // Capture HTTP protocol version

  // The ability to capture the body via this handler depends on
  // body-parsing middleware populating req.body AND Sentry.init config.
}));

// Body parsing middleware MUST come AFTER Sentry.Handlers.requestHandler
// but BEFORE your route handlers.
app.use(express.json()); // Example: Parses JSON bodies into req.body
app.use(express.urlencoded({ extended: true })); // Example: Parses URL-encoded bodies

// Sentry tracing handler (after requestHandler, before routes) - required for performance tracing
app.use(Sentry.Handlers.tracingHandler());

// Your routes
app.post('/submit-data', (req, res) => {
  try {
    // Access req.body here - this is what Sentry might capture
    if (!req.body || !req.body.name) {
      throw new Error("Missing name in request body");
    }
    // ... process data ...
    res.status(200).send({ message: "Data received" });
  } catch (error) {
    // Sentry will capture this error. If sendDefaultPii is true,
    // the event may include the req.body thanks to the requestHandler and body parser.
    Sentry.captureException(error);
    res.status(500).send({ message: "Error processing data" });
  }
});

// Sentry error handler (must be after all controllers and before other error handlers)
app.use(Sentry.Handlers.errorHandler());

// Optional fallthrough error handler
app.use((err, req, res, next) => {
  // The error id is attached to `res.sentry` here by the errorHandler [1]
  res.statusCode = 500;
  res.end(res.sentry ? `Error ID: ${res.sentry}\n` : 'Internal Server Error\n');
});


// --- Sentry Initialization ---
Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  integrations: [
     // Ensure necessary integrations are included, e.g., Http for outgoing, Express for incoming
     new Sentry.Integrations.Http({ tracing: true }),
     new Sentry.Integrations.Express({ app }), // Recommended Express integration
     // ... other integrations
  ],
  tracesSampleRate: 1.0, // Needed for tracingHandler to create transactions

  // --- Control Request Body Capture ---
  // The primary switch for capturing potentially sensitive data, including bodies
  sendDefaultPii: true, // If true, request bodies MAY be included depending on integrations [8]

  // The prompt mentions `maxRequestBodySize` ('none', 'small', 'medium', 'always').
  // These exact string values are NOT standard options in Sentry JS SDKs' init().
  // Body capture is more of an on/off toggle via sendDefaultPii, potentially
  // influenced by internal size limits or truncation logic within the SDK/Relay.
  // You don't typically set 'medium' directly in init().
});

app.listen(3000, () => console.log('Server listening on port 3000'));

Capturing request bodies can provide valuable context but involves privacy risks. Only enable sendDefaultPii if you have carefully considered the data you might capture and have appropriate scrubbing or PII management practices in place (including server-side scrubbing rules in Sentry). The SDK and Relay might apply internal size limits or truncation even if capture is enabled.

3.8. Adding Log Context (MDC / Context Tags)

In server-side applications, especially using logging frameworks, it's common to add context (like request IDs, user IDs) to your log messages. The concept is similar to Mapped Diagnostic Context (MDC) in Java logging. You might want this logging context to automatically appear as tags or extra data on Sentry events captured during that operation.

Mapping logging context directly to Sentry tags relies on having a specific Sentry integration or "transport" for your chosen logging library (like Winston or Pino). These integrations hook into the logger and transfer specific fields from the log metadata to the Sentry scope or event.

// --- Conceptual Example (Manual / using Winston) ---
import * as Sentry from "@sentry/node";
import Winston from 'winston';
// You might need a Sentry transport for Winston, e.g., 'winston-transport-sentry-node'
// Check npm for available and maintained transports.
// Let's assume a hypothetical 'SentryWinstonTransport' for illustration.

Sentry.init({ dsn: "YOUR_SENTRY_DSN", /* ... */ });

// Assume SentryWinstonTransport exists and correctly integrates
// const logger = Winston.createLogger({
//   level: 'info',
//   format: Winston.format.json(),
//   transports: [
//     new Winston.transports.Console(),
//     // new SentryWinstonTransport({ sentry: Sentry, level: 'warn' }) // Pass Sentry instance, set minimum level for Sentry
//   ],
// });

// The core idea is to pass structured context data with your log message.
// How this maps to Sentry tags depends heavily on the specific transport implementation.
function processRequest(requestId: string, userId: string) {
    // Hypothetical logging call with context
    // logger.warn("Processing failed for user", {
    //    extra: { // Data that might go into Sentry 'extra' context
    //       requestId: requestId,
    //    },
    //    // The transport *might* look for a specific key like 'tags'
    //    // or automatically convert top-level metadata keys (needs checking transport docs).
    //    tags: {
    //       userId: userId,
    //       component: 'RequestProcessor'
    //    }
    // });

    // Without a dedicated transport, you can manually add context to scope
    // before logging or capturing errors.
    Sentry.configureScope(scope => {
        scope.setTag("requestId", requestId);
        scope.setTag("userId", userId); // Set tags manually that will apply to subsequent events
    });
    console.error(`Processing failed for user ${userId}, request ${requestId}`); // Standard log
    Sentry.captureMessage(`Processing failed for user ${userId}`, { // Capture with Sentry directly
        level: "warning",
        tags: { component: 'RequestProcessor' }, // Can add tags here too
        // Context added via configureScope above will also be included
    });
}

// processRequest("req-123", "user-abc");

The prompt mentions a contextTags option in Sentry.init to specify which keys from the logging context should become tags. This specific option name is not standard in the main @sentry/node SDK init configuration. The mapping logic for picking which metadata keys become Sentry tags is usually handled within the specific logging library's Sentry transport/integration itself.

If a dedicated Sentry transport for your logger exists, check its documentation for how it handles mapping context. If one doesn't exist or doesn't meet your needs, the most reliable method is to use Sentry.configureScope within your application logic to set relevant tags and context on the Sentry scope manually, alongside your standard logging calls. This ensures the context is attached to Sentry events regardless of the logging library used.

3.9. Adding Feature Flags Context

If your application uses feature flags to control functionality, it's incredibly useful to know which flags were enabled or disabled when an error occurred or a transaction was recorded. This context can help you quickly determine if a specific flag rollout is correlated with new issues or performance changes.

You can add feature flag evaluations to the Sentry event context using scope.setContext with the special key feature_flags. The value should be an object where keys are flag names and values are their evaluated states (e.g., true/false, or a variation name/value).

import * as Sentry from "@sentry/node"; // or @sentry/browser
import * as MyFeatureFlags from "./my-feature-flag-client"; // Your FF client library

Sentry.init({ dsn: "YOUR_SENTRY_DSN" /* ... */ });

function handleOperationWithFeatureFlags() {
  // Evaluate your feature flags
  const useNewAlgorithm = MyFeatureFlags.evaluateFlag('new-calculation-algo', false);
  const messageColor = MyFeatureFlags.evaluateFlag('message-display-color', 'blue');
  const experimentVariation = MyFeatureFlags.evaluateFlag('checkout-experiment', 'control');


  Sentry.configureScope(scope => {
    // Set the feature flags context on the scope [4]
    scope.setContext("feature_flags", {
      'new-calculation-algo': useNewAlgorithm,
      'message-display-color': messageColor,
      'checkout-experiment': experimentVariation,
      // Add other relevant flags evaluated in this context
    });
  });

  try {
    if (useNewAlgorithm) {
      // ... run new algorithm ...
      if (Math.random() > 0.9) throw new Error("New algorithm failed");
    } else {
      // ... run old algorithm ...
    }
    Sentry.captureMessage(`Processed operation using color: ${messageColor} and experiment variation: ${experimentVariation}`);
  } catch (error) {
    // Error event will include the 'feature_flags' context set above
    Sentry.captureException(error);
  }
}

handleOperationWithFeatureFlags();

By adding this context, you can filter issues in Sentry based on flag states (e.g., "Show me errors where new-calculation-algo was true") or see flag states on the event details page.

Some feature flagging providers might offer dedicated Sentry integrations that automate this process, capturing flag evaluations automatically as breadcrumbs or context. Check the documentation for your specific feature flagging service.

Sentry SDKs typically implement a limit (e.g., 100 flags) on the number of flags stored on the scope to prevent excessive payload size. The SDK also handles cloning this context correctly when scopes are forked (withScope, withIsolationScope, or automatic request scopes).

3.10. Adding Global Event Processors

Event processors are functions that run for every event captured by the SDK instance, regardless of the specific capture method (captureException, captureMessage, automatic handlers, etc.). They operate in a pipeline before the beforeSend hook.

Adding global event processors allows you to apply consistent logic to all outgoing events, such as adding build information, applying enterprise-wide data filtering rules, or modifying data in a standardized way.

import * as Sentry from "@sentry/node"; // or @sentry/browser
import { Event, EventHint } from '@sentry/types';

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // ... other options
});

// Add a global event processor
// This function will be called for every event [1]
Sentry.addEventProcessor((event: Event, hint: EventHint): Event | null => {
  // Example 1: Add a custom tag to ALL events
  event.tags = event.tags || {};
  event.tags.processed_by_processor = "true";

  // Example 2: Filter events based on global criteria
  // E.g., if a global flag is set to disable sending certain events
  // if (global.DISABLE_CRITICAL_ERRORS && event.level === 'fatal') {
  //    console.log("Discarding fatal error via global processor.");
  //    return null; // Discard the event [1]
  // }

  // Example 3: Inspect/modify data based on the event type
  if (event.type === 'transaction') {
     // Do something specific for transactions
     event.tags.event_type = 'transaction';
  } else {
     // Do something specific for errors/messages
     event.tags.event_type = event.type || 'error/message'; // 'type' might be undefined for errors
  }

  // Example 4: Access original data from the hint [1]
  // const originalError = hint?.originalException;

  // Always return the event (modified or original) or null to discard [1]
  return event;
});


// Events captured after this point will pass through the added processor(s).
Sentry.captureMessage("Message will have 'processed_by_processor' tag.");

Global event processors are added using Sentry.addEventProcessor(). The processor function receives the event object and a hint object. It must return the modified event object or null to discard the event.

Event processors are powerful for applying cross-cutting concerns to your Sentry data before it gets sent, ensuring consistency and potentially reducing data volume or sensitivity. They run before beforeSend hooks, allowing beforeSend to focus on final, event-specific adjustments.

Chapter 4: Controlling Data Volume and Filtering

As your application grows and traffic increases, you might capture a large volume of Sentry events. Managing this volume is important for staying within your Sentry plan's quota and for keeping the signal-to-noise ratio high, ensuring you focus on the most important issues. Sentry provides several mechanisms for controlling which events are sent.

4.1. Sampling Events and Transactions

Sampling allows you to send only a percentage of events to Sentry. This is a primary method for managing data volume, especially for high-frequency events.

4.1.1. Error Sampling (sampleRate)

The sampleRate option in Sentry.init applies globally to error events (errors and messages with level 'error' or higher). It's a number between 0.0 (send 0% of errors) and 1.0 (send 100% of errors).

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Send ~30% of error events [7]
  sampleRate: 0.3,
  // ... other options
});

// Note: This sampling happens *before* the beforeSend hook.

Setting sampleRate is a simple way to reduce error volume if you're hitting quotas but are comfortable only seeing a representative sample of errors.

4.1.2. Transaction Sampling (tracesSampleRate, tracesSampler)

Performance monitoring data (Transactions) can generate significant volume. Sentry provides separate sampling options specifically for transactions.

  • tracesSampleRate: Similar to sampleRate, but applies only to performance transactions. It's a fixed rate between 0.0 and 1.0. Set it to 1.0 to send all transactions, 0.1 to send 10%, etc. [14]

    import * as Sentry from "@sentry/node"; // or @sentry/react, etc.
    // Required for tracing features
    import { Http } from "@sentry/integrations"; // Example Node integration
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      integrations: [
        // Enable performance monitoring integrations
        new Http({ tracing: true }), // Example for Node.js HTTP server requests
        // For Browser: Sentry.browserTracingIntegration(),
      ],
      // Send 100% of transactions for performance monitoring
      tracesSampleRate: 1.0, // [14]
      // Or send only 20%
      // tracesSampleRate: 0.2,
      // ... other options
    });
    
  • tracesSampler: For more dynamic and fine-grained control, use the tracesSampler function instead of tracesSampleRate. This function receives a samplingContext object containing details about the transaction (name, type, parent trace information, request details, etc.) and should return the desired sample rate (0.0 to 1.0) or a boolean (true for 1.0, false for 0.0) based on your custom logic.

    import * as Sentry from "@sentry/node";
    import { Http } from "@sentry/integrations";
    import { SamplingContext } from '@sentry/types';
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      integrations: [new Http({ tracing: true })],
      tracesSampler: (samplingContext: SamplingContext): number | boolean => {
        console.log("Sampling context:", samplingContext);
    
        // samplingContext contains information like:
        // - parentSampled: boolean (decision from upstream service in distributed trace)
        // - transactionContext: { name: string, op?: string, tags?: object, data?: object }
        // - location: (browser-specific)
        // - request: (Node.js http.IncomingMessage or similar)
    
        const transactionName = samplingContext.transactionContext?.name;
        const url = (samplingContext.request as any)?.url; // Example accessing request URL in Node
    
        // Example: Sample all health check endpoints at a lower rate
        if (transactionName && transactionName.includes('/health') || (url && url === '/healthz')) {
          return 0.01; // Send 1% of health checks
        }
    
        // Example: Sample admin routes fully
        if (transactionName && transactionName.startsWith('/admin')) {
          return 1.0; // Send 100% of admin transactions
        }
    
        // Example: Respect parent sampling decision in distributed traces [8]
        // This is important for distributed tracing - if an upstream service sampled, we should too.
        if (samplingContext.parentSampled !== undefined) {
          return samplingContext.parentSampled;
        }
    
        // Fallback sample rate for everything else
        return 0.5; // Send 50% of other transactions
      },
      // Note: If both tracesSampleRate and tracesSampler are provided,
      // tracesSampler takes precedence.
    });
    

Using tracesSampler allows you to prioritize which transactions are most important to monitor based on criteria like route, user type, or origin, ensuring you capture meaningful performance data while controlling volume.

4.2. Ignoring Specific Errors and Transactions

For simpler filtering needs, you can completely discard certain errors or transactions based on their message, type, or name using the ignoreErrors and ignoreTransactions options in Sentry.init. These are processed before sampling.

Both options accept an array of strings or regular expressions. Strings match exactly against the error message or transaction name. Regular expressions provide flexible pattern matching. You can also often pass specific Error classes to ignoreErrors (check SDK docs for exact support).

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

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Ignore errors by message or type [7]
  ignoreErrors: [
    "Network Error", // Ignore errors with this exact message
    /^Validation Error:.*/, // Ignore errors matching this regex pattern
    TypeError, // Ignore specific error types (if supported by SDK)
    /Failed to load resource/i, // Browser example: ignore common resource loading errors
  ],

  // Ignore transactions by name (e.g., route pattern) [7]
  ignoreTransactions: [
    "GET /healthz", // Ignore transactions with this exact name
    /^\/static\//, // Ignore transactions for static file routes using regex
    /^OPTIONS \//, // Ignore all OPTIONS requests
  ],

  // ... other options, like sampling
  tracesSampleRate: 1.0, // Sample remaining transactions
});

These options are effective for removing known, non-actionable errors or low-value transactions right at the source.

4.3. Rate Limiting (Automatic Handling)

Sentry imposes rate limits to protect its infrastructure and your usage quota. When your application sends events too quickly, the Sentry server (or a Sentry Relay) will respond with HTTP status code 429 ("Too Many Requests") and include Retry-After or X-Sentry-Rate-Limits headers indicating how long the client should wait before sending more events of a specific category (error, transaction, session, attachment).

The good news is that the Sentry SDK automatically respects these rate limits. You generally do not need to implement any rate-limiting logic in your application code. The SDK's transport mechanism handles the back-off based on the server's response headers.

  • The SDK pauses sending events of the specified category for the duration requested by the server.
  • This prevents overwhelming Sentry and helps manage your quota by ensuring you don't send events that would just be dropped by the server anyway.
  • This happens internally within the SDK's transport. If you were using a custom transport, you would need to implement this header parsing and back-off logic yourself.

So, while rate limiting is a Sentry feature, its implementation in the SDK is automatic, requiring no explicit configuration in your Sentry.init options or application code beyond standard setup.

4.4. Filtering and Modifying Events with Hooks

For complex filtering or modification logic that goes beyond simple sampling or ignoring by name/message, Sentry provides hook functions that are called just before events or spans are processed or sent. These hooks give you direct access to the event/span data and the ability to modify it or discard it entirely.

4.4.1. beforeSend (for Errors/Messages)

The beforeSend hook is a function you provide in Sentry.init that's called for every error event (errors and messages with level 'error' or higher) just before it's sent to Sentry. It receives the event object and a hint object (which may contain the original error or other context). You can modify the event object in place or return null to discard the event entirely [9].

import { Event, EventHint } from '@sentry/types';
import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  beforeSend(event: Event, hint?: EventHint): PromiseLike<Event | null> | Event | null {
    // Example 1: Discard errors based on custom logic
    if (event.message?.includes("Specific error to ignore via hook")) {
      console.log("Discarding specific error via beforeSend.");
      return null; // Discard the event [9]
    }

    // Example 2: Modify the event - e.g., add a custom tag or scrub data
    event.tags = event.tags || {};
    event.tags["custom_tag"] = "added_in_beforeSend";

    // Careful scrubbing example (replace potentially sensitive user ID if present in extra)
    if (event.extra?.userId) {
       event.extra.userId = "user_id_scrubbed"; // Scrub value from extra data
    }
    // Similarly, scrub event.user, context, breadcrumb data if needed.

    // You can access the original exception from the hint [9]
    const originalException = hint?.originalException as Error;
    if (originalException instanceof Error && originalException.name === 'SyntaxError') {
       // Change grouping (more details in Grouping chapter) [9]
       event.fingerprint = ['custom-syntax-error-grouping'];
    }

    console.log("Processing event via beforeSend:", event.event_id);
    return event; // Return the event (possibly modified) to send it
  },
  // ... other options
});

Sentry.captureException(new Error("This error will pass through beforeSend."));
Sentry.captureMessage("Specific error to ignore via hook - will be discarded.", "error");

The beforeSend hook is powerful for implementing custom logic that requires access to the full event payload or the original error object. It runs after sampling and automatic context collection, but before the event is sent to the transport. It can be synchronous or asynchronous (returning a Promise).

4.4.2. beforeSendTransaction (for Transactions)

Similar to beforeSend, the beforeSendTransaction hook is specific to performance transaction events. It allows you to inspect and modify transaction data or discard the transaction entirely before it's sent. It receives the transaction event object (which is a type of Event) and a hint.

import { Event, EventHint } from '@sentry/types'; // Transaction is also an Event type
import * as Sentry from "@sentry/node"; // Or framework SDK like @sentry/react

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  tracesSampleRate: 1.0, // Ensure transactions are being sampled
  // Enable integrations that create transactions (e.g., Http for Node, BrowserTracing for browser)
  integrations: [ /* ... your performance integrations ... */ ],

  beforeSendTransaction(event: Event, hint?: EventHint): PromiseLike<Event | null> | Event | null {
    // Example 1: Discard transactions for health check endpoints
    if (event.transaction?.startsWith("GET /health")) {
      console.log("Discarding health check transaction via beforeSendTransaction.");
      return null; // Discard the transaction
    }

    // Example 2: Add a tag based on transaction duration
    const duration = event.timestamp && event.start_timestamp ? (event.timestamp - event.start_timestamp) * 1000 : 0;
    if (duration > 3000) { // If duration > 3 seconds
      event.tags = event.tags || {};
      event.tags["performance_issue"] = "slow_transaction";
    }

    // Example 3: Scrub sensitive data from span attributes (less common here, see beforeSendSpan)
    // You could iterate through event.spans and modify attributes, but beforeSendSpan is often better for this.


    console.log("Processing transaction via beforeSendTransaction:", event.event_id);
    return event; // Send the transaction (possibly modified)
  },
  // ... other options
});

Use beforeSendTransaction for logic specific to filtering or enriching your performance monitoring data.

4.4.3. beforeSendSpan (for Spans)

For even more granular control over performance data, the beforeSendSpan hook allows you to inspect and modify individual span objects just before they are processed within a transaction. It receives the span object.

However, it's important to note a key limitation: returning null from beforeSendSpan will NOT discard the span. [2] If you need to discard a span or a group of spans, you typically need to discard the entire transaction using beforeSendTransaction.

beforeSendSpan is primarily useful for modifying span attributes, names, or operations based on custom logic.

import * as Sentry from "@sentry/node"; // or @sentry/browser
import { Span } from "@sentry/types';

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  tracesSampleRate: 1.0,
  beforeSendSpan(span: Span): Span | null {
    // Example: Remove a potentially sensitive attribute from a span
    if (span.op === 'db.query' && span.data?.["db.statement"]) { // Use span.data for attributes in SDK versions
       // Be careful with potentially sensitive query strings
       span.data["db.statement"] = "[Scrubbed]"; // Replace or remove [9]
    }

    // Example: Modify a span's description [2]
    if (span.description === "Internal calculate function") {
       span.description = "calculate logic (renamed)";
    }

    // NOTE: Returning null from beforeSendSpan will NOT drop the span [2]
    // Use beforeSendTransaction to drop the root span and its children.

    return span; // Always return the span (modified or original)
  },
});

beforeSendSpan is useful for cleaning up or standardizing span data.

4.5. Managing Maximum Event Size

Sentry enforces a maximum payload size for events (errors, transactions) and attachments. If an event or attachment exceeds this limit (configured on the Sentry server/Relay, not in the SDK init), it will be rejected. While the SDK attempts to manage event size by limiting things like breadcrumbs or truncating data, very large context objects or attachments can cause events to be dropped.

The exact maxEventSize or maxAttachmentSize limits are enforced server-side by Sentry or your Relay. There isn't a standard maxEventSize option you set in the JavaScript SDK Sentry.init to control this client-side processing limit.

Your primary ways to manage event size from your TypeScript code are:

  • Limit Breadcrumbs: Use the maxBreadcrumbs option in Sentry.init (covered in Chapter 3).
  • Be Mindful of Context/Extra Data: Avoid adding excessively large strings, arrays, or deeply nested objects to scope.setExtra or scope.setContext if not strictly necessary for debugging.
  • Manage Attachment Size: Manually check the size of files or data you intend to attach and avoid attaching very large ones. The maxAttachmentSize limit applies per-attachment and is enforced server-side.
  • Filter/Truncate in beforeSend: Use the beforeSend hook to detect potentially large events and manually remove large fields or truncate long strings before returning the event.
import { Event, EventHint } from '@sentry/types';
import * as Sentry from "@sentry/node"; // or @sentry/browser
import { Buffer } from 'buffer'; // Node.js Buffer for size estimation

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // maxBreadcrumbs: 50, // Limit breadcrumb count (covered in Chap 3)
  // ... other options

  beforeSend(event: Event, hint?: EventHint): PromiseLike<Event | null> | Event | null {
     // Example: Check approximate event size (requires converting to JSON string)
     // Note: JSON string size is not the exact payload size but a rough estimate.
     // This check is a heuristic; Sentry/Relay perform the final check.
     const eventString = JSON.stringify(event);
     const eventSizeInBytes = Buffer.from(eventString).length; // Node.js example

     // Define a threshold below Sentry's actual limit (e.g., 500KB)
     const SOFT_MAX_SIZE = 500 * 1024;

     if (eventSizeInBytes > SOFT_MAX_SIZE) {
       console.warn(`Event size (${eventSizeInBytes} bytes) exceeds soft threshold. Attempting to reduce.`);
       // Example reduction: Remove large extra data or attachments
       if (event.extra?.debugData) { // Assuming 'debugData' might be large
          delete event.extra.debugData;
          console.warn("Removed large extra data.");
       }
       if (event.attachments && event.attachments.length > 1) { // Keep only the first attachment if multiple
          event.attachments = event.attachments.slice(0, 1);
          console.warn(`Reduced attachments to ${event.attachments.length}.`);
       }
       // You could add more sophisticated logic to trim breadcrumbs, etc.

       // Optionally, re-check size and discard if still too large
       // if (Buffer.from(JSON.stringify(event)).length > SOFT_MAX_SIZE * 2) { // Use a slightly higher threshold for discarding
       //    console.error("Event still too large after reduction, discarding.");
       //    return null;
       // }
     }

    return event; // Send the event (possibly modified)
  },
});

While you can attempt to manage size client-side, the ultimate enforcement happens on the Sentry backend. The best approach is to be mindful of the volume of data you're adding to events, especially in extra, context, and attachments.

4.6. Backpressure Management (Node.js)

Backpressure management is a feature primarily relevant to server-side SDKs like @sentry/node that handle potentially very high volumes of events. It refers to the SDK's internal logic to detect if it's generating events (particularly transactions) faster than it can send them and to dynamically adjust its behavior (like downsampling) to avoid overwhelming the application process or the network stack.

This is largely an internal SDK mechanism and not something you typically configure directly with a simple init option in the JavaScript/TypeScript SDKs [9]. The SDK's internal queues and transport logic attempt to manage the load. If the outgoing queue grows excessively large, the SDK might start dropping events (especially transactions) to prevent unbounded memory usage or blocking I/O.

  • Implementation: Handled internally by the SDK's transport and queuing system, especially relevant for @sentry/node.
  • Configuration: There isn't a standard, simple init flag like enableBackpressureManagement: true. Its effectiveness depends on the SDK's internal heuristics.
  • Observation: You might observe dropped events due to backpressure if your application generates transactions at an extremely high rate, which could be indicated in debug logs if enabled. If you consistently see dropped events and suspect backpressure, consider reviewing your sampling rates or the volume of data being added to events and spans.

For most standard Node.js applications, the default internal backpressure handling is sufficient.

Chapter 5: Performance Monitoring and Tracing

Beyond error tracking, Sentry offers powerful Performance Monitoring capabilities. This involves collecting data about how long operations take, identifying bottlenecks, and understanding the flow of requests through your system using Distributed Tracing. In Sentry, performance data is organized into Transactions and Spans.

5.1. Enabling Performance Monitoring

Performance monitoring is enabled by configuring sampling for transactions in your Sentry.init call, using either tracesSampleRate or tracesSampler (as discussed in Chapter 4).

  • Setting tracesSampleRate to a value greater than 0.0 (typically 1.0 in development, a lower value like 0.1 or 0.01 in production) enables the collection of transaction data.
  • Using tracesSampler provides dynamic control over which transactions are sampled.

Performance monitoring focuses on Transactions, which represent a single distinct operation or unit of work in your application, such as:

  • An incoming HTTP request to a server (http.server operation)
  • A page load or navigation in a browser (pageload, navigation)
  • A background job execution (queue.process)
  • A cron job run (cron)
  • A function execution triggered by an event (function)

These root transactions contain Spans which represent smaller, individual operations within the transaction.

5.2. Instrumenting Operations with Spans

Spans are the building blocks of a trace. They represent discrete units of work within a transaction. For example, within an HTTP request transaction, there might be spans for database queries, external HTTP calls, template rendering, or specific function executions.

Sentry's automatic integrations create spans for common operations like HTTP requests or database access (in some environments). You can also manually create spans to instrument custom logic in your application using Sentry.startSpan or Sentry.startSpanManual.

  • Sentry.startSpan(spanContext, callback): The recommended method for most cases. It takes a spanContext object (defining the span's name, operation, attributes, etc.) and a callback function. The span is automatically started before the callback runs and automatically finished when the callback resolves (if it's asynchronous) or finishes executing (if synchronous). The callback receives the newly created span instance [9, 15].
  • Sentry.startSpanManual(spanContext): Creates a span but requires you to manually call span.end() when the operation is complete. This is useful when the span's lifetime doesn't align cleanly with a function's execution or promises. [9, 15]
import * as Sentry from "@sentry/node"; // or @sentry/browser, etc.
import { Span } from "@sentry/types'; // Import Span type

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  tracesSampleRate: 1.0, // Enable tracing
  // Ensure performance integrations are enabled (e.g., HTTP, framework integrations)
  integrations: [ /* ... your tracing integrations ... */ ],
});

// --- Creating Spans ---

// Option 1: Using startSpan (recommended for most cases)
// Automatically handles active span context and finishes the span [9, 15]
async function processUserData(userId: string) {
  // Start a span for this function execution
  await Sentry.startSpan({
    name: "processUserData", // Span description - visible in UI
    op: "function",         // Span operation - groups similar spans, often searchable [9]
    attributes: {           // Add searchable attributes/tags [9, 15]
      "user.id": userId,    // Use semantic conventions for attribute names if applicable
      "data_source": "database",
    }
  }, async (span) => { // Callback with the active span instance
       try {
         console.log(`Processing data for user ${userId}...`);
         // Simulate fetching data
         await new Promise(resolve => setTimeout(resolve, 100));

         // Start a child span for the database operation
         await Sentry.startSpan({
            name: "Fetch User Data", // Child span description
            op: "db.query",          // Child span operation
            attributes: {
               "db.system": "postgres", // Example database attributes
               "db.operation": "SELECT",
               // "db.statement": "SELECT * FROM users WHERE id = $1", // Careful with PII
            }
         }, async () => {
             // Simulate DB query
             await new Promise(resolve => setTimeout(resolve, 50));
         });

         // Add an attribute to the parent span *after* it's started [9, 15]
         span?.setAttribute("processing_status", "success");
         span?.setStatus("ok"); // Set span status on success [8]

         console.log("User data processed.");
       } catch (e) {
          // If an error occurs within this span, it will be linked to the span in Sentry [9]
          span?.setStatus("internal_error"); // Set span status on error [8]
          Sentry.captureException(e);
          throw e; // Re-throw to propagate the error
       }
  }); // Span automatically finishes when the callback ends/awaits resolve
}

// Option 2: Using startSpanManual (if you need more control over span lifetime) [9, 15]
async function sendEmailNotification(email: string) {
    const span = Sentry.startSpanManual({
        name: "Send Email",
        op: "messaging.send",
        attributes: {
            "messaging.system": "smtp",
            "recipient.email": email, // Careful with PII
        }
    }); // Start the span manually

    try {
        console.log(`Sending email to ${email}...`);
        // Simulate sending email
        await new Promise(resolve => setTimeout(resolve, 200));
        span.setStatus("ok"); // Set status on success [8]
        console.log("Email sent.");
    } catch (e) {
        span.setStatus("internal_error"); // Set status on error [8]
        Sentry.captureException(e); // Error within manual span is still captured
        throw e;
    } finally {
        span.end(); // Manually end the span [9, 15]
    }
}

// --- Getting the Active Span ---
// You can get the current active span (the one created by the innermost startSpan/startSpanManual)
// using Sentry.getActiveSpan(). This is useful for adding attributes dynamically.
function logProcessingStep(stepName: string) {
  const activeSpan = Sentry.getActiveSpan(); // Get the current active span
  if (activeSpan) {
    activeSpan.setAttribute(`step.${stepName}.timestamp`, Date.now()); // Add attributes to the active span [9]
    // You could also add a child span here if the step is significant.
  }
}

// processUserData("user456");
// sendEmailNotification("user456@example.com");

By strategically placing spans around key operations in your code, you build a detailed timeline visible in Sentry's Trace View, helping you identify which parts of your application are contributing most to latency.

5.3. Adding Searchable Attributes to Spans

Context on spans is provided via Attributes (sometimes referred to as span "data" or "tags"). These are key-value pairs attached to a span that describe the specific instance of the operation. For example, a database query span might have attributes like db.system, db.operation, db.user, or db.statement (with PII caution).

These attributes are crucial because they enable you to search and filter your performance data in Sentry's Trace Explorer and Discover features. Attributes added via the attributes option during startSpan/startSpanManual or via span.setAttribute(key, value) are searchable.

import * as Sentry from "@sentry/node"; // or @sentry/browser

// Assuming Sentry.init with tracing enabled is done...

// Using startSpan with initial attributes
Sentry.startSpan({
  name: "processOrder",
  op: "order.process",
  attributes: { // Add attributes when creating the span [9, 15]
    "order.id": "XYZ123", // Use semantic conventions where possible
    "customer.tier": "gold",
    "process.step": "validation_start",
  }
}, async (span) => {
    // Simulate work
    await new Promise(resolve => setTimeout(resolve, 50));

    // Add attributes dynamically during the span's lifetime [9, 15]
    span?.setAttribute("process.step", "payment_processing");
    span?.setAttribute("payment.method", "credit_card");

    await new Promise(resolve => setTimeout(resolve, 100));

    span?.setAttribute("process.step", "fulfillment");
    span?.setAttribute("fulfillment.service", "shipping_partner");

    // Span finishes automatically here
});

// Example of manually adding attributes to the current active span
function checkInventoryStatus(productId: string) {
  const activeSpan = Sentry.getActiveSpan();
  if (activeSpan) {
    activeSpan.setAttribute("inventory.product_id", productId); // Attribute added to the current span
    activeSpan.setAttribute("inventory.check_time", Date.now());
  }
  // ... check inventory ...
}

// Sentry.startSpan({ name: 'full-checkout-process', op: 'checkout' }, () => {
//    checkInventoryStatus("PROD-ABC");
//    // ... other steps ...
// });

Using widely adopted semantic conventions for attribute names (like those from OpenTelemetry) is highly recommended. Sentry understands many of these conventions, enabling richer visualizations and analyses in the UI.

You can search for spans in Sentry using queries like:

  • span.op: "db.query"
  • span.name: "Fetch User Data"
  • span.data.order.id: "XYZ123" (or data.order_id depending on naming and Sentry version)
  • span.data.customer.tier: "gold"

Well-defined span operations and attributes are the backbone of useful performance monitoring in Sentry.

5.4. Automatic Tracing Integrations

Many of Sentry's performance monitoring capabilities for common operations are provided automatically through integrations. These integrations instrument standard libraries or framework entry points to create transactions and spans without manual coding.

5.4.1. HTTP Client/Server Instrumentation

Handling incoming HTTP requests (server SDKs) and making outgoing HTTP requests (both server and browser SDKs) are fundamental operations automatically instrumented by Sentry's HTTP integrations.

  • Node.js (@sentry/node): The Http integration (often enabled by default) instruments Node's built-in http and https modules. When configured with tracing: true, it automatically creates:

    • Transactions for incoming server requests (e.g., GET /users/:id).
    • Client spans for outgoing requests made using http.request or https.request. It also handles propagating trace context headers (sentry-trace, traceparent, baggage) to outgoing requests if the destination matches tracePropagationTargets.
    import * as Sentry from "@sentry/node";
    import { Http } from "@sentry/integrations"; // Usually not needed unless customizing
    import http from 'http';
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      integrations: [
        // Http integration is on by default. Specify explicitly to customize options. [4, 5]
        new Http({
          tracing: true, // Enable creation of spans for outgoing requests and transactions for incoming [4]
          // breadcrumbs: false, // Optionally disable breadcrumbs for HTTP requests
        }),
        // ... other integrations like Express/Koa for server request handling
      ],
      tracesSampleRate: 1.0, // Needed for transactions/spans to be sent
    
      // Propagate tracing headers to these URLs (required for distributed tracing) [4]
      // Includes 'localhost' by default, add your own internal/external APIs.
      tracePropagationTargets: ['localhost', /^\//, 'api.external.com'], // Regex or string matches
      // Be careful with sensitive URLs or sending headers to untrusted destinations.
    
      // Option for capturing HTTP Client errors as Sentry error events (check docs for exact naming/availability) [2]
      // captureFailedRequests: true, // Example, might be named differently [2]
      // failedRequestStatusCodes: [ { min: 500, max: 599 }, 404 ], // Example [2]
      // failedRequestTargets: [ /api\.example\.com/ ], // Example [2]
    });
    
    // When an incoming request hits your Node.js server (if using a framework integration),
    // a transaction is automatically started.
    // When you make an outgoing http.get call:
    Sentry.startSpan({ name: 'make-external-request', op: 'function' }, () => { // Ensure there is an active span/transaction
        http.get('http://httpbin.org/get', (res) => {
            // This request will automatically:
            // - Generate a breadcrumb (if breadcrumbs enabled)
            // - Generate a span 'http.client' as a child of the active span/transaction
            // - Include 'sentry-trace', 'traceparent', 'baggage' headers (if tracing enabled and target matches tracePropagationTargets)
            console.log('Response status:', res.statusCode);
            res.resume(); // Consume response data
        }).on('error', (e) => {
            console.error(`Got error: ${e.message}`);
            // If captureFailedRequests were enabled and criteria match, Sentry might capture this
        });
    });
    
  • Browser (@sentry/browser): Default integrations automatically instrument fetch and XMLHttpRequest calls to create breadcrumbs. The BrowserTracing integration (often included in framework SDKs like @sentry/react) extends this to create client spans for these requests and automatically sample transactions for page loads and navigations. It also handles trace header propagation for requests matching tracePropagationTargets.

    import * as Sentry from "@sentry/browser";
    import { browserTracingIntegration } from "@sentry/react"; // Example if using React - import path varies
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      integrations: [
        // Fetch & XHR are instrumented by default integrations (for breadcrumbs).
        // Add browserTracingIntegration for performance spans [7]
        Sentry.browserTracingIntegration({ // Use SDK export if available
           // Options for browser tracing, like tracePropagationTargets [7]
           tracePropagationTargets: ['localhost', /^\//, 'api.yourdomain.com'], // Regex or string matches
        }),
        // ... other integrations
      ],
      tracesSampleRate: 1.0, // Required for performance traces
    
      // Option for capturing HTTP Client errors as Sentry errors (check docs for exact naming/availability) [2]
      // captureFailedRequests: true, // Example [2]
      // failedRequestStatusCodes: [ { min: 500, max: 599 } ], // Example [2]
      // failedRequestTargets: [ /api\.yourdomain\.com/ ], // Example [2]
    });
    
    // Example fetch request
    async function fetchData() {
      // If this fetch is part of a page load or within a manually started span/transaction,
      // the fetch will automatically create a child span.
      // Sentry.startSpan({ name: "fetch-data-operation", op: "custom" }, async () => { // Optional: wrap in custom span
        try {
          const response = await fetch('/api/data');
          // This fetch will automatically:
          // - Create a breadcrumb
          // - Create a span 'fetch'/'xhr' as a child of the active transaction/span
          // - Add tracing headers if origin matches tracePropagationTargets
          console.log("Fetch status:", response.status);
          if (!response.ok) {
             // If captureFailedRequests were enabled and status matches, Sentry might capture this as an Error event
          }
        } catch(e) {
           // Network errors (like connection refused) are typically captured by default GlobalHandlers or captureException
           Sentry.captureException(e);
        }
      // }); // End custom span
    }
    fetchData();
    

The captureFailedRequests feature, mentioned in the source, allows automatically capturing client-side HTTP responses that indicate errors (like 5xx status codes) as distinct Sentry Error events, rather than just noting them as failed transactions or spans. Configuration involves enabling the feature and specifying which status codes and URLs should trigger an error event [2]. Availability and exact option names may vary by SDK version; check the specific SDK documentation.

5.4.2. GraphQL Client/Server Instrumentation

Applications using GraphQL often make HTTP POST requests to a single endpoint. While Sentry's standard HTTP integrations will instrument the underlying HTTP call, specific GraphQL integrations or manual instrumentation can provide richer context like the operation name, type (query/mutation), and variables.

  • General Approach: Since GraphQL typically runs over HTTP, the standard HTTP Client Integrations (Point 5.4.1) automatically provide basic instrumentation (breadcrumbs, tracing spans for the HTTP part).
  • Dedicated Integrations: Look for specific Sentry packages or integrations tailored for popular GraphQL clients (like Apollo Client, urql) or servers. These integrations can automatically capture GraphQL errors and add span attributes specific to the GraphQL operation.
  • Manual Instrumentation / Enrichment: If a dedicated integration doesn't exist or doesn't meet your needs, you can manually enhance the data captured by the HTTP integration:
    • Add Breadcrumbs: Use Sentry.addBreadcrumb in your GraphQL client's links or middleware to log operation names, types, and results.
    • Add Span Data: Within an active Sentry trace, use Sentry.startSpan or Sentry.getActiveSpan()?.setAttribute() to add spans or attributes specific to the GraphQL operation. Use op values like graphql.execute, graphql.parse, graphql.validate, graphql.resolve. Add attributes like graphql.operation.name, graphql.operation.type, graphql.document, graphql.variables (with PII care).
    • Capture Errors: Implement error handling in your GraphQL client/server logic (e.g., in Apollo Links) to check for GraphQL errors in the response payload and explicitly capture them using Sentry.captureException or Sentry.captureMessage, adding relevant GraphQL context.
// --- Conceptual Example (Manual / using Apollo Client Links) ---
// This demonstrates manual enrichment on top of Sentry's default HTTP instrumentation.
import * as Sentry from "@sentry/browser"; // or Node
import { ApolloClient, InMemoryCache, createHttpLink, ApolloLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context'; // Optional: for adding context like headers
import { onError } from "@apollo/client/link/error"; // Apollo error handling link

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  tracesSampleRate: 1.0,
  // Ensure HTTP integration is enabled (part of defaults or explicitly added with tracing: true)
  integrations: [ /* ... Sentry's HTTP integration is needed here ... */ ],
  tracePropagationTargets: [ /* ... your GraphQL endpoint URL pattern ... */ ], // Ensure headers are propagated
});


// 1. HTTP Link (gets instrumented by Sentry HTTP integration by default - provides http span)
const httpLink = createHttpLink({ uri: '/graphql' });

// 2. Error Link (for capturing GraphQL errors from the response payload)
const errorLink = onError(({ graphQLErrors, networkError, operation, response }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      // Capture each GraphQL error as a Sentry event
      Sentry.withScope(scope => {
        scope.setTag("graphql.operation.name", operation.operationName || 'unknown_operation');
        scope.setTag("graphql.operation.type", operation.query.definitions[0]?.kind === 'OperationDefinition'
                                                ? operation.query.definitions[0].operation
                                                : 'unknown_type');
        scope.setContext("graphql", {
          // query: operation.query.loc?.source.body, // Be mindful of PII/size - potentially large schema string
          variables: operation.variables,          // Be mindful of PII - sensitive input variables
          errorMessage: message,
          locations: locations, // Error locations in the query
          path: path, // Path to the error in the response data
        });
        // Consider custom fingerprinting for grouping errors by operation and path
        // This helps group errors from the same field/resolver regardless of input data.
        scope.setFingerprint(['graphql', operation.operationName || 'unknown_op', path ? path.join('.') : 'unknown_path', message]);
        Sentry.captureMessage(`GraphQL Error: ${message}`, 'error'); // Or captureException(new Error(message)) for better stack trace if possible
      });
    });
  }

  if (networkError) {
    // Network errors related to the request itself are often caught by HTTP integration
    // but you could capture them here too if needed, adding GraphQL context.
    Sentry.withScope(scope => {
       scope.setTag("graphql.operation.name", operation.operationName || 'unknown_operation');
       // Add other relevant context...
       Sentry.captureException(networkError);
    });
  }
});

// 3. Link for Adding Breadcrumbs / Spans (Example)
const sentryLink = new ApolloLink((operation, forward) => {
  const operationName = operation.operationName || 'UnnamedGraphQLOp';
  const operationType = operation.query.definitions[0]?.kind === 'OperationDefinition'
                       ? operation.query.definitions[0].operation
                       : 'unknown';

  // Add Breadcrumb for the start of the operation
  Sentry.addBreadcrumb({
    category: 'graphql',
    message: `GraphQL Operation Start: ${operationType} ${operationName}`,
    data: {
       type: operationType,
       name: operationName,
       // variables: operation.variables, // Careful with PII/size
    },
    level: 'info',
  });

  // Add Performance Span (if inside an active Sentry transaction)
  // This span will be a child of the HTTP span created by Sentry's HTTP integration.
  const span = Sentry.getActiveSpan()?.startChild({
    op: `graphql.client.${operationType}`, // e.g., graphql.client.query, graphql.client.mutation
    description: operationName,
    attributes: {
        // Add relevant attributes for searching and context [9, 15]
        "graphql.operation.name": operationName,
        "graphql.operation.type": operationType,
        // "graphql.document": operation.query.loc?.source.body, // Careful with size/PII
        // "graphql.variables": operation.variables, // Careful with PII
    }
  });

  return forward(operation).map((response) => {
    // Map is called on success or failure (with errors)
    span?.setStatus(response.errors ? 'internal_error' : 'ok'); // Set span status based on GraphQL errors [8]
    span?.end(); // Manually end the span after response is processed

    // Add Breadcrumb for the response
    Sentry.addBreadcrumb({
        category: 'graphql',
        message: `GraphQL Operation Response: ${operationType} ${operationName}`,
        level: response.errors ? 'error' : 'info',
        data: { hasErrors: !!response.errors, numErrors: response.errors?.length || 0 }
    });

    return response; // Return the response to the next link
  }).finally(() => {
      // Ensure span ends even if there's an error in the map function or a network error
      if (span && !span.isRecording()) { // Check if span is still active
          span.end();
      }
  });
});


// Combine links in the order they should execute
const client = new ApolloClient({
  link: ApolloLink.from([
      sentryLink, // Custom Sentry span/breadcrumb link
      errorLink,  // Sentry error handling link
      setContext((_, { headers }) => { // Example: Link for adding custom headers (auth etc.)
        // return new headers object
         return {
            headers: {
              ...headers,
              // add headers here
            }
         }
      }),
      httpLink    // Base HTTP link (instrumented by Sentry default)
  ]),
  cache: new InMemoryCache(),
});


// Using the client...
// client.query(...) or client.mutate(...)

This manual approach, while more verbose than a dedicated integration, gives you full control over the Sentry data generated from your GraphQL interactions. Remember to apply appropriate PII scrubbing to variables or document strings.

5.5. Node.js Performance Profiling

For Node.js applications, Sentry offers the ability to collect CPU performance profiles alongside your traces. This helps pinpoint the exact functions consuming the most CPU time within a sampled transaction.

This feature requires a separate package, @sentry/profiling-node, and must be explicitly enabled during initialization. It leverages Node.js's V8 CPU profiler.

// In your Node.js application entry file, very early:
// Ensure this is imported *before* any other modules if using manual profiling [5]
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node"; // [3]

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  integrations: [
    // Add the profiling integration [3, 5]
    nodeProfilingIntegration(),
    // Ensure you also have performance tracing enabled
    // (e.g., Http integration with tracing: true, or manual tracing)
    new Sentry.Integrations.Http({ tracing: true }),
    // ... other integrations
  ],
  tracesSampleRate: 1.0, // Tracing must be enabled for profiling to work with trace mode [3]
  // Set a sample rate for profiling sessions.
  // This determines what percentage of sampled transactions get a profile attached. [3]
  profilesSampleRate: 1.0, // Profile all sampled transactions [3]
  // profilesSampleRate: 0.5, // Profile only 50% of sampled transactions

  // Configure the profiling lifecycle ('trace' or 'manual') [3, 10]
  // 'trace' mode automatically profiles based on active spans (common and recommended)
  profileLifecycle: 'trace',
  // 'manual' mode requires explicitly starting/stopping the profiler (less common)
  // profileLifecycle: 'manual', // If using manual mode
});

// --- If using 'manual' profileLifecycle mode --- [10]
// You would explicitly start and stop the profiler around code you want to profile.
// This requires accessing the internal profiler instance.
// async function manuallyProfiledFunction() {
//     const profiler = Sentry.getClient()?.getProfilingIntegration(); // Method to get integration might vary
//     if (!profiler) { console.warn("Profiling integration not found."); return; }
//     profiler.start(); // Start the profiler [10]
//     try {
//         // ... code to profile ...
//         await new Promise(resolve => setTimeout(resolve, 100));
//     } finally {
//         profiler.stop(); // Stop the profiler [10]
//     }
// }
// Note: Manual profiling is less common; 'trace' mode is often preferred for automatic profiling.

// With 'trace' mode enabled, any code executed within a sampled transaction or span
// will be automatically profiled based on your profilesSampleRate. [3]
Sentry.startSpan({ name: 'MyProfiledOperation', op: 'custom' }, async () => {
   await new Promise(resolve => setTimeout(resolve, 100));
   console.log("This code was profiled.");
});

Node.js profiling is a powerful tool for drilling down into CPU-bound performance issues. It's important to note that this feature is only available for Node.js environments, not in browsers, as it relies on V8's specific profiling capabilities. Ensure your tracesSampleRate is set correctly, as profiling is attached to sampled traces.

5.6. Performance Trend and Regression Detection

One of the key benefits of sending performance data to Sentry is the automatic analysis of Performance Trends and the detection of Regressions. Sentry's backend analyzes the performance data (transaction and span durations) you send for each release. It identifies statistically significant changes in key metrics (like p95 latency or error rates) for specific transactions compared to previous releases.

This is primarily an automatic analysis feature of the Sentry backend/UI and not something you implement in your TypeScript code.

Your role in enabling this feature is to:

  1. Enable and Configure Performance Monitoring: Ensure tracesSampleRate or tracesSampler is set to collect transaction data.
  2. Instrument Your Code: Use automatic integrations and manual spans to generate meaningful performance data for your key operations.
  3. Set the release Option: Crucially, you must configure the release option in Sentry.init with a unique identifier for each code version you deploy. This allows Sentry to correlate performance data with specific releases and identify changes that occur after a deployment.
import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  tracesSampleRate: 1.0, // Or tracesSampler function to enable tracing
  // VERY IMPORTANT: Set the release version [14]
  // Automate this during your build/deployment process.
  release: process.env.MY_APP_VERSION || "my-app@1.0.0",
  // Configure relevant performance integrations
  integrations: [ /* ... your tracing integrations ... */ ],
  // ... other options
});

// Ensure your code is instrumented to create transactions and spans (as shown in previous sections).

// Sentry will automatically analyze the performance data sent for this release
// and compare it to previous releases configured with a different 'release' value.
// Performance regressions (e.g., a specific transaction suddenly getting slower)
// will be highlighted in the Sentry UI based on this data, often correlated with the new release.

By consistently setting the release and sending transaction data, you unlock Sentry's ability to automatically identify performance issues introduced in new code versions, making it easier to pinpoint and roll back problematic deployments.

5.7. Linking Traces (Span Links)

In distributed systems or complex applications, an operation might involve multiple distinct traces. For example, a single incoming web request might trigger a message queue event, which is then processed by a separate background worker. You might want to link the trace of the incoming request to the trace of the background job execution to understand the full end-to-end flow.

Span Links (sometimes referred to just as "Links") allow you to represent these relationships between spans or traces. A link is an explicit connection from one span to the root span of another trace.

While Sentry's automatic HTTP integrations handle distributed tracing for standard request/response flows (by propagating trace headers like sentry-trace and baggage [8, 18]), manual span links are useful for linking operations that don't naturally share HTTP context, like triggering a background job or processing a message queue.

The API for adding links is part of the Span object itself. You typically add links when creating a span or immediately after starting it. The API might resemble adding attributes. The link points to the traceId and spanId of the target span (usually the root span of the linked trace).

import * as Sentry from "@sentry/node"; // or @sentry/browser
import { Span, TraceparentData, SpanStatus } from '@sentry/types'; // Import types

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  tracesSampleRate: 1.0,
  // ... other options
});

// --- Linking Traces (Advanced) ---
// This is a conceptual example based on OpenTelemetry/Sentry API patterns.
// The exact API methods might vary slightly between SDK versions. [8]

async function handleIncomingRequestAndTriggerJob(requestData: any) {
    // Assume Sentry's request handler starts a transaction for this incoming request
    const requestTransaction = Sentry.getActiveSpan(); // Get the request transaction span

    // Simulate triggering a background job and obtaining its trace/span info
    // In a real scenario, the background job start would be instrumented,
    // and you'd pass the parent trace context, *or* the background job would
    // generate its own trace, and you'd get its trace/span IDs back somehow.
    const { traceId: backgroundJobTraceId, spanId: backgroundJobRootSpanId } = await triggerBackgroundJobProcessing(requestData);


    // Add a span link from the request transaction to the background job trace's root span
    if (requestTransaction && backgroundJobTraceId && backgroundJobRootSpanId) {
        // The Span interface/API for adding links might vary slightly; check latest SDK docs. [8]
        // Example conceptual API for adding a link to an existing span's context:
        const link = {
          context: {
            traceId: backgroundJobTraceId,
            spanId: backgroundJobRootSpanId, // Link to the root span of the background job trace
          },
          attributes: { // Optional attributes for the link itself
             "sentry.link.type": "triggered_background_job" // Custom attribute for the link
          }
        };
        // Check if the Span interface has an addLink method or similar
        // (This is an OpenTelemetry concept integrated into Sentry's Span)
        // requestTransaction.addLink(link); // Conceptual method to add links [8]
        console.log(`Added span link from request trace ${requestTransaction.spanContext().traceId} to job trace ${backgroundJobTraceId}`);
    }

    // ... rest of request handling ...

    // The request transaction span finishes (automatically by handler or manually)
}

// Simulate a background job that creates its own trace
async function processBackgroundJob(jobData: any): Promise<{ traceId: string, spanId: string }> {
    // Start a NEW transaction for this background job
    const jobTransaction = Sentry.startSpanManual({
       name: "processBackgroundJob",
       op: "queue.process",
       attributes: { "job.id": jobData.id },
       // If the job was triggered with parent trace context, you could pass it here
       // parentSpanId: jobData.parentSpanId, traceId: jobData.traceId,
       // If you want to link *back* to the triggering request, you could add a link here
       // links: [ { context: { traceId: jobData.triggerTraceId, spanId: jobData.triggerSpanId }, attributes: { "sentry.link.type": "triggered_by_request" } } ]
    });


    try {
        console.log(`Processing job ${jobData.id}`);
        // Simulate job work with child spans
        await Sentry.startSpan({ name: "parse_job_data", op: "job.step" }, async () => { /* ... */ await new Promise(r=>setTimeout(r, 50)); });
        await Sentry.startSpan({ name: "save_job_result", op: "db.write" }, async () => { /* ... */ await new Promise(r=>setTimeout(r, 100)); });

        jobTransaction.setStatus("ok"); // Set status [8]
        console.log(`Job ${jobData.id} finished.`);
    } catch (e) {
        jobTransaction.setStatus("internal_error"); // Set status [8]
        Sentry.captureException(e); // Capture error within the job trace
        throw e;
    } finally {
        jobTransaction.end(); // Manually end the job transaction [9, 15]
    }

    return {
        traceId: jobTransaction.spanContext().traceId, // Return the new trace ID
        spanId: jobTransaction.spanContext().spanId // Return the root span ID
    };
}

// Conceptual function to trigger the job and return its trace info
async function triggerBackgroundJobProcessing(data: any): Promise<{ traceId: string, spanId: string }> {
    // In a real scenario, this might involve sending a message to a queue,
    // and the worker picks it up and starts the processBackgroundJob function.
    // The trace/span IDs would need to be correlated and passed.
    console.log("Simulating triggering background job...");
    // For this example, we'll just call the function directly and get its trace info.
    // In a real async message queue system, getting these IDs back might be complex or require different correlation mechanisms.
    const jobTraceInfo = await processBackgroundJob({ id: Math.random().toString(8).slice(2), ...data });
    return jobTraceInfo;
}

// Simulate an incoming request that triggers the job
// handleIncomingRequestAndTriggerJob({ payload: '...' });

Implementing manual span links is an advanced technique for visualizing complex, distributed application flows in Sentry's Trace View. It requires careful coordination to ensure the necessary traceId and spanId are available at the point where the link is created.

Chapter 6: Debugging with Source Code & Grouping

When errors occur, seeing the stack trace is essential. But seeing just lines like app.js:123:45 isn't very helpful. Sentry offers features to show you the original TypeScript source code, identify which code is yours, and control how similar errors are grouped together.

6.1. Source Maps for Surrounding Source and Desymbolication

JavaScript/TypeScript code is typically minified, bundled, and transpiled for production. This process makes the code smaller and more compatible, but it obscures the original source location in stack traces. Source Maps are special files (.js.map) that provide a mapping between locations in the transformed code and locations in the original source files.

Sentry relies on Source Maps to:

  1. Show Surrounding Source: Display the original TypeScript code snippet around the line where an error occurred.
  2. Desymbolication: Translate function names and file paths in the minified stack trace back to their original, human-readable names and locations in your source code.

This isn't an SDK option you toggle with a flag. It's primarily a build process and tooling concern [12, 15].

Steps to enable Source Maps:

  1. Generate Source Maps: Configure your TypeScript compiler (tsconfig.json) and your bundler (Webpack, Rollup, Vite, esbuild, etc.) to produce source maps during your production build. Ensure the source maps reference your original source files correctly.

    • tsconfig.json:

      {
        "compilerOptions": {
          // ... other options
          "sourceMap": true, // Generate .map files
          // "inlineSources": true, // Optionally embed source content directly in the map (larger map files, fewer separate requests for Sentry)
        }
      }
      
*   Bundler Configuration: Set the appropriate source map option (e.g., `devtool: 'source-map'` or `'hidden-source-map'` in Webpack). Consult your bundler's documentation. Choose a source map type that works well with production (e.g., `hidden-source-map` doesn't include sourceMappingURL comments in the output JS, reducing size slightly).
  1. Upload Source Maps to Sentry: After building, you need to upload the generated .js files, their corresponding .js.map files, and your original source files to Sentry. This must be done for the specific release and dist (if used) that your application is reporting. Sentry needs the source maps to be available before an event from that release/dist is processed.

    The most common tools for uploading are:

    • Sentry CLI: A command-line tool used in your CI/CD pipeline.
    • Bundler Plugins: Plugins for Webpack, Rollup, Vite, etc., that automate the upload as part of the build.
    # Example using Sentry CLI
    # Ensure SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT environment variables are set
    
    # Get the release name (should match the 'release' option in Sentry.init)
    export SENTRY_RELEASE=$(node -p "require('./package.json').version")
    
    # Create a new release in Sentry
    sentry-cli releases new $SENTRY_RELEASE
    
    # Upload source maps and source files from your build output directory (e.g., 'dist')
    # --url-prefix is CRITICAL: It tells Sentry the URL path where the JS files will be served from
    sentry-cli releases files $SENTRY_RELEASE upload-sourcemaps ./dist \
      --url-prefix '~/js' # Example: if your JS is served from https://your-app.com/js/bundle.js
    
    # Finalize the release - makes it visible in Sentry UI
    sentry-cli releases finalize $SENTRY_RELEASE
    

    Or using a bundler plugin (e.g., Webpack @sentry/webpack-plugin):

    // webpack.config.js
    const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");
    const packageJson = require("./package.json");
    
    module.exports = {
      // ... other webpack config
      devtool: "source-map", // Or 'hidden-source-map' for production
    
      plugins: [
        // ... other plugins
        sentryWebpackPlugin({
          org: process.env.SENTRY_ORG,
          project: process.env.SENTRY_PROJECT,
          authToken: process.env.SENTRY_AUTH_TOKEN,
          // Use the same release name as in Sentry.init
          release: {
             name: process.env.SENTRY_RELEASE || packageJson.version,
          },
          // Point to the directory containing your built JS and map files
          include: "./dist",
          // Optional: Ignore paths that shouldn't be uploaded
          ignoreFile: ".sentryignore",
          // CRITICAL: Match the URL prefix where your static assets are served
          // urlPrefix: "~/static/assets", // Example: if served from https://your-app.com/static/assets/bundle.js
    
          // Optional: Clean up artifacts from previous releases
          // cleanArtifacts: true,
        }),
      ],
    };
    

Providing source maps is essential for efficient debugging in Sentry, allowing you to see the familiar TypeScript code instead of obfuscated JavaScript.

6.2. Identifying In-App Frames

Stack traces often include frames from your application code, framework code (React, Express), library code (lodash, axios), and the underlying runtime (Node.js, browser engine). Sentry attempts to identify which frames belong to your application code ("in-app" frames) versus external libraries.

This helps Sentry prioritize and highlight the parts of the stack trace most relevant to you, often collapsing frames from known libraries by default in the UI.

Sentry makes an educated guess about in-app frames based on common patterns (like node_modules paths). You can refine this using the inAppIncludes and inAppExcludes options in Sentry.init. These options accept arrays of strings or regular expressions that are matched against the abs_path property of stack frames.

import * as Sentry from "@sentry/node"; // or @sentry/browser

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",

  // --- Automatic Detection ---
  // Many SDKs (like Next.js, Remix) try to configure this automatically.
  // For standard Node/Browser, you might need manual config if defaults aren't right.

  // --- Manual Configuration --- [1]
  // Specify modules or paths considered part of YOUR application [1]
  // Useful if your compiled code structure is non-standard or includes internal libraries.
  // Paths are matched against the `abs_path` (absolute path) of the stack frame
  // as resolved by source maps (if used) or the runtime. Be mindful of paths after compilation.
  inAppIncludes: [
    // Match frames originating from files within 'src/app' directory (common source structure)
    // Needs to match the path in the stack trace, which might be relative to the build output.
    "app/src/app", // Example based on potential compiled path structure
    "my-internal-module", // Example: Match frames from an internal npm module
    // Use patterns if needed, check SDK docs for exact pattern support
    // "**/my-app-modules/**"
  ],

  // Optionally, exclude specific paths/modules even if matched by `inAppIncludes` [1]
  inAppExcludes: [
     // Example: Exclude a vendored library that ended up inside your 'app/src/app' build output
    "app/src/app/vendor",
    /node_modules/, // Usually excluded by default, but can be explicit
  ],

  // ... other options
});

Correctly configuring inAppIncludes and inAppExcludes helps Sentry present stack traces in a more focused and actionable way, making debugging faster.

6.3. Customizing Error Grouping (Fingerprinting)

Sentry automatically groups similar error events into issues based on characteristics like the error type, message, and stack trace. This prevents your Sentry project from being flooded with duplicate errors and allows you to focus on unique problems.

While Sentry's default grouping algorithm is effective, you might sometimes need to override it to force specific events to group together differently. This is done using Custom Fingerprinting from the SDK by setting the fingerprint property on the event object.

You typically set the fingerprint in the beforeSend hook, where you have access to the full event data and the original error. The event.fingerprint property is an array of strings. Sentry concatenates these strings (with commas) to create the unique identifier for grouping. Events with the same final fingerprint string will be grouped into the same Sentry issue [2].

import * as Sentry from "@sentry/node"; // or @sentry/browser
import { Event, EventHint } from "@sentry/types"; // Import necessary types

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // ... other options

  beforeSend(event: Event, hint?: EventHint): PromiseLike<Event | null> | Event | null {
    // Access the original exception from the hint to make grouping decisions [9]
    const originalException = hint?.originalException as Error;

    // Example 1: Group all network errors from a specific API endpoint together
    if (originalException?.message.includes("Network Error") && event.request?.url?.includes("/api/external/")) {
      // Set a custom fingerprint. All events with this exact fingerprint string
      // will be grouped into the same Sentry issue. [2]
      event.fingerprint = ["external-api-network-error"];
      console.log("Applied custom fingerprint for external API network error.");
    }

    // Example 2: Group errors by a specific business ID from the message or context
    // This is useful if the error message contains a unique ID that would normally
    // cause Sentry to group them separately, but you want them grouped by the root cause.
    // Or, if you want to group errors based on a specific parameter from the request/context.
    const orderIdMatch = originalException?.message.match(/Order ID: (\w+)/);
    const currentOrderId = orderIdMatch ? orderIdMatch[1] : (event.tags?.orderId as string | undefined);

    if (originalException instanceof Error && originalException.message.includes("Order processing failed") && currentOrderId) {
        // Group by error name (Error) and the order prefix, ignoring the specific Order ID number itself
        event.fingerprint = [originalException.name, "order-processing-failure"];
        console.log(`Applied custom fingerprint for order processing failure (Order ID: ${currentOrderId}).`);
    }


    // Example 3: Combine default grouping with custom context (Advanced)
    // While server-side rules support {{ default }} placeholders, SDK fingerprinting
    // usually involves manually constructing the array. You can include the original
    // error message or type alongside your custom criteria.
    const component = event.tags?.component as string | undefined;
    if (component) {
         // Group by component, error type, and error message
         event.fingerprint = [`component-${component}`, event.exception?.values?.[0]?.type || 'unknown_type', event.message || 'unknown_message'];
         console.log(`Applied component-based fingerprint: ${event.fingerprint.join(', ')}`);
    }


    // Always return the event (modified or original) or null to discard [9]
    return event;
  },
});

// Example usage:
function processOrder(orderId: string) {
    Sentry.configureScope(scope => {
        scope.setTag('orderId', orderId);
        scope.setTag('component', 'order-processor'); // Example: set a component tag
    });

    try {
        // Simulate an error that includes the order ID in its message
        if (Math.random() > 0.5) {
           throw new Error(`Order processing failed for Order ID: ${orderId}`);
        }
    } catch (e) {
        Sentry.captureException(e); // beforeSend will potentially modify the fingerprint
    }
}

processOrder("ORDER-XYZ-789");
processOrder("ORDER-ABC-456");
processOrder("ORDER-XYZ-789"); // Another error for the same order ID - should group with the first if fingerprinting works


// Simulate a network error from the external API
try {
    // Assume this fetches from /api/external/data
    throw new Error("Network Error: Could not connect to external service.");
} catch(e) {
    Sentry.captureException(e, {
        request: {
            url: 'https://your-backend.com/api/external/data'
        }
    }); // This should be grouped by "external-api-network-error" fingerprint
}

Custom fingerprinting in beforeSend is a powerful way to tailor Sentry's grouping to the specific needs and error patterns of your application. Remember that Sentry's server-side fingerprinting rules, configured in the Sentry UI project settings, take precedence over SDK-provided fingerprints. It's often strategic to use SDK fingerprinting for highly specific, code-driven grouping logic and server-side rules for broader patterns or overrides.

6.4. Limitations: Capturing Local Variables in JS/TS

In some programming languages and environments, Sentry SDKs can capture the values of local variables for each frame in the stack trace when an error occurs. This provides deep context and can significantly speed up debugging.

However, due to limitations in the standard JavaScript runtime environment (especially in browsers and production Node.js where code is optimized), capturing local variables reliably and efficiently is generally not possible or commonly implemented in the core Sentry JavaScript/TypeScript SDKs (@sentry/browser, @sentry/node) [13].

JavaScript engines typically do not expose a public, stable, or performant API that allows external code like Sentry to inspect the values of variables within the scope of arbitrary function calls in the stack trace.

  • Availability: You'll find this feature in Sentry SDKs for platforms where runtime introspection is more feasible, such as Python, .NET, Java, or native mobile SDKs.
  • Workaround: The best alternative in JavaScript/TypeScript is to manually add relevant variable values to the Sentry event's context or extra data at points where errors might occur. This requires you to explicitly decide which variables are important and capture them before an error happens or is caught.
// Example workaround: Manually add key variables to scope context or extra
function processUserData(user: { id: string, name: string, email?: string }, items: string[]) {
  // Add potentially relevant variables to the scope *before* a potential error
  Sentry.configureScope(scope => {
    // Careful: filter sensitive data!
    scope.setExtra("processingUser", { id: user.id, name: user.name });
    scope.setExtra("processingItemCount", items.length);
    // Avoid adding very large data structures directly
    // scope.setExtra("processingItems", items); // Be cautious with large arrays
  });

  try {
    // ... code that might fail using 'user' or 'items' ...
    if (!user.email) {
       throw new Error("User email is missing");
    }
    if (items.length > 100) {
        throw new Error("Too many items to process.");
    }
  } catch (error) {
    // The captured error will include the 'processingUser' and 'processingItemCount' extra data
    Sentry.captureException(error);
  } finally {
     // Clean up scope data if necessary
     Sentry.configureScope(scope => {
        scope.setExtra("processingUser", undefined);
        scope.setExtra("processingItemCount", undefined);
     });
  }
}

// The `includeLocalVariables` option, while present in some other SDKs,
// does not have a functional effect for capturing stack frame local variables
// in the core Sentry JavaScript/TypeScript SDKs due to platform limitations [13].

While not as automatic as native local variable capture, manually adding crucial context data to your Sentry events is the standard practice in JavaScript/TypeScript and is highly effective when done thoughtfully.

Chapter 7: User Interaction and Release Health

Sentry isn't just for developers; it also helps you understand your application's health from the user's perspective. Features like user feedback and release health metrics bridge the gap between technical issues and the user experience.

7.1. User Feedback Collection

When an error occurs, providing users with a way to report what happened can be invaluable for debugging. Sentry integrates with user feedback collection, allowing users to submit comments, their name, and email address linked directly to a specific error event.

  • Frontend/User-Facing Platforms (@sentry/browser, Framework SDKs):
    The easiest way to collect user feedback on the frontend is using the built-in Sentry.showReportDialog() function. This presents a modal dialog to the user. You typically call this function after you've captured an error and received its eventId.

    import * as Sentry from "@sentry/browser"; // Or @sentry/react, @sentry/vue, etc.
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      // ... other options
    });
    
    function handleError(error: Error) {
      const eventId = Sentry.captureException(error); // Capture the error and get its ID
    
      // Show the feedback dialog linked to this specific error event [7]
      // It's good practice to only show this for errors you capture, not all uncaught ones.
      if (eventId) { // Check if event was successfully captured/not filtered
        Sentry.showReportDialog({
            eventId: eventId, // Link feedback to this error [7]
            // You can pre-fill user info if available [7]
            user: {
               name: "Jane Doe",
               email: "jane.doe@example.com",
            },
            // Customize dialog options (translations, labels, etc.) [7]
            title: "It looks like we hit an error.",
            subtitle: "Our team has been notified.",
            subtitle2: "If you'd like to help, tell us what happened below.",
            labelName: "Your Name",
            labelEmail: "Your Email",
            labelComments: "What happened?",
    labelClose: "Close",
            labelSubmit: "Submit Feedback",
            errorFormEntry: "Sorry, an error occurred while submitting your feedback.",
            errorGeneric: "An unknown error occurred while submitting your feedback.",
            successMessage: "Thanks! Your feedback has been sent.",
        });
      }
    }
    
    // Example usage:
    try {
      // ... code that might fail ...
      if (Math.random() > 0.5) {
        throw new Error("Failed to load user profile due to a backend issue.");
      }
      console.log("Profile loaded successfully.");
    } catch (e) {
      handleError(e as Error); // Call your error handling function
    }
    

    If you prefer to build your own custom feedback form, you can use Sentry.captureUserFeedback() to programmatically send the feedback data, linking it to a specific eventId.

    import * as Sentry from "@sentry/browser";
    import { UserFeedback } from "@sentry/types";
    
    function submitCustomFeedback(eventId: string, name: string, email: string, comments: string) {
      const userFeedback: UserFeedback = {
        event_id: eventId, // Link feedback to the error event [6]
        name: name,
        email: email,
        comments: comments,
      };
    
      Sentry.captureUserFeedback(userFeedback); // Send the feedback [6]
      console.log("User feedback submitted for event:", eventId);
    }
    
    // Example: Assuming you captured an error, got the eventId,
    // and collected feedback via your custom UI form.
    // const capturedErrorId = ...; // Get this from Sentry.captureException call
    // const feedbackName = ...;
    // const feedbackEmail = ...;
    // const feedbackComments = ...;
    // submitCustomFeedback(capturedErrorId, feedbackName, feedbackEmail, feedbackComments);
    
  • Backend Platforms (@sentry/node):
    Backend SDKs don't interact directly with users. The common pattern for backend errors is to capture the error, retrieve the eventId, and send this ID back to the frontend. The frontend then uses this eventId with showReportDialog or captureUserFeedback from the browser SDK.

    // --- Backend (@sentry/node in an Express example) ---
    import * as Sentry from "@sentry/node";
    import express from 'express';
    
    const app = express();
    // Sentry request handler must be the first middleware
    app.use(Sentry.Handlers.requestHandler());
    // TracingHandler creates transactions (if tracing enabled)
    app.use(Sentry.Handlers.tracingHandler());
    
    app.get('/process-data', (req, res, next) => { // Added next for consistent error handling
      try {
        // ... some operation fails ...
        if (Math.random() > 0.5) {
           throw new Error("Backend processing failed!");
        }
        res.status(200).json({ message: "Success!" });
      } catch (error) {
        // Capture and get ID. Pass error to next middleware (Sentry's errorHandler)
        const eventId = Sentry.captureException(error); // [1]
        // Or, pass the error to next(), and the Sentry errorHandler will capture it
        // return next(error);
    
        // Send the event ID back to the client in the response payload
        res.status(500).json({
          errorMessage: "An internal server error occurred.",
          errorId: eventId // Send ID to frontend [1]
        });
      }
    });
    
    // Sentry error handler must be before any other error middleware and after all controllers
    app.use(Sentry.Handlers.errorHandler({
      // Options can include captureException: false if you already captured above
    }));
    
    // Optional fallthrough error handler (after Sentry's)
    app.use((err, req, res, next) => {
      // The error id is attached to `res.sentry` by Sentry.Handlers.errorHandler [1]
      const errorId = res.sentry;
      res.statusCode = res.statusCode || 500;
      res.end(`Error occurred. ${errorId ? `Reference ID: ${errorId}` : ''}\n`);
    });
    
    // --- Frontend (using fetch and @sentry/browser) ---
    // Assuming @sentry/browser is initialized in the frontend...
    // import * as Sentry from "@sentry/browser"; Sentry.init(...)
    
    async function fetchData() {
      try {
        const response = await fetch('/process-data');
        if (!response.ok) {
           const errorData = await response.json();
           console.error("Server error:", errorData.errorMessage);
           if (errorData.errorId) {
              // Use the ID from the backend to show the dialog
              Sentry.showReportDialog({ eventId: errorData.errorId });
           }
           return;
        }
        // ... process successful response ...
        console.log("Data fetched successfully.");
      } catch (networkError) {
        // Capture frontend network errors separately if needed
        console.error("Frontend network error:", networkError);
        Sentry.captureException(networkError);
      }
    }
    
    // Call fetchData() on user action or page load
    // fetchData();
    

The prompt mentions onCrashedLastRun. This callback is a feature primarily for native mobile/desktop SDKs (like React Native, Electron, iOS, Android). It's designed to detect crashes from a previous application session when the app starts up again and prompt the user for feedback about that past crash. Standard browser and Node.js SDKs don't have a direct public API for this specific "crashed last run" workflow, as sessions are handled differently and Node restarts are often less user-interactive. Error reporting and feedback in JS/TS typically happen within the session where the error occurs.

7.2. Session Tracking and Release Health

Sentry tracks "sessions" to understand user impact and the stability of your application over time. A session represents a user's interaction with your application. By tracking sessions, Sentry can report metrics like crash-free sessions and crash-free users for a specific release, which are key indicators of release health.

  • Browser (@sentry/browser, Framework SDKs):
    The standard browser SDK automatically tracks sessions using the browserSessionIntegration (which is included in default integrations). A session typically starts when a user visits your page and ends after a period of inactivity (usually 30 minutes by default, configurable) or when the tab/browser is closed.

    import * as Sentry from "@sentry/browser"; // or @sentry/react, etc.
    
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      // Session tracking is enabled by default via browserSessionIntegration [8]
      // You generally don't need to configure anything for basic session tracking.
      // Ensure the 'release' option is set! This is crucial for release health.
      release: process.env.SENTRY_RELEASE || "my-app@1.0.0",
    });
    
    // Sentry will automatically start and manage sessions as users interact with your app.
    // If an error occurs within a session, that session is marked as 'crashed'.
    // If no errors occur, the session is eventually counted as 'healthy'.
    // You can view session data in the Sentry UI under Release Health.
    
  • Node.js (@sentry/node):
    Session tracking in Node.js is less straightforward as there isn't a direct concept of a "user session" like in a browser. The Node SDK does not automatically track sessions in the same way. If you need to track the health of long-running server processes or specific background job lifecycles as 'sessions' for release health, you might model these using transactions or explore custom session tracking if necessary, though this is less common than in browser/mobile SDKs.

  • Release Health: The primary value of session tracking is powering the Release Health feature in Sentry. By capturing session data and associating it with a specific release version, Sentry can calculate metrics like the percentage of sessions that completed without errors. This provides a high-level view of the stability of your code deployments. Setting the release option is mandatory to use Release Health effectively.

7.3. Screenshots (Note: Not Applicable for Browser/Node.js)

Automatically capturing a screenshot of the application UI when an error occurs is a highly valuable debugging feature. However, it is primarily available for native mobile and desktop SDKs (like @sentry/react-native, @sentry/electron, @sentry/cocoa for iOS, @sentry/java for Android/desktop) where the SDK has access to the operating system's UI rendering layer or screenshot capabilities [19].

This feature is generally not available or feasible in standard web browser environments (@sentry/browser) due to browser security restrictions that prevent web pages from taking screenshots of the entire viewport or screen programmatically. Similarly, Node.js (@sentry/node) is a server-side runtime without a visual UI, so screenshot capture is not applicable.

  • Availability: Expect attachScreenshot: true or similar options and functionality in SDKs designed for environments with UI access (Mobile, Desktop).
  • Web/Node: Not supported. For visual debugging on the web, Sentry Session Replay is the relevant feature, which records user interaction videos.

Chapter 8: Advanced and Specific Topics

This final chapter covers a few advanced topics and features specific to certain environments or use cases that you might encounter when working with Sentry in TypeScript.

8.1. Interacting with the Sentry Hub

Internally, the Sentry SDK uses a concept called the "Hub." The Hub is a central object that manages the Sentry Client (which holds the configuration and integrations) and a stack of Scope objects. When you call top-level Sentry methods like captureException or configureScope, you are implicitly interacting with the "current Hub."

For most standard applications, you can stick to the top-level Sentry API. However, directly interacting with the Hub is necessary for advanced scenarios, such as:

  • Running multiple, independent Sentry clients within the same application process.
  • Manually managing the Scope stack or the current Hub in complex asynchronous flows where automatic context propagation might not cover your specific pattern.

  • Getting the Current Hub: You can access the Hub instance associated with the current execution context using Sentry.getCurrentHub().

    import * as Sentry from "@sentry/node"; // or @sentry/browser
    import { Hub } from "@sentry/types";
    
    Sentry.init({ dsn: "YOUR_SENTRY_DSN" /* ... */ });
    
    const currentHub: Hub = Sentry.getCurrentHub(); // Get the Hub for the current context [1]
    console.log("Retrieved the current Sentry Hub.");
    
    // You can now call methods directly on the hub, which is what top-level Sentry methods do internally:
    // currentHub.configureScope(...)
    // currentHub.captureException(...)
    // currentHub.addBreadcrumb(...)
    // ... and access the client managed by the hub:
    // const client = currentHub.getClient();
    
  • Creating a New Hub (Advanced): You can instantiate a new Client and a new Hub if you need to send events to a different DSN or with entirely separate configuration within the same process. You would then need to manage when this new Hub is considered "current."

    import * as Sentry from "@sentry/node";
    import { Hub, Scope, NodeClient } from "@sentry/types"; // Import NodeClient type if needed
    
    Sentry.init({ dsn: "YOUR_SENTRY_DSN_MAIN" /* ... */ }); // Default client/hub
    
    // Create a new client for a specific purpose (e.g., a library or module)
    const myOtherClient = new Sentry.NodeClient({ // Use appropriate Client class (NodeClient, BrowserClient etc.)
        dsn: "YOUR_SENTRY_DSN_OTHER", // A different DSN for these events
        release: 'my-special-module@1.0.0',
        environment: 'production',
        // ... other specific options for this client
    });
    
    // Create a new Hub instance associated with the new client
    const myOtherHub = new Hub(myOtherClient);
    const myOtherScope = new Scope(); // New Hubs start with an empty scope stack, so push an initial one
    myOtherHub.pushScope(myOtherScope); // Push the initial scope onto the new hub's stack
    
    // Now you can capture events using this specific hub instance
    function processSpecialModuleData() {
       try {
           // ... code related to the special module ...
           if (Math.random() > 0.8) throw new Error("Error in my special module!");
       } catch (e) {
           // Capture the exception using the specific hub [1]
           myOtherHub.captureException(e, { tags: { module: 'special' } });
       }
    }
    
    // Process data using the special module handler
    // processSpecialModuleData();
    
    // Async Context: Managing the "current" Hub and Scope across async operations
    // is complex. Sentry SDKs use async context mechanisms (like AsyncLocalStorage in Node.js)
    // to automate this. You usually don't need manual Hub switching unless these
    // mechanisms don't cover your specific async pattern.
    // Sentry.runWithAsyncContext(() => { // Use this helper to wrap async operations [1, 11]
    //    Sentry.makeCurrent(myOtherHub); // Temporarily make myOtherHub the 'current' hub for this async context
          // ... call async code that should use myOtherHub ...
    //    Sentry.makeCurrent(mainHub); // Switch back explicitly or rely on async context restoration
    // });
    

Direct Hub manipulation is an advanced topic. For typical web/server applications, relying on automatic async context propagation provided by framework integrations and helpers like Sentry.runWithAsyncContext [1, 11] when wrapping non-standard async patterns is sufficient.

8.2. Sending Custom Metrics

While Sentry performance monitoring focuses on durations and throughput of traced operations, you might want to track arbitrary numerical data specific to your application's business logic or internal state. Sentry allows you to send custom metrics (Counters, Gauges, Distributions/Timers, Sets) for this purpose.

These metrics are aggregated by the SDK over short periods and sent to Sentry, where you can visualize and query them in the Metrics section.

import * as Sentry from "@sentry/node"; // or @sentry/browser
import { getMetricAggregator } from '@sentry/core'; // Access the metric aggregator [3]

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Metrics are often sent via the same transport, ensure DSN is configured.
  // There might be specific options for metrics in Sentry.init if needed.
  // enableMetrics: true, // Conceptual option, check SDK docs if needed
  // metricsSampleRate: 1.0, // Conceptual sample rate for metrics
});

// --- Sending Metrics ---

// Counter: Increment a value [3] - useful for counting events
Sentry.metrics.increment('user_login_total', 1, { // Key, value (default 1)
  tags: { method: 'password' }, // Optional tags [3]
  unit: 'count', // Optional unit (use OpenTelemetry units if possible) [3]
  // timestamp: ... // Optional timestamp
});
Sentry.metrics.increment('order_processed', 1, { tags: { status: 'success' }});


// Gauge: Set a value that can go up or down [3] - captures the LAST value within an aggregation period
Sentry.metrics.gauge('queue_size', 15, { tags: { queue_name: 'processing' }});
Sentry.metrics.gauge('active_users', 120);


// Distribution / Timer: Track values over time (e.g., latencies, sizes) [3]
// Reports aggregates like min, max, avg, p95, etc. based on the values sent.
// Often used for timers (hence 'timing'), but can be any distribution of values.
const startTime = Date.now();
// ... some operation ...
const duration = Date.now() - startTime;
Sentry.metrics.distribution('operation_duration', duration, {
   unit: 'millisecond', // Use OpenTelemetry units [3]
   tags: { operation: 'fetch_data' }
});

// Set: Count unique values within an aggregation period [3] - useful for counting unique users, items, etc.
Sentry.metrics.set('unique_user_ids', 'user-abc', { tags: { cohort: 'new' }});
Sentry.metrics.set('unique_product_views', 'product-xyz');


// --- Manual Metric Flushing (Less common, happens automatically by default) ---
// The SDK aggregates metrics in memory over a period (e.g., 10 seconds) and sends them automatically. [3]
// You can manually flush the aggregator if needed (e.g., before shutdown or after a critical event),
// but be careful not to do it too frequently as it increases network traffic.
// const aggregator = getMetricAggregator(); // Get the internal aggregator instance [3]
// aggregator?.flush(); // Manually trigger sending aggregated metrics [3]

The Sentry metrics API provides a simple way to add custom monitoring points to your application. Using appropriate metric types and adding descriptive tags enables powerful analysis and alerting on your application's key operational statistics within Sentry. Using OpenTelemetry units makes the metrics easier to interpret.

8.3. Listing Loaded Libraries/Modules (Node.js)

For Node.js applications, knowing which packages and their exact versions were loaded when an error occurred can be crucial for diagnosing compatibility issues or bugs related to specific library versions.

The @sentry/node SDK includes a Modules integration (enabled by default) that captures the versions of packages listed in your application's package.json dependencies [1]. This information is sent as part of the event payload.

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

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // The 'Modules' integration is included in defaultIntegrations [1]
  // Unless you set defaultIntegrations: false, you get this automatically.
});

// When an event is sent, it will include a 'modules' section in the UI,
// listing package names and versions found in package.json dependencies.
Sentry.captureException(new Error("Error with module context."));

// To disable this feature (not recommended unless causing issues):
// Sentry.init({
//   dsn: "YOUR_SENTRY_DSN",
//   defaultIntegrations: false, // Disable all defaults
//   integrations: [
//      // Add back other essential integrations, but omit ModulesIntegration
//   ]
// });
// Or find the Modules integration within the default list and remove it if defaultIntegrations is an array you modify.

This provides a snapshot of your application's dependency environment at the time of the event, aiding debugging. This feature is less applicable in browser environments as the concept of "loaded modules" or a central package manifest is different.

8.4. Offline Caching / Buffer to Disk

Ensuring that Sentry events are not lost due to temporary network outages or the application being offline is important for robust monitoring. This capability, often referred to as "offline caching" or "buffer to disk," allows the SDK to persist events locally and attempt to send them later when connectivity is restored.

In standard Sentry SDKs for Node.js (@sentry/node) and Browser (@sentry/browser), persistent offline caching to disk or IndexedDB is generally NOT a built-in default feature [23].

  • Browser: Events are typically queued in memory briefly. If the browser tab is closed while offline or before sending completes, events might be lost. Robust offline event submission in PWAs might require custom Service Worker implementations to intercept and queue requests.
  • Node.js: Events are queued in memory. If the process terminates before the queue is flushed (e.g., due to a crash or sudden shutdown without Sentry.close()), events in memory are lost. Disk buffering is not default.

However, Sentry SDKs designed for environments where offline scenarios are common DO support offline caching:

  • Electron (@sentry/electron): The Electron SDK leverages file system access in the main process for offline caching [23].
  • React Native (@sentry/react-native): The React Native SDK utilizes native device storage for caching events while offline.
// In your Electron main process:
import * as Sentry from "@sentry/electron/main"; // Use the main process SDK

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  // Offline caching is typically enabled by default in @sentry/electron/main
  // You might find options related to cache directory or size in specific versions,
  // consult the @sentry/electron documentation.
});

While the standard Node/Browser SDKs don't offer persistent disk buffering out-of-the-box, their transports do handle transient network failures with limited internal retries. But they won't store events across application restarts or long offline periods without specific platform SDKs or custom transport implementations. Rate limiting (HTTP 429) is handled by pausing sending, not usually by disk buffering by default.

8.5. Start-Up Crash Detection

Detecting and reporting crashes that happen very early in an application's startup lifecycle can be challenging. The application might crash before the SDK has fully initialized or before background sending can complete.

This feature, often described as using a "marker file" to detect a previous unclean shutdown and flushing a pending event queue on the next startup, is primarily a mechanism implemented by native SDKs that integrate with low-level crash handlers [24]. This is common in SDKs like @sentry/electron or @sentry/react-native which utilize native crash reporting tools (like Breakpad, KSCrash).

// Example conceptual flow (React Native/Electron) - happens mostly automatically
// This relies on the native part of the SDK and its crash handler.
// 1. App Starts, Sentry.init() called (configures native crash handler)
// 2. App Crashes very soon after init (e.g., during bootstrapping)
// 3. Native Crash Handler catches it, saves crash report to disk, app terminates.
// --- Next Launch ---
// 4. App Starts again, Sentry.init() called
// 5. Sentry SDK detects the saved crash report from the previous run (e.g., checks for the marker file).
// 6. SDK sends the cached crash report from the previous session to Sentry.

For standard @sentry/browser and @sentry/node SDKs, this specific "start-up crash detection" mechanism using marker files is not a standard public API or behavior [24]. Browser sessions are ephemeral, and while the Node SDK focuses on graceful shutdown, a truly catastrophic crash immediately after init could potentially lose events if the process terminates instantly. Ensuring Sentry.init is called as the very first thing in your application's entry point is the primary way to maximize the chance of capturing early errors in these environments.

Conclusion

Sentry provides a comprehensive suite of tools for monitoring the health and performance of your TypeScript applications, whether they run in the browser or on Node.js. From automatic error capturing and rich context enrichment to powerful performance tracing and customizable data filtering, the Sentry SDKs empower developers to gain deep visibility into how their code behaves in real-world scenarios.

Key takeaways for TypeScript developers include:

  • Initialization is Crucial: Configure Sentry.init carefully with your DSN, environment, release, and relevant integrations to lay a solid foundation.
  • Leverage Automatic Features: Default integrations provide significant value out-of-the-box, automatically capturing errors, breadcrumbs, and context.
  • Enrich with Context: Use Scopes (configureScope, withScope) and dedicated features (setUser, addBreadcrumb, setContext('feature_flags', ...)) to add application-specific information that aids debugging.
  • Master Data Control: Employ sampling (sampleRate, tracesSampleRate/tracesSampler), ignoring (ignoreErrors, ignoreTransactions), and hooks (beforeSend, beforeSendTransaction) to manage data volume and filter out noise.
  • Instrument for Performance: Enable tracing, use automatic HTTP integrations, and add custom spans with searchable attributes to understand performance bottlenecks and distributed transactions.
  • Source Maps are Essential: Invest in your build process to generate and upload source maps; they are fundamental for debugging minified/transpiled code.
  • Think Beyond Errors: Utilize features like user feedback and custom metrics to gain a broader understanding of user experience and application state.

By integrating these Sentry features thoughtfully into your TypeScript projects, you move from reactive firefighting to proactive monitoring and continuous improvement.

Best Practices for Effective Sentry Usage:

  • Automate Release Management: Always set the release option in Sentry.init from your build process.
  • Use Environments: Clearly separate data using the environment option.
  • Enrich Context: Add relevant user, request, and application state data using scopes.
  • Strategic Breadcrumbs: Use manual breadcrumbs to mark key steps in user flows or background processes.
  • Be Mindful of PII: Carefully consider sensitive data capture and utilize scrubbing features or avoid capturing PII where possible.
  • Implement Graceful Shutdown: Call Sentry.close() in Node.js applications before exiting.
  • Set Sampling Rates Appropriately: Balance data volume with visibility, especially for high-throughput applications.
  • Upload Source Maps Reliably: Make source map uploads part of your automated deployment process.
  • Instrument Key Performance Paths: Add custom spans for critical operations not covered by automatic integrations.

Next Steps:

This whitepaper provides an overview, but the Sentry documentation is your most detailed and up-to-date resource.

  1. Consult Official Docs: Refer to the specific documentation for your Sentry SDK package (e.g., @sentry/node, @sentry/browser, framework SDKs) for the most precise API details and advanced configuration.
  2. Explore the Sentry UI: Spend time in the Sentry application itself. Explore the Issues, Performance, Metrics, and Release Health sections to see how the data you send is presented and how you can query and analyze it.
  3. Experiment: Try implementing some of the features discussed here in a development or staging environment to see their impact.

Happy monitoring!