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

# Widget events (onEvent)

> Mirror chat widget activity into your own analytics with the onEvent callback.

The chat widget can mirror its lifecycle events into the host page through a single `onEvent` callback. Both hosted surfaces accept it: `WaniWani.chat.init({ onEvent })` on the script embed, and the `onEvent` prop on the React component.

The embed mounts in your page's DOM (the shadow root isolates styles only), so the callback runs in your page's own JS context. Forward events straight into `window.amplitude`, `window.analytics`, or `gtag` and your page-side analytics SDK attaches its own user identity automatically — no server-side identity plumbing, and no Amplitude/Segment SDK is ever loaded inside the widget.

<Note>
  **When to use which channel.** `onEvent` mirrors widget activity into **your** analytics on the host page — it never leaves the browser unless your callback sends it somewhere. It is separate from platform ingest tracking (`WaniWani.chat.track`, funnel events, the typed event catalog), which sends events **to the Waniwani platform** for funnel analytics. Use `onEvent` to see chat activity next to your existing product analytics; use [tracking events](/sdk/tracking/events) to power Waniwani funnels. The two channels are independent and can be used together.
</Note>

## The events

| Event                | Fired when                                                                                                                                         | Extra `properties`                                             |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `chat.ready`         | Widget mounted and config resolved, or the readiness timeout elapses (all modes; fires even on pages where visibility rules hide the floating bar) | —                                                              |
| `chat.opened`        | Full chat panel opened (floating mode only; the collapsed dock does not count)                                                                     | —                                                              |
| `chat.closed`        | Full chat panel closed (floating mode only; a page-gate hiding an open panel also emits this)                                                      | —                                                              |
| `message.sent`       | The visitor (or the imperative API, e.g. `sendMessage`) submits a message (never includes the message text)                                        | —                                                              |
| `message.received`   | Assistant reply finished (never includes the message text)                                                                                         | —                                                              |
| `session.started`    | Server assigned the session id on the first exchange (restored threads don't re-fire)                                                              | `{ sessionId }`                                                |
| `thread.changed`     | Thread created or switched (requires thread history)                                                                                               | `{ threadId }`                                                 |
| `chat.error`         | Chat request failed                                                                                                                                | `{ message }` (truncated to 200 chars)                         |
| `suggestion.clicked` | Suggestion pill or welcome card clicked (dock pills, in-chat pills, welcome cards)                                                                 | `{ text, index }` (`index` is `-1` when the origin is unknown) |
| `link.clicked`       | Anchor clicked inside the conversation                                                                                                             | `{ url }` (absolute URL)                                       |

Do not assume `chat.opened` precedes the first `message.sent`: a send from the docked bar opens the panel and sends in one action, and the open transition is reported after the send.

<Note>
  **Separate from the `track()` taxonomy.** Some `onEvent` names (`session.started`, `link.clicked`) also exist as event names in the hosted `track()` funnel taxonomy — same concepts, but a separate client-side channel with different payload shapes. `onEvent` events are not `client.track()` events, do not feed the platform funnel dashboard, and should not be forwarded into `client.track()` as-is even where names coincide.
</Note>

## The payload

Every callback receives a `WidgetEvent` — exported from `@waniwani/sdk/chat` alongside `WidgetEventName` and `WidgetMode`:

```ts theme={null}
import type { WidgetEvent, WidgetEventName, WidgetMode } from "@waniwani/sdk/chat";

// WidgetEvent is a discriminated union on `name`:
// {
//   name: WidgetEventName;              // one of the 10 events above
//   mode: WidgetMode;                   // "inline" | "floating"
//   sessionId?: string;                 // undefined before the first exchange
//   timestamp: number;                  // epoch milliseconds at emit time
//   properties?: { ... };               // per-event extras from the table above
// }
```

`mode` is the embed surface: `"floating"` for the floating bar, `"inline"` for an in-page mount. `<WaniwaniChat>` reports `"inline"` — it is an in-page mount, and this is the same `mode` tag its server-side events (chat requests, `page.viewed`) carry, so both streams correlate.

Narrowing on `name` types `properties` automatically:

```ts theme={null}
function handle(event: WidgetEvent) {
  if (event.name === "suggestion.clicked") {
    // event.properties is { text: string; index: number }
  }
}
```

Event names are neutral and fixed — the widget never adopts your analytics naming. Map them to your own schema inside the callback.

## Script embed → Amplitude

`onEvent` is programmatic-only on the script embed: a function cannot be expressed as a `data-*` attribute, so pass it to `WaniWani.chat.init()`.

```html theme={null}
<script src="https://app.waniwani.ai/embed.js" defer></script>
<script>
  document.addEventListener("DOMContentLoaded", () => {
    const names = {
      "chat.opened": "chat_opened",
      "chat.closed": "chat_closed",
      "message.sent": "user_message",
      "message.received": "bot_message",
    };
    WaniWani.chat.init({
      token: "wwp_...",
      mode: "floating",
      onEvent: (event) => {
        window.amplitude.track(names[event.name] || event.name, {
          page: window.location.pathname,
          sessionId: event.sessionId,
        });
      },
    });
  });
</script>
```

Because the Amplitude snippet on your page already identified the user, every mirrored event lands in Amplitude under that same identity.

## React → Segment

The `WaniwaniChat` component takes the same callback as an `onEvent` prop (`chat.opened`/`chat.closed` are floating-embed-only and never fire here):

```tsx theme={null}
import { WaniwaniChat, type WidgetEvent } from "@waniwani/sdk/chat";

function Support() {
  const handleWidgetEvent = (event: WidgetEvent) => {
    window.analytics.track(event.name, {
      mode: event.mode,
      sessionId: event.sessionId,
      ...(event.name === "chat.error"
        ? { errorMessage: event.properties.message }
        : {}),
    });
  };

  return (
    <WaniwaniChat token="wwp_..." channelId="..." onEvent={handleWidgetEvent} />
  );
}
```

## Privacy

Message content never passes through this channel. `message.sent` and `message.received` tell the host *that* a message was exchanged, never what was said. The one text field that does flow through is `suggestion.clicked`'s `text` — suggestion pills are operator-authored config, not user input.

## Error isolation

Exceptions thrown by your callback are swallowed and logged as a console warning; they never break the widget or block other subscribers. Still, keep the callback fast — it runs synchronously on the widget's event path.

<Warning>
  `ChatEmbed`, the bring-your-own-backend React primitive, does **not** expose `onEvent`. Like `track`/`identify`, it is a hosted-surface feature of `WaniwaniChat` and the script embed.
</Warning>
