@zoomrx/zena-chatbot-client
v1.0.4
Published
Zena AI chatbot web component for ZoomRx applications
Readme
Zena Chatbot
@zoomrx/zena-chatbot-client is a standalone web component (<zena-chatbot>) built with Svelte 5 and bundled via Vite into a single IIFE file. It is embedded inside host applications as a script tag — no framework dependency required on the host side.
Quick start
npm install
npm run dev # dev server at http://localhost:5173Requires Node.js 24 or newer. Use
nvm use --ltsif you manage multiple runtimes.
Installation
npm install @zoomrx/zena-chatbot-clientTech stack
- Framework: Svelte 5 (runes:
$state,$derived,$effect,$props) - Build tool: Vite 8 (outputs ES module + IIFE)
- Styles: Tailwind CSS 4
- HTTP: ky with a memoized instance factory
- Testing: Vitest + @testing-library/svelte + MSW
- Error tracking: Sentry (
@sentry/browser) - Analytics: PostHog
- E2E: Playwright
Scripts
| Command | Purpose |
|---|---|
| npm run dev | Dev server at http://localhost:5173 with MSW + PostHog stub |
| npm run build | Production IIFE + ES module in dist/ |
| npm run preview | Serve the production build locally |
| npm test -- --run | Run all unit tests once |
| npm run test:coverage | Coverage report |
| node e2e-test.cjs | Playwright E2E test against localhost:5173 |
Web Component API
Usage
The recommended approach is to pass all configuration via get-config — one event, one place.
<script src="path/to/zena-chatbot-client.iife.js"></script>
<zena-chatbot></zena-chatbot>
<script>
const chatbot = document.querySelector('zena-chatbot');
// All config — URLs, user ID, tokens, analytics keys — resolved in one place.
chatbot.on('get-config', ({ resolve }) => {
resolve({
authToken: 'Bearer eyJ...', // required
zenaServerUrl: 'https://your-zena-api.com',
panelistServerUrl: 'https://your-panelist-server.com',
userId: '12345',
availableSurveyIds: '[1, 2, 3]',
nativeApp: false,
fabStyle: { bottom: '20px', right: '20px' },
fabShape: 'circle',
autoSuggestions: ["I'm stuck in the survey"],
posthogKey: 'phc_xxx',
posthogHost: 'https://app.posthog.com',
sentryDsn: 'https://[email protected]/123',
sentryRelease: '[email protected]', // optional
});
});
</script>HTML attributes are also supported as a fallback (e.g. for static HTML pages), but get-config values take precedence when both are provided.
HTML Attributes (fallback)
All of these can alternatively be passed via get-config (see Events below). When both are provided, get-config wins.
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| zena-server-url | string | Yes* | — | Base URL for the Zena API |
| user-id | string | No | '' | User identifier passed to analytics |
| available-survey-ids | string (JSON array) | No | '[]' | Array of IDs (or objects with survey_id) currently on the user's dashboard |
| panelist-server-url | string | No | '' | Base URL for the panelist server (survey fetching) |
| native-app | boolean (presence) | No | false | Set to enable native app mode |
| fab-style | string (JSON object) | No | {} | CSS properties for FAB position/size (e.g. {"bottom":"20px","right":"20px"}) |
| fab-shape | string | No | 'circle' | 'circle' or 'rectangle' |
| auto-suggestions | string (JSON array) | No | [] | Priority suggestions prepended to defaults |
*Not required if zenaServerUrl is passed via get-config.
Methods
| Method | Returns | Description |
|---|---|---|
| close() | void | Programmatically close the chat panel |
| generateConversationSummary() | string | Returns last user + bot message as plain text (useful for support ticket context) |
| setFabStyle(style) | void | Update FAB position/style without remounting — preserves chat state |
| on(event, handler) | void | Subscribe to a component event |
| off(event, handler) | void | Unsubscribe from a component event |
Events
| Event | Payload | Description |
|---|---|---|
| get-config | { resolve } | Fired once on init. Call resolve(config) to provide sensitive credentials. See below. |
| before-send | { message, resolve } | Fired before each message is sent. Call resolve(data) with any extra data to merge into the request. |
| get-available-surveys | { resolve } | Fired once when the chat is first opened. Call resolve(ids) with the user's available survey IDs so the widget can skip its own fetch. See below. |
| sentry-additionaldata | { event, resolve } | Fired before an error is sent to Sentry. Call resolve(context) with { tags, extra } to enrich it, or resolve(null) to send it unchanged. See below. |
| stuck-survey-selected | { survey } | Fire-and-forget. Fired when the user selects a survey from a stuck-survey dropdown, before the support ticket is created. |
get-config
Fired once when the component connects. The host must call resolve with a
ZenaChatbotConfig object. If the host does not handle the event within 2 seconds
the component mounts with an empty auth token (API calls will fail).
chatbot.on('get-config', ({ resolve }) => {
resolve({
// Credentials (required / recommended)
authToken: 'Bearer eyJ...', // required
sentryDsn: 'https://...', // optional — omit to disable Sentry
sentryRelease: '1.0.0', // optional — defaults to package version
posthogKey: 'phc_xxx', // optional — omit to disable analytics
posthogHost: 'https://app.posthog.com', // optional
// Component config (can also be set via HTML attributes as fallback)
zenaServerUrl: 'https://your-zena-api.com', // required
panelistServerUrl: 'https://your-panelist.com', // optional
userId: '12345', // optional
availableSurveyIds: '[1, 2, 3]', // optional
nativeApp: false, // optional
fabStyle: { bottom: '20px', right: '20px' }, // optional
fabShape: 'circle', // optional — 'circle' | 'rectangle'
autoSuggestions: ["I'm stuck in the survey"], // optional
defaultOpen: false, // optional — open panel on mount
initialMessage: { // optional — hidden trigger sent to server;
type: 'assistant', // only the assistant response is shown.
text: 'Hello! How can I help?', // Skipped on page reload automatically.
},
});
});before-send
chatbot.on('before-send', ({ message, resolve }) => {
resolve({ userId: 42, sessionToken: 'abc' });
});get-available-surveys
Fired once, when the chat is first opened (not on mount), so the widget can learn which surveys the user currently has available — needed to answer "is this survey still available?" prompts. Asking lazily at open time means the host's survey list has finished loading, avoiding the staleness of a value frozen at connect.
Call resolve with an array of survey IDs (numbers or strings). Resolve null
(or an empty array) if the host has no list to share — the widget then falls
back to fetching users/get_surveys itself. If the host does not answer within
~1.5 seconds, the widget assumes no list and fetches.
chatbot.on('get-available-surveys', ({ resolve }) => {
// Host already has the user's surveys loaded — hand over the IDs.
resolve(surveys.map(s => s.survey?.id).filter(Boolean));
// ...or, when the host has no such list (e.g. inside a survey):
// resolve(null);
});When the host supplies IDs this way (or via the available-survey-ids
attribute / availableSurveyIds config), the widget makes no
users/get_surveys call.
sentry-additionaldata
Fired before each error is sent to Sentry, letting the host attach its own
context. The component's @sentry/browser captures every error on the page —
including the host's own unhandled rejections — so a host typically filters by
origin and only enriches its own errors.
The payload is { event, resolve }, where event is the Sentry ErrorEvent
about to be sent. Call resolve({ tags, extra }) to merge in context, or
resolve(null) to send the event unchanged. If the host does not answer within
the timeout, the event is sent with no extra data.
chatbot.on('sentry-additionaldata', ({ event, resolve }) => {
// Inspect event (e.g. by stack-frame origin) to decide whether it's yours
const isMine = event?.exception?.values?.[0]?.stacktrace?.frames
?.some(f => (f.filename || '').includes('/my-app/'));
if (!isMine) return resolve(null);
resolve({
tags: { module: 'survey' },
extra: { responseId: 123 },
});
});stuck-survey-selected
Fired when the user picks a survey from a stuck_survey_dropdown response. Fires before the support-ticket API call — the host receives it even if ticket creation subsequently fails.
chatbot.on('stuck-survey-selected', ({ survey }) => {
// survey — the selected survey object { name, survey_id, wave_id, users_wave_id, ... }
console.log('User is stuck in survey:', survey.name);
});Sentry Error Tracking
The package ships with Sentry support but does not auto-initialize it. Provide
sentryDsn via the get-config event — the component initialises Sentry after
credentials are resolved:
chatbot.on('get-config', ({ resolve }) => {
resolve({
authToken: '...',
sentryDsn: 'https://[email protected]/XXXXXX',
sentryRelease: '[email protected]', // optional
});
});sentryReleaseis optional. When omitted it defaults tozena-chatbot-client@<package version>and must match the release name used when uploading sourcemaps in CI (sentry-cli sourcemaps upload).- Omit
sentryDsnfrom the resolved config in local / UAT environments and Sentry will never initialise. tracesSampleRateis always0— errors only, no performance tracing overhead.
Capturing errors manually
captureError is the only Sentry helper exported from the package:
import { captureError } from '@zoomrx/zena-chatbot-client';
try {
// ...
} catch (err) {
captureError(err);
}For contribution guidelines, staging workflow, and publishing instructions see CONTRIBUTING.md.
