Documentation
Feedback, from snippet to report
One script tag on your site. Anyone looking at a page can point at what is wrong, say why, and send you the picture. This page covers every setting and every function the widget has.
Install the widget
- 1
Create a project
One per site or application. An email address is enough, no card.
- 2
Paste the snippet
Copy it from the project's Install panel and add it to the layout every page shares.
- 3
Send a test report
Open the launcher, point at something, send. The report appears in your project.
The snippet in the app already carries your project's key, and the settings you chose there. Paste it once, just before the closing body tag. Written by hand, it looks like this:
<script
src="https://cdn.feedback.o27.io/widget.js"
data-project-key="YOUR_PROJECT_KEY"
defer
></script>The tag configures itself from its own data-* attributes and loads the latest widget, so updates reach your site without touching the snippet. In a single-page application, load it once in the HTML shell: it stays mounted while your router changes pages.
Without a project key the widget does not appear, and the browser console says why.
If your site sends a Content Security Policy, allow the address in the snippet's src in script-src, and https://feedback.o27.io in connect-src: that is where reports are sent.
How a report is made
What your visitors see, so you can tell them about it.
- Open. The launcher expands into a panel the reporter can drag anywhere. It doubles as the legend in the image.
- Point. Choose Point, then click an element to select it, or drag to draw an area. Escape cancels.
- Comment. Each selection gets a numbered outline on the page and a comment field in the panel. Note adds a message about the whole page, without selecting anything.
- Frame. The image shows the visible area when every selection fits in it, otherwise the band of the page that holds them all. One click switches to the full page.
- Copy or send. Copy puts the image on the clipboard; Send saves it to your project with the comments. Preview is optional and shows the exact image first.
- Minimize or close. Minimize keeps the report and hides the outlines; Close discards it. Clear removes every selection and offers Undo for a few seconds.
Selections belong to the current visit and are not stored. Once a report is sent, Send stays off until it changes, so your team never receives the same one twice.
Options
Set options as attributes on the script tag, or pass them to init() under the name shown beneath each attribute. The project's Install panel writes the common ones for you.
- data-project-keyinit() option
projectKey· Default none - Your project's embed key. Reports are sent to that project.
- data-positioninit() option
position· Defaultbottom-right - Corner the launcher starts in:
top-left,top-right,bottom-leftorbottom-right. - data-offset-xinit() option
offsetX· Default20 - Distance in pixels from the left or right edge the position anchors to.
- data-offset-yinit() option
offsetY· Default20 - Distance in pixels from the top or bottom edge the position anchors to.
- data-compactinit() option
compact· Defaultauto alwaysshows the icon alone.autodrops the label below 480 pixels.- data-accentinit() option
accent· Default#6d5dfc - Colour of the launcher and of the outlines.
- data-localeinit() option
locale· Default browser - Force the panel's language:
en,frornl. Otherwise the visitor's own choice, then their browser's. - data-visibilityinit() option
visibility· Defaultprotected protectedfor an application showing customer data,publicfor a showcase or documentation site. See What a capture shows.- data-excludeinit() option
exclude· Default none - CSS selectors, separated by commas, for data to blur that the page cannot mark itself.
- data-consoleinit() option
console· Defaultoff onrecords log levels and timings with each report, never the messages. See Console diagnostics.- data-tag-<key>init() option
tags· Default none - One label per attribute, such as
data-tag-environment="staging". See Tags. - data-reporter-email
data-reporter-nameinit() option
reporter· Default none - Who is sending the report. See Who is reporting.
- data-storage-keyinit() option
storageKey· Defaulto27-feedback - Where the launcher's position is saved. Only needed when two widgets share one domain.
- data-api-urlinit() option
apiUrl· Defaulthttps://feedback.o27.io - Where reports are sent. Change it only for a self-hosted platform.
- init() onlyinit() option
vocabulary· Default none - Your interface's strings in the active language, so they stay readable in captures. See Interface and data.
- init() onlyinit() option
consoleMessages· Default none - Exact, static log messages whose text may be kept. See Console diagnostics.
Placing the launcher
An application whose header owns the top edge and whose composer owns the bottom has no free corner. Keep the corner and push the launcher clear of them with an offset:
<script
src="https://cdn.feedback.o27.io/widget.js"
data-project-key="YOUR_PROJECT_KEY"
data-position="bottom-right"
data-offset-y="88"
defer
></script>Your values are a starting point, not a lock: a launcher the visitor dragged keeps its own position, and picking a corner in the panel's settings returns to yours.
Who is reporting
Name the person sending the report so your team can follow up. On a page your server renders, write it on the tag:
<script
src="https://cdn.feedback.o27.io/widget.js"
data-project-key="YOUR_PROJECT_KEY"
data-reporter-email="ada@example.com"
data-reporter-name="Ada Lovelace"
defer
></script>In an application that learns who the user is after the page loads, call setReporter once they are known, and with null on sign-out:
function withFeedback(run, tries = 50) {
const feedback = window.o27?.feedback
if (feedback) run(feedback)
else if (tries > 0) setTimeout(() => withFeedback(run, tries - 1), 200)
}
withFeedback((feedback) => feedback.setReporter({ email: user.email, name: user.name }))
// on sign-out
withFeedback((feedback) => feedback.setReporter(null))Both fields are optional and trimmed. An address that is not one is dropped, and an empty pair clears the reporter.
Not verified. Anyone holding the embed key can send any value, so the reporter is a label, not proof of identity. It appears on the report in Feedback, in the Slack message and in the Linear issue: send only what the people reading them may see.
The window.o27.feedback object
The script exposes four functions on window.o27.feedback once it has loaded.
- init(options) → { destroy() }
- Mounts the widget again with these options merged over the tag's and the previous call's. Use it to pass the options only JavaScript can hold, or to change the accent when your theme changes.
- destroy()
- Removes the widget from the page.
init()brings it back. - setTags(tags)
- Merges labels into the set every report carries. A
nullvalue removes a key. - setReporter(reporter | null)
- Replaces the reporter, unlike
setTags, which merges.nullclears it.
The script tag is deferred, so the object does not exist while your own code first runs, and a call made too early is dropped without an error. Wait for it:
function withFeedback(run, tries = 50) {
const feedback = window.o27?.feedback
if (feedback) run(feedback)
else if (tries > 0) setTimeout(() => withFeedback(run, tries - 1), 200)
}
withFeedback((feedback) =>
feedback.init({ vocabulary: () => Object.values(i18n.messages) }),
)The script carries no type declarations. In TypeScript, declare the global once:
declare global {
interface Window {
o27?: {
feedback?: {
init(options?: Record<string, unknown>): { destroy(): void }
destroy(): void
setTags(tags: Record<string, string | number | boolean | null>): void
setReporter(reporter: { email?: string; name?: string } | null): void
}
}
}
}
export {}What a capture shows
Every image the widget produces protects what the reporter did not point at. This applies to Copy, Send and the download offered when a send fails, on every plan, and the reporter cannot turn it off.
An application (data-visibility="protected", the default) blurs every piece of data outside the reporter's selections. In the image sent to your project, that text is first replaced by placeholder text of the same length, so it cannot be recovered from the blur.
A public site (data-visibility="public") blurs nothing unless the page marks it private.
What the reporter selects is always shown as it is. The page itself is never changed: only the image is.
Interface and data
On an application, what counts as interface, and stays readable, is decided in three layers. Each one overrides the one before.
- 1Structure. Text inside
nav,header,footer,button,label,th,legend,summaryand their ARIA roles is interface. Form fields, iframes, images, table cells and every other text run are data, headings included. The nearest ancestor decides, so a field inside a nav is still data. - 2Your strings. Pass the active language's interface strings as
vocabulary; a text run equal to one of them stays readable wherever it sits. Pass a function if the catalogue loads later or the language changes: it is read at capture time. - 3Your markers.
data-feedback="private"blurs an element and everything inside it;data-feedback="public"keeps it readable. The nearest marked ancestor wins, so a public toolbar can hold a private counter. Markers work on both kinds of site.
<header>Acme <span data-feedback="private">Jean Dupont</span></header>
<section data-feedback="private">Customer account details</section>
<nav data-feedback="public">…</nav>
<script
src="https://cdn.feedback.o27.io/widget.js"
data-project-key="YOUR_PROJECT_KEY"
data-exclude=".customer-details, #billing-summary"
defer
></script>No access to the markup? List selectors in data-exclude; they are treated as private. An invalid selector blocks the capture rather than letting data through.
Console diagnostics
Off by default. With data-console="on", each report carries the level and timing of the last 60 console calls. Their content, error messages, stacks and objects are replaced with [redacted].
To keep the text of specific messages, approve them through init(). Only a call with one string argument matching an entry exactly keeps its text:
withFeedback((feedback) =>
feedback.init({
console: true,
consoleMessages: ['Checkout failed', 'Search unavailable'],
}),
)Approve only static messages that name no person: adding an argument or interpolating an identifier redacts the whole message again.
What a report contains
It contains the protected image of the chosen frame, the comments the reporter wrote, where each selection sits, the page's origin, the viewport size, and browser facts: user agent, language, pixel ratio, screen size, appearance preferences and time zone. Plus your tags, the reporter, and console diagnostics when you turned them on.
It does not contain the page title, the URL's path, query or fragment, or any CSS class or id.
Checking your integration
- Put recognisable test content inside a protected area, choose Copy and look at the image: data outside your selections must be unreadable, controls and selections sharp.
- Repeat with filled-in form fields and with the selectors you listed in
data-exclude. - In your browser's network tools, open the capture request: no title, the URL's origin only, and no console field unless you turned diagnostics on.
Slack and Linear
Reports always land in your Feedback project. Connect Slack or Linear once, in Integrations, and each project can also send them on. Integrations are part of the Team plan.
Slack posts each report to a channel, image included. Invite the Feedback app to that channel, or the post is refused.
Linear turns a report into an issue in a team, with its image and comments. Pick the status, project and labels new issues start with.
Destinations are set per project and per environment, read from the environment tag, so staging reports can go to one channel and production ones to another. Each destination is automatic, receiving every new report, or on send, receiving a report when someone sends it from its page in Feedback.
Troubleshooting
The launcher does not appear
Check that the tag carries your project's key, or uses the project address copied from the app. The browser console says when a key is missing. On a site with a Content Security Policy, see Install the widget.
The launcher covers one of my own buttons
Add data-offset-x or data-offset-y, or pick another corner. See Placing the launcher.
Reports arrive without the reporter
setReporter was probably called before the script loaded, and dropped. Wait for window.o27.feedback, as shown in The window.o27.feedback object.
Parts of my interface are blurred
Text that is not in a structural element counts as data. Mark the area data-feedback="public", or pass your interface strings as vocabulary.
Customer data is readable in captures
Check that the tag does not say data-visibility="public". Then mark the area data-feedback="private", or list it in data-exclude.
The capture fails straight away
An invalid selector in data-exclude blocks every capture. Check the list in the project's advanced options.
An image, a font or a video looks wrong in the capture
See Known limitations.
Known limitations
- Images served from another domain without CORS, some web fonts, and the content of iframes, canvases and WebGL may not appear exactly as on screen.
- Selections and comments live for the current visit only. The launcher's position is the one thing kept.