Docs
Docs

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 a key logs [UserWants] init() requires a 'key'. and stops. The one-line snippet auto-starts only when the tag carries data-userwants-key.
  • The script is injected by a tag manager, or added dynamically. The widget reads document.currentScript to learn both its key and which server to talk to. A script appended by GTM or by your own JavaScript has no currentScript, 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 call window.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.app and connect-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 same script-src. Styles are injected as a constructable stylesheet, which is not subject to style-src — you do not need 'unsafe-inline' for us.
  • Something on the page hides it. The host element is pinned with !important on every layout-critical property and sits at z-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:// versus https://.
  • acme.com versus www.acme.com.
  • A non-default port. http://localhost:3000 is fine, because localhost always works, but https://staging.acme.com:8443 needs the port in the entry.
  • https://*.acme.com matches app.acme.com and does not match acme.com or a.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.

  1. 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.
  2. 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.
  3. 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.js finished 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 401 with 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_status is not in the tool list. Your role is member. 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.