Skip to content

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.

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.

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>

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',
});
OptionTypeDefaultDescription
projectKeystringPublic project key from the dashboard. Required.
locale'en' | 'ru'auto-detectWidget UI language. Falls back to navigator.language.
apiUrlstringscript origin, else https://room.patchrooms.comIngest API base URL.
userIdstringOptional user identifier, forwarded with every report.
extraRecord<string, unknown>Extra fields attached to context.extra on every submission.
captureConsoleErrorsbooleantrueHook console.error so the last 10 errors auto-attach to reports.
mascotMascotId'fox'Mascot character. See mascots.
shapeShapeId'bubble'Container shape glued to the viewport edge. See shapes.
anchorWidgetAnchor'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().
sizenumber44Trigger size in px (square shell), clamped to 32–64.
offsetXnumber0Horizontal offset from the anchor in px (positive = inward).
offsetYnumber0Vertical offset from the anchor in px (positive = inward).
widgetIdstringautoUnique id for cross-widget collision avoidance.
attributesRecord<string, unknown>Initial host attributes, copied into custom.* at init time.
onChannelChange(next, prev) => voidCalled whenever the active channel changes (including to null). Each argument is { key: string } | null.
beforeReport() => BeforeReportResult | Promise<…> | voidCalled once right before each report is sent, to attach fresh context. See Enriching reports at send time.
pushToTalkKeystringKeyboardEvent.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.
artifactArtifactMetaArtifact 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> | nullOverrides how quote and pinpoint blocks compute their DOM anchor. See Selector hook.
testIdAttributestringExtra test-id attribute name to try first when computing selectors, ahead of the built-in list. See Selector hook.

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 artifact to every report as context.artifact, and mirrors artifact.id to context.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 localStorage draft key from artifact.id (falling back to projectKey), 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>

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().

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
profile: { email: '[email protected]', name: 'Ada' },
},
});

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.

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:

FieldTypeEffect
extraRecord<string, unknown>Merged on top of init extra, into context.extra.
messagesstring[]Appended to the report as text blocks — the ergonomic way to attach a log or snapshot as readable content.
blocksBlock[]Advanced: raw blocks appended verbatim (e.g. a screenshot/audio block whose blobId you uploaded via POST /ingest/blob).
channelKeystringOverrides 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().

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.

gecko · owl · fox · cat · axolotl · raccoon · chameleon · otter · robot · blob

frame-tab · bubble · tab · pill · blob

top-left · top-center · top-right · middle-left · middle-right · bottom-left · bottom-center · bottom-right

All methods below are properties of the global Patchrooms object. Except for init() and destroy(), they throw if the widget has not been initialized.

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 }],
});
OptionTypeDescription
messagestringReport text. Becomes a single text block.
extraRecord<string, unknown>Merged into context.extra for this report.
filesFileAttachmentInput[]Files to attach as file blocks, uploaded before the report is sent. See attachFile() for the accepted input shapes.

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:

ShapeBehavior
FileUploaded as-is, using its own name.
BlobUploaded 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.

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');

Shows the widget.

Hides the widget.

Opens the report panel programmatically.

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.

Sets a single attribute on the channel engine. Attributes feed channel matching.

Patchrooms.setAttribute('custom.plan', 'pro');

Sets multiple attributes at once.

Patchrooms.setAttributes({ 'custom.plan': 'pro', 'custom.role': 'admin' });

Forces the active channel. The optional weight (number | 'max' | 'suggest') controls how strongly the override applies.

Patchrooms.setChannel('bug', { weight: 'max' });

Clears a manual channel override and lets automatic matching resume.

Returns the currently active channel (or null).

Returns the list of channels currently known to the widget.

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.