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:
The same snapshot is also returned as a serializable object you can pipe to support tooling:
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:
”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.