@tatlacas/brevwick-sdk
v1.0.1
Published
Brevwick core SDK — submit issues from any browser app. AI-formatted into clean GitHub-ready issues.
Maintainers
Readme
@tatlacas/brevwick-sdk
Framework-agnostic core SDK for Brevwick. Submit issues from any browser app — screenshot, redact, send. AI-formatted into clean, triage-ready GitHub issues.
For React apps, use @tatlacas/brevwick-react (provider, hook, floating-action-button widget). This package is the underlying primitive and ships the entire wire protocol.
Install
npm install @tatlacas/brevwick-sdk@betaOr with pnpm / yarn / bun — same name. Pre-1.0 releases track the beta dist-tag.
Quick start
Drop into any HTML page — no build step
The fastest possible install. Paste this anywhere a <script type="module"> tag works (static sites, Webflow, WordPress, classic templating layers).
The SDK core ships no built-in floating-action button — the FAB lives in
@tatlacas/brevwick-react. For a no-build install, you wire any HTML button (or your own UI) tobw.submit()directly, as below. If you need the FAB UX without React, install the React package and ship a tiny wrapper.
<button id="feedback-btn" type="button">Report a bug</button>
<script type="module">
import { createBrevwick } from 'https://esm.sh/@tatlacas/[email protected]';
const bw = createBrevwick({ projectKey: 'pk_live_...' });
bw.install();
document
.getElementById('feedback-btn')
.addEventListener('click', async () => {
const result = await bw.submit({
description: prompt('What went wrong?') ?? '',
attachments: [await bw.captureScreenshot()],
});
alert(
result.ok
? `Filed issue ${result.issue_id}`
: `Failed: ${result.error.message}`,
);
});
</script>Either CDN works — pick whichever your CSP allows:
| CDN | URL |
| -------- | ----------------------------------------------------------------------- |
| esm.sh | https://esm.sh/@tatlacas/[email protected] |
| jsdelivr | https://cdn.jsdelivr.net/npm/@tatlacas/[email protected]/+esm |
The CDN URLs pin a specific pre-1.0 version so no-build users never silently shift under you. Bump the pin when you upgrade — the latest published beta is the version published to npm under @latest.
End-to-end runnable example (no build tool, serve over python -m http.server): examples/vanilla/static.
With a bundler (Vite / Webpack / Rollup / Next / etc.)
import { createBrevwick } from '@tatlacas/brevwick-sdk';
const bw = createBrevwick({
projectKey: 'pk_live_...',
buildSha: process.env.BUILD_SHA,
});
// Start capturing console + network + route rings. Safe to call multiple times.
bw.install();
const result = await bw.submit({
description: 'Checkout hangs after pressing Pay the second time',
expected: 'Order completes and confirmation page loads',
actual: 'Button stays spinning for 30s, then nothing',
attachments: [await bw.captureScreenshot()],
});
if (result.ok) {
console.log('Issue filed:', result.issue_id);
} else {
console.error(result.error.code, result.error.message);
}submit() never throws for normal failures — callers discriminate on result.ok.
Button-driven submit (no FAB)
For sites without a floating-action-button widget — wire any DOM event to bw.submit(). The example below assumes you already have a form / dialog / chat composer in your own UI:
import { createBrevwick } from '@tatlacas/brevwick-sdk';
const bw = createBrevwick({ projectKey: 'pk_live_...' });
bw.install();
document.querySelector('#report-bug')?.addEventListener('click', async () => {
const description =
document.querySelector<HTMLTextAreaElement>('#bug-body')?.value ?? '';
const screenshot = await bw.captureScreenshot();
const result = await bw.submit({ description, attachments: [screenshot] });
if (!result.ok) {
console.error('[brevwick]', result.error.code, result.error.message);
}
});Configuration
createBrevwick(config: BrevwickConfig): BrevwickBrevwickConfig
| Field | Type | Default | Description |
| ------------------- | -------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| projectKey | string | required | Public ingest key, e.g. pk_live_xxx or pk_test_xxx. Safe to ship in client bundles. |
| endpoint | string | https://api.brevwick.com | Override the ingest endpoint. Useful for self-hosted or staging. |
| environment | 'dev' \| 'stg' \| 'prod' | unset | Tag issues with the environment they came from. |
| enabled | boolean | true | Set false to make every method a no-op. Useful in tests or during incidents. |
| buildSha | string | unset | Build SHA included on every issue. Typically process.env.BUILD_SHA or your CI commit. |
| release | string | unset | Released app version, e.g. 1.4.2. |
| userContext | () => Record<string, unknown> | unset | Resolved at submit time and merged into user_context. Use a function so changing values (route, feature flags, auth state) are captured at the moment of submission, not at SDK init. |
| user | { id: string; [k: string]: unknown } | unset | Opaque user identity attached to issues. id is required; any extra fields ride along. |
| rings | { console?, network?, route? } | all true | Per-ring toggles. Each accepts the legacy boolean shorthand or the object form below for finer-grained control. |
| redact | { disable?, custom? } | unset | Tune the on-device redactor — selectively disable built-in patterns and/or extend with project-specific regexes. |
| fingerprintOptOut | boolean | false | Send X-Brevwick-Fingerprint-Optout: 1 to skip the server-side salted fingerprint. |
rings.console
Pass true (or omit) for the default — capture all five console levels (log / info / warn / error / debug) into a 50-entry FIFO. Pass false to disable. Pass an object to narrow:
rings: {
console: {
levels: ['warn', 'error'], // default: all five
max: 100, // default: 50, hard ceiling 200
},
}To reproduce the legacy errors-only behaviour:
rings: {
console: {
levels: ['error'],
},
}rings.network
Pass true (or omit) for the default — capture every completed fetch + XHR (success and failure) into a 20-entry FIFO. Pass false to disable. Pass an object to narrow:
rings: {
network: {
captureSuccess: false, // default: true — opt out for failures-only mode
max: 50, // default: 20, hard ceiling 100
},
}Wire contract — behaviour change. The submitted issue payload renames the network ring's wire field from
network_errors→network_calls. The server-side ingest (sanitiser + schema shape-lock fixtures) mirrors the rename in lockstep. Consumers that previously readnetwork_errorsoff captured payloads must follow the rename — this is a breaking wire change in the pre-1.0 series.
redact
The redactor runs on every string field that leaves the device. Built-in
patterns scrub Authorization / Cookie headers, Bearer … tokens, JWTs,
emails, credit-card numbers (Luhn-gated), IPv4 / IPv6 literals, US SSN /
UK NI numbers, E.164 phone numbers, AWS access keys, GitHub tokens, and
long base64 blobs.
redact: {
// Selectively turn off built-ins. Names accepted:
// 'auth', 'cookie', 'bearer', 'jwt', 'email',
// 'card', 'ip', 'ssn', 'phone', 'aws', 'github', 'base64'
disable: ['phone'], // common case: free-text fields with phone-like order numbers
// Add project-specific patterns. A bare RegExp is replaced with [redacted];
// pass an object to control the replacement string.
custom: [/secret-\w+/g, { pattern: /widget-\d+/g, replacement: '[w]' }],
}The phone-number matcher is the most false-positive-prone pattern (any 8–15 digit run with separators looks like a phone number). Concretely, the following inputs will be masked by the phone matcher today — flip it off via disable: ['phone'] if any of these hurt you:
- ISO-8601 timestamps:
2026-05-01T10:30:45→[phone]T10:30:45 - SHA hashes whose leading 8+ characters happen to be all digits
- Order numbers, tracking IDs, or national IDs of 8–15 digits (e.g. SA-ID
9001015800087→[phone])
It is on by default because real phone leaks in free-text fields are higher-impact than the false positives above; the trade-off is exposed via disable rather than tightened in the regex so consumers stay in control.
Example with everything set
const bw = createBrevwick({
projectKey: 'pk_live_abc123',
environment: 'prod',
buildSha: process.env.NEXT_PUBLIC_BUILD_SHA,
release: process.env.NEXT_PUBLIC_APP_VERSION,
user: { id: currentUser.id, plan: currentUser.plan },
userContext: () => ({
route: window.location.pathname,
locale: document.documentElement.lang,
}),
rings: { console: true, network: true, route: true },
});
bw.install();The Brevwick instance
createBrevwick(config) returns a Brevwick object with the following methods.
install(): void
Starts the enabled rings (console / network / route). Safe to call more than once — subsequent calls while already installed are no-ops. Full no-op in non-browser contexts (SSR, workers).
uninstall(): void
Restores every patched global and drains internal buffers. A second call is a no-op. After uninstall(), calling install() again on the same instance is not supported and will throw — create a new instance if you need to restart.
submit(input: FeedbackInput): Promise<SubmitResult>
Send a feedback issue. Resolves to a tagged union — does not throw for ingest errors.
FeedbackInput:
| Field | Type | Description |
| ------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| description | string | Required. The body of the issue — what happened, from the user's perspective. |
| title | string | Optional issue title. Defaults to the first line of description, truncated to 120 chars. |
| expected | string | What the user expected to see. |
| actual | string | What actually happened. |
| attachments | Array<Blob \| FeedbackAttachment> | Up to 5 files, each ≤ 10 MB, MIME in image/png, image/jpeg, image/webp, video/webm. |
| use_ai | boolean | Per-issue AI formatting opt-in/out. Only honoured when the project enables submitter choice; omit otherwise and the server applies the project default. |
FeedbackAttachment:
interface FeedbackAttachment {
blob: Blob;
filename?: string;
}Plain Blobs also work — Brevwick will derive a filename from the MIME type.
SubmitResult:
type SubmitResult =
| { ok: true; issue_id: string }
| { ok: false; error: { code: SubmitErrorCode; message: string } };SubmitErrorCode:
| Code | When it fires |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ATTACHMENT_UPLOAD_FAILED | Client-side validation rejected an attachment (count > 5, size > 10 MB, disallowed MIME), or the presign / R2 PUT failed before the issue POST was reached. |
| INGEST_REJECTED | Ingest endpoint returned a 4xx (e.g. quota exceeded, payload too large). Not retried — the same payload would be rejected again. The server-echoed message is appended (capped at 256 chars, already redacted). |
| INGEST_RETRY_EXHAUSTED | Ingest POST hit the max retry count (one initial + two backoffs) on 5xx / thrown fetch and never succeeded. |
| INGEST_TIMEOUT | The 30 s total-budget AbortController fired before the pipeline completed. |
| INGEST_INVALID_RESPONSE | Ingest returned 2xx with a body that didn't parse as JSON or didn't include a string issue_id. |
The one case where submit() rejects instead of resolving is an environmental failure before the pipeline runs — the lazy submit chunk fails to load (offline, CDN outage, deploy mismatch). A rejection is the honest signal that the request never reached the ingest. Wrap in .catch if your app runs in hostile environments.
captureScreenshot(opts?): Promise<Blob>
Capture a WebP screenshot of the current page (or a sub-tree). Never throws — on failure, returns a 1×1 transparent WebP placeholder so callers that always attach the result still get a valid image/webp blob.
const blob = await bw.captureScreenshot({
element: document.getElementById('app') ?? undefined,
quality: 0.9,
});Options:
| Field | Type | Default | Description |
| --------- | -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| element | HTMLElement | document.body | Sub-tree to capture. Only descendants with [data-brevwick-skip] are scrubbed — a skip marker on the root itself is ignored. |
| quality | number (0–1) | 0.85 | WebP encoder quality, forwarded to modern-screenshot's domToBlob. |
About the document.body default: prior to this version the default
was document.documentElement. We changed it because the
documentElement default reproduced as a ~2 KiB blank image in at least
one consumer integration and modern-screenshot's own README captures
against <body>. The exact failure mode of the
documentElement path is not yet root-caused — treat the new default as
a behaviour-improving change rather than a verified upstream fix. Two
behaviour changes worth knowing:
- CSS custom properties declared on
:root(html) or<body>are still inherited by the cloned<body>subtree becausemodern-screenshotinlines computed styles before reparenting under<foreignObject>, andgetComputedStyle(body)already resolves ancestor-declared tokens. No action needed for typical design-system setups (Tailwind, CSS variables on:root, theme classes onbody). - Portals into
<html>(browser extensions, atypical portal libraries) are no longer inside the capture tree. Most React/Solid/Vue portals land indocument.bodyand continue to be captured. To preserve the previous behaviour, passopts.element: document.documentElementexplicitly.
Screenshot privacy: any element marked data-brevwick-skip is hidden before capture and restored afterwards, even on failure. Use it on password fields, PII, card numbers, anything that should never land in a bug report:
<input data-brevwick-skip type="password" />
<div data-brevwick-skip>{customerEmail}</div>A tree-shakable top-level captureScreenshot is also exported for standalone use (no Brevwick instance required). It's dynamically imported on first call so modern-screenshot stays out of your initial bundle:
import { captureScreenshot } from '@tatlacas/brevwick-sdk';
const blob = await captureScreenshot({ quality: 0.9 });getConfig(): Promise<ProjectConfig | null>
Fetches project-level AI config from GET /v1/ingest/config. Used by the React widget to decide whether to render the per-issue "Format with AI" toggle. Returns null on non-2xx, malformed JSON, or thrown fetch — treat null as "no submitter choice, use server default". Cached per instance for the session.
interface ProjectConfig {
ai_enabled: boolean;
ai_submitter_choice_allowed: boolean;
}Redaction
Every payload runs through the redactor before it leaves the browser:
- Console ring —
log/info/warn/error/debugmessages, deduped across identical repeats within a 500 ms window. All five levels by default; narrow viarings.console.levels. - Network ring — every completed fetch + XHR (success and failure) by default. Request body capped at 2 kB, response body at 4 kB, both redacted. Headers are allow-listed. Opt into failures-only mode with
rings.network: { captureSuccess: false }. - Route ring —
pushState/popstate/hashchangeroute transitions.
Built-in redact patterns cover Authorization / Cookie / Bearer headers, JWTs, emails, credit-card numbers (Luhn-gated to skip false positives), IPv4 / IPv6 literals, US SSN + UK NI numbers, E.164 phone numbers, AWS access keys, GitHub tokens, and long base64 blobs. Tune via BrevwickConfig.redact: { disable, custom } (see the redact section above).
Server-side sanitisation runs as defence-in-depth, but the client redactor is the primary guarantee — nothing leaves the device unredacted.
Bundle size
Enforced in CI via size-limit and asserted in tests:
- Eager core (
createBrevwick+ console ring + network ring + ring config validation): ≤ 8 kB gzip. The console + network rings ride the eager path on purpose — they have to be live before the first user error or fetch fires, otherwise the issue you're trying to file arrives missing the very evidence you opened the widget to report. - On first
captureScreenshot()call:modern-screenshotdynamic-imports and adds up to ≤ 25 kB gzip. - On first
submit()call: the submit pipeline (presign / upload / ingest / retry) dynamic-imports.
The screenshot encoder, the submit pipeline, and the project-config fetch are all dynamic-imported — importing this package does not pull them in until you actually call them.
sideEffects: false
The package is marked "sideEffects": false. Bundlers will tree-shake away everything you don't import.
TypeScript
First-class TS — the package ships .d.ts for both ESM and CJS. Key types re-exported:
import type {
Brevwick,
BrevwickConfig,
BrevwickRedactConfig,
BrevwickRingsConfig,
CaptureScreenshotOpts,
ConsoleLevel,
ConsoleRingConfig,
Environment,
FeedbackAttachment,
FeedbackInput,
NetworkRingConfig,
ProjectConfig,
RedactCustomPattern,
RedactPatternName,
SubmitError,
SubmitErrorCode,
SubmitResult,
} from '@tatlacas/brevwick-sdk';Browser support
ES2020 targets — modern evergreen browsers (Chrome/Edge 90+, Firefox 90+, Safari 15+). No IE, no transpile-down. Runs fine inside SSR / workers as a no-op (methods defer to real work on first client mount).
Links
- Docs / dashboard: brevwick.dev
- React bindings:
@tatlacas/brevwick-react - Source: github.com/tatlacas-com/brevwick-sdk-js
- Issues: github.com/tatlacas-com/brevwick-sdk-js/issues
