@prios/feedback-widget
v1.1.0
Published
Plug-and-play feedback widget that opens GitHub issues via the issue-tracker /intake endpoint.
Maintainers
Readme
@prios/feedback-widget
Plug-and-play feedback widget that opens GitHub issues via the issue tracker's
/intake endpoint. Framework-agnostic (Web Component + Shadow DOM); one line
to integrate.
Features
For the person reporting
- Category chips: Bug Report, Feature Request, UX Issue, Performance, Content Quality (configurable). The choice becomes the issue's label.
- Title, with AI help: "✨ Suggest" turns the description, and any title already typed, into a short, specific title such as "Save button spins forever on the checkout page". The reporter reviews it, then clicks Use this or Discard.
- Description, with AI help: "✨ Improve with AI" rewrites a rough draft into a clear report (summary, steps to reproduce, expected vs. actual). Both AI tools only use what the reporter wrote. They don't invent steps, versions or causes.
- "Already reported?": while the reporter types, the widget looks for open reports in the same project with a similar title and description, and lists up to 3 with their status. Sending anyway still helps; see grouping below.
- Screenshots: capture the current screen in one click, upload images, paste a screen grab (Ctrl/⌘+V), or drop an image onto the form. Up to 4 images, 5 MB each, PNG/JPEG/GIF/WebP. The widget hides itself while it captures the screen.
- Clear error messages when something fails (see Troubleshooting).
For the team receiving it
- Lands in the issue tracker:
- For a project with one repo, the report is published straight to GitHub.
- For a project with several repos, it waits in triage for a PM to route.
- Similar reports are grouped:
- A report that closely matches an open issue is held back from GitHub and listed under that issue in the console.
- A PM either merges it (closed, and its text and screenshots are copied onto the original as a comment) or marks it a different problem (published as its own issue).
- Context captured automatically: page URL and browser, plus anything your
app returns from
getContext()(user id, app version…). - Controlled from the console:
- A project's dashboard settings can move the widget's corner, or switch the widget off entirely, without a redeploy.
For the site embedding it
- One line to add:
- The whole form lives in a Shadow DOM, so your CSS can't break it and its CSS can't leak into your page.
- It renders in the browser's top layer, so it shows above your app's own stacking contexts.
- Spam protection: Cloudflare Turnstile CAPTCHA, a hidden honeypot field, and per-visitor rate limits.
- Privacy:
- The similar-reports hint only shows reports your own users filed (through the widget or a shared link), and only their title and status.
- It never shows report text, the reporter, repo names or GitHub links.
- Issues created inside the console or directly on GitHub never appear.
Usage
Build-based app (React / Vue / Angular / Svelte / anything):
import { mountFeedbackWidget } from "@prios/feedback-widget";
mountFeedbackWidget({ projectKey: "nolab" });Static site / no build step:
<script
src="https://cdn.jsdelivr.net/npm/@prios/feedback-widget@1"
data-project="nolab"
></script>projectKey must match a project registered in the issue tracker (see
step 2). Keys are lowercase slugs of the project's name —
nolab, issue-tracker, prios-portal — and are matched case-insensitively.
Integrating
Install the package.
npm install @prios/feedback-widget # or: yarn add / pnpm add @prios/feedback-widgetMake sure your project is registered. Check yourself — this public endpoint returns only keys and display names, never the underlying repos:
curl https://devpipeline.prios.no/issue_tracker/api/intake/projectsIf it isn't there, an admin creates it once on the console's Projects page (or with the CLI). The key is derived from the project name ("NOLAB App" →
nolab-app) and shown on that page.Mount it once, near your app's entry point:
import { mountFeedbackWidget } from "@prios/feedback-widget"; mountFeedbackWidget({ projectKey: "nolab" });Call it inside your framework's mount lifecycle, not at module scope —
mountFeedbackWidgettouches the DOM (document.body), so it needs to run after your app has mounted, and should be torn down withdestroy():- React —
useEffecton mount,w.destroy()in the cleanup function. - Vue 3 —
onMounted/onUnmounted. - Angular —
ngOnInit/ngOnDestroyin your root component. - Svelte —
onMount/onDestroy. - Static/no build — skip this step entirely; use the
<script data-project>tag instead (see above).
- React —
(Optional) Attach app context — whatever
getContext()returns rides along with every submission (e.g. logged-in user id, app version). It's called at submit time, so read current state rather than capturing it once:mountFeedbackWidget({ projectKey: "nolab", getContext: () => ({ userId: currentUser?.id, appVersion: APP_VERSION }), });Allow it on your domain.
- The widget's CAPTCHA (Cloudflare Turnstile) only works on hostnames listed on the Turnstile widget in Cloudflare — ask an admin to add yours.
- If your site sends a Content Security Policy, allow
https://challenges.cloudflare.com(script-src,frame-src) andhttps://devpipeline.prios.no(connect-src). Loading from the CDN also needshttps://cdn.jsdelivr.netinscript-src.
Verify it end-to-end. Click the trigger, submit a test message, and confirm a new issue shows up in the console dashboard (or the mapped GitHub repo).
Nothing else to configure. apiUrl and turnstileSiteKey are baked into
the published package — you only pass projectKey, plus any of the optional
props below.
Options
mountFeedbackWidget(config) returns { open(), close(), destroy() }.
| Option | Notes |
| --- | --- |
| projectKey | Required. Registered project key. |
| getContext | () => Record<string, unknown>, captured at submit. |
| theme | "dark" (default) or "light". |
| position | "bottom-right", "bottom-left", "top-right", "top-left". Normally set per project from the console dashboard; passing it here pins it. |
| categories | Category chips; the first is selected by default. |
| triggerLabel, triggerSubtitle | Text on the floating button. |
| avatars | Up to 3 image URLs stacked on the button. |
| maxDescription | Character limit for the description. |
| apiUrl, turnstileSiteKey | Baked-in defaults — override only for local testing. |
The script tag takes the same settings as attributes: data-project,
data-theme, data-position, data-trigger-subtitle, data-avatars
(comma-separated URLs), and — for local testing only — data-api and
data-turnstile-site-key.
Types: FeedbackConfig and WidgetHandle are exported from the package.
Integration examples
React — mount once in a top-level effect:
import { useEffect } from "react";
import { mountFeedbackWidget } from "@prios/feedback-widget";
useEffect(() => {
const w = mountFeedbackWidget({
projectKey: "nolab",
getContext: () => ({ userId: userRef.current?.id }),
});
return () => w.destroy();
}, []);Vue 3 — in App.vue:
import { onMounted, onUnmounted } from "vue";
import { mountFeedbackWidget } from "@prios/feedback-widget";
let widget;
onMounted(() => (widget = mountFeedbackWidget({ projectKey: "nolab" })));
onUnmounted(() => widget?.destroy());Angular — in your root component:
import { Component, OnDestroy, OnInit } from "@angular/core";
import { mountFeedbackWidget, type WidgetHandle } from "@prios/feedback-widget";
@Component({ selector: "app-root", templateUrl: "./app.component.html" })
export class AppComponent implements OnInit, OnDestroy {
private widget?: WidgetHandle;
ngOnInit() {
this.widget = mountFeedbackWidget({ projectKey: "nolab" });
}
ngOnDestroy() {
this.widget?.destroy();
}
}Svelte — in App.svelte:
import { onMount, onDestroy } from "svelte";
import { mountFeedbackWidget } from "@prios/feedback-widget";
let widget;
onMount(() => (widget = mountFeedbackWidget({ projectKey: "nolab" })));
onDestroy(() => widget?.destroy());Static / any site:
<script
src="https://cdn.jsdelivr.net/npm/@prios/feedback-widget@1"
data-project="prios-portal"
></script>Troubleshooting
The widget shows the reason when something fails. The common ones:
| Message | Cause |
| --- | --- |
| "This feedback form isn't set up correctly (unknown project key)" | projectKey isn't registered — see step 2. |
| "Couldn't verify you're human" | Your domain isn't on the Turnstile widget's allowed hostnames, or a CSP blocks challenges.cloudflare.com — see step 6. |
| "Couldn't reach the feedback server" | Offline, or a CSP / ad-blocker blocks the API URL. |
| "Too many submissions / AI requests" | Per visitor, per minute: 5 submissions, 5 description rewrites, 10 title suggestions. Wait a minute. |
| "Screenshots must be PNG, JPEG, GIF or WebP images" | Other formats (e.g. HEIC, SVG) are rejected. |
| "The AI assist is unavailable right now" | The server has no AI key configured, or the AI provider is down. The form still works without it. |
| "✨ Suggest" fails with "Request failed (404)" | Widget 1.1+ is talking to an older API. Deploy the API first. |
The "Already reported?" hint never shows an error. If the lookup fails, the panel just stays hidden.
Building & publishing
npm install
npm test
FEEDBACK_API_URL=https://devpipeline.prios.no/issue_tracker/api \
FEEDBACK_TURNSTILE_KEY=0x4AAAAAAEey0XGnTTYKFGWl \
npm run buildOutputs dist/feedback-widget.js (ES), .cjs, .iife.js, index.d.ts, and
dist/cli/index.js. apiUrl and turnstileSiteKey are baked into the bundle
at build time; without the env vars the build falls back to local-dev values
(http://localhost:3001, no CAPTCHA) — fine for testing, never for release.
Publishing
Through CI: bump version in package.json, then push a
<<<<<<< HEAD
widget-v<version> tag (e.g. widget-v1.1.0);
widget-v<version> tag (e.g. widget-v1.0.7);
c58f08797a928d9d83bc3d4656f09c405c24e67b
.github/workflows/publish-widget.ymlpublishes with the production values.
By hand, from feedback-widget/ — pick patch, minor or major to
match the change; npm version bumps the version and creates a git tag, and
npm publish runs the publish checks and builds the package automatically:
npm version patch
FEEDBACK_API_URL=https://devpipeline.prios.no/issue_tracker/api \
FEEDBACK_TURNSTILE_KEY=0x4AAAAAAEey0XGnTTYKFGWl \
npm publish
git push --follow-tagsSet FEEDBACK_API_URL to the API origin without /intake. The build embeds this
URL and the Turnstile site key in the published bundle; they are public client
configuration, not secrets. npm publish refuses to run unless
FEEDBACK_API_URL is a non-local https URL and FEEDBACK_TURNSTILE_KEY is a
real (non-test) site key — that's what stops a local build shipping.
CLI
npx @prios/feedback-widget init <projectKey>
# prints the integration snippet
FEEDBACK_API_URL=https://devpipeline.prios.no/issue_tracker/api \
ADMIN_TOKEN=<an admin's login token> \
npx @prios/feedback-widget register "<Display name>" <repo> [repo…]
# registers a project and prints its keyVersioning
- Semver. Apps depend on
"^1"; CDN users load@1. - A breaking change (e.g. a renamed config prop) ships as v2, opted into deliberately.
- Because the API URL and Turnstile key are baked in at build time, changing either means rebuild + republish — apps pick up the new values on their next update with no code change.
- jsDelivr caches what
@1resolves to for up to ~12 hours. To pick up a release immediately, pin the exact version (@1.1.0) or purgehttps://purge.jsdelivr.net/npm/@prios/feedback-widget@1.
