Install it
Two script tags and a key. The snippets below are addressed at this deployment, so they work as pasted.
1. Get a key
Create a workspace, then Install mints an ingest key. It is shown once; what is stored is a sha256 and a readable prefix.
The key is public by design — it sits in your page, like a Sentry DSN — so it is scoped until that is survivable: it can file a report into one workspace and nothing else. It cannot read a report, list one, see a screenshot or change a setting, because none of those accept a key at all. Fill in the origins your product is served from and a lifted key cannot be used from another website.
2. Paste two tags
<!-- The flight recorder. Blocking, and as early as your template allows. -->
<script src="https://olomon.com/relay-client/recorder.js"
data-relay-surface="your-app"
data-relay-endpoint="https://olomon.com/api/relay/report"
data-relay-key="rlk_…"
data-relay-build="${YOUR_COMMIT_SHA}"></script>
<!-- The camera and the panel. Deferred; nothing needs it until a press. -->
<script type="module" src="https://olomon.com/relay-client/relay.js"></script>The first tag blocks on purpose. A deferred recorder installs after your own bundles and misses the requests that were already in flight — which is the most common way a feature like this ends up collecting nothing useful. It is one small file.
data-relay-build is the single highest-value attribute here. Without it, which commit the person was running is a guess, and a fix verified against the wrong one looks like it did not work.
Moving the button, or using your own
Four attributes on the same tag. Position is the one that matters: bottom right is where a chat bubble and a cookie banner already are, so a product that cannot move ours ends up with two controls on top of each other. An unrecognised value falls back rather than being applied, because a typo should not put the control somewhere nobody looks.
data-relay-position="top-left" <!-- or bottom-right (default), bottom-left, top-right --> data-relay-theme="inverse" <!-- or navy (default), light --> data-relay-label="Report a bug" <!-- default: Something's broken --> data-relay-launcher="off" <!-- render nothing; you provide the control -->
There is no colour, on purpose: three weights whose contrast is known, because a pale button on a pale page means nobody reports anything and that failure looks like ours rather than like a setting. If your design system needs its own control, turn ours off and call one function from wherever you like. Alt+Shift+B keeps working either way, and this is not the same as data-relay-surface="off", which turns the reporter off altogether.
<button onClick={() => window.__olomonRelay.open()}>Report a problem</button>3. Say who is reporting
No cookie of ours reaches your origin, on purpose, and the key says which workspace rather than which person. So your page is the only thing that knows who pressed the button. Pass your own signed-in user and every report carries them: the queue shows them, search finds them, and whoever reads the report can answer them.
// When your session resolves, and again whenever it changes.
// `id` is your own handle for them, and the only one of these the brief carries.
window.__olomonRelayRecorder.identify({
id: user.id,
email: user.email,
name: user.fullName,
traits: { plan: user.plan, tenant: user.orgSlug },
});
// Signing out. It replaces rather than merges, so this is the whole of it.
window.__olomonRelayRecorder.identify(null);A server-rendered template has the person in hand already and nowhere to call a function from, so the same three fields are attributes on the recorder tag. Any other data-relay-user-* becomes a trait.
data-relay-user-id="acct_8812" data-relay-user-email="dana@northwind.co" data-relay-user-name="Dana Okafor" data-relay-user-plan="team" <!-- anything else is a trait -->
Two things worth knowing. This is your page’s word rather than something we verified, because the key in it is public, so the console labels it as such and never merges it into an address a session proved. And the address and the name stay in the console: only the id and the traits go into the brief, because the brief becomes an issue body or an agent prompt, and a public repository is not somewhere to publish a customer’s email address by accident.
4. Teach it about your product
The extractors are guessing; you are not. One adapter that returns whatever actually matters is the highest-value thing you can add, and it is ranked above every inference in the brief.
window.__olomonRelayRecorder.adapter({
name: "Batch state",
match: () => location.pathname.startsWith("/payments"),
extract: () => ({
batchId: store.batch.id,
rejected: store.batch.rejected,
locked: store.batch.locked,
}),
});What happens with no endpoint
Leave data-relay-endpoint off and the button still works: it renders the brief in the browser and offers Copy and Download. That is also what happens automatically when the relay cannot be reached, so a report is never lost to a network error.
Things that are easy to get wrong
Where reports go
Routing connects Cursor, Claude, GitHub, Linear or a signed webhook with your own credentials, per workspace. Nothing is a default; a workspace with nothing connected still captures and still stores, and the console is still where a person reads them.
Redaction has three levels and the workspace’s setting is a floor: a reporter can ask for more than it and never for less.