@ganeshdole/brainbox-core
v0.3.0
Published
The Brainbox capture engine: screenshots, marks, session recording, console errors and a Transport that sends the report. No React, no UI.
Readme
@ganeshdole/brainbox-core
The Brainbox capture engine. Plain TypeScript, runs in the browser, no React
and no UI. It takes the screenshot, bakes the user's marks into it, records
the session, keeps the last console errors, and hands the finished report to
a Transport. Every Brainbox UI, including our own widget, is built on it.
Use it directly when you are building your own feedback UI. If you want
Brainbox's UI, see @ganeshdole/brainbox-widget; if you are in React, @ganeshdole/brainbox-react
holds an instance for you.
Install
Inside this monorepo the package is consumed from source:
{ "dependencies": { "@ganeshdole/brainbox-core": "workspace:*" } }Outside it, from npm:
npm install @ganeshdole/brainbox-corepnpm build emits dist/ (JS plus .d.ts); the published package's
exports point at it (publishConfig).
Use
import { createBrainbox, brainboxTransport } from "@ganeshdole/brainbox-core";
const brainbox = createBrainbox({
// Brainbox Cloud, or your own backend (see Transport below)
transport: brainboxTransport({ endpoint: "https://app.brainbox.sh/ingest", projectKey: "pk_..." }),
identity: { id: user.id, email: user.email },
onSubmitted: ({ id }) => console.log("filed", id),
});
// One report in progress. `exclude` is your own UI's root element, left out
// of the screenshot.
const draft = brainbox.draft({ exclude: panelEl });
draft.subscribe(() => render(draft.getState()));
draft.captureScreenshot(); // start rasterising the page now
draft.markUp(marks); // the marks' bounds become the region; baked in the background
await draft.submit({ text, audio }); // waits for the bake, then transport.send()
draft.cancel(); // drop everything; the draft can be used again
brainbox.destroy(); // restores console.error, cancels live draftsA recording instead of a screenshot:
const rec = draft.startRecording({ onAutoStop: () => void rec.stop() });
rec.annotations.show(mark); // drawn into the page, so it shows in the replay
rec.setMicMuted(true);
await rec.stop(); // attaches the session and voice, takes a thumbnailDraftState is what a UI renders from: screenshotUrl, screenshotPending,
screenshotFailed, hasSession, hasVoice. It is a new object on every
change, so useSyncExternalStore and friends can compare by reference.
Transport
Core does not know about project keys or endpoints. brainboxTransport()
speaks the Brainbox backend's /ingest format. To send reports somewhere
else, implement the one-method interface:
import type { Transport } from "@ganeshdole/brainbox-core";
const mine: Transport = {
async send(report) {
const fd = new FormData();
fd.append("json", JSON.stringify({ text: report.text, region: report.region, metadata: report.metadata }));
if (report.screenshot) fd.append("screenshot", report.screenshot, "screenshot.png");
const res = await fetch("/api/feedback", { method: "POST", body: fd, headers: { Authorization: token } });
return { id: (await res.json()).id };
},
};Report carries text, region, metadata (CapturedMetadata from
@ganeshdole/brainbox-shared), and the screenshot, session and audio blobs.
Rules
- Importing does nothing.
createBrainbox()is the only way in, anddestroy()undoes everything it set up. - No module-level state, so two instances on one page do not share buffers.
The one shared thing is the
console.errorhook, installed when the first instance asks and removed when the last one goes. - Privacy defaults in recordings: inputs are masked;
.rr-block,.rr-maskand.rr-ignoreon host elements are honoured.
