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

# Track widget events

> Listen to the widget's postMessage events and forward opens, closes, and messages to Google Tag Manager or any analytics tool

After this guide, every open, close, and message in the widget reaches your analytics. The widget reports activity with `window.postMessage`; the only callback option is `onNavigate`, covered in the [configuration reference](/channels/web-widget/configuration#navigation).

**Before you begin**

* The widget installed per the [quickstart](/channels/web-widget/quickstart).
* For the Tag Manager step, a GTM container on the page.

<Steps>
  <Step title="Listen for widget events">
    Every event is posted to the page's own `window` with `type: "LUA_POP_EVENT"`. Register the listener before or after `init()`; the order doesn't matter.

    ```js theme={null}
    window.addEventListener("message", (event) => {
      if (event.data?.type !== "LUA_POP_EVENT") return;
      const { eventType, timestamp, data } = event.data;
      console.log(eventType, new Date(timestamp), data);
    });
    ```

    The payload is `{ type: "LUA_POP_EVENT", eventType, timestamp, data? }`: `timestamp` is `Date.now()` at emit time and `data` exists only on `message_received`. No event carries an end user identifier or the session ID.
  </Step>

  <Step title="Handle each event type">
    Four `eventType` values exist.

    | `eventType`        | When it fires                                                                                                     | `data`                                |
    | ------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
    | `widget_opened`    | The end user opens the floating chat window                                                                       | —                                     |
    | `widget_closed`    | The end user closes it                                                                                            | —                                     |
    | `message_sent`     | The end user submits text, files, a voice message, or a conversation starter, whether or not the request succeeds | —                                     |
    | `message_received` | An agent reply is added to the conversation, including messages the agent starts                                  | `{ content: string }`, the reply text |

    `widget_opened` and `widget_closed` never fire in embedded mode, where the chat is always visible. `message_received` doesn't fire for a reply whose stream fails part-way, nor for replies delivered as rich UI components instead of text.
  </Step>

  <Step title="Forward events to Google Tag Manager">
    GTM triggers fire on `dataLayer` pushes, so forward each event from a Custom HTML tag, then create a Custom Event trigger for `lua_message_sent` or any of the other names.

    ```js theme={null}
    window.addEventListener("message", (event) => {
      if (event.data?.type !== "LUA_POP_EVENT") return;
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({ event: "lua_" + event.data.eventType });
    });
    ```
  </Step>

  <Step title="Optional: resize an iframe that hosts the widget">
    When the widget runs inside an iframe, it also posts a metadata-only copy of each event to `window.parent`, plus a `LUA_POP_RESIZE` message once when it mounts and whenever the floating window opens or closes. Handle that message in the parent page.

    ```js theme={null}
    window.addEventListener("message", (event) => {
      if (event.data?.type !== "LUA_POP_RESIZE") return;
      const iframe = document.getElementById("chat-frame");
      if (!iframe) return;
      iframe.style.width = event.data.width;
      iframe.style.height = event.data.height;
    });
    ```

    `width` and `height` are `"350px"` and `"500px"` while the window is open and `"auto"` when it is closed. The parent copy of `message_received` omits `data`, because the parent may be on another origin.
  </Step>

  <Step title="Verify">
    Open the widget and send a message. The console from the first step prints `widget_opened`, then `message_sent`, then `message_received` with the reply text.
  </Step>
</Steps>

## Options you may need

### Google Analytics 4

Call `gtag` directly instead of going through `dataLayer`.

```js theme={null}
window.addEventListener("message", (event) => {
  if (event.data?.type !== "LUA_POP_EVENT") return;
  gtag("event", "chat_interaction", { event_label: event.data.eventType });
});
```

## If it isn't working

<AccordionGroup>
  <Accordion title="No widget_opened or widget_closed events">
    The widget is in embedded mode, where the chat never opens or closes. Use `message_sent` as the engagement signal.
  </Accordion>

  <Accordion title="Events arrive without data in the parent page">
    You are listening in the page that hosts the iframe. Only the copy posted to the widget's own window carries `data`.
  </Accordion>

  <Accordion title="Events can be forged">
    Any script on the page can post the same messages. Use them for analytics and UX, never for access control.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Widget configuration" href="/channels/web-widget/configuration">`onNavigate` and every other option.</Card>
  <Card title="Send proactive messages" href="/build/send-proactive-messages">Messages the agent starts also fire `message_received`.</Card>
</Columns>
