Feedback button
A small button where people tell you what is confusing on the page where it happens, with the page, the time and the visit, emailed or posted to a webhook.
A small button on your pages where a person can tell you that something is confusing, broken or missing, at the moment it happens. Each note arrives with the page it was sent from and the time, and in journey mode with a link to that visit. You read notes on the dashboard's Feedback page, and they can be emailed to your team or posted to a webhook. Feedback is off until you turn it on.
Turn it on
- Switch it on in the dashboard, under Settings, Feedback, or with
littlefriend feedback enable. - Add
data-feedbackto the lf.js tag. lf.js then loads the feedback script from the same host. Pages without the attribute load nothing extra. - Add a sentence to your privacy page, as shown under Privacy.
<script defer src="https://cdn.littlefriend.io/lf.js" data-site="lf_YOUR_SITE_KEY" data-feedback></script>The script asks the collector whether feedback is on for the page before it shows anything, so you can add the attribute first and switch feedback on later. It can also run as its own tag, next to lf.js or on a page without it:
<script defer src="https://cdn.littlefriend.io/lf-feedback.js" data-site="lf_YOUR_SITE_KEY"></script>It comes from the same host as lf.js and sends to the same collector, so the Content Security Policy entries you added for lf.js cover it. Its styles live in a shadow root, so they never touch your page's styles and yours never reach it.
With npm
pnpm add @littlefriend/feedbackimport { init } from '@littlefriend/tracker';
import { openFeedback, startFeedback } from '@littlefriend/feedback';
init({ site: 'lf_YOUR_SITE_KEY', mode: 'journey' });
startFeedback({ site: 'lf_YOUR_SITE_KEY' });
// From your own Help menu, with the placement set to No button
helpButton.addEventListener('click', () => openFeedback());The button
Under Settings, Feedback you choose the button's words (Feedback by default), an optional question above the text box, and where the button sits:
bottom_right: Bottom right.bottom_left: Bottom left.right_edge: Right edge.manual: No button (open it from your own control).
A person can pick a category, or none:
confusing: Confusing.bug: Something broke.idea: Idea.other: Other.
The box works from the keyboard and with screen readers: focus moves into it and stays there until it closes, Escape closes it and returns focus to the button, and the result of a send is announced. It follows the visitor's light or dark setting and their reduced motion setting. Notes hold up to 2,000 characters.
From your own control or form
With the placement set to manual there is no button. Open the box from your own Help menu or link:
// Opens the feedback box, for example from your own Help menu
lf('feedback');If you already have a feedback form, send its note through Little Friend instead. Show the same consent line the box shows next to your form, and send only when the person submits it.
// Your own form: show the consent line next to it, then send on its submit
lf('feedback', { text: form.note.value, category: 'confusing' }, (result) => {
if (!result.ok) showError('Your note was not sent. Please try again.');
});What is sent
- The note, as typed.
- The category, if the person picked one.
- The page's route, cleaned like a page view: no query, no fragment, ids and anything that looks like personal data replaced.
- The time it arrived.
- In journey mode, the visit's session id, so you can open that visit's timeline or recording. Never in aggregate mode, before consent, under Global Privacy Control or after an opt-out.
- The device class: desktop, mobile or tablet.
Nothing else. The script never reads any other text on the page, any form or any cookie, and no IP address or user agent is stored. A note is sent only when the person presses Send: closing the box, leaving the page or typing sends nothing. Card numbers in a note are replaced with [card number removed] before it is stored.
Before sending, the box shows a line like this one under the text box:
Your note, this page's address, the time, your device type and a link to this visit go to the team behind example.com when you press Send, and are kept for 90 days. Please leave out passwords and card numbers.
Email and webhooks
Under Settings, Feedback, add up to 5 email addresses and one https webhook. Each new note is emailed to every address and posted to the webhook. A delivery that fails is tried again after 30 seconds, 2 minutes, 8 minutes, 32 minutes, 2 hours, 6 tries in all. Each note on the Feedback page shows whether its email and webhook went out.
Notes are emailed only to members of your workspace with a verified email, since anyone on your site can write one. Invite a teammate before you add their address. Each address gets at most 10 feedback emails an hour; more notes than that arrive together in one digest email.
{
"type": "feedback.created",
"id": "fb_8Qm2xWb7TzL4pN8vR1cYe3kd",
"project": { "id": "prj_7Hc2", "name": "Example", "domain": "example.com" },
"receivedAt": "2026-10-16T14:30:00.000Z",
"route": "/settings/billing",
"category": "confusing",
"text": "I can't tell whether the new plan starts today or next month.",
"mode": "journey",
"sessionId": "Vh2kQ9xLp3Nw7aZcRt5m",
"deviceClass": "desktop",
"noteUrl": "https://app.littlefriend.io/p/prj_7Hc2/feedback?note=fb_8Qm2xWb7TzL4pN8vR1cYe3kd",
"replayUrl": "https://app.littlefriend.io/p/prj_7Hc2/replay?session=Vh2kQ9xLp3Nw7aZcRt5m"
}Every delivery is signed. x-lf-timestamp is the time it was sent in Unix seconds, and x-lf-signature is v1= and the hex HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with your webhook's signing secret. x-lf-delivery stays the same across tries of one delivery. The secret is shown once, when you save the webhook or make a new one: keep it in your app's secrets.
A note can arrive more than once, for example when a try times out after your app got it. Dedupe on the body's id, which is signed. x-lf-delivery is not signed, so treat it as a hint only. A note goes only to the webhook URL saved when it arrived: change the URL and notes already waiting are not sent to the new one.
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody is the request body exactly as received, before JSON parsing
export function fromLittleFriend(rawBody: string, headers: Headers, secret: string): boolean {
const timestamp = headers.get('x-lf-timestamp') ?? '';
const signature = headers.get('x-lf-signature') ?? '';
// Refuse deliveries more than 5 minutes old, so a captured one cannot be sent again later
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = 'v1=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
return signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}Answer with any 2xx status once you have the note. Redirects are not followed, and the webhook's host must resolve to a public address.
How long notes are kept
90 days by default, from 1 day up to 365, under Settings, Feedback, or with littlefriend projects update --feedback-days. This is its own setting: it does not follow how long raw events are kept. An editor can delete one note at any time, erasing a person under Settings, Replay deletes the notes sent from their sessions, and deleting a project deletes all of them.
Privacy
A note is personal data: it is whatever the person chose to write. Add this to your privacy page, with FEEDBACK_DAYS replaced by your retention, such as 90 days. In aggregate mode, leave out the part about the visit.
You can send us a note from any page with the feedback button. We receive what you type, that page's address, the time and your device type, and a link to your visit when visits are connected. Nothing is sent until you press Send, and notes are deleted after FEEDBACK_DAYS.
Limits
- 2,000 characters per note.
- One address can send a note about every 10 seconds, with a few at once, and a project stores up to 1,000 notes a day.
- Notes from crawlers and other automated clients are refused.