> ## Documentation Index
> Fetch the complete documentation index at: https://launchdarkly.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sending error events from observability tools

<View title="Developer" />

<View title="Federal docs" />

<View title="EU docs" />

This topic explains how to forward errors from your observability or error monitoring tool to LaunchDarkly as `custom` metric events. After your errors arrive in LaunchDarkly, you can build metrics from them and attach those metrics to [guarded rollouts](/docs/home/releases/guarded-rollouts) or [experiments](/docs/home/experimentation). This helps you determine whether a new feature increases the number of generated errors, and enables you to automatically roll back a release in response to a regression.

The approach in this topic works with any tool that lets you run code when it captures an error. This includes tools like Sentry, Datadog, Rollbar, Bugsnag, or your own error handler. It works on every LaunchDarkly plan, in every LaunchDarkly instance, and with every LaunchDarkly SDK that supports the track feature.

If you use Sentry, read [Example: forwarding Sentry errors](#example-forwarding-sentry-errors) for complete code samples.

## Concepts

Forwarding errors as metric events uses two LaunchDarkly building blocks:

* **Custom events.** Your SDK sends a `custom` event with an event key you choose. You choose when to send the event, and you attach your own data to it. To learn more, read [Tracking custom events](/docs/sdk/features/events).
* **Custom metrics.** A LaunchDarkly metric with the same event key aggregates those events. A metric with a success criterion of "lower is better" treats a rise in errors as a regression. To learn more, read [Components of a metric](/docs/home/metrics/components).

You do not need an integration, a webhook, or the events API to do this.

## Choose an approach

LaunchDarkly offers several ways to get error data into a metric. Use the following table to choose the approach that fits your situation:

<table>
  <thead>
    <tr>
      <th>Approach</th>
      <th>When to use it</th>
      <th>Availability</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>Forwarding errors with the track feature, as described in this topic</td>
      <td>You already capture errors in a third-party tool and want to reuse them in LaunchDarkly. You want control over the event payload, or you need this to work in an instance or on a plan where the prebuilt integration is not available.</td>
      <td>All plans, all instances</td>
    </tr>

    <tr>
      <td>[Sentry integration](/docs/integrations/sentry)</td>
      <td>You use Sentry and prefer a configuration-only setup that does not require changing your application code.</td>
      <td>Select plans. Not available in the federal or EU instances.</td>
    </tr>

    <tr>
      <td>[LaunchDarkly error monitoring](/docs/home/observability/errors)</td>
      <td>You want LaunchDarkly to capture the errors. The observability SDKs report errors, and LaunchDarkly autogenerates metrics from them.</td>
      <td>Select plans</td>
    </tr>

    <tr>
      <td>[Importing events](/docs/home/metrics/import-events)</td>
      <td>Your errors are already in a data warehouse or pipeline, or you need to backfill historical events.</td>
      <td>Select plans</td>
    </tr>
  </tbody>
</table>

> [!NOTE]
>
> **You can combine approaches**
>
> Nothing prevents you from using more than one of these approaches. For example, you can capture errors with LaunchDarkly error monitoring and also forward a subset of high-severity errors from Sentry under your own event key.

## Prerequisites

You need the following prerequisites to forward error events to LaunchDarkly:

* A LaunchDarkly SDK, initialized in the application where the errors occur. To learn which SDKs support the track feature, read [Tracking custom events](/docs/sdk/features/events).
* An observability tool that lets you run your own code when it captures an error, or a place in your application where you already handle errors.
* Permission to create metrics in your LaunchDarkly project.

## Forward errors to LaunchDarkly

To use your errors as a LaunchDarkly metric, complete the following steps:

1. [Choose an event key](#choose-an-event-key)
2. [Call track from your error handler](#call-track-from-your-error-handler)
3. [Flush the event buffer](#flush-the-event-buffer)
4. [Create a metric](#create-a-metric)
5. [Attach the metric to a rollout or experiment](#attach-the-metric-to-a-rollout-or-experiment)

### Choose an event key

The event key is a string you choose. LaunchDarkly groups events by this key, and the metric you create later must use the same key.

Use one stable, low-cardinality key for all of your errors, such as `application-error`. Put the details of each error in the event's `data` object instead of in the key.

> [!WARNING]
>
> **Do not put error details in the event key**
>
> If you build the event key from the error message, error code, or stack trace, you create a new event key for every distinct error. This produces a large number of keys that you cannot aggregate, and you would need a separate metric for each one. Keep the key constant and use [metric filters](/docs/home/metrics/event-filters) to create multiple metrics per error type.

Event keys that begin with `$ld:telemetry` are reserved for LaunchDarkly observability features. Do not send events with a reserved key. To learn more, read [Observability metrics](/docs/home/metrics/autogen/observability).

> [!WARNING]
>
> **Event keys and metric keys are different**
>
> Sending custom events to LaunchDarkly requires a unique **event key**. You can set the event key to anything you want. Adding this event key to your codebase lets your SDK track actions customers take in your app as events. To learn more, read [Tracking custom events](/docs/sdk/features/events).
>
> LaunchDarkly also automatically generates a **metric key** when you create a metric. You only use the metric key to identify the metric in API calls. To learn more, read [Creating and managing metrics](/docs/home/metrics/create-metrics).

### Call track from your error handler

Call the track feature in your SDK from the place where your observability tool hands you the error. Attach a flat object of details as the event data.

Here is an example track call in several client-side SDKs:

```js title="JavaScript" lines wrap theme={null}
ldClient.track('application-error', {
  source: 'sentry',
  platform: 'browser',
  error_type: 'TypeError',
  screen: 'checkout',
});
```

```swift title="Swift" lines wrap theme={null}
let data: LDValue = [
    "source": "sentry",
    "platform": "ios",
    "error_type": "NSURLErrorDomain",
    "screen": "checkout"
]
try? LDClient.get()?.track(key: "application-error", data: data)
```

```kotlin title="Kotlin" lines wrap theme={null}
val data = LDValue.buildObject()
    .put("source", "sentry")
    .put("platform", "android")
    .put("error_type", "IllegalStateException")
    .put("screen", "checkout")
    .build()
client.trackData("application-error", data)
```

In client-side SDKs, `track` uses whichever context the client has identified. In server-side SDKs, you must pass the context in the `track` call:

```js title="Node.js" lines wrap theme={null}
client.track('application-error', context, {
  source: 'sentry',
  platform: 'node',
  error_type: 'TypeError',
});
```

```python title="Python" lines wrap theme={null}
ld_client.track(
    "application-error",
    context,
    data={
        "source": "sentry",
        "platform": "python",
        "error_type": "ValueError",
    },
)
```

> [!WARNING]
>
> **Track from the context that evaluated the flag**
>
> LaunchDarkly can only attribute an event to a flag variation if the event comes from the same context that evaluated the flag. In client-side SDKs, this means calling `track` after you call `identify`. In server-side SDKs, it means passing the same context you passed to the flag evaluation.
>
> Error handlers in server-side applications often run outside the scope of the request that caused the error, where the context is no longer available. If this is the case in your application, store the evaluation context in request-scoped storage, such as Python's `contextvars` or Node.js's `AsyncLocalStorage`, then read it back in your error handler.

To learn about the exact signature in your SDK, read [Tracking custom events](/docs/sdk/features/events).

### Flush the event buffer

SDKs buffer events and send them on an interval, which is 30 seconds by default in many SDKs. If your application crashes, is killed, or is backgrounded after an error, you lose any events still in the buffer. Because an error is often the last thing that happens before an application stops, call `flush` immediately after you track an error:

```js title="JavaScript" lines wrap theme={null}
ldClient.flush();
```

```swift title="Swift" lines wrap theme={null}
LDClient.get()?.flush()
```

```kotlin title="Kotlin" lines wrap theme={null}
client.flush()
```

Flushing is a request to send queued events as soon as possible. It is not a guarantee that delivery completes before your application stops. To learn more, read [Flushing events](/docs/sdk/features/flush).

### Create a metric

Create a metric that uses the event key you chose. The metric kind determines what question the metric answers:

* To measure **the percentage of contexts that experienced at least one error**, create a [custom conversion binary metric](/docs/home/metrics/custom).
* To measure **the average number of errors per context**, create a [custom conversion count metric](/docs/home/metrics/custom-count).

For a metric that measures the percentage of end users who experienced an error, select the following options:

* Event kind: **Custom**
* Event key: `application-error`
* Metric definition: **Occurrence (Percent)**

Use the metric definition: Percentage of **User** units that sent the event where **Lower is better**

> [!WARNING]
>
> **Set the success criterion to "Lower is better"**
>
> Success criteria tell LaunchDarkly which direction indicates an improvement. If you leave an error metric set to "Higher is better," LaunchDarkly reads a rise in errors as a successful release rather than a regression, and a guarded rollout will not roll back.

The metric's analysis unit must include the randomization unit of the release you attach it to. For example, if a guarded rollout randomizes by user, an error metric analyzed by user works, but one analyzed only by account does not. To learn more, read [Analysis units](/docs/home/metrics/components#analysis-units).

To use one event key for several metrics, add [metric filters](/docs/home/metrics/event-filters) for the properties you sent in the event data. For example, you can define one metric for events where `error_type` is `TypeError` and another where `platform` is `ios`.

> [!WARNING]
>
> **Metric filters do not read nested data**
>
> Filters only match top-level string, number, and boolean values in the event's `data` object. LaunchDarkly ignores nested objects and arrays when it applies filters. Keep your event data flat.

### Attach the metric to a rollout or experiment

Attach the metric to a [guarded rollout](/docs/home/releases/guarded-rollouts) as one of the metrics it monitors, or add it to an [experiment](/docs/home/experimentation). During a guarded rollout, LaunchDarkly compares the error rate of the contexts receiving the new variation against the contexts receiving the old one. If you enable auto-rollback, LaunchDarkly reverts the release when it detects a regression.

You can also add the metric to a [release policy](/docs/home/releases/release-policies) as a release guardrail metric, so that LaunchDarkly attaches it automatically to every guarded release where the policy applies.

## Best practices

Follow these practices to keep your error events useful:

* **Keep the event key stable and low cardinality.** Use one key for all errors and put the details in the event data.
* **Keep the event data flat.** Metric filters ignore nested objects and arrays.
* **Truncate free-text fields.** Error messages and stack traces can be very long. Truncate messages to a few hundred characters before you attach them.
* **Track the occurrence, not the render.** If an error state causes a component or view to re-render, guard your tracking so you send one event per actual failure rather than one per render.
* **Forward only what you want to measure.** Most tools capture warnings, informational messages, and performance data alongside errors. Filter to error-level events so your metric measures only errors.
* **Flush after every user-visible error.** To learn more, read [Flush the event buffer](#flush-the-event-buffer).

## Example: forwarding Sentry errors

Sentry calls the `beforeSend` hook once for each error event, just before it sends the event to Sentry. This makes it a good place to forward the error to LaunchDarkly. To learn more, read Sentry's filtering documentation for [JavaScript](https://docs.sentry.io/platforms/javascript/configuration/filtering/), [Apple](https://docs.sentry.io/platforms/apple/guides/ios/configuration/filtering/), or [Android](https://docs.sentry.io/platforms/android/configuration/filtering/).

These examples assume the Sentry SDK and the LaunchDarkly SDK are both already initialized in your application.

> [!WARNING]
>
> **Return the event from beforeSend**
>
> `beforeSend` controls whether Sentry sends the event. Return the event to let Sentry continue processing it as usual. If you return `null`, Sentry discards the error and it will not appear in Sentry.

### Browser JavaScript

```js title="JavaScript" lines wrap theme={null}
Sentry.init({
  dsn: 'YOUR_SENTRY_DSN',

  beforeSend: (event) => {
    const isError =
      (event.exception?.values?.length ?? 0) > 0 ||
      event.level === 'error' ||
      event.level === 'fatal';

    // The LaunchDarkly client may not exist yet if the error happens during startup.
    if (isError && ldClient) {
      const exception = event.exception?.values?.[0];
      const originalMessage = exception?.value ?? event.message ?? 'No error message';

      ldClient.track('application-error', {
        source: 'sentry',
        platform: 'browser',
        sentry_event_id: event.event_id ?? '',
        error_type: exception?.type ?? 'UnknownError',
        message: originalMessage.slice(0, 500),
        level: event.level ?? '',
        release: event.release ?? '',
        environment: event.environment ?? '',
        transaction: event.transaction ?? '',
      });

      // Immediately request sending all queued LaunchDarkly events.
      ldClient.flush();
    }

    // Continue sending the event to Sentry.
    return event;
  },
});
```

Sentry calls `beforeSend` only for error events. It calls `beforeSendTransaction` for performance data, so you do not need to filter transactions out yourself.

### iOS

```swift title="Swift" lines wrap theme={null}
SentrySDK.start { options in
    options.dsn = "YOUR_SENTRY_DSN"

    options.beforeSend = { event in
        let isError =
            !(event.exceptions?.isEmpty ?? true) ||
            event.level == .error ||
            event.level == .fatal

        if isError, let client = LDClient.get() {
            let exception = event.exceptions?.last

            let errorType = exception?.type ?? "UnknownError"

            let originalMessage =
                exception?.value ??
                event.message?.formatted ??
                "No error message"

            let message = String(originalMessage.prefix(500))

            let data: LDValue = [
                "source": "sentry",
                "platform": "ios",
                "sentry_event_id": event.eventId.sentryIdString,
                "error_type": errorType,
                "message": message,
                "level": String(describing: event.level),
                "release": event.releaseName ?? "",
                "environment": event.environment ?? "",
                "transaction": event.transaction ?? ""
            ]

            try? client.track(key: "application-error", data: data)

            // Immediately request sending all queued LaunchDarkly events.
            client.flush()
        }

        // Continue sending the event to Sentry.
        return event
    }
}
```

### Android

```kotlin title="Kotlin" lines wrap theme={null}
SentryAndroid.init(context) { options ->
    options.dsn = "YOUR_SENTRY_DSN"

    options.setBeforeSend { event, _ ->
        val isError =
            event.exceptions?.isNotEmpty() == true ||
            event.level == SentryLevel.ERROR ||
            event.level == SentryLevel.FATAL

        // LDClient.get() throws if the client is not initialized yet.
        val client = try {
            LDClient.get()
        } catch (e: LaunchDarklyException) {
            null
        }

        if (isError && client != null) {
            val exception = event.exceptions?.lastOrNull()

            val originalMessage =
                exception?.value
                    ?: event.message?.formatted
                    ?: "No error message"

            val data = LDValue.buildObject()
                .put("source", "sentry")
                .put("platform", "android")
                .put("sentry_event_id", event.eventId?.toString() ?: "")
                .put("error_type", exception?.type ?: "UnknownError")
                .put("message", originalMessage.take(500))
                .put("level", event.level?.name ?: "")
                .put("release", event.release ?: "")
                .put("environment", event.environment ?: "")
                .put("transaction", event.transaction ?: "")
                .build()

            client.trackData("application-error", data)

            // Immediately request sending all queued LaunchDarkly events.
            client.flush()
        }

        // Continue sending the event to Sentry.
        event
    }
}
```

### Choose which Sentry fields to send

The examples above send a set of fields that are useful for both filtering and debugging:

<table>
  <thead>
    <tr>
      <th>Event property</th>
      <th>Purpose</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>`source`, `platform`</td>
      <td>Low-cardinality values that let you filter one metric down to a single tool or platform.</td>
    </tr>

    <tr>
      <td>`error_type`</td>
      <td>The exception class. Low cardinality, so it works well as a metric filter.</td>
    </tr>

    <tr>
      <td>`level`</td>
      <td>Lets you define a separate metric for fatal errors only.</td>
    </tr>

    <tr>
      <td>`sentry_event_id`</td>
      <td>Lets you find the full error, including its stack trace, in Sentry. High cardinality, so use it for debugging rather than filtering.</td>
    </tr>

    <tr>
      <td>`message`</td>
      <td>Human-readable context when you review events. High cardinality, so use it for debugging rather than filtering.</td>
    </tr>

    <tr>
      <td>`release`, `environment`, `transaction`</td>
      <td>Let you narrow a metric to a specific deployment, environment, or code path.</td>
    </tr>
  </tbody>
</table>

Adjust these fields to match what you want to measure. Every property you send is available as a [metric filter](/docs/home/metrics/event-filters) as long as it is a top-level string, number, or boolean.

## Adapt this pattern to other tools

Any tool that exposes a hook, callback, or interceptor around error capture works the same way. Find the equivalent of `beforeSend` in your tool, then call `track` and `flush` from it:

* **Datadog RUM** provides the `beforeSend` callback in its initialization options. Datadog also offers a LaunchDarkly integration. To learn more, read [Datadog](/docs/integrations/datadog).
* **Rollbar** provides `transform` and `checkIgnore` callbacks.
* **Bugsnag** provides `onError` callbacks.
* **OpenTelemetry** lets you record exceptions on spans. You can call track from the same place you record the exception. To learn more, read [OpenTelemetry](/docs/sdk/features/opentelemetry-client-side).
* **Your own error handler.** If you already have a central error boundary, middleware, or logging function, add the track call there. You do not need an observability tool at all.

## Verify your events

After you deploy your change, confirm that the events arrive in LaunchDarkly:

1. Trigger an error in your application.
2. Navigate to the **Metrics** list in LaunchDarkly and open your metric, or review the **Live events** page for the environment.
3. Confirm that events with your event key appear, and that the properties you sent are present in the event data.

To learn more, read [Viewing incoming events](/docs/home/metrics/view-incoming-events).

If your events do not appear, check the following:

* The SDK is sending events. Confirm you have not disabled event sending in your SDK configuration.
* The event key in your code matches the event key in your metric exactly, including capitalization.
* Your client is identified as a context before you call track. Events from a client that has not been identified cannot be attributed to a release.
* Your error handler is running. Add a log statement inside the hook to confirm your tool calls it.
* Ad blocking software in the browser is not blocking your events. To learn more, read [Tracking custom events](/docs/sdk/features/events).
