Median

Feedback panel

MedianFeedback props, how a note is filed as a bug or a suggestion, and the API routes for notes collected elsewhere.

Updated Oct 1, 20267 minute read
import { MedianFeedback } from "@mediansh/widget";

<MedianFeedback apiKey="median_pk_...">
  <button>Share feedback</button>
</MedianFeedback>;

A visitor writes a note and presses Send. A model files it as a bug or a suggestion in Signal, or drops it as spam. Mounted alongside MedianSupport, the two share a visitor, so a person who writes in and leaves a note is one customer.

Props

NameTypeDescription
apiKeystringThe browser-safe median_pk_ id, derived from MEDIAN_KEY with publicKeyFromMedianKey. Required unless identity is set.
identitystringPath to a route built with medianIdentity. The same prop MedianSupport takes. See Identity.
childrenReactNodeYour trigger. Rendered where you put it, inside an inline span that takes the click. A press opens the panel, a second press closes it. With identity and no apiKey, the trigger renders once the identity route answers, and not at all if the route fails. Pass apiKey too to show it at once.
anchorstringA CSS selector for an element to place the panel against, when children cannot be used. Ignored when children are passed.
userMedianUserThe signed in user, if any. Without it, the note belongs to this browser's visitor.
telemetrybooleanSend browser context with the note: time zone, language, browser, OS, device, screen, and the page it was written on. Default: true.
screenshotbooleanAttach a picture of the page when the note is sent. The panel and anything else Median renders are left out. Default: true.
diagnosticsboolean | { errors?: boolean; network?: boolean; console?: boolean }Attach uncaught errors, and optionally failed requests and console.error calls, to the note. Default: false.
appStateRecord<string, string | number | boolean>Facts about your app to send with the note. Read when the note is sent. Sent only when diagnostics is on.
openbooleanHold the open state yourself. See Control from code.
onOpenChange(open: boolean) => voidCalled when the panel's own controls open or close it. Not called by medianFeedback.open() and the other control functions.
widthstringAny CSS length. The panel never grows wider than the window minus 2rem. Default: "20rem".
titlestringDefault: "Share feedback".
descriptionstringDefault: "Bugs, ideas, anything.".
placeholderstringDefault: "Tell us what could be better.".

MedianUser, telemetry, screenshot, diagnostics and appState work as they do on MedianSupport. See Context and diagnostics. To open the panel from a menu or a shortcut, see Control from code.

Where a note goes

The panel says thanks as soon as the note is saved. A model reads it afterwards.

VerdictWhat happens
BugFiled on the bug list with a title, a write-up and a priority
SuggestionFiled on the suggestion list the same way
SpamDropped. Nothing is filed and nobody is notified
  • A note about something already on the list joins that signal instead of adding a row. Ten people with the same complaint make one signal with ten reporters. Each person's own words stay on their report.
  • Spam is only what nobody could act on: adverts, links to unrelated products, keyboard mashing, and tests of whether the box sends. A rude note is not spam. Neither is a one word complaint.
  • Priority is urgent, high, medium or low.
  • Reading a note uses your workspace's AI credits. See Plans.
  • If the model cannot read the note, nothing is filed. The visitor has already been thanked.

Turn filing on or off

Notes are filed only while Track bugs and suggestions is on in Agent โ†’ Behavior. It is on by default. The same switch controls what the agent files from conversations.

SwitchWhat the panel does
OnTakes the note and files it
OffShows "Feedback is currently unavailable. Try again later." and keeps the draft
Turned off after a note was sent, before it was readThe note is not filed

Pictures

A visitor adds pictures three ways:

  • The Add a picture button, which opens the file picker for images.
  • Dragging an image onto the panel. The box reads "Drop to attach" while it is over the panel.
  • Pasting an image into the panel.
RuleLimit
Pictures per note6, counting the automatic screenshot
Size25 MB each
TypesImages only. SVG is refused

A picture that breaks a rule stays in the panel with its reason, and Send stays off until it is removed.

Reason on the pictureCause
Larger than the 25 MB limitThe file is over 25 MB
Only pictures can go hereThe file is not an image
This kind of file cannot be sentThe file is an SVG or another type browsers can run
Only 6 at a timeA seventh picture was added

Automatic screenshot

When the visitor presses Send, the panel takes a picture of the page and adds it to the note.

CaseResult
screenshot={false}No picture is taken
The visitor already added 6 picturesNo picture is taken
The capture fails or takes over 6 secondsThe note is sent without it
The upload failsThe send stops, the reason shows, and the draft stays

Pictures show on the report in Signal, and on the Linear or GitHub issue when the signal is pushed to one.

Placement

SetupWhere the panel opens
childrenAgainst your trigger
anchor, no childrenAgainst the element the selector matches, looked up each time the panel is placed
Neither, or anchor matches nothingThe bottom right corner of the window

Against a trigger or an anchor:

  • The panel opens 8px below it, or above it when there is not enough room below and more room above.
  • Its left edge lines up with the trigger's left edge. If that would run off the window, the right edges line up instead.
  • It stays 16px inside the window and moves with scrolling and resizing.

The anchor is only a position. Pressing it does not open or close the panel, and closes an open panel like any press outside. An invalid selector logs Median: the anchor passed to <MedianFeedback> is {selector}, which is not a valid CSS selector. and the panel opens in the corner.

From a dropdown

Choosing a menu item closes the menu and removes the item, so the panel has nothing to sit against. Mount the panel outside the menu and point anchor at the button the menu opens from:

import { MedianFeedback, medianFeedback } from "@mediansh/widget";

<MedianFeedback apiKey="median_pk_..." anchor="#account-button" />;

<DropdownMenuItem onSelect={medianFeedback.open}>
  Share feedback
</DropdownMenuItem>;

Sending

KeyDoes
EnterAdds a new line
Cmd+Enter or Ctrl+EnterSends
EscapeCloses the panel
  • Send is off while the box is empty or a picture has a problem.
  • After a send the panel shows "Thanks for your feedback." and closes itself 1.6 seconds later. It opens empty next time.
  • Closing the panel without sending keeps the draft until the page reloads.
  • A failed send keeps the draft and the pictures and shows the reason under the box.
Notice under the boxCause
Feedback is currently unavailable. Try again later.Track bugs and suggestions is off
Enter your feedback before sending.The note reached the server empty
Still connecting. Try again in a moment.The panel had no session yet
Too many requests. Wait a moment and try again.A rate limit. See Rate limits

A "Powered by Median" line sits beside the buttons. Hide it with Show Median branding in Settings โ†’ Billing, on a plan that allows it. See Plans.

Limits

WhatLimit
Note2,000 characters. The box stops there, and the server trims the note and cuts it at 2,000
Pictures6 per note, 25 MB each, images only
RequestsSee Rate limits

Send notes from the CLI, API or MCP

Send notes collected outside the browser, such as app store reviews or survey answers, through the same reader. These routes wait for the verdict, usually a second or two, and return it.

median feedback submit --from user_8812 --body "The export button does nothing on my phone." --image https://files.acme.com/8812.png
curl https://api.median.sh/v1/feedback \
  -H "Authorization: Bearer median_key_..." \
  -H "content-type: application/json" \
  -d '{"session":"user_8812","body":"The export button does nothing on my phone.","images":["https://files.acme.com/8812.png"]}'
await median.feedback.submit({
  sessionId: "user_8812",
  body: "The export button on the billing page does nothing on my phone.",
  images: [
    { url: "https://files.acme.com/8812.png", name: "Billing page on iOS" },
  ],
});

The HTTP route is POST /v1/feedback. It takes a Median key or an OAuth token. See Authentication. Every CLI flag is in the CLI reference.

FieldTypeRules
sessionstring, requiredA stable id for the person, such as your user id. Notes with the same session come from one reporter. 8 to 128 characters after trimming. Otherwise the request fails with "Session tokens are 8 to 128 characters."
bodystring, requiredThe note. Trimmed and cut at 2,000 characters
user{ name?, email?, avatarUrl? }Who wrote it
page{ url?, title?, referrer? }Where they were. Only http and https URLs are kept, without the query string or fragment
images(string | { url, name? })[]Picture addresses. Addresses that are not http or https, or longer than 2,000 characters, are dropped. An entry that is not a string or an object with a string url fails the request, for example "images[0]" has to be a URL or an object with a "url". So does a name that is not a string. A repeated address counts once, and only the last 6 are kept

Over MCP, send sessionId in place of session, and pass each image as { url, name? }.

The response is { outcome, signalId }.

outcomeMeaningsignalIdCLI prints
bugFiled on the bug list, or joined a signal already thereThe signal's idFiled as a bug (<signal id>).
suggestionFiled on the suggestion list, or joined a signal already thereThe signal's idFiled as a suggestion (<signal id>).
spamDroppednullRead as spam, so nothing was filed.
unreadNothing filed. The reader was unavailable, the note was empty, or Track bugs and suggestions is offnullNothing was filed: the reader was unavailable, or filing is off for this organization.

Still need help?

    Esc