@helpai/elements 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -12
- package/configurator.mjs +1 -1
- package/docs/README.md +183 -0
- package/docs/analytics.md +551 -0
- package/docs/api-reference.md +637 -0
- package/docs/browser-support.md +302 -0
- package/docs/configuration.md +1014 -0
- package/docs/events-and-hooks.md +483 -0
- package/docs/faq.md +435 -0
- package/docs/frameworks.md +643 -0
- package/docs/installation.md +626 -0
- package/docs/options.md +323 -0
- package/docs/quickstart.md +229 -0
- package/docs/recipes.md +725 -0
- package/docs/security-privacy.md +636 -0
- package/docs/theming.md +570 -0
- package/docs/troubleshooting.md +523 -0
- package/elements-web-component.esm.js +39 -39
- package/elements-web-component.esm.js.map +4 -4
- package/elements.cjs.js +39 -39
- package/elements.cjs.js.map +4 -4
- package/elements.esm.js +39 -39
- package/elements.esm.js.map +4 -4
- package/elements.js +39 -39
- package/elements.js.map +4 -4
- package/index.d.ts +107 -3
- package/index.mjs +13 -13
- package/jsx-react.d.ts +15 -0
- package/jsx-svelte.d.ts +14 -0
- package/jsx-vue.d.ts +14 -0
- package/llms-full.txt +7954 -0
- package/llms.txt +37 -0
- package/loader.js +3 -3
- package/loader.min.js +1 -1
- package/package.json +36 -1
- package/react.d.ts +3575 -0
- package/react.mjs +62 -0
- package/schema.d.ts +56 -0
- package/schema.json +24 -2
- package/schema.mjs +1 -1
- package/web-component.d.ts +1583 -17
- package/web-component.mjs +14 -14
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# Analytics integration
|
|
2
|
+
|
|
3
|
+
Wire chat-widget activity into Google Analytics, Mixpanel, Segment,
|
|
4
|
+
Amplitude, PostHog, Sentry, or any custom tracker. Multiple providers
|
|
5
|
+
can run side-by-side — every action fires once on the widget's
|
|
6
|
+
internal event bus, and you subscribe as many listeners as you want.
|
|
7
|
+
|
|
8
|
+
> **No SDK plugin needed.** The widget exposes a typed pub/sub API.
|
|
9
|
+
> Whatever your analytics provider's SDK looks like, you call it
|
|
10
|
+
> inside a listener.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## TL;DR — one-line install
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<!-- 1. Load your analytics SDK as you normally would -->
|
|
18
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
|
|
19
|
+
<script>
|
|
20
|
+
window.dataLayer = window.dataLayer || [];
|
|
21
|
+
function gtag() {
|
|
22
|
+
dataLayer.push(arguments);
|
|
23
|
+
}
|
|
24
|
+
gtag('js', new Date());
|
|
25
|
+
gtag('config', 'G-XXXXXXX');
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<!-- 2. Load the widget -->
|
|
29
|
+
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>
|
|
30
|
+
|
|
31
|
+
<!-- 3. Subscribe to every widget event with one wildcard hook -->
|
|
32
|
+
<script>
|
|
33
|
+
HelpAI('hook', 'default', '*', (chat, event, payload) => {
|
|
34
|
+
gtag('event', `chat_${event}`, flatten(payload));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function flatten(p) {
|
|
38
|
+
if (p == null) return {};
|
|
39
|
+
if (typeof p !== 'object') return { value: p };
|
|
40
|
+
return p;
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Reload the page, click around the widget, watch GA4's DebugView. Every
|
|
46
|
+
open / close / send / theme change / file attach shows up as its own
|
|
47
|
+
event.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Event catalogue
|
|
52
|
+
|
|
53
|
+
Every interactive action emits one event with a typed payload. Subscribe
|
|
54
|
+
via `chat.on(name, fn)` (instance) or `HelpAI.hook(widgetId, name, fn)`
|
|
55
|
+
(global, pre-boot-safe). Full schema in [Events & hooks](./events-and-hooks.md).
|
|
56
|
+
|
|
57
|
+
| Category | Event | Payload |
|
|
58
|
+
| ---------------- | --------------------- | -------------------------------------------------- |
|
|
59
|
+
| **Panel** | `open` | `void` |
|
|
60
|
+
| | `close` | `void` |
|
|
61
|
+
| | `expand` | `boolean` (true = expanded) |
|
|
62
|
+
| | `fullscreen` | `boolean` (true = entering fullscreen) |
|
|
63
|
+
| | `resize` | `{ width, height }` |
|
|
64
|
+
| **Conversation** | `message` | `{ role, text }` (fires for user + assistant) |
|
|
65
|
+
| | `send` | `{ text, attachmentCount }` (fires on user submit) |
|
|
66
|
+
| | `stop` | `void` (user cancelled in-flight stream) |
|
|
67
|
+
| | `clear` | `void` (new conversation) |
|
|
68
|
+
| | `selectConversation` | `{ conversationId }` (resumed from history) |
|
|
69
|
+
| | `suggestion` | `{ text }` (clicked a quick-reply chip) |
|
|
70
|
+
| | `toggleHistory` | `{ view: "chat" \| "history" }` |
|
|
71
|
+
| **Composer** | `attach` | `{ count, totalBytes }` |
|
|
72
|
+
| | `removeAttachment` | `{ id }` |
|
|
73
|
+
| | `voiceStart` | `void` |
|
|
74
|
+
| | `voiceStop` | `{ durationMs }` |
|
|
75
|
+
| | `voiceCancel` | `void` |
|
|
76
|
+
| **Preferences** | `localeChange` | `string` (BCP-47 tag, e.g. `"fr"`) |
|
|
77
|
+
| | `themeChange` | `"auto" \| "light" \| "dark"` |
|
|
78
|
+
| | `soundToggle` | `{ muted }` |
|
|
79
|
+
| | `sidebarToggle` | `{ collapsed }` (page mode) |
|
|
80
|
+
| | `calloutDismiss` | `void` |
|
|
81
|
+
| **Lifecycle** | `error` | `unknown` (any unhandled error) |
|
|
82
|
+
| | `handshake` | `HandshakeResponse` (full handshake payload) |
|
|
83
|
+
| | `conversationRebound` | `{ previous, current }` |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## The two subscription APIs
|
|
88
|
+
|
|
89
|
+
### `chat.on(event, fn)` — instance-scoped
|
|
90
|
+
|
|
91
|
+
Returns an `off` function. Use when you have a handle to the mounted
|
|
92
|
+
instance (NPM / ESM / `mount()` returns it; `HelpAI.getInstance()`
|
|
93
|
+
fetches the default).
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const off = chat.on('send', ({ text, attachmentCount }) => {
|
|
97
|
+
console.log('user sent', text, 'with', attachmentCount, 'files');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Later — unsubscribe
|
|
101
|
+
off();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `HelpAI.hook(widgetId, event, fn)` — global, pre-boot-safe
|
|
105
|
+
|
|
106
|
+
Targets every widget that matches `(widgetId, event)`. Use when:
|
|
107
|
+
|
|
108
|
+
- You want to register **before** the bundle finishes loading (queue replays once boot completes).
|
|
109
|
+
- You have **multiple widgets** on one page and want a single listener for all of them (`widgetId: "*"`).
|
|
110
|
+
- You want to track every event with one listener (`event: "*"`).
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<!-- This runs BEFORE elements.js parses. The queued call replays after boot. -->
|
|
114
|
+
<script>
|
|
115
|
+
window.HelpAI =
|
|
116
|
+
window.HelpAI ||
|
|
117
|
+
function () {
|
|
118
|
+
(HelpAI.q = HelpAI.q || []).push(arguments);
|
|
119
|
+
};
|
|
120
|
+
HelpAI('hook', '*', '*', (chat, event, payload) => {
|
|
121
|
+
track(`chat_${event}`, { widgetId: chat.widgetId, ...payload });
|
|
122
|
+
});
|
|
123
|
+
</script>
|
|
124
|
+
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The listener receives `(chat, event, payload)` — `chat` is the
|
|
128
|
+
`ChatWidgetInstance` (so you can branch on `chat.widgetId`), `event`
|
|
129
|
+
is the string event name, `payload` is the typed payload (or
|
|
130
|
+
`undefined` for `void` events).
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Multiple analytics providers — three patterns
|
|
135
|
+
|
|
136
|
+
### Pattern A — one listener per provider (simplest)
|
|
137
|
+
|
|
138
|
+
Each provider registers its own wildcard listener. Order doesn't
|
|
139
|
+
matter; one provider erroring out doesn't break the others (the bus
|
|
140
|
+
isolates listener exceptions).
|
|
141
|
+
|
|
142
|
+
```html
|
|
143
|
+
<script>
|
|
144
|
+
HelpAI(() => {
|
|
145
|
+
const chat = HelpAI.getInstance();
|
|
146
|
+
|
|
147
|
+
// GA4
|
|
148
|
+
chat.on('send', ({ text, attachmentCount }) => gtag('event', 'chat_send', { text_length: text.length, attachments: attachmentCount }));
|
|
149
|
+
chat.on('themeChange', (mode) => gtag('event', 'chat_theme', { mode }));
|
|
150
|
+
|
|
151
|
+
// Mixpanel
|
|
152
|
+
chat.on('send', ({ text }) => mixpanel.track('Chat Send', { textLength: text.length }));
|
|
153
|
+
chat.on('themeChange', (mode) => mixpanel.track('Chat Theme', { mode }));
|
|
154
|
+
|
|
155
|
+
// Sentry for errors only
|
|
156
|
+
chat.on('error', (err) => Sentry.captureException(err, { tags: { source: 'chat-widget' } }));
|
|
157
|
+
});
|
|
158
|
+
</script>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Pattern B — one wildcard listener, fan out internally
|
|
162
|
+
|
|
163
|
+
When you want every event in every provider, declare a small
|
|
164
|
+
`providers` array and let one listener push to all of them. New
|
|
165
|
+
provider? Add a function to the array.
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
HelpAI(() => {
|
|
169
|
+
const chat = HelpAI.getInstance();
|
|
170
|
+
|
|
171
|
+
// Each provider is `(event, payload) => void`.
|
|
172
|
+
const providers = [
|
|
173
|
+
// GA4
|
|
174
|
+
(event, payload) => gtag('event', `chat_${event}`, flatten(payload)),
|
|
175
|
+
|
|
176
|
+
// Mixpanel
|
|
177
|
+
(event, payload) => mixpanel.track(`Chat ${titleCase(event)}`, flatten(payload)),
|
|
178
|
+
|
|
179
|
+
// Segment / RudderStack
|
|
180
|
+
(event, payload) => analytics.track(`Chat ${titleCase(event)}`, flatten(payload)),
|
|
181
|
+
|
|
182
|
+
// Amplitude
|
|
183
|
+
(event, payload) => amplitude.track(`chat_${event}`, flatten(payload)),
|
|
184
|
+
|
|
185
|
+
// PostHog
|
|
186
|
+
(event, payload) => posthog.capture(`chat_${event}`, flatten(payload)),
|
|
187
|
+
|
|
188
|
+
// Server-side beacon (your own endpoint)
|
|
189
|
+
(event, payload) => navigator.sendBeacon('/api/telemetry', JSON.stringify({ event, payload })),
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
chat.on('open', () => fanOut('open'));
|
|
193
|
+
chat.on('close', () => fanOut('close'));
|
|
194
|
+
chat.on('send', (p) => fanOut('send', p));
|
|
195
|
+
chat.on('message', (p) => fanOut('message', p));
|
|
196
|
+
chat.on('clear', () => fanOut('clear'));
|
|
197
|
+
chat.on('selectConversation', (p) => fanOut('select_conversation', p));
|
|
198
|
+
chat.on('suggestion', (p) => fanOut('suggestion', p));
|
|
199
|
+
chat.on('themeChange', (m) => fanOut('theme_change', { mode: m }));
|
|
200
|
+
chat.on('localeChange', (l) => fanOut('locale_change', { locale: l }));
|
|
201
|
+
chat.on('attach', (p) => fanOut('attach', p));
|
|
202
|
+
chat.on('removeAttachment', (p) => fanOut('remove_attachment', p));
|
|
203
|
+
chat.on('voiceStart', () => fanOut('voice_start'));
|
|
204
|
+
chat.on('voiceStop', (p) => fanOut('voice_stop', p));
|
|
205
|
+
chat.on('expand', (v) => fanOut('expand', { expanded: v }));
|
|
206
|
+
chat.on('fullscreen', (v) => fanOut('fullscreen', { fullscreen: v }));
|
|
207
|
+
chat.on('resize', (p) => fanOut('resize', p));
|
|
208
|
+
chat.on('toggleHistory', (p) => fanOut('toggle_history', p));
|
|
209
|
+
chat.on('soundToggle', (p) => fanOut('sound_toggle', p));
|
|
210
|
+
chat.on('sidebarToggle', (p) => fanOut('sidebar_toggle', p));
|
|
211
|
+
chat.on('calloutDismiss', () => fanOut('callout_dismiss'));
|
|
212
|
+
chat.on('error', (e) => fanOut('error', { description: String(e) }));
|
|
213
|
+
chat.on('handshake', (r) => fanOut('handshake', { aiAgentId: r.agent?.id }));
|
|
214
|
+
|
|
215
|
+
function fanOut(event, payload) {
|
|
216
|
+
for (const send of providers) {
|
|
217
|
+
try {
|
|
218
|
+
send(event, payload);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.error('[analytics]', e);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
function flatten(p) {
|
|
227
|
+
return p == null ? {} : typeof p === 'object' ? p : { value: p };
|
|
228
|
+
}
|
|
229
|
+
function titleCase(s) {
|
|
230
|
+
return s.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase());
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Pattern C — single wildcard hook, multi-provider fan-out (most concise)
|
|
235
|
+
|
|
236
|
+
The `'*' / '*'` wildcard hook receives every event from every widget
|
|
237
|
+
on the page. Combined with a provider array, this is the same idea as
|
|
238
|
+
Pattern B but in 5 lines.
|
|
239
|
+
|
|
240
|
+
```html
|
|
241
|
+
<script>
|
|
242
|
+
// Provider table — add / remove rows as you onboard more trackers.
|
|
243
|
+
const TRACKERS = [(event, payload) => gtag('event', `chat_${event}`, payload), (event, payload) => mixpanel.track(`chat_${event}`, payload), (event, payload) => analytics.track(`chat_${event}`, payload)];
|
|
244
|
+
|
|
245
|
+
HelpAI('hook', '*', '*', (chat, event, payload) => {
|
|
246
|
+
const flat = payload == null ? {} : typeof payload === 'object' ? payload : { value: payload };
|
|
247
|
+
for (const send of TRACKERS) {
|
|
248
|
+
try {
|
|
249
|
+
send(event, { widgetId: chat.widgetId, ...flat });
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.error('[analytics]', e);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
</script>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Multiple GA4 properties
|
|
261
|
+
|
|
262
|
+
Same SDK, multiple `config` calls — gtag sends every event to every
|
|
263
|
+
configured measurement id by default. No widget-specific change needed:
|
|
264
|
+
|
|
265
|
+
```html
|
|
266
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PRIMARY"></script>
|
|
267
|
+
<script>
|
|
268
|
+
window.dataLayer = window.dataLayer || [];
|
|
269
|
+
function gtag() {
|
|
270
|
+
dataLayer.push(arguments);
|
|
271
|
+
}
|
|
272
|
+
gtag('js', new Date());
|
|
273
|
+
gtag('config', 'G-PRIMARY');
|
|
274
|
+
gtag('config', 'G-SECONDARY');
|
|
275
|
+
gtag('config', 'G-DEBUG-PROPERTY');
|
|
276
|
+
</script>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Need to send some events to only one of them? Use `gtag`'s `send_to`:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
chat.on('send', ({ text, attachmentCount }) =>
|
|
283
|
+
gtag('event', 'chat_send', {
|
|
284
|
+
text_length: text.length,
|
|
285
|
+
attachments: attachmentCount,
|
|
286
|
+
send_to: ['G-PRIMARY', 'G-DEBUG-PROPERTY'], // skip secondary
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Per-provider recipes
|
|
294
|
+
|
|
295
|
+
### Google Tag Manager (dataLayer)
|
|
296
|
+
|
|
297
|
+
GTM consumes the `dataLayer` push — every push becomes a GTM event
|
|
298
|
+
you can use as a trigger.
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
chat.on('send', ({ text, attachmentCount }) => {
|
|
302
|
+
window.dataLayer.push({
|
|
303
|
+
event: 'chat_send',
|
|
304
|
+
chat: { textLength: text.length, attachments: attachmentCount, widgetId: chat.widgetId },
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
In GTM, create a custom-event trigger on `chat_send` and a variable
|
|
310
|
+
chain reading `chat.textLength` etc.
|
|
311
|
+
|
|
312
|
+
### Segment
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
chat.on('send', ({ text, attachmentCount }) => analytics.track('Chat Send', { textLength: text.length, attachmentCount }));
|
|
316
|
+
chat.on('clear', () => analytics.track('Chat Cleared'));
|
|
317
|
+
chat.on('selectConversation', ({ conversationId }) => analytics.track('Chat Resumed', { conversationId }));
|
|
318
|
+
chat.on('themeChange', (mode) => analytics.track('Chat Theme Changed', { mode }));
|
|
319
|
+
chat.on('error', (err) => analytics.track('Chat Error', { description: String(err) }));
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Amplitude
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
chat.on('send', ({ text, attachmentCount }) => amplitude.track('chat_send', { text_length: text.length, attachments: attachmentCount }));
|
|
326
|
+
chat.on('themeChange', (mode) => amplitude.track('chat_theme', { mode }));
|
|
327
|
+
chat.on('voiceStop', ({ durationMs }) => amplitude.track('chat_voice', { duration_ms: durationMs }));
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### PostHog
|
|
331
|
+
|
|
332
|
+
```js
|
|
333
|
+
chat.on('send', ({ text, attachmentCount }) => posthog.capture('chat_send', { text_length: text.length, attachments: attachmentCount }));
|
|
334
|
+
chat.on('handshake', (res) => posthog.capture('chat_connected', { agent: res.agent?.id }));
|
|
335
|
+
chat.on('error', (err) => posthog.capture('chat_error', { description: String(err) }));
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Sentry (error tracking)
|
|
339
|
+
|
|
340
|
+
The `error` event fires for **every** caught error — the stream, async
|
|
341
|
+
handlers, AND render crashes (the widget wraps its UI in an error boundary, so a
|
|
342
|
+
render throw degrades to a quiet fallback instead of white-screening the host
|
|
343
|
+
page; the error is reported here, never swallowed). It's also delivered to the
|
|
344
|
+
per-instance `options.onError` callback.
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
chat.on('error', (err) => {
|
|
348
|
+
Sentry.captureException(err, {
|
|
349
|
+
tags: { source: 'chat-widget', widgetId: chat.widgetId },
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Forward logs to your observability stack (`onLog`)
|
|
355
|
+
|
|
356
|
+
Beyond semantic events, the widget's scoped logger can stream **every log line**
|
|
357
|
+
to a sink — pipe warnings/errors/debug into Sentry breadcrumbs, Datadog, etc.
|
|
358
|
+
One global sink per page; it respects the active level (warnings + errors by
|
|
359
|
+
default), so raise it with `setDebug("trace")` to capture everything.
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
// Pre-boot safe — wire it in the loader snippet before the bundle loads.
|
|
363
|
+
HelpAI('onLog', ({ level, scope, message, payload }) => {
|
|
364
|
+
if (level === 'error' || level === 'warn') {
|
|
365
|
+
Sentry.addBreadcrumb({ category: `widget:${scope}`, level, message, data: payload });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
HelpAI('setDebug', 'debug'); // capture more than just warnings/errors
|
|
369
|
+
|
|
370
|
+
// Or on a handle: chat.onLog(fn); pass null to detach.
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Custom backend beacon
|
|
374
|
+
|
|
375
|
+
`navigator.sendBeacon` survives page unload — useful for logging
|
|
376
|
+
the last action before a visitor navigates away.
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
chat.on('close', () => {
|
|
380
|
+
navigator.sendBeacon(
|
|
381
|
+
'/api/telemetry',
|
|
382
|
+
JSON.stringify({
|
|
383
|
+
event: 'chat_close',
|
|
384
|
+
widgetId: chat.widgetId,
|
|
385
|
+
ts: Date.now(),
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Common patterns
|
|
394
|
+
|
|
395
|
+
### Identify the visitor
|
|
396
|
+
|
|
397
|
+
The widget mints a stable anonymous `visitorId` on first load and
|
|
398
|
+
includes it in the `handshake` event's payload. Surface it to your
|
|
399
|
+
analytics for cross-conversation attribution.
|
|
400
|
+
|
|
401
|
+
```js
|
|
402
|
+
chat.on('handshake', (res) => {
|
|
403
|
+
const visitorId = res.visitorId;
|
|
404
|
+
// Segment
|
|
405
|
+
analytics.identify(visitorId);
|
|
406
|
+
// Mixpanel
|
|
407
|
+
mixpanel.identify(visitorId);
|
|
408
|
+
// PostHog
|
|
409
|
+
posthog.identify(visitorId);
|
|
410
|
+
// GA4 user_id
|
|
411
|
+
gtag('config', 'G-XXXXXXX', { user_id: visitorId });
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Sample noisy events
|
|
416
|
+
|
|
417
|
+
`message` fires twice per turn (user + assistant) and adds up fast.
|
|
418
|
+
Drop the assistant half if you only care about user activity:
|
|
419
|
+
|
|
420
|
+
```js
|
|
421
|
+
chat.on('message', ({ role, text }) => {
|
|
422
|
+
if (role !== 'user') return;
|
|
423
|
+
track('chat_message', { length: text.length });
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Sanitize PII before sending
|
|
428
|
+
|
|
429
|
+
User-typed text may contain emails / IDs / etc. Hash or truncate
|
|
430
|
+
before forwarding to third-party trackers:
|
|
431
|
+
|
|
432
|
+
```js
|
|
433
|
+
chat.on('send', async ({ text }) => {
|
|
434
|
+
const hash = await sha256(text);
|
|
435
|
+
track('chat_send', { text_hash: hash, text_length: text.length });
|
|
436
|
+
// text itself never leaves the page.
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Conditionally enable (consent-aware)
|
|
441
|
+
|
|
442
|
+
```js
|
|
443
|
+
HelpAI(() => {
|
|
444
|
+
if (!userConsentedToAnalytics()) return;
|
|
445
|
+
const chat = HelpAI.getInstance();
|
|
446
|
+
chat.on('send', (payload) => gtag('event', 'chat_send', payload));
|
|
447
|
+
// … rest of subscriptions …
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Disable on `localhost` / preview deploys
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
const isProd = location.hostname === 'help.ai';
|
|
455
|
+
if (isProd) {
|
|
456
|
+
chat.on('send', (payload) => gtag('event', 'chat_send', payload));
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Multi-widget pages — branch on widgetId
|
|
461
|
+
|
|
462
|
+
```js
|
|
463
|
+
HelpAI('hook', '*', 'send', (chat, _evt, payload) => {
|
|
464
|
+
if (chat.widgetId === 'support') {
|
|
465
|
+
gtag('event', 'support_chat_send', payload);
|
|
466
|
+
} else if (chat.widgetId === 'sales') {
|
|
467
|
+
gtag('event', 'sales_chat_send', payload);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## TypeScript
|
|
475
|
+
|
|
476
|
+
The event names + payloads are typed — your IDE autocompletes the
|
|
477
|
+
shape of every callback argument:
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
import type { ChatWidgetInstance, EventMap } from '@helpai/elements';
|
|
481
|
+
|
|
482
|
+
function wireAnalytics(chat: ChatWidgetInstance) {
|
|
483
|
+
chat.on('send', (payload) => {
|
|
484
|
+
// ^? { text: string; attachmentCount: number }
|
|
485
|
+
gtag('event', 'chat_send', { text_length: payload.text.length });
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
chat.on('themeChange', (mode) => {
|
|
489
|
+
// ^? "auto" | "light" | "dark"
|
|
490
|
+
gtag('event', 'chat_theme', { mode });
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Iterate every event for the fan-out pattern.
|
|
495
|
+
const events: Array<keyof EventMap> = ['open', 'close', 'send' /* … */];
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Troubleshooting
|
|
501
|
+
|
|
502
|
+
### "My listener never fires"
|
|
503
|
+
|
|
504
|
+
- Are you registering **before mount completes**? Use the global
|
|
505
|
+
`HelpAI(() => { ... })` queue form, or `HelpAI("hook", …)` (both
|
|
506
|
+
are pre-boot-safe).
|
|
507
|
+
- Wrong `widgetId`? Auto-boot uses `"default"`; pass `widgetId` to
|
|
508
|
+
`init()` / `mount()` for named or multiple programmatic instances.
|
|
509
|
+
Use `"*"` to match every widget.
|
|
510
|
+
- Spelling — event names are case-sensitive (`themeChange`, not `theme_change`).
|
|
511
|
+
|
|
512
|
+
### "Listener fires twice"
|
|
513
|
+
|
|
514
|
+
Every `chat.on(name, fn)` subscribes one listener. If you wired it
|
|
515
|
+
inside a re-running effect (React `useEffect` without cleanup),
|
|
516
|
+
you're registering N copies. Always call the returned `off` in your
|
|
517
|
+
cleanup:
|
|
518
|
+
|
|
519
|
+
```jsx
|
|
520
|
+
useEffect(() => {
|
|
521
|
+
const off = chat.on("send", payload => analytics.track(...));
|
|
522
|
+
return off; // <-- critical
|
|
523
|
+
}, [chat]);
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### "I want to see what's firing"
|
|
527
|
+
|
|
528
|
+
Enable trace logging — every action fires a structured log line at
|
|
529
|
+
`info` level:
|
|
530
|
+
|
|
531
|
+
```js
|
|
532
|
+
chat.setDebug('info'); // or "trace" for SSE-frame-level detail
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Or from the page URL: `?helpai-debug=info`. Persists in `localStorage`.
|
|
536
|
+
|
|
537
|
+
### "One provider throws and breaks the others"
|
|
538
|
+
|
|
539
|
+
The event bus already isolates listener exceptions — one throw in
|
|
540
|
+
listener A doesn't prevent listener B from firing. But within a
|
|
541
|
+
single fan-out loop (Pattern B/C), wrap each call in try/catch
|
|
542
|
+
yourself — see the snippets above.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## See also
|
|
547
|
+
|
|
548
|
+
- [Events & hooks](./events-and-hooks.md) — full event reference, payload shapes, pre-boot queue semantics
|
|
549
|
+
- [API reference](./api-reference.md) — `ChatWidgetInstance` shape
|
|
550
|
+
- [Recipes](./recipes.md) — other end-to-end snippets
|
|
551
|
+
- [Security & privacy](./security-privacy.md) — what data flows where
|