Step 1: Call debug()
When the widget isn’t behaving the way you expect, the first thing to do is grab a snapshot of its current state. Open your browser console on the page where the widget should be and run:
| What you see | What it tells you |
|---|---|
sdk 0.x.y | Which SDK build the page actually loaded. Check this against the latest release when chasing version-specific bugs. |
org=... user=... | Whether init() and identify() got the values you expected. user=anonymous means identify() either wasn’t called or was called with "anonymous". |
identity: token=set getUserToken=yes blocked=false hash=(unset) apiKey=set | Which auth credentials the SDK has. blocked=true means user-scoped requests are paused after an identity failure. getUserToken=yes means an async refresh callback is configured. Important for orgs with identity verification enabled. |
agent: Sales Agent (agt_xyz) or agent: not loaded yet | Whether the agent config has been fetched. If “not loaded yet” persists past the initial page load, check the network tab for failures on GET /api/sdk/config. |
session: open=true/false returning=true/false | Whether the chat panel is currently expanded, and whether this is a continuation of a prior conversation. |
ui flags: ... | The resolved UI suppression flags after ui config and manualOnPaths evaluation. If you set ui: 'manual' and see flags as true here, your config didn’t take effect (typo, or init() was called twice). |
dom: host=... trigger=... panel=... | What’s actually present in the DOM right now. Mismatches with ui flags mean a render path isn’t respecting the flags (please report). |
liveAgent: active=true name=Sarah | Whether a human teammate is currently handling this conversation. |
scheduledProactiveTimers=N | How many proactive teasers are queued to fire. |
userToken, apiKey) are reduced to presence flags, never echoed, so the snapshot is safe to share.
Common issues
”The widget never appears”
Runwindow.HaloAgents.getInstance()?.debug(). If it returns undefined, the SDK isn’t initialized:
- Check that the
<script>tag forcdn.haloagents.ai/sdk/latest/haloagents.umd.jsis actually loading. Open the network tab, refresh, and look for it. If the request fails with a CSP violation, see Content Security Policy. - Check that
window.HaloAgents.init({ orgId: "..." })is being called. Browser console:typeof window.HaloAgentsshould be"object". - Confirm
init()is called only once. A second call withoutdestroy()is a no-op and logs a warning.
debug() runs but dom.host=false, the host element was unmounted. Check whether something else on the page is calling destroy().
”init() ran but nothing works / getInstance() is undefined”
If you load the SDK dynamically (async, next/script, or document.createElement("script")), onload can fire before window.HaloAgents is defined. Poll for the global before calling init():
”The widget appears on marketing pages but not on /app/*”
Check the agent’s Page URL Rules in the dashboard (AI Agents → [Agent] → Channels → Chat Widget → Configure). If a rule excludes the current path, the widget unmounts entirely.debug() would return undefined in this case.
If debug() returns a snapshot but dom.trigger=false, you’ve likely set ui: { trigger: false } or ui: 'manual'. Run debug() and look at ui flags to confirm.
”Identify isn’t working / users show as Anonymous”
Rundebug() and check the user= line. Three failure modes:
| Symptom | Cause |
|---|---|
user=anonymous | identify() was never called with a real user ID. |
user=user_42 but inbox shows “Anonymous” | identify() was called with the ID but no name or email traits. Pass at least both. |
identity: token=(unset) for an org with identity verification on | The host page never minted a JWT, or didn’t pass it via userToken to init() / identify(). See Identity Verification. |
”My in-app message didn’t show (hidden widget / Contact Support button)”
In-app messages render independently of the floating launcher. They do not require the chat panel to be open. Common causes:init()was deferred until the user clicked Contact Support. In-app messages only fetch afterinit()runs. Callinit()on page load and useopenChat()on your button click instead.- The embedder opted out. Run
HaloAgents.getInstance()?.debug()and checkui flags.inAppMessages. If it’sfalse, the host setui: { inAppMessages: false }(or pinned an SDK version before 0.9.0 withui: 'manual'). - Audience / identify. Segment- or field-targeted messages require
identify()with a matching user. Checkuser=in thedebug()output. - Previously dismissed. In-app dismissals persist in
localStorageunderhaloagents_dismissed_{orgId}.
”I want zero overlays in manual mode (SDK ≥ 0.9.0)”
Since 0.9.0,ui: 'manual' and manualOnPaths allow dashboard banners and in-app messages by default. To restore the old behavior, pass a ui object with explicit opt-outs:
debug() and confirm banners=false and inAppMessages=false.
”I set ui: 'manual' but a banner still showed up (SDK < 0.9.0)”
In SDK 0.5.1 through 0.8.x, ui: 'manual' suppressed banners and in-app messages. If you’re on ≥ 0.9.0, that is expected: manual mode now allows dashboard outreach. Upgrade intentionally or add the opt-out flags above if you want zero overlays.
”Console says ‘HaloAgents is already initialized’”
HaloAgents.init() was called twice without an intervening destroy(). The second call is a no-op and returns the existing instance, so the widget continues working with the first config (not the second).
In SPAs this usually means init() is being called inside an effect that re-runs. Either:
- Move
init()to a singleton guard outside React/Vue lifecycles, or - Call
HaloAgents.getInstance()?.destroy()before the secondinit()if you genuinely want to reinitialize with new config.
”Chat works on localhost but is blocked in production”
Almost always CSP. Open the production page, open DevTools console, look for messages mentioning “Content Security Policy” or “Refused to connect”. The fix is documented at Content Security Policy.”Chat works in production but is blocked on localhost”
Inverse of the CSP case, and the cause is almost always Allowed Domains (origin lockdown). When that feature is on, every SDK request from a domain not in the list is rejected with a 403, andlocalhost will never match a production hostname.
Open DevTools → Network. If the failing request is GET /api/sdk/config or POST /api/sdk/events with status 403 and a body like:
- Add a loopback entry to your allow-list. Go to Setup → Install → Allowed Domains in the dashboard and add
127.0.0.1orlocalhost. Then run your dev server on the host you added (http://127.0.0.1:3000orhttp://localhost:3000). - Disable Allowed Domains while developing. Toggle it off in the dashboard. The widget will accept any origin until you re-enable it. Identity Verification (the per-user JWT defense) keeps working independently.
”Voice doesn’t work / microphone errors”
Microphone access requiresPermissions-Policy: microphone=(self) on the page (or the parent if the SDK is iframed). See Microphone permissions.
”I see ‘config issue’ warnings on init”
SDK 0.6.0+ validates the config object you pass toinit() and surfaces three classes of issue as a single grouped console.warn. The widget keeps working in every case; the warning just makes mistakes visible that previously failed silently.
Typo. A common typo gets a “did you mean?” suggestion when there’s a near match:
manualOnPaths: ["/app/*"]).
Reporting a bug
When something genuinely seems wrong, the most useful bug report includes:- Output of
HaloAgents.getInstance().debug()(full snapshot, structured object). - The pathname where the issue reproduces.
- Browser console errors (red), if any.
- What you expected vs. what happened.