JavaScript widget
The Patchrooms widget is a small browser script that docks a feedback room to the
edge of your app. It ships as a global Patchrooms object once loaded.
There are two ways to load it.
Loading the widget
Section titled “Loading the widget”Per-project loader (recommended)
Section titled “Per-project loader (recommended)”Drop one script tag into your page. The URL carries your project key, and the server returns a tiny loader with your widget configuration and channels baked in — the room renders themed immediately and initializes itself.
<script src="https://room.patchrooms.com/v1/patchrooms/pr_xxx.js"></script>Replace pr_xxx with your public project key from the dashboard. No
Patchrooms.init() call is needed — the loader auto-initializes.
Programmatic loader
Section titled “Programmatic loader”Load the shared bundle and call init() yourself. Use this when you need to
control timing (for example, a single-page app that re-parents the widget
between routes).
<script src="https://room.patchrooms.com/v1/patchrooms.js"></script><script>Patchrooms.init({ projectKey: 'pr_xxx' });</script>init(options)
Section titled “init(options)”Initializes the widget and mounts it. init() is idempotent — calling it again
tears the widget down and rebuilds it with the new options. projectKey is the
only required field.
Patchrooms.init({ projectKey: 'pr_xxx', locale: 'en', anchor: 'bottom-right',});InitOptions
Section titled “InitOptions”| Option | Type | Default | Description |
|---|---|---|---|
projectKey | string | — | Public project key from the dashboard. Required. |
locale | 'en' | 'ru' | auto-detect | Widget UI language. Falls back to navigator.language. |
apiUrl | string | script origin, else https://room.patchrooms.com | Ingest API base URL. |
userId | string | — | Optional user identifier, forwarded with every report. |
extra | Record<string, unknown> | — | Extra fields attached to context.extra on every submission. |
captureConsoleErrors | boolean | true | Hook console.error so the last 10 errors auto-attach to reports. |
mascot | MascotId | 'fox' | Mascot character. See mascots. |
shape | ShapeId | 'bubble' | Container shape glued to the viewport edge. See shapes. |
anchor | WidgetAnchor | 'middle-right' | Anchor corner/edge of the viewport. See anchors. |
triggerVariant | 'frame-tab' | 'devwidget' | 'none' | 'frame-tab' | Trigger launcher style. 'none' is headless — no launcher; open the panel yourself with openForm(). |
size | number | 44 | Trigger size in px (square shell), clamped to 32–64. |
offsetX | number | 0 | Horizontal offset from the anchor in px (positive = inward). |
offsetY | number | 0 | Vertical offset from the anchor in px (positive = inward). |
widgetId | string | auto | Unique id for cross-widget collision avoidance. |
attributes | Record<string, unknown> | — | Initial host attributes, copied into custom.* at init time. |
onChannelChange | (next, prev) => void | — | Called whenever the active channel changes (including to null). Each argument is { key: string } | null. |
beforeReport | () => BeforeReportResult | Promise<…> | void | — | Called once right before each report is sent, to attach fresh context. See Enriching reports at send time. |
pushToTalkKey | string | — | KeyboardEvent.key for push-to-talk audio capture while the panel is open. Opt-in. |
mode | 'default' | 'artifact-review' | 'default' | Operating mode. See artifact-review mode. |
artifact | ArtifactMeta | — | Artifact under review — see artifact-review mode. |
reporter | { token: string; profile: {...} } | — | Pre-authenticated reporter identity, minted by your backend. Skips the reporter gate. |
selector | (node: Element) => Partial<SelectorInfo> | null | — | Overrides how quote and pinpoint blocks compute their DOM anchor. See Selector hook. |
testIdAttribute | string | — | Extra test-id attribute name to try first when computing selectors, ahead of the built-in list. See Selector hook. |
artifact-review mode
Section titled “artifact-review mode”Point the widget at one artifact — a build, a branch, a generated page — and its
feedback collects in that artifact’s Room. Set mode: 'artifact-review' and pass an artifact:
Patchrooms.init({ projectKey: 'pr_xxx', mode: 'artifact-review', artifact: { id: 'preview-pr-482', title: 'Checkout redesign', tool: 'lovable', goal: 'Rework the checkout step', constraints: ['Keep the existing Stripe flow', 'Mobile-first'], url: 'https://preview-482.example.dev', },});In this mode the widget:
- attaches
artifactto every report ascontext.artifact, and mirrorsartifact.idtocontext.artifactId— the key that groups reports into a Room; - enables pinpoint click-to-comment, so reporters can point at a specific element on the artifact;
- derives its
localStoragedraft key fromartifact.id(falling back toprojectKey), so drafts don’t bleed between artifacts.
ArtifactMeta has one required field, id (max 200 chars); the rest are optional:
title, tool, source, goal, constraints (string[]), url (http/https),
meta (Record<string, string>). See Rooms & artifacts for the
concept.
To scope each build or branch to its own Room, feed a stable-per-artifact id — a branch name, preview-deploy id, or content hash — not a fresh value on every load:
Patchrooms.init({ projectKey: 'pr_xxx', mode: 'artifact-review', artifact: { id: `branch-${process.env.GIT_BRANCH}` },});With the script-tag loader, set data-mode="artifact-review" and data-artifact-id
instead:
<script src="https://room.patchrooms.com/v1/patchrooms/pr_xxx.js" data-mode="artifact-review" data-artifact-id="preview-pr-482"></script>Headless / launcher-less
Section titled “Headless / launcher-less”Set triggerVariant: 'none' to render no launcher at all. Nothing docks to
the viewport edge; the report panel is opened entirely from your own UI by calling
openForm(). Use this to put “Leave feedback” behind an existing
button — e.g. a dev-tools menu — with no second floating widget.
Patchrooms.init({ projectKey: 'pr_xxx', mode: 'artifact-review', artifact: { id: 'preview-pr-482' }, triggerVariant: 'none',});
// Your own button opens the form on demand:document.querySelector('#leave-feedback') .addEventListener('click', () => Patchrooms.openForm());When the panel is closed the widget shows nothing and captures no clicks. Headless
mode is driven from init() — you supply the button that calls openForm().
Pre-authenticated reporter identity
Section titled “Pre-authenticated reporter identity”If your project has email or oauth reporter-auth modes enabled, the widget
normally shows a gate asking the reporter to identify themselves. If your host
app already has a signed-in user, pass reporter to skip the gate and attach
that identity directly:
Patchrooms.init({ projectKey: 'pr_xxx', reporter: { token: '…', // minted by GET /auth/reporter-token },});reporter overwrites whatever choice is cached in localStorage for this
project, so re-pass it on every init() call to keep it in sync. See
Reporter auth
for how to mint the token.
Enriching reports at send time
Section titled “Enriching reports at send time”init extra is frozen at init time, and setAttributes() feeds channel
matching — not the report body. To attach fresh, time-sensitive context to
each report (the user’s last actions, recent errors, an app-state snapshot,
analytics identity), pass a beforeReport hook. It runs once, right before the
report is sent, and its return value enriches that report:
Patchrooms.init({ projectKey: 'pr_xxx', beforeReport: () => ({ // merged into context.extra extra: { ...getAmplitudeContext(), route: location.pathname }, // appended to the report as text blocks (visible in the dashboard) messages: [formatRecentActions(), formatRecentErrors()], }),});The hook may be sync or async and can return any of:
| Field | Type | Effect |
|---|---|---|
extra | Record<string, unknown> | Merged on top of init extra, into context.extra. |
messages | string[] | Appended to the report as text blocks — the ergonomic way to attach a log or snapshot as readable content. |
blocks | Block[] | Advanced: raw blocks appended verbatim (e.g. a screenshot/audio block whose blobId you uploaded via POST /ingest/blob). |
channelKey | string | Overrides the channel this report is filed into. |
The snapshot is frozen into the payload, so queued retries never re-invoke the
hook. It is fail-open: if the hook throws or returns nothing, the report
sends unchanged — a host bug never blocks the user’s feedback. beforeReport
runs for widget submissions and for programmatic Patchrooms.report().
Selector hook
Section titled “Selector hook”Quote (selection) and pinpoint blocks carry a target alongside their plain
selector string — a richer, agent-friendly anchor for the DOM element the
reporter quoted or clicked:
interface SelectorInfo { css: string; // e.g. '[data-testid="submit"]' or '#hero-title' xpath: string; // positional path from <body>, e.g. '/body[1]/div[1]/p[2]' testIds: string[]; // test attributes from the node and up to 10 ancestors, closest first reactComponent?: string; // best-effort React component name}The built-in css generator prioritizes, in order: a test-id attribute
(data-testid, data-test-id, data-test, data-cy, plus your
testIdAttribute if set), a non-hash-like id, then a class + :nth-of-type
chain — stopping as soon as the assembled selector is unique in the document.
Hash-like ids/classes (CSS-modules, styled-components-style suffixes) are
skipped, since they’re regenerated on every build and make poor anchors.
To override the computation entirely — for example, to prefer your own
component-id attribute, or to skip reactComponent extraction — pass
selector:
Patchrooms.init({ projectKey: 'pr_xxx', selector: (node) => { const id = node.closest('[data-component-id]')?.getAttribute('data-component-id'); return id ? { css: `[data-component-id="${id}"]` } : null; // null → built-in fallback },});selector fully replaces the built-in result for that element — it isn’t
merged field-by-field, so a partial return only sends the fields you set.
Return null (or throw) to fall back to the built-in computation instead.
Mascot IDs
Section titled “Mascot IDs”gecko · owl · fox · cat · axolotl · raccoon · chameleon · otter · robot · blob
Shape IDs
Section titled “Shape IDs”frame-tab · bubble · tab · pill · blob
Anchors
Section titled “Anchors”top-left · top-center · top-right · middle-left · middle-right · bottom-left · bottom-center · bottom-right
Methods
Section titled “Methods”All methods below are properties of the global Patchrooms object. Except for
init() and destroy(), they throw if the widget has not been initialized.
report(options?)
Section titled “report(options?)”Files a one-shot report programmatically. The message becomes a single text
block in the active channel. Returns a Promise that rejects if the submission
fails.
await Patchrooms.report({ message: 'Checkout button is misaligned on mobile', extra: { route: '/checkout' }, files: [{ name: 'console.log', content: recentConsoleDump }],});| Option | Type | Description |
|---|---|---|
message | string | Report text. Becomes a single text block. |
extra | Record<string, unknown> | Merged into context.extra for this report. |
files | FileAttachmentInput[] | Files to attach as file blocks, uploaded before the report is sent. See attachFile() for the accepted input shapes. |
attachFile(input)
Section titled “attachFile(input)”Adds a file block to the current thread programmatically — the same block the
composer’s file-attach button adds, without opening the panel. Uploads
immediately; the block shows up next time the thread renders (already, if the
panel is open). Returns a Promise that rejects if the upload fails.
// A real File, e.g. from an <input type="file"> or drag-and-drop:await Patchrooms.attachFile(fileFromInput);
// Or build one from text on the fly (a captured log, a JSON dump):await Patchrooms.attachFile({ name: 'console.log', content: recentConsoleDump, description: 'Console output leading up to the crash',});input accepts:
| Shape | Behavior |
|---|---|
File | Uploaded as-is, using its own name. |
Blob | Uploaded as-is; name falls back to 'file' (no filename on a bare Blob). |
{ name, content, mime?, description? } | content is a string (wrapped into a text/plain Blob, or mime if given) or a Blob (used as-is, unless mime overrides its type). |
Same limits as the composer’s file attach: any file type is accepted, size is the only gate — 5 MB per file on Free, 100 MB on paid plans.
alpha(featureKey)
Section titled “alpha(featureKey)”Checks whether an alpha feature flag is enabled for the current user and, if so,
marks it on the widget. Returns a Promise<boolean>.
const enabled = await Patchrooms.alpha('new-dashboard');show()
Section titled “show()”Shows the widget.
hide()
Section titled “hide()”Hides the widget.
openForm()
Section titled “openForm()”Opens the report panel programmatically.
destroy()
Section titled “destroy()”Tears down the widget and clears SDK state. Safe to call when not initialized —
it becomes a no-op. After destroy() the SDK can be re-initialized with a
different projectKey or apiUrl.
setAttribute(key, value)
Section titled “setAttribute(key, value)”Sets a single attribute on the channel engine. Attributes feed channel matching.
Patchrooms.setAttribute('custom.plan', 'pro');setAttributes(obj)
Section titled “setAttributes(obj)”Sets multiple attributes at once.
Patchrooms.setAttributes({ 'custom.plan': 'pro', 'custom.role': 'admin' });setChannel(key, options?)
Section titled “setChannel(key, options?)”Forces the active channel. The optional weight (number | 'max' | 'suggest')
controls how strongly the override applies.
Patchrooms.setChannel('bug', { weight: 'max' });clearChannelOverride()
Section titled “clearChannelOverride()”Clears a manual channel override and lets automatic matching resume.
getActiveChannel()
Section titled “getActiveChannel()”Returns the currently active channel (or null).
getChannels()
Section titled “getChannels()”Returns the list of channels currently known to the widget.
Report history
Section titled “Report history”The panel footer shows a History button (clock icon). It opens a rolling
log of the last 20 reports this browser has successfully sent for the project,
plus anything still queued for retry. It’s read entirely from localStorage —
no request to the server — so it works without the reporter being logged in at
all, scoped per project key.