Troubleshooting
The cases where the widget, the screenshot or the MCP server behave in a way that looks like a bug and is not.
Organised by what you are seeing, not by which part of the system it comes from.
The widget does not appear at all
Check the browser console first. If the widget refused to start, it says so there under [UserWants].
- The snippet has no key.
init()without akeylogs[UserWants] init() requires a 'key'.and stops. The one-line snippet auto-starts only when the tag carriesdata-userwants-key. - The script is injected by a tag manager, or added dynamically. The widget reads
document.currentScriptto learn both its key and which server to talk to. A script appended by GTM or by your own JavaScript has nocurrentScript, so nothing auto-starts and API calls fall back to a relative path — which hits your domain and 404s. If you must inject it, load the bare tag and callwindow.UserWants.init({ key: '...' })yourself. - The script tag is duplicated. The widget refuses to load twice on one page. That is intentional, but it means the second tag is silently ignored — including a second tag with a different key.
- Your Content Security Policy blocks it. You need
script-src https://userwants.appandconnect-src https://userwants.app. If you use screenshots, the widget loads a second file from the same origin on demand, so that is covered by the samescript-src. Styles are injected as a constructable stylesheet, which is not subject tostyle-src— you do not need'unsafe-inline'for us. - Something on the page hides it. The host element is pinned with
!importanton every layout-critical property and sits atz-index: 2147483000, so this is rare, but a page that sets an even higher stacking context on an overlay will cover it.
"This widget is not configured for this site"
This is the origin check, and it is the single most common thing people hit. The domain the widget is running on is not in Widget → Allowed domains.
The confusing part: a rejected origin is answered without CORS headers on purpose. The browser therefore blocks the response and prints a generic CORS error in the console instead of the real reason. That is deliberate — it keeps someone probing keys from learning which projects exist — but it means the console is not going to tell you what is wrong. Check the allowlist.
Things that look like the same domain and are not:
http://versushttps://.acme.comversuswww.acme.com.- A non-default port.
http://localhost:3000is fine, because localhost always works, buthttps://staging.acme.com:8443needs the port in the entry. https://*.acme.commatchesapp.acme.comand does not matchacme.comora.b.acme.com. Wildcards are one level, and the bare domain is a separate entry.- Preview deployments. Each Vercel or Netlify preview gets its own hostname, so add the wildcard for the preview domain or the widget will only work on the branch you remembered to allow.
If the allowlist is empty, only localhost, 127.0.0.1 and [::1] work. That is the state a new project starts in.
"This widget is not configured correctly"
The key does not resolve to a project. Usually the snippet was copied from a project that was since deleted, or the key was truncated on paste — a full key is uw_pk_ followed by 32 hexadecimal characters.
"We're not collecting suggestions right now"
Widget → Accept submissions from the widget is off. The widget stays visible and tells visitors this rather than disappearing.
The visitor saw "Thanks!" but nothing arrived in the inbox
This is the one that looks most like data loss and is not. Three anti-spam rules answer with a success response and write nothing, deliberately — a bot that learns it was caught just adapts.
- The hidden honeypot field was filled in. Real people never see it. An autofill extension, a password manager, or an accessibility tool that fills every field on the page will trip it.
- The form was submitted less than 1.5 seconds after the panel opened. Nobody types a bug report that fast — but a test where you paste a title and hit send immediately will be dropped.
- The panel had been open for more than 24 hours. A tab left open overnight and submitted the next morning is treated as a stale replay.
If you are testing, wait a couple of seconds before sending and check that no extension is filling fields for you.
"You've sent a few already. Please try again later."
Rate limits, counted three ways:
| Bucket | Limit |
|---|---|
| Per IP, per project | 5 per 10 minutes |
| Per project | 60 per hour |
| Per IP | 30 per hour |
The one that catches teams is the first. Everyone testing from one office or one VPN shares an IP, so five reports across the whole team exhausts it for ten minutes.
Note that the limit is checked after validation, so a visitor fumbling the form does not lock themselves out — only real submissions count.
The screenshot is missing or wrong
The screenshot is uploaded as a second request, after the report is already saved. That is what stops a long stack trace from pushing a report over the size limit, and it means a failed image never costs the visitor their words. It also means the image can go missing on its own:
- The upload has a five-minute window. A report older than that will not accept an image, and a report that already has one will not accept a second.
- The image is capped at 2 MB. At the settings we use — WebP, quality 0.8, viewport only, 1× even on retina — a normal page lands around 150 KB, so this is mostly a very large page problem.
- Cross-origin images without CORS headers are skipped. They would taint the canvas and abort the whole render, so one broken image in the shot is the deliberate trade. If your product images come from a CDN without
Access-Control-Allow-Origin, expect gaps.
And on what the image actually is: it is a render of the DOM, not a browser screenshot. Iframes, <canvas> content, video frames, and some exotic CSS will not reproduce. It captures the visible viewport only — not the full scrollable page — and the widget's own panel is always excluded, so the form is never in the picture.
Screenshot links in the inbox and from get_bug are signed and expire after five minutes. Reload the page to get a fresh one; the link is not broken, it is old.
Bug reports arrive without technical details
- Widget → Collect technical details is off. Reports arrive with the text only.
- The report is a feature request, not a bug. Diagnostics are attached to bug reports only.
- The error happened before
widget.jsfinished loading. The collectors start when the script runs, so an error thrown during your app's boot, before a deferred script executes, is not in the buffer. - More than 10 errors or 50 steps happened. Both are fixed-size ring buffers — the oldest fall off, and you get the most recent.
- The diagnostics blob was too large. Anything over 4,000 characters serialised is dropped whole rather than truncated.
Stack traces are truncated at 2,000 characters, and a minified bundle gives you minified frames. If you want readable ones, ship source maps.
The reporter never got an email
Notification mail is best-effort and time-boxed to three seconds. The report is committed to the database before the email is attempted, so a lost email never means a lost report — but it does mean a slow mail provider results in no email rather than a slow response for the visitor.
Status-change emails from the editor are a separate thing: set_bug_status does not notify unless you explicitly ask it to.
The editor cannot connect to MCP
- Nothing happens when the editor connects. The endpoint answers
401with a pointer to who can issue tokens, which is what makes an editor start the OAuth flow. An editor that does not support OAuth for MCP will just sit there. - "No access to that project." One message, two causes: the slug in the URL is wrong, or you are not a member of that project. They answer identically on purpose — told apart, the endpoint would let anyone with an account enumerate every project slug in the database. Check the URL on the project's MCP tab first.
- A note saying some reports are locked. The project owner is past their free report allowance. The tools keep working on the reports the owner can read. Your own plan does not matter; if you are a member of someone else's project, they are the one who has to subscribe.
- "This token was not issued to an OAuth client." You are sending a session token from the website rather than a token your editor obtained. Let the editor run the consent flow.
set_bug_statusis not in the tool list. Your role ismember. Only owners and admins get the write tool, matching what the admin panel would let you do.- The project slug changed. The slug is in the endpoint URL, so renaming a project invalidates every editor config pointing at the old one.
Rate limits on MCP
300 calls per hour per person, 600 per hour per project. If you hit them, the endpoint answers 429 with Retry-After: 60.
Still stuck? Email userwants@pampabit.com with the project slug and roughly when it happened.