@reprokitapp/browser-sdk
v1.3.0
Published
Framework-agnostic TypeScript browser SDK for ReproKit. Captures safe runtime context (errors, unhandled rejections, failed requests, rage clicks, console errors, resource errors) and posts batched events to the ReproKit API. Privacy-first defaults; zero
Readme
ReproKit Browser SDK
Framework-agnostic TypeScript browser SDK for ReproKit.
ReproKit turns vague user bug reports into reproducible, evidence-backed issues. The browser SDK captures safe runtime signals from your web app — JavaScript errors, failed network requests, action breadcrumbs, optional user reports — and sends compact batched events to the ReproKit API.
- Privacy-first defaults: no input values, no cookies, no storage, no request/response bodies.
- Zero runtime dependencies.
- ESM, CJS, and script-tag (IIFE) builds with strict TypeScript declarations.
- Works with any framework, or none.
Install
npm install @reprokitapp/browser-sdkQuick start
import { ReproKit } from '@reprokitapp/browser-sdk';
const rk = new ReproKit({
projectKey: 'pk_live_...',
apiUrl: 'https://api.reprokit.app',
environment: 'production',
release: '[email protected]'
});
rk.start();Or via the convenience function, which constructs and starts in one call:
import { initReproKit } from '@reprokitapp/browser-sdk';
const rk = initReproKit({
projectKey: 'pk_live_...',
apiUrl: 'https://api.reprokit.app'
});Script-tag (IIFE) usage via a public CDN:
<script src="https://unpkg.com/@reprokitapp/[email protected]/dist/reprokit.global.js"></script>
<script>
ReproKit.init({
projectKey: 'pk_live_...',
apiUrl: 'https://api.reprokit.app'
});
</script>The same path works on jsDelivr: https://cdn.jsdelivr.net/npm/@reprokitapp/[email protected]/dist/reprokit.global.js. Always pin a version on CDN URLs.
The browser project key is a public identifier, not a secret. Access control happens server-side: per-project allowed origins, rate limits, and payload limits.
What it captures
Defaults are conservative. Anything riskier is opt-in.
| Signal | Default | Config key |
|---|---|---|
| JS errors (window.error) | on | capture.errors |
| Unhandled promise rejections | on | capture.unhandledRejections |
| Failed network requests (5xx + network errors; 4xx opt-in) | on | capture.failedRequests |
| console.error calls | off | capture.consoleErrors |
| console.warn calls | off | capture.consoleWarnings |
| Resource load failures (img, script, link, …) | off | capture.resourceErrors |
| Rage clicks | off | capture.rageClicks |
| Action breadcrumbs (clicks/submits/nav keys/route changes) | on | breadcrumbs.enabled |
Every event includes a small context: RuntimeContext snapshot: sanitized URL, path, sanitized referrer, user agent, language(s), timezone, viewport, screen size, devicePixelRatio, online status, visibility state, navigation timing summary, SDK name + version.
By default the snapshot also carries browser setup context to help diagnose which browsers and devices an issue affects: touch/pointer capability (maxTouchPoints, coarse/fine pointer, hover) and — on browsers that expose them — raw User-Agent Client Hints from navigator.userAgentData (brands, mobile flag, platform, plus high-entropy hints like platformVersion, architecture, and model). The SDK sends these raw facts as-is and never derives browser/OS/device labels itself. Exact device model is best-effort: browsers only expose it through Client Hints (in practice Chromium on Android); on Safari, Firefox, iOS, and most desktops it is simply unavailable. Disable with privacy: { captureDeviceContext: false }, or keep low-entropy hints but skip the high-entropy request with privacy: { captureHighEntropyHints: false }.
Every event also carries a snapshot of the most recent user-action breadcrumbs (see Action breadcrumbs).
What it never captures
The SDK does not collect, and has no configuration that enables collecting:
- request bodies
- response bodies
- cookies
localStorage/sessionStorage- form input values (inputs and textareas are treated as sensitive)
- full DOM snapshots
- session replay
- screen recording
- IP addresses, geolocation, or ISP/network-provider information
Sensitive query params (token, access_token, api_key, password, …) and sensitive headers (Authorization, Cookie, X-Api-Key, …) are redacted from anything that does get captured. See Privacy guarantees.
Production checklist
- Pin the version. Follow your release policy:
^1.3.0to take compatible updates, or an exact version for fully reproducible builds. From 1.0.0 onward, the public API and configuration follow semantic versioning. CDN users should always pin (@1.3.0). - Keep
debug: false. Debug-mode logger output is visible to anyone with DevTools open. - Configure CSP. The SDK posts to
${apiUrl}/api/sdk/events. Your Content-Security-Policy must include the ingest origin underconnect-src, e.g.connect-src https://api.reprokit.app. Without it, the browser blocks ingest silently apart from a console warning. - Add your site origin to the project's allowed origins in the ReproKit admin. CORS alone is not enough — the API validates the origin allow-list per request.
- Enable optional capture deliberately. Console capture, resource errors, rage clicks, 4xx capture, and the report widget are all off by default. Turn each on intentionally and review what it adds to your payloads; the widget injects a floating "Report a problem" button on every page.
Configuration
const rk = new ReproKit({
projectKey: 'pk_live_...',
apiUrl: 'https://api.reprokit.app',
environment: 'production',
release: '[email protected]',
capture: {
consoleErrors: true,
consoleWarnings: false,
resourceErrors: true,
rageClicks: true,
rageClickThreshold: 5,
rageClickWindowMs: 1500
},
filters: {
ignoreBrowserExtensions: true,
ignoreIframes: true,
ignoreThirdPartyScripts: true,
allowedScriptOrigins: ['https://cdn.example.com']
},
network: {
captureServerErrors: true, // 5xx (default)
captureNetworkErrors: true, // status 0 / aborts / timeouts (default)
captureClientErrors: false, // 4xx — off by default to keep noise low
captureXhr: true, // also instrument XMLHttpRequest (default)
ignoreUrls: ['/health', /\/_next\//],
ignoreMethods: ['OPTIONS']
},
privacy: {
maskInputs: true,
maskTextareas: true,
redactUrlQueryParams: true,
captureAccessibleNames: true, // safe action labels (aria-label/label/title/short text)
captureDeviceContext: true, // touch/pointer signals + raw userAgentData Client Hints
captureHighEntropyHints: true // platformVersion/architecture/model via getHighEntropyValues
},
sampleRate: 1,
flushIntervalMs: 5000,
maxQueueSize: 50,
debug: false
});
rk.start();Legacy flat config
The following flat options are accepted for backward compatibility. The nested form is preferred.
| Legacy flat | Replacement |
|---|---|
| enableErrorCapture | capture.errors |
| enableUnhandledRejectionCapture | capture.unhandledRejections |
| enableNetworkCapture | capture.failedRequests |
| enableConsoleCapture | capture.consoleErrors |
| enableResourceErrorCapture | capture.resourceErrors |
| enableRageClickCapture | capture.rageClicks |
When both are present the nested value wins.
Runtime API
After start(), the SDK exposes:
rk.captureError(error: unknown, context?: { metadata?: Record<string, unknown> }): void;
rk.captureMessage(message: string, context?: { metadata?: Record<string, unknown> }): void;
rk.addBreadcrumb(breadcrumb: BreadcrumbInput): void;
rk.captureReport(input: CaptureReportInput): Promise<{ id: string; accepted: true }>;
rk.flush(): Promise<void>;
rk.stop(): void;
rk.setUser(userId: string | undefined): void;
rk.clearUser(): void;
rk.setContext(key: string, value: unknown): void;
rk.removeContext(key: string): void;
rk.clearContext(): void;Identifying users and adding context
After a login flow:
rk.setUser(currentUser.id);
rk.setContext('plan', currentUser.plan);
rk.setContext('feature_flags', { newCheckout: true });setContext values are sanitized with the same redaction rules as event payloads (sensitive keys become [REDACTED]), and the SDK caps the number of custom keys (currently 20) to avoid unbounded payloads.
stop() removes all listeners, restores patched browser APIs (fetch, XMLHttpRequest, console.*, history.*), and cancels in-flight sends. start() can be called again afterwards.
Action breadcrumbs
The SDK keeps a small ring buffer of the last user actions (default 15) and attaches a snapshot to every captured event under event.breadcrumbs, so ReproKit can reconstruct what the user did before a failure.
Captured automatically:
| Type | What is recorded | What is never recorded |
|---|---|---|
| click | safe selector of the nearest interactive ancestor (tag + id/classes only, depth-limited), a short privacy-filtered accessible label, current path/url | input/textarea/select values, placeholder, text of non-interactive containers |
| submit | safe form selector, current path/url | field names, field values, form data |
| navigation | from/to path, sanitized url (history.pushState/replaceState/popstate) | hash fragments verbatim; sensitive query params are redacted |
| key | only navigation keys (Enter, Escape, Tab, ArrowUp/Down/Left/Right); Tab/arrow keys are suppressed when the target is an input/textarea/contenteditable | typed characters, key codes for letters/digits, input value |
Each breadcrumb is:
- sanitized via the redaction helpers (sensitive metadata keys like
token,password,apiKeybecome[REDACTED]) - length-capped per field
- bounded by
maxItems— oldest is dropped first
Configuration:
const rk = new ReproKit({
projectKey: 'pk_live_...',
apiUrl: 'https://api.reprokit.app',
breadcrumbs: {
enabled: true,
maxItems: 15, // ring buffer size, clamped to 0–50
captureClicks: true,
captureNavigation: true,
captureSubmits: true,
captureKeys: true
}
});To disable entirely:
new ReproKit({ projectKey, apiUrl, breadcrumbs: { enabled: false } });Adding custom breadcrumbs
Apps can record domain breadcrumbs through the public API. The call is safe to make any time after construction and never throws into the host app. Metadata is redacted with the same rules as event payloads.
rk.addBreadcrumb({
type: 'custom',
message: 'feature flag evaluated',
category: 'flags',
metadata: { flag: 'newCheckout', variant: 'B' }
});addBreadcrumb is a no-op when breadcrumbs.enabled is false.
Action labels
Click breadcrumbs (and rage-click events via targetLabel) resolve the raw click target to the nearest interactive ancestor — clicking the <i> icon inside <button aria-label="Like"> records the button, not the icon — and attach a short accessible label derived in priority order from:
aria-label- the associated
<label>for form controls (never the control's value) title- short visible text — interactive elements only, never form controls
Candidates are whitespace-normalized and rejected outright (never truncated) when they exceed 64 characters, look like emails, phone numbers, long numeric identifiers, UUIDs/hashes/tokens, or URLs. When nothing safe remains, the label is omitted and the sanitized selector stands alone. Disable with privacy: { captureAccessibleNames: false }.
Privacy guarantees (breadcrumbs)
- Input/textarea/select/contenteditable values are never read;
placeholderis never used as a label. - Accessible labels come only from interactive elements, are length-capped, and are dropped when they look identifying (emails, ids, tokens, URLs).
textContentof non-interactive containers is not captured.- The safe selector helper only emits tag name,
id, and up to two class names per ancestor (depth-limited to 4). - Navigation URLs are sanitized — sensitive query parameters are redacted before being attached.
- Form submits record the safe form selector only — no field names or values.
- Custom breadcrumb metadata is filtered through the same depth/key redaction rules as event payloads.
Breadcrumbs give ReproKit enough action context to help generate reproduction steps. They are not a session-replay stream and they never carry user-typed content.
User reports
Optional, user-initiated bug reports — distinct from the automatic signals above. Two ways in:
- Programmatic API. Call
rk.captureReport({ message, … })from your own feedback button or shortcut handler. - Built-in floating widget. A "Report a problem" button that opens a 2-step modal + confirmation. Off by default; enable with
reporting.widget: true.
captureReport
captureReport is an instance method, not a static one. The same call shape works in both npm/bundler mode and IIFE/script-tag mode — capture the instance returned by init() and call .captureReport(...) on it.
npm / bundler:
import { initReproKit } from '@reprokitapp/browser-sdk';
const rk = initReproKit({ projectKey, apiUrl });
await rk.captureReport({ message: 'Pay button keeps loading' });Script-tag / IIFE:
<script src="https://unpkg.com/@reprokitapp/[email protected]/dist/reprokit.global.js"></script>
<script>
const rk = ReproKit.init({ projectKey: 'pk_live_...', apiUrl: 'https://api.reprokit.app' });
rk.captureReport({ message: 'Pay button keeps loading' });
</script>Full payload — building your own feedback form:
const files = Array.from(fileInput.files ?? []).slice(0, 2);
await rk.captureReport({
message: 'Pay button keeps loading',
expectedBehavior: 'The payment should complete and show the receipt page.',
blockingLevel: 'Blocking', // 'NotBlocking' | 'Annoying' | 'Blocking' (camelCase accepted, normalized server-side)
reporterEmail: currentUser.email,
screenshot: screenshotBlob, // Blob | File from your own input/capture flow; see Screenshots
attachments: files, // File[] | FileList, max 2
user: { // visible to admins; redacted server-side; not sent to AI
id: currentUser.id,
email: currentUser.email,
plan: currentUser.plan,
role: currentUser.role
},
metadata: { // same redaction/visibility/AI guarantees as `user`
checkoutId,
cartSize: cart.items.length,
experiment: 'new-checkout-flow'
}
});
// → { id: 'rk-…', accepted: true }Screenshot input options:
screenshot: Blob | File— used as-is. Drop in any image your app already produced (host-rendered preview, your own masked<canvas>, etc.).screenshot: true— asks the SDK to capture the page viahtml-to-image. Requires the optional library to be supplied —reporting.screenshotLibraryfor npm/bundler users, or a UMD<script>exposingwindow.htmlToImagefor script-tag users. See Screenshots. If capture fails, the wholecaptureReportcall rejects withReproKitReportError.- omitted /
false— no screenshot is attached.
Attachments: pass File[] or a FileList (e.g., directly from fileInput.files). The SDK enforces the API's max 2 files, 10 MB each cap and the content-type allow-list (image/png, image/jpeg, image/webp, video/mp4, video/webm, video/quicktime, text/plain); over-cap or wrong-type files are silently dropped and a debug warning is logged.
user and metadata: stored alongside the report and visible to admins in Issue Central. Sensitive-looking values (token, password, apiKey, …) are redacted by the SDK before send and again server-side before persistence. They are not included in the AI enrichment prompt — only message, expectedBehavior, blockingLevel, the browser context allow-list, and breadcrumbs are sent to AI.
Auto-filled by the SDK so callers don't have to:
projectKey,environment,release— frominiturl,path— fromlocation.href/location.pathname(sanitized)context.userAgent,context.viewport,context.locale,context.theme— from the browserbreadcrumbs— the current breadcrumb snapshot
Validation / sanitization:
messageis required (non-empty after trim).- All strings are capped at the API limits (
message8000,title200,expectedBehavior2000,reporterEmail320,url/path2048, …). blockingLevelis normalized to the API's wire values:NotBlocking,Annoying,Blocking.notBlocking,annoying,blocking,not-blocking,not_blocking,blockingMeall map to the canonical value.metadatapasses through the same redaction used for events (token,password,apiKey, … become[REDACTED]).- Returns
{ id, accepted: true }. Failures throwReproKitReportValidationError(client-side) orReproKitReportError(network/HTTP).
Endpoints used
| When | Endpoint | Body |
|---|---|---|
| No screenshot, no attachments | POST {apiUrl}/api/sdk/reports | JSON |
| Otherwise | POST {apiUrl}/api/sdk/reports/multipart | multipart/form-data |
The multipart body sends the JSON in a report form field, the optional screenshot under screenshot, and each attachment under the same attachments field name repeated. Headers on both requests: X-ReproKit-Project: <projectKey>, X-ReproKit-Sdk, X-ReproKit-Sdk-Version. Reports are sent immediately, not queued; the call returns the server's id on success.
Limits
| Field | Client cap | Server cap |
|---|---|---|
| message | 2000 in the widget UI, 8000 via the API | 8000 |
| title | 200 | 200 |
| expectedBehavior | 2000 | 2000 |
| reporterEmail | 320 | 320 |
| url / path | 2048 | 2048 |
| Attachments | up to 2 files, 10 MB each | 2 × 10 MB |
| Screenshot | 10 MB | 10 MB |
| Allowed attachment types | image/png, image/jpeg, image/webp, video/mp4, video/webm, video/quicktime, text/plain | same |
| Allowed screenshot types | image/png, image/jpeg, image/webp | same |
Over-cap or wrong-type files are dropped client-side; the report is still sent without them and the SDK logs a warning (debug mode only).
Built-in widget
new ReproKit({
projectKey,
apiUrl,
reporting: {
widget: true, // mount the floating button. Default: false
position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
label: 'Report a problem', // button text
buttonVariant: 'label', // 'label' | 'icon' | 'auto' (icon uses `label` as aria-label)
screenshot: true, // show "Attach screenshot" in Step 2 (default true). Capture is always user-triggered.
attachments: true, // show the attachments row
maxAttachments: 2, // clamped to 2
maxFileSizeMB: 10, // clamped to 10
allowedAttachmentTypes: ['image/png', 'image/jpeg'], // optional narrowing
screenshotTarget: '#app-shell', // optional capture target
screenshotLibrary: () => import('html-to-image'), // supplies the optional dependency (npm users; see Screenshots)
user: { id: currentUser.id, plan: currentUser.plan }, // static user context
metadata: { app: 'web' } // or () => Record<string, unknown> for dynamic
}
});Widget behavior:
- Floating button in the chosen corner. The modal renders in a top-level
<div id="__reprokit-report-widget">inside<body>; CSS is namespaced under.rk-and injected once. No Shadow DOM — the namespace plus single root element keep styles isolated from the host app. - Step 1: required "What happened?" textarea (2000-char UI limit), optional "What were you trying to do?", a
NotBlocking / Annoying / Blocking mesegmented control, optional email for follow-up. - Step 2: shows the runtime context that will be sent (browser, current page, breadcrumb count), an attachments row (up to 2 files, with remove buttons), and a privacy callout right above the Send button.
- Confirmation: copy-to-clipboard button for the server-assigned report id, Close.
- Keyboard: Escape closes; Tab/Shift+Tab is trapped inside the modal; focus returns to the previously-focused element on close.
- ARIA:
role="dialog",aria-modal="true", labelled buttons for Close/Back/Send/Copy. - SSR-safe: returns an inert handle when
document/windoware missing.
Screenshots
Screenshot capture is user-triggered, lazy-loaded, and privacy-masked:
- Manual. In the widget, Step 2 shows an "Attach screenshot" button. The SDK does not capture on modal open; the user must click the button. A thumbnail preview appears before send; "Remove" and "Retake" are both one click away.
- Lazy-loaded. The screenshot library (
html-to-image, ~50 KB raw / ~13 KB gzip) is resolved only when capture is actually requested. Bundler users supply it as a lazy provider (reporting: { screenshotLibrary: () => import('html-to-image') }) so their bundler emits it as a separate chunk. The base SDK bundle is unaffected for callers who never trigger screenshots. - Privacy masking. Before capture, the SDK adds inline
color: transparent !important(plus-webkit-text-fill-color,text-shadow,caret-color) to every<input>(except checkboxes/radios/buttons/file/etc.),<textarea>,<select>,contenteditableelement, password field, and any element withdata-reprokit-mask. Original styles are restored after capture. Input values are never read. - Widget exclusion. The widget root, anything with
data-reprokit-ignoreor.reprokit-ignore, and<script>/<style>/<link>/<meta>/<noscript>tags are filtered out of the capture. The widget root + modal are also visibility-hidden during capture so they never appear in the output. - Graceful failure. Cross-origin images, fonts, video elements, and
<iframe>content may not render; in the widget, capture failure surfaces a non-blocking warning and the report can still be sent without a screenshot. ProgrammaticcaptureReport({ screenshot: true })failures reject withReproKitReportError— the caller asked for a screenshot explicitly, so the SDK doesn't silently send without it.
Installation
The screenshot feature requires the optional html-to-image peer dependency. It is marked optional, so installs that don't use screenshots never pull it in. The SDK does not import the package itself — you supply it through one of the mechanisms below, checked in this order:
reporting.screenshotLibraryfrom configwindow.htmlToImage(script-tag/UMD users)- a native dynamic
import('html-to-image')— works only when the page has an import map entry forhtml-to-image
Bundler users (Vite, Webpack, Rollup, esbuild, Angular, …) — install the package and pass it in config:
npm install html-to-imagenew ReproKit({
projectKey, apiUrl,
reporting: {
widget: true,
// Lazy provider (recommended): your bundler code-splits it, and it loads
// only when a screenshot is actually captured.
screenshotLibrary: () => import('html-to-image')
}
});If you prefer (at the cost of putting it in your main bundle), import it eagerly and pass the module object: import * as htmlToImage from 'html-to-image' → screenshotLibrary: htmlToImage. A configured screenshotLibrary takes precedence over window.htmlToImage, and if it fails the error is surfaced rather than silently falling back.
Script-tag / IIFE users — load the UMD bundle before your ReproKit script tag. It exposes the window.htmlToImage global, which the SDK picks up automatically:
<script src="https://unpkg.com/[email protected]/dist/html-to-image.js"></script>
<script src="https://unpkg.com/@reprokitapp/[email protected]/dist/reprokit.global.js"></script>If the library cannot be resolved through any mechanism, the SDK throws a ReproKitScreenshotError whose message includes the exact instructions for both modes — surfaced to the developer via the SDK logger, not to the end user.
Excluding nodes from capture
Add data-reprokit-ignore (or the class reprokit-ignore) to any element you don't want in the screenshot — for example, a sidebar containing account details:
<aside data-reprokit-ignore>…account details…</aside>You can also configure a specific capture target:
new ReproKit({
projectKey, apiUrl,
reporting: {
widget: true,
screenshotTarget: '#app-shell' // CSS selector, Element, or () => Element | null
}
});Advanced capture behavior
Failed network requests
Failed-request capture comes from the SDK's fetch wrapper and (when enabled) XMLHttpRequest wrapper — it is independent of console capture.
Default policy (status buckets):
| Bucket | What it covers | Default |
|---|---|---|
| network.captureNetworkErrors | status === 0: DNS, offline, CORS preflight failures, aborts, timeouts | on |
| network.captureServerErrors | 5xx (500, 502, 503, 504, …) | on |
| network.captureClientErrors | 4xx (400, 401, 404, 409, 422, …) | off (opt-in) |
| network.captureXhr | also instrument XMLHttpRequest in addition to fetch | on |
2xx and 3xx are never captured. 4xx is off by default because client-error noise (auth refreshes, validation errors, abandoned 404s) tends to dwarf actionable signal at product scale; set network.captureClientErrors: true if you want it. URL sanitization, ignoreUrls, and ignoreMethods apply to everything captured.
XHR instrumentation
Enabled by default. Wraps XMLHttpRequest.prototype.open and .send to:
- record
method,requestUrl(sanitized),status,durationMs, andreasononloadend/error/timeout/abortevents - apply the same bucket policy and
captureStatusesoverride asfetch - skip the SDK self-ingest URL,
ignoreUrls, andignoreMethods - never read
xhr.responseText,xhr.response, the request body, headers, or cookies - restore the original prototype methods on
stop()
Disable with network: { captureXhr: false } if you only want fetch capture.
network.captureStatuses (advanced override)
When set to a non-empty array it wins — only those exact statuses are captured and the three bucket flags are ignored. Empty or undefined lets the buckets apply.
// Capture exactly these statuses, ignoring buckets:
network: { captureStatuses: [0, 408, 429, 500, 502, 503, 504] }
// Capture everything >= 400 via buckets:
network: { captureClientErrors: true, captureServerErrors: true, captureNetworkErrors: true }Console capture
console.error and console.warn are independent toggles:
| Toggle | Patches | Default |
|---|---|---|
| capture.consoleErrors | console.error only | off |
| capture.consoleWarnings | console.warn only | off |
new ReproKit({
projectKey,
apiUrl,
capture: {
consoleErrors: true, // only console.error
consoleWarnings: false // console.warn untouched
}
});When both toggles are off the SDK does not patch console.* at all. Captured console arguments pass through the same redaction and truncation rules as other events.
Noise filters
To reduce browser-extension, iframe, and third-party-script noise from window.error and resource-error capture, the SDK applies safe defaults:
| Option | Effect | Default |
|---|---|---|
| filters.ignoreBrowserExtensions | Drops events whose source URL is chrome-extension://, moz-extension://, safari-web-extension://, etc. | on |
| filters.ignoreIframes | Skips installing capture in non-top-level browsing contexts. | on |
| filters.ignoreThirdPartyScripts | Applies ReproKit's bundled deny-list of known third-party telemetry noise (Amplitude, Google Analytics / Tag Manager, DoubleClick, …). | on |
| filters.ignoredSourcePatterns | Customer-supplied deny-list. Wins over everything else — including allowedScriptOrigins and same-origin. Use it to suppress noisy first-party endpoints too. See pattern shapes below. | [] |
| filters.allowedScriptOrigins | Allow-list of origins (in addition to location.origin) that pass — overrides the bundled list and extension drop, but not an explicit ignoredSourcePatterns match. | [] |
Precedence (highest first):
ignoredSourcePatternsmatches → drop (even same-origin, even if also on the allow-list)- origin is on
allowedScriptOriginsor equalslocation.origin→ keep ignoreBrowserExtensionsand URL is an extension scheme → dropignoreThirdPartyScriptsand bundled list matches → drop- otherwise → keep
Filtering applies to: failed network requests (fetch + XHR), resource errors, window.error/unhandled-rejection source URLs, and (when console capture is enabled) console events whose body or stack contain a matching URL. Same-origin app errors that lack a source URL are never dropped — the filter only acts on URLs it can positively classify.
Pattern shapes for ignoredSourcePatterns (the API enforces the same semantics server-side as defense-in-depth):
api2.amplitude.com— exact host match (case-insensitive)*.amplitude.com— wildcard for subdomains only; does not match the bareamplitude.comrootchrome-extension://— scheme prefix; matches any URL with that protocolwww.google.com/g/collect— host + path prefix; matches the exact path or any deeper segment under it (e.g.…/g/collect,…/g/collect/x,…/g/collect?v=2) but not…/g/collect2. Full-URL inputs likehttps://www.google.com/g/collect?v=2are accepted and normalized down to host + path.
Paths are prefix-matched with a / boundary so a pattern like /g/collect does not absorb unrelated paths like /g/collect2. Query strings and fragments are ignored during matching. There is no regex support, no path wildcards, and no naive substring matching. Malformed patterns are silently dropped at config-normalization time so a typo doesn't accidentally suppress real signal.
new ReproKit({
projectKey,
apiUrl,
filters: {
ignoreBrowserExtensions: true,
ignoreIframes: true,
ignoreThirdPartyScripts: true, // apply the bundled noise list
ignoredSourcePatterns: [ // your additional deny-list
'segment.io',
'*.segment.com',
'*.intercom-messenger.com'
],
allowedScriptOrigins: ['https://cdn.example.com'] // exceptions that always pass
}
});To capture everything, including extension and third-party noise (useful while debugging an integration):
filters: {
ignoreBrowserExtensions: false,
ignoreIframes: false,
ignoreThirdPartyScripts: false
}Local debugging with onEvent
For local development or QA, pass an onEvent callback. It fires once per event after sampling and sanitization, before the event is queued. The SDK swallows any error your callback throws.
const rk = new ReproKit({
projectKey: 'pk_test_local',
apiUrl: 'https://api.example.com',
debug: true,
onEvent: event => {
console.log('[reprokit]', event.type, event);
}
});Ingestion contract
The SDK POSTs batched events to ${apiUrl}/api/sdk/events. apiUrl is the API base only (e.g. https://api.reprokit.app); the SDK adds the path internally — never include /api/sdk/events in apiUrl yourself.
| Aspect | Behavior |
|---|---|
| Endpoint | POST {apiUrl}/api/sdk/events |
| Auth | Anonymous. The browser project key is a public identifier, not a secret. |
| Origin policy | The site's origin must be on the project's allowed-origins list, configured in the ReproKit admin. CORS allows the origin at the protocol layer; the API validates the allow-list per request. |
| Body | JSON EventBatchPayload: projectKey, environment, release?, sdk: { name, version }, events[], sentAt (ISO 8601 string). |
| Per-event timestamp | Epoch milliseconds. |
| Header (fetch path) | X-ReproKit-Project: <projectKey> (also present in the body — the two must match). X-ReproKit-Sdk and X-ReproKit-Sdk-Version are sent for telemetry. |
| Header (sendBeacon path) | sendBeacon cannot set custom headers. The SDK sends an application/json Blob with the body unchanged (projectKey still present in the body). |
| Credentials | credentials: 'omit'. No cookies. |
| Success | Any 2xx is treated as success. The SDK does not depend on the response body. |
| Response (debug only) | When debug: true, the SDK opportunistically parses the response as SdkIngestResponse ({ accepted, rejected, stored, serverTime }) and logs it. |
| Retry | 5xx, 408, and 429 are retried a limited number of times with exponential backoff. Other 4xx are terminal. |
| Self-capture | The fetch-network capture short-circuits requests whose origin+path match the ingest URL, so SDK retries never become events. |
Limits enforced by the API
The API may reject individual events or whole batches that exceed these limits — counts appear in the SdkIngestResponse.rejected field:
- Max batch size: 50 events.
- Max individual event payload: 64 KB of serialized JSON.
- Max request size: 4 MB.
The SDK applies its own conservative caps (strings 2,000 characters, stacks 4,000, depth-capped metadata redaction) so single events normally stay well under 64 KB.
Privacy guarantees
| Guarantee | Implementation |
|---|---|
| Input values never collected | No DOM serialization; inputs/textareas treated as sensitive |
| Cookies never collected | The SDK does not read document.cookie |
| Storage never collected | The SDK does not read localStorage / sessionStorage |
| IP/location/ISP never collected | No geolocation API, no network lookups; device context is limited to browser-exposed facts (userAgentData, touch/pointer, viewport/screen, language, timezone) and is disabled via privacy.captureDeviceContext |
| Request/response bodies never collected | The fetch wrapper never reads .body / .text() / .json() |
| Sensitive query params redacted | ?token, ?access_token, ?api_key, ?password, … become [REDACTED] |
| Sensitive headers redacted | Authorization, Cookie, X-Api-Key, … become [REDACTED] in any captured headers |
| URL credentials redacted | https://user:pw@host/ has its password redacted |
| SDK ingest requests never self-captured | The fetch wrapper short-circuits requests to the configured ingest URL |
| SDK debug logs do not appear in console capture | The logger keeps its own references to the original console methods |
| All capture callbacks are wrapped | No SDK error escapes into the host app after construction |
| stop() restores patched APIs | window.fetch, XMLHttpRequest, console.*, and history.* are returned to their prior references |
Builds and browser support
| Output | Format | Purpose |
|---|---|---|
| dist/index.js | ESM | Modern bundlers, native ESM |
| dist/index.cjs | CJS | Legacy Node-friendly callers |
| dist/reprokit.global.js | IIFE | <script src="…"> usage; assigns window.ReproKit |
| dist/index.d.ts / .d.cts | Types | Strict TS declarations |
- Targets ES2020 and modern evergreen browsers. No old-browser polyfills are bundled.
- SSR-safe: importing or constructing the SDK never touches browser globals; browser work begins at
start(). In SSR frameworks, callstart()from a client-only code path. - Zero runtime dependencies.
html-to-imageis an optional peer dependency used only for screenshot capture.
Not included
The SDK intentionally does not provide:
- session replay, DOM snapshots, or screen recording
- request/response body capture
- cookie or
localStorage/sessionStoragecapture - OpenTelemetry, Sentry, Datadog, or Hotjar integrations
- source map upload or release-tracking automation
- a plugin system
License
MIT © ReproKit
