# Send events

> Send playback events to your analytics service.

The tracker includes adapters for [Chunkify Analytics](/docs/integration/analytics/server), PostHog, Datadog RUM, and OpenTelemetry via OTLP/HTTP. You can also send events to your own HTTP endpoint or write a custom handler.

Use the PostHog adapter to capture playback events with your PostHog browser client, or the Datadog RUM adapter to record them as custom actions. Both work without a Chunkify Analytics Server.

If you followed [player setup](/docs/integration/analytics), replace the console destination with one of these examples. You can add several destinations to send the same events to more than one service.

## Choose a destination

Each example creates a `destination` that you can pass to your player's analytics options. Add the imports at the top of your browser module.

<tabs>
    <tab title="Chunkify Analytics">
        [Set up an Analytics Server](/docs/integration/analytics/server/setup), hosted by Chunkify or by you. Copy its server URL and public **Player** key. Append `/v1/events` to the server URL:

        ```javascript
        import { createChunkifyAdapter } from '@chunkify/player-analytics/chunkify';

        const destination = createChunkifyAdapter(
            'https://analytics.example.com/v1/events',
            {
                ingestKey: 'YOUR_PUBLIC_PLAYER_KEY',
            },
        );
        ```

        The adapter sends one complete JSON event with `X-Chunkify-Ingest-Key`. It adds browser, device, language, screen, and current page context in `browser_context`.

        Set `collectBrowserContext: false` to omit that context. The core playback measurements are still sent.

        Temporary delivery failures are retried by default. This adapter preserves the Analytics Server's expected schema and has no custom mapping option.

        Add your website origin in [server configuration](/docs/integration/analytics/server/configuration#browser-access), then open the [Chunkify dashboard](/docs/integration/analytics/server/dashboard) to inspect the results.
    </tab>
    <tab title="PostHog">
        Install the browser client if your application does not already use it:

        ```sh
        npm install posthog-js
        ```

        Initialize it with your project's public token and ingestion host, then pass the client to the adapter. Reuse your existing client if it is already initialized.

        ```javascript
        import posthog from 'posthog-js';
        import { createPostHogAdapter } from '@chunkify/player-analytics/posthog';

        posthog.init('YOUR_POSTHOG_PROJECT_TOKEN', {
            api_host: 'https://us.i.posthog.com',
        });

        const destination = createPostHogAdapter(posthog);
        ```

        The adapter captures one PostHog event per tracker event, keeping its name and turning its fields into flat properties. Pass a mapper as the second argument to customize those properties.

        Use your project's ingestion host. PostHog's own automatic collection, persistence, and delivery settings remain part of your [PostHog configuration](https://posthog.com/docs/libraries/js).
    </tab>
    <tab title="Datadog RUM">
        Install the RUM client if needed:

        ```sh
        npm install @datadog/browser-rum
        ```

        Initialize it with your RUM application ID, public client token, and Datadog site. Reuse your existing client if it is already initialized.

        ```javascript
        import { datadogRum } from '@datadog/browser-rum';
        import { createDatadogAdapter } from '@chunkify/player-analytics/datadog';

        datadogRum.init({
            applicationId: 'YOUR_DATADOG_APPLICATION_ID',
            clientToken: 'YOUR_DATADOG_RUM_CLIENT_TOKEN',
            site: 'datadoghq.com',
        });

        const destination = createDatadogAdapter(datadogRum);
        ```

        The adapter adds one custom RUM action per tracker event, using the event fields as its context. Pass a mapper as the second argument to customize that context.

        Use your organization's Datadog site and client token.

        See [Datadog's browser setup](https://docs.datadoghq.com/real_user_monitoring/application_monitoring/browser/setup/client/) for its RUM configuration.
    </tab>
    <tab title="HTTP">
        Use the HTTP adapter for an endpoint provided by your application. It sends one JSON POST per event:

        ```javascript
        import { createHttpAdapter } from '@chunkify/player-analytics/http';

        const destination = createHttpAdapter('/api/video-analytics');
        ```

        Your endpoint handles validation, storage, or forwarding. The complete tracker event is sent by default.

        Use `mapEvent` to change the payload and `headers` to add a public ingest key when needed.
    </tab>
    <tab title="OTLP">
        Use this adapter for an external OTLP/HTTP JSON gateway. Send to the gateway's full logs endpoint; use the Chunkify adapter above for the Analytics Server:

        ```javascript
        import { createOtlpAdapter } from '@chunkify/player-analytics/otlp';

        const destination = createOtlpAdapter(
            'https://telemetry.example.com/v1/logs',
        );
        ```

        Each tracker event becomes an OTLP log record. Event fields become typed log attributes; `null` values are omitted. The event name, timestamp, and package scope come from the original event.

        Use `mapAttributes` to customize the attributes and `headers` for a public ingest key. The gateway handles private provider credentials, storage, and aggregation.
    </tab>
    <tab title="Custom">
        A destination can be any object with a `send(event)` method. Run your own code for each event, or forward it through a client your application already uses:

        ```javascript
        const destination = {
            send(event) {
                if (event.event_name !== 'chunkify.video.view_ended') return;

                console.log('Playback summary', {
                    videoId: event.video_id,
                    viewId: event.view_id,
                    playingSeconds: event.playing_time_ms / 1000,
                    finished: event.reason === 'ended',
                });
            },
        };
        ```

        This handler processes only final records for this destination. Other destinations still receive their selected events.

        `send()` can return `void` or a Promise, so it can also be `async`. The tracker does not add retries or persistence around custom code.
    </tab>
</tabs>

Use the resulting `destination` in your selected [player example](/docs/integration/analytics#connect-your-player). For instance, with a native `<video>` already on the page:

```javascript
import { monitor } from '@chunkify/player-analytics';

const video = document.querySelector('video');
if (!video) throw new Error('Missing video element');

const session = monitor(video, {
    videoId: 'ast_3Ljtoej7c3Y1plabZoElbQwdwLh',
    destinations: [destination],
});
```

Keep the matching connector for hls.js, Video.js 10, or Shaka Player. Each accepts the same `destinations` option. Call `session.destroy()` when monitoring stops.

## Map event fields

Use `createEventMapper()` to select fields, rename them, or calculate values. Only the listed fields are included in the mapped object. The original event is preserved for other destinations.

This example keeps the event identifiers and converts playing time to seconds for an HTTP endpoint:

```javascript
import { createEventMapper } from '@chunkify/player-analytics';
import { createHttpAdapter } from '@chunkify/player-analytics/http';

const mapEvent = createEventMapper({
    eventId: 'event_id',
    eventName: 'event_name',
    viewId: 'view_id',
    videoId: 'video_id',
    sequence: 'sequence',
    occurredAt: 'occurred_at',
    playingSeconds: (event) => (
        typeof event.playing_time_ms === 'number'
            ? event.playing_time_ms / 1000
            : undefined
    ),
});

const destination = createHttpAdapter('/api/video-analytics', { mapEvent });
```

A string copies the named source field. A function calculates its value. Missing fields and values returned as `undefined` are omitted; `null` is preserved by the mapper.

For example, `playingSeconds` is omitted from a rendition-change record because that event has no cumulative playing time. See [Events and metrics](/docs/integration/analytics/events) for each payload.

| Destination | Apply your mapper |
| --- | --- |
| HTTP | `createHttpAdapter(endpoint, { mapEvent })` |
| PostHog | `createPostHogAdapter(client, mapEvent)` |
| Datadog RUM | `createDatadogAdapter(client, mapEvent)` |
| OTLP | `createOtlpAdapter(endpoint, { mapAttributes: mapEvent })` |
| Custom `send()` | Call `mapEvent(event)` inside your handler. |
| Chunkify Analytics | Sends the complete Chunkify Analytics schema. No mapper option. |

Mapping changes a destination's payload. To choose which event types are delivered, use [`includeEvents`](/docs/integration/analytics/events#choose-events).

## Use multiple destinations

Add each destination to the array. For example, send complete events to your endpoint while also inspecting them in the console:

```javascript
import { monitor } from '@chunkify/player-analytics';
import { createHttpAdapter } from '@chunkify/player-analytics/http';

const video = document.querySelector('video');
if (!video) throw new Error('Missing video element');

const session = monitor(video, {
    videoId: 'ast_3Ljtoej7c3Y1plabZoElbQwdwLh',
    destinations: [
        createHttpAdapter('/api/video-analytics'),
        { send: (event) => console.log(event) },
    ],
});
```

Delivery runs independently for each destination. A thrown error or rejected Promise is isolated so it does not interrupt playback or another destination. Handle errors in your custom code when you need delivery diagnostics.

## Browser delivery

HTTP-based adapters accept HTTPS URLs or root-relative paths. Endpoints cannot contain credentials, query parameters, or fragments. Loopback HTTP is also accepted for local development.

The HTTP, OTLP, and Chunkify adapters send JSON without cookies or a page referrer. A cross-origin endpoint must allow your page origin, POST, `Content-Type`, and any configured public ingest header through CORS.

Keep private provider credentials and analytics secret keys on your server. Browser examples use public client or ingest keys.

### Retries and limits

| Destination | Retry behavior |
| --- | --- |
| Chunkify Analytics | Enabled by default. Set `retry: false` to disable. |
| HTTP and OTLP | Disabled by default. Set `retry: true` to enable. |
| PostHog and Datadog RUM | Managed by their browser clients. |
| Custom `send()` | Managed by your code. |

When enabled, HTTP retries cover network failures, request timeouts, and HTTP `429`, `502`, `503`, and `504`. There are at most two retries after the initial request.

The HTTP transport times out each request after five seconds and allows 32 pending sends per adapter. Further sends fail immediately while it is full. It has no offline storage or waiting queue.

A readable `Retry-After` header can extend the backoff. A delay over ten seconds ends delivery instead. Cross-origin endpoints must expose `Retry-After` for the browser to read it.

Retries reuse the same serialized event and browser context. Your receiver must tolerate duplicates; preserve `event_id` when mapping fields. Use `sequence` to order records within a view.

The Analytics Server keeps the first delivery of an event ID received within its deduplication window, seven days by default. Use view-level report totals, and deduplicate raw events by `event_id` in your own consumers.

Final HTTP events use a keepalive request, but delivery when a page exits is best effort. A successful HTTP send means the endpoint returned a 2xx status.
