# Web RUM SDK

> Set up the Last9 Web RUM SDK to monitor real user performance, errors, and behavior in your web application.

Source: https://last9.io/docs/real-user-monitoring/web/

Discover Applications tracks how your users experience your web application by collecting Core Web Vitals, performance data, JavaScript errors, and user interactions directly from their browsers. The RUM SDK provides lightweight, non-blocking instrumentation that captures real-world application behavior without impacting performance.

The stable CDN path currently serves Web RUM SDK `2.8.0`.

## Prerequisites

Before installing the RUM SDK, ensure you have:

- **Client Token**: A `Client — Web Browser` token from [Ingestion Tokens](https://app.last9.io/control-plane/ingestion-tokens). This token authenticates your application's data collection.
- **Applications Base URL**: Your base URL from the [Applications integration page](https://app.last9.io/integrations?integration=RUM). This endpoint receives your application's monitoring data.

:::note
The SDK calls https://get.geojs.io/v1/ip/geo.json to resolve user IP metadata. Make sure this origin is whitelisted in your client-side CORS policy.
:::

## Supported Frameworks

The RUM SDK integrates with popular frontend stacks out of the box:

- React
- Angular
- Vue
- Next.js, with support for SSR

If you run into framework-specific issues or need guidance for another setup, reach out to the Last9 team and we'll help you get started quickly.

## Installation & Setup

**React**

    1. Add the RUM SDK script to your `index.html` file in the `<head>` section:

       ```html
       <script src="https://cdn.last9.io/rum-sdk/builds/stable/v2/l9.umd.js"></script>
       ```

    2. Initialize RUM in your application's main component (typically `App.js` or `index.js`):

       ```jsx
       import { useEffect } from 'react';

       function App() {
         useEffect(() => {
           L9RUM.init({
             baseUrl: "https://your-base-url",
             headers: {
               clientToken: "your-client-token",
             },
             resourceAttributes: {
               serviceName: "your-app-name",
               deploymentEnvironment: "production",
               appVersion: "1.0.0",
             },
           });
         }, []);

         return (
           // Your app components
         );
       }
       ```

    3. Add user tracking (optional):

       ```jsx
         L9RUM.identify({ id, email, name, fullName, roles }) // on login
         L9RUM.clearUser() // on logout
       ```

**Vanilla JavaScript**

    1. Add the SDK script and initialization to your HTML file's `<head>` section:

       ```html
       <!DOCTYPE html>
       <html>
       <head>
         <!-- Load the RUM SDK -->
         <script src="https://cdn.last9.io/rum-sdk/builds/stable/v2/l9.umd.js"></script>

         <script>
           // Initialize RUM SDK
           L9RUM.init({
             baseUrl: "https://your-base-url",
             headers: {
               clientToken: "your-client-token",
             },
             resourceAttributes: {
               serviceName: "your-app-name",
               deploymentEnvironment: "production",
               appVersion: "1.0.0",
             },
           });
         </script>
       </head>
       <body>
         <!-- Your application content -->
       </body>
       </html>
       ```

    2. Add user tracking (optional):

       ```jsx
         L9RUM.identify({ id, email, name, fullName, roles }) // on login
         L9RUM.clearUser() // on logout
       ```

## Configuration

### Required Configuration

These settings are mandatory for the RUM SDK to function:

```javascript
L9RUM.init({
  baseUrl: "https://your-base-url", // Your Applications base URL
  headers: {
    clientToken: "your-client-token", // Browser-safe RUM client token
  },
  resourceAttributes: {
    serviceName: "your-app-name", // Application identifier
    deploymentEnvironment: "production", // Environment (production, staging, development)
    appVersion: "1.0.0", // Version identifier (semver, git hash, etc.)
  },
});
```

:::caution
Use only a `Client — Web Browser` token in browser code. Do not embed server-side ingestion credentials, API keys, basic-auth headers, cookies, or bearer tokens in a frontend bundle.
:::

### Optional Configuration

Customize the RUM SDK behavior with additional settings:

```javascript
L9RUM.init({
  // Required settings...

  // Optional settings
  sampleRate: 40, // Percentage of sessions to monitor (1-100, default: 40)
  debug: false, // Enable console logging for troubleshooting (default: false)

  // Control error tracking
  errors: {
    console: true, // Console errors (default: true)
    global: true, // Unhandled exceptions (default: true)
    report: true, // Browser reporting API (default: true)
    network: true, // Failed requests (default: true)
    ignorePatterns: [/ResizeObserver/i], // Regex patterns to ignore
    beforeSend: (event) => {
      // Filter or enrich errors before sending
      if (event.attributes["exception.message"]?.includes("ExpectedNoise")) {
        return null; // Drop this error
      }
      return event;
    },
  },

  // Network tracking & backend trace correlation
  network: {
    // Control browser-side network tracking
    enabled: true, // default: true; set false to disable all network spans
    ignorePatterns: {
      // Optionally ignore noisy endpoints
      // Matches against the full URL (includes origin + path + query)
      fullUrl: ["https://cdn.example.com", /^https:\/\/.*\\.example\\.com/],
      // Matches against just the path portion (e.g., /healthz, /metrics)
      pathname: ["/healthz", /^\\/internal\\/metrics/],
      // Matches against just the hostname (e.g., api.example.com)
      hostname: ["localhost", "api-internal.example.com"],
    },

    // Backend trace correlation
    backendCorrelation: {
      enabled: true,
      corsAllowedOrigins: ["https://api.internal.example"],
      customHeaders: { "x-app-name": "web-client" },
      injectToAllRequests: false,

      // W3C Baggage propagation (propagate custom attributes to backend)
      baggage: {
        enabled: true,
        allowedKeys: ["user.id", "tenant.id"], // only these keys are propagated
      },
    },
  },

  // Capture user interactions
  interactions: {
    enabled: true, // opt in explicitly
    trackClicks: true,
    trackScroll: true, // throttled; default 500ms
    trackKeyboard: false, // off by default for privacy
    trackForm: true, // focus/input on non-sensitive fields
    trackTouch: false,
    captureElementText: false, // trim to 100 chars when enabled
    selectorDepth: 3, // depth used for simple selectors
    throttleMs: 500,
  },

  // Geo-IP enrichment (optional, privacy-sensitive)
  geoIpEnabled: false, // default: false; set true to resolve IP + coarse geo via GeoJS

  // Override inferred geo with values your app already knows
  geoAttributesOverride: {
    country: "India",
    country_code: "IN",
    city: "Bangalore",
    organization: "Airtel",
  },
});
```

### Network Tracking Controls

The SDK records browser network activity (fetch, XHR, and static resources) as spans attached to the active View. Use the `network` configuration to tune what is collected:

- **`network.enabled`**: Master toggle for browser-side network tracking.
  - Default: `true`.
  - Set `network: { enabled: false }` to completely disable network spans (fetch, XHR, and resource loads) while keeping the rest of the SDK active.
- **`network.ignorePatterns`**: Filter out noisy or internal endpoints to keep dashboards focused and reduce cardinality.
  - `fullUrl`: Matches against the complete URL (e.g., `https://cdn.example.com/assets/app.js`).
  - `pathname`: Matches against just the path (e.g., `/healthz`, `/metrics`).
  - `hostname`: Matches against the hostname (e.g., `api.internal.example`).
  - Each array accepts **strings** (substring match) or **regular expressions**.

By default, the SDK automatically ignores requests to `localhost` and `127.0.0.1` (including their IPv6 variants) so that local development noise does not pollute production-like data. You can still opt in to local traffic by sending data to a non-localhost hostname (for example, a tunneled URL or a staging endpoint).

### Geo Attribute Overrides

If your application already knows the user's location (for example, from a backend lookup), you can pass those values into the SDK instead of relying on GeoJS IP inference. Pass any subset of these keys — they map onto the standard `enduser.geo.*` attributes:

| Key            | Maps to                    |
| -------------- | -------------------------- |
| `country`      | `enduser.geo.country`      |
| `country_code` | `enduser.geo.country_code` |
| `city`         | `enduser.geo.city`         |
| `organization` | `enduser.geo.organization` |

```javascript
L9RUM.init({
  // ...required settings
  geoAttributesOverride: {
    country: "India",
    country_code: "IN",
    city: "Bangalore",
    organization: "Airtel",
  },
});
```

Overrides take precedence over GeoJS-resolved values. Set only the keys you have — missing keys fall through to the inferred value (when `geoIpEnabled: true`) or stay unset.

### Sample Rate Guidelines

Choose your sample rate based on traffic volume and environment:

| Traffic Level       | Daily Users  | Recommended Rate | Purpose                                                       |
| ------------------- | ------------ | ---------------- | ------------------------------------------------------------- |
| High Traffic        | >10,000      | 1-10%            | Manage data volume while maintaining statistical significance |
| Medium Traffic      | 1,000-10,000 | 10-25%           | Balance coverage with data volume                             |
| Low Traffic         | &lt;1,000    | 25-50%           | Maximize insights with comprehensive data collection          |
| Development/Staging | Any          | 50-100%          | Thorough testing and validation before production             |

## User Interaction Tracking

Instrument real user actions as spans, attached to the active View:

- `enabled`: Master toggle (default: `false`)
- `trackClicks`, `trackScroll`, `trackKeyboard`, `trackForm`, `trackTouch`: Turn on specific interaction types (all default to `false`)
- `throttleMs`: Minimum gap between scroll spans (default: `500`ms)
- `captureElementText`: Include element text up to `elementTextMaxLength` (default: `false`, 100 chars)
- `selectorDepth`: Depth for building simple CSS selectors (default: `3`)

Privacy defaults: keyboard tracking is off unless you opt in, sensitive form fields (password/credit-card/email, etc.) are skipped automatically, and captured element text is trimmed to the configured length.

## Backend Trace Correlation

Connect frontend requests to backend services for end-to-end visibility. When enabled, the SDK adds W3C trace headers (`traceparent`/`tracestate`) to outbound requests, allowing you to see complete request flows from browser to backend.

```javascript
network: {
  backendCorrelation: {
    enabled: true,
    corsAllowedOrigins: ["https://api.internal.example"], // Domains to include
    customHeaders: { "x-app-name": "web-client" }, // Additional headers
    injectToAllRequests: false, // Set true only if all APIs support it
  },
}
```

Start with same-origin APIs, then progressively add cross-origin domains after verifying they accept the headers. Enable `debug: true` to identify configuration issues.

When backend correlation is enabled, validate the flow end to end:

1. Open browser DevTools and trigger a backend request.
2. Confirm the request includes W3C trace-context headers such as `traceparent` and `tracestate`.
3. If baggage propagation is enabled, confirm only the allowed baggage keys are sent.
4. Open the backend trace in Last9 and confirm the browser span and backend service span are part of the same trace.
5. Check that the backend service uses the same environment naming convention as the frontend application.

If another browser monitoring SDK is also installed, treat coexistence as a validation step. Watch for duplicate browser spans, missing trace headers, overwritten trace context, double fetch/XHR instrumentation, or CORS failures. Keep `injectToAllRequests: false` unless every destination is allowed to receive trace-context headers.

### Disabling Trace Header Propagation

If your backend does not accept browser-originated trace context, or the extra headers cause CORS preflight failures, set `propagateTraceHeaders: false`. The SDK stops injecting `traceparent`, `tracestate`, and B3 headers into outgoing fetch/XHR requests while keeping network span collection, baggage propagation, and custom headers intact.

```javascript
L9RUM.init({
  network: {
    backendCorrelation: {
      enabled: true,
      propagateTraceHeaders: false, // no traceparent / tracestate / B3 headers
    },
  },
});
```

`propagateTraceHeaders` defaults to `true`. The `propagationFormats`, `forceOverwrite`, and `isolateTracePerRequest` options only take effect while it is `true`. To disable all backend correlation features — including baggage and custom headers — set `enabled: false` instead.

### W3C Baggage Propagation

Baggage lets you forward custom key-value pairs from the browser to your backend services via the standard [W3C Baggage](https://www.w3.org/TR/baggage/) HTTP header. This is useful when your backend needs context that originates in the browser, for example linking a session ID from another tool, or passing a tenant identifier for multi-tenant routing.

```javascript
L9RUM.init({
  // ...required settings
  network: {
    backendCorrelation: {
      enabled: true,
      corsAllowedOrigins: ["https://api.myapp.com"],
      baggage: {
        enabled: true,
        allowedKeys: ["user.id", "tenant.id"],
      },
    },
  },
});

// Set the attributes you want propagated
L9RUM.spanAttributes({
  "user.id": currentUser.id,
  "tenant.id": currentUser.tenantId,
});
```

The SDK sends these as a `baggage` header alongside the existing `traceparent` / `tracestate` headers:

```
baggage: user.id=user-456,tenant.id=tenant-xyz
```

:::warning
Baggage values travel as plaintext HTTP headers. Only include **non-sensitive** attributes in `allowedKeys`. Never add secrets, tokens, passwords, or PII such as emails or phone numbers.
:::

#### Baggage Configuration Reference

| Option             | Type                   | Default | Description                                                                                                                                   |
| ------------------ | ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`          | `boolean`              | `false` | Enable W3C Baggage propagation                                                                                                                |
| `allowedKeys`      | `string[]`             | `[]`    | Whitelist of attribute keys to propagate. Keys not listed here are never sent, even if set via `spanAttributes()`                             |
| `trackedUrls`      | `(string \| RegExp)[]` | —       | Restrict baggage to specific URLs. If omitted, baggage follows the same rules as trace context (`corsAllowedOrigins` / `injectToAllRequests`) |
| `maxTotalBytes`    | `number`               | `8192`  | Maximum baggage header size in bytes (W3C recommendation). Entries are truncated if exceeded                                                  |
| `warnAtPercentage` | `number`               | `80`    | Log a warning when baggage size exceeds this percentage of `maxTotalBytes`                                                                    |

#### Linking Sessions from Another Tool

A common use case is propagating a session ID from another tool:

```javascript
L9RUM.init({
  // ...required settings
  network: {
    backendCorrelation: {
      enabled: true,
      forceOverwrite: true, // required when running alongside another RUM SDK
      baggage: {
        enabled: true,
        allowedKeys: ["external.session_id"],
      },
    },
  },
});

// Capture the session ID from the other tool
L9RUM.spanAttributes({
  "external.session_id": otherRum.getSessionId(),
});
```

:::note
When running Last9 RUM alongside another RUM library, set `forceOverwrite: true` so Last9 owns the W3C trace context while preserving the other vendor's `tracestate` entries.
:::

#### Extracting Baggage on the Backend

Your backend services can read the `baggage` header using any OpenTelemetry SDK. Below are examples for common languages.

**Node.js**

Register a `BaggageSpanProcessor` so every span automatically picks up baggage entries as attributes:

```javascript
const api = require("@opentelemetry/api");

// Copies all W3C Baggage entries onto every span as attributes
class BaggageSpanProcessor {
  onStart(span, parentContext) {
    const baggage = api.propagation.getBaggage(
      parentContext || api.context.active(),
    );
    if (!baggage) return;
    for (const [key, entry] of baggage.getAllEntries()) {
      span.setAttribute(key, entry.value);
    }
  }
  onEnd() {}
  forceFlush() {
    return Promise.resolve();
  }
  shutdown() {
    return Promise.resolve();
  }
}

// Add to your tracer provider setup
const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new BaggageSpanProcessor());
provider.register();
```

Once registered, attributes like `user.id` and `tenant.id` appear on every backend span without per-route middleware.

**Go**

```go
import (
    "net/http"

    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/baggage"
    "go.opentelemetry.io/otel/trace"

)

func handler(w http.ResponseWriter, r \*http.Request) {
ctx := r.Context()
bag := baggage.FromContext(ctx)

    userID := bag.Member("user.id").Value()
    tenantID := bag.Member("tenant.id").Value()

    span := trace.SpanFromContext(ctx)
    if userID != "" {
        span.SetAttributes(attribute.String("user.id", userID))
    }
    if tenantID != "" {
        span.SetAttributes(attribute.String("tenant.id", tenantID))
    }

}

````

**Python**

```python
from opentelemetry import baggage, trace
from opentelemetry.propagate import extract

def handle_request(request):
    ctx = extract(request.headers)
    bag = baggage.get_all(ctx)

    user_id = bag.get("user.id")
    tenant_id = bag.get("tenant.id")

    span = trace.get_current_span()
    if user_id:
        span.set_attribute("user.id", user_id)
    if tenant_id:
        span.set_attribute("tenant.id", tenant_id)
````

## GraphQL Network Observability

The SDK automatically enriches GraphQL requests made over `fetch` and XHR — no GraphQL client dependency or configuration is required. Enrichment is driven entirely by the request body, so plain REST calls (including JSON `POST`s to non-GraphQL endpoints) are never misclassified as GraphQL.

When a GraphQL operation is detected, the network span is renamed to a descriptive name and tagged with operation metadata:

| Span attribute           | Example                    | Notes                                        |
| ------------------------ | -------------------------- | -------------------------------------------- |
| Span name                | `GraphQL: "GetUser" query` | Anonymous operations become `GraphQL: query` |
| `graphql.operation.name` | `GetUser`                  | Operation name parsed from the request body  |
| `graphql.operation.type` | `query`                    | `query`, `mutation`, or `subscription`       |
| `l9.span.category`       | `network`                  | Categorizes the span for dashboard filtering |

GraphQL servers commonly return errors with an HTTP `200` status and an `errors[]` array in the response body. The SDK inspects the response body and flags these as errors so they surface in Error Tracking:

| Span attribute        | Value          | Notes                                                 |
| --------------------- | -------------- | ----------------------------------------------------- |
| `error.type`          | `GraphQLError` | Set when the response contains a non-empty `errors[]` |
| `graphql.error.count` | number         | Number of entries in the `errors[]` array             |

Request bodies are scanned up to 256KB and response bodies are peeked up to 64KB; requests larger than these limits are still traced as ordinary network spans without GraphQL enrichment.

## Dynamic segments in network span names

From `2.8.0`, network span names fold fully-numeric and UUID path segments to `?` so per-endpoint metrics aggregate instead of exploding by id. The same rule is used for view naming; version-like segments such as `v2` are preserved.

| Request URL                                       | Span name            |
| ------------------------------------------------- | -------------------- |
| `GET /workspaces/1`                               | `GET /workspaces/?`  |
| `GET /users/550e8400-e29b-41d4-a716-446655440000` | `GET /users/?`       |
| `GET /api/v2/orders`                              | `GET /api/v2/orders` |

This is a behavior change for the emitted `span_name` on fetch, XHR, and resource spans. The full raw URL remains on `http.url`. GraphQL span names are operation-based and are not folded.

## Adding Custom Context

### Global Attributes

Add business context to all monitoring data. Call `L9RUM.spanAttributes()` whenever context changes:

```typescript
L9RUM.spanAttributes({
  "app.org_slug": "acme-corp",
  "feature.flag.beta_checkout": true,
});

// Clear attributes (e.g., on logout)
L9RUM.spanAttributes(null);
```

**Note:** Each call replaces all previous attributes, so always provide the complete set. These attributes become available as filters in the dashboard.

### Custom Events

Track user actions and business events beyond standard page views:

```typescript
L9RUM.addEvent("checkout_completed", {
  plan: "pro",
  amount: 99,
});
```

Each call dual-emits:

- A **span event** on the active view span, so the event shows up on the view's timeline in RUM.
- An **OTLP log record** carrying `event.type=custom`, `event.name`, your attributes, and `session.id`/user attributes. When a view is active the log is correlated to it via `trace.id`/`span.id`/`view.id`, so you can pivot between the log and the RUM session.

Log emission is unconditional. If no view is active when you call `addEvent`, the span event is dropped (no view span is fabricated to attach it to) but the log is still sent as an orphan, omitting `trace.id`/`span.id`/`view.id`. A custom event never silently vanishes because it fired outside a view.

### Manual Error Capture

Explicitly track handled exceptions to maintain visibility into recoverable failures:

```typescript
try {
  await searchProducts(query);
} catch (error) {
  L9RUM.captureError(error, {
    handled: true,
    message: "search_query_timeout",
    query,
    page: 3,
  });
}
```

Manual captures default to `handled: true`. All errors — automatic and manual, pass through the `errors.beforeSend` hook for filtering.

The SDK normalizes non-`Error` throws and promise rejections, including strings, primitives, plain objects, and objects with throwing properties. `exception.type` is inferred from the payload or stack header, and emitted error logs prefer the active RUM view span for `trace.id` and `span.id`.

## Data Collection

The RUM SDK automatically collects performance and context data without affecting your application's performance.

### Core Web Vitals

- **Largest Contentful Paint (LCP)**: Loading performance measurement for largest visible content element
- **First Contentful Paint (FCP)**: Initial rendering time for first visible content
- **Cumulative Layout Shift (CLS)**: Visual stability score measuring unexpected layout shifts
- **Interaction to Next Paint (INP)**: Responsiveness metric for user interactions
- **Time to First Byte (TTFB)**: Server response time from initial request

Each metric is recorded with **attribution data** so you can see what caused a regression, not just that it happened:

- **LCP** — the largest-contentful element (CSS selector + ID when available), the resource URL, and the timing breakdown (`time_to_first_byte`, `resource_load_delay`, `resource_load_duration`, `element_render_delay`)
- **CLS** — the element with the largest layout shift (`largest_shift_target`), its time, and value
- **INP** — the event type (click, keydown, etc.), the target element, and the interaction phase that contributed most (`input_delay`, `processing_duration`, `presentation_delay`)
- **FCP** — the timing breakdown leading up to first paint
- **TTFB** — DNS, connection, request, and waiting durations

### Document Load Timing

A full Navigation Timing breakdown is recorded for each page load, with derived metrics for the phases that matter:

- DOM processing duration
- Page rendering duration
- Network phase breakdown (DNS, connection, request, response)
- Total load time, DOM-interactive time, and DOM-complete time

### Long Tasks

Main-thread blocking is tracked automatically. Each long task (>50ms) is recorded with its duration, start time, and attribution to the script or container that caused it. A per-view **Total Blocking Time** is derived so you can spot interaction lag at a glance.

### User Context

- **Browser Information**: Name, version, and complete user agent string
- **Device Details**: Type (desktop, mobile, tablet) and screen dimensions
- **Network Conditions**: Effective connection type (4g, 3g, 2g, slow-2g), downlink bandwidth estimate, round-trip time, and Data Saver mode — recorded per view so you can separate backend slowness from network flakiness
- **Geolocation**: Country, region, city, IP, etc (only when `geoIpEnabled: true` and GeoJS enrichment succeeds)
- **User Identity**: Name, email, role, and ID when provided via `identify()`

### Page Information

- **Navigation Data**: Page URL, path, referrer, and query parameters
- **Route Changes**: Single-page application navigation and route transitions
- **Timing Metrics**: Load times, navigation timing, and resource loading performance
- **Network Activity**: API calls, resource loading, and network errors

## Verification

Confirm the RUM SDK is working correctly:

    1. **Check Browser Console**: Look for RUM initialization messages. Enable `debug: true` in your configuration to see detailed logging.
    2. **Verify Network Requests**: Open browser DevTools > Network tab and look for requests to your Applications base URL. Successful requests indicate data is being sent.
    3. **Inspect API Calls**: Confirm trace headers appear on backend requests if correlation is enabled.
    4. **View Applications Dashboard**: Navigate to [Discover > Applications](https://app.last9.io/applications) and confirm data appears within 2-3 minutes of page loads.

## Next Steps

With Applications monitoring successfully installed, explore the monitoring capabilities:

- **[Performance Monitoring](/docs/discover-applications-performance/)**: Track Core Web Vitals, analyze page performance, and identify bottlenecks
- **[Error Tracking](/docs/discover-applications-errors/)**: Monitor JavaScript exceptions and failed requests with detailed analysis
- **[Session Analysis](/docs/discover-applications-sessions/)**: Analyze user journeys and navigation patterns

---

## Troubleshooting

- **No data showing?** Verify your `baseUrl` and `clientToken` are correct. Check the browser console for initialization errors.
- **Trace headers missing?** Confirm `network.backendCorrelation.enabled` is `true` and target domains are in `corsAllowedOrigins`.
- **Baggage not arriving on backend?** Verify `baggage.enabled` is `true`, the attribute keys are listed in `allowedKeys`, and the values are set via `L9RUM.spanAttributes()` before the request fires. Open DevTools → Network and check that the `baggage` header appears on outgoing requests.
- **Errors not appearing?** Check that error toggles in the `errors` configuration are enabled and `sampleRate` is appropriate for your traffic.
- **"You don't have access to this resource" on the beacon endpoint?** Your application's origin is not in the ingestion token's allowed origins list. Go to **Control Plane → [Ingestion Tokens](https://app.last9.io/control-plane/ingestion-tokens)**, edit your Client token, and add your domain (e.g., `https://app.yourdomain.com`). See [origin configuration](/docs/ingestion-tokens/) for format details.

Please get in touch with us on [Discord](https://discord.com/invite/Q3p2EEucx9) or [Email](mailto:support@last9.io) if you have any questions.
