Browser
BugWatch in the browser (any framework)link
Use this page for vanilla JS/TS, Svelte, Angular, Solid, Lit, or any browser app without a dedicated BugWatch adapter. React, Vue, and Next.js have their own adapters, but everything below still applies to them.
Why the browser never holds your secret keylink
Your projectKey looks like <keyId>:<secret> and grants ingest for your whole project. Anything you ship to the browser is public, so the key must stay on your server.
The browser instead uses a session token: your backend calls BugWatch with the secret key and gets back a short lived, ingest only token scoped to one project and environment. The browser SDK fetches that token from an endpoint on your domain and sends events to https://api.newinstance.cloud/api/v1/bugwatch/ingest/browser with an x-bugwatch-session header. The secret never leaves your server.
The wire format for that ingest call is documented in Browser Ingest. You do not need it to use the SDK, only if you are hand rolling a client.
browser --GET /bugwatch/session--> your backend --x-api-key--> BugWatch (mint)
browser --x-bugwatch-session--> BugWatch (/ingest/browser)
Step 1: mint endpoint on your backendlink
Express, one line:
import { bugWatchBrowserSessionHandler } from "@newinstance/bugwatch/express";
// Must answer GET: the browser SDK fetches sessionUrl with method GET.
app.get(
"/bugwatch/session",
bugWatchBrowserSessionHandler({ projectKey: process.env.BUGWATCH_KEY! }),
);Any other backend (Fastify, Hono, Hapi, Nest, Django proxy, a Next.js route handler) uses the core helper:
import { mintBrowserSession } from "@newinstance/bugwatch";
// Inside your own GET /bugwatch/session route:
const session = await mintBrowserSession({
projectKey: process.env.BUGWATCH_KEY!,
});
// session = { token: "...", expiresAt: 1771000000000 } <- epoch milliseconds
return Response.json(session);The response body must be { token, expiresAt }. expiresAt should be epoch milliseconds; if it is not a number the SDK assumes the token lives one hour. The Express helper answers 502 { error: "failed to mint BugWatch session" } when minting fails, so a broken key never leaks details to the page.
Step 2: one call in your entry filelink
// src/main.ts (or app.ts, index.ts, whatever loads first)
import { initBrowser } from "@newinstance/bugwatch";
export const client = initBrowser({
sessionUrl: "/bugwatch/session",
environment: "production",
release: "web@1.4.0",
});That is the whole browser setup. initBrowser guarantees:
- Never throws. Bad config, unreachable session URL, anything: it warns once (via
console.warn, orconsole.errorfor the paths-alias stub diagnosis) and returns a working no-op client. A crash reporter cannot crash your app. - SSR safe. When
windowis undefined it returns the existing client or a no-op, so the same module is safe to import in a server rendered bundle and safe at module scope. - Idempotent. The client lives on a global symbol; a second call returns the same instance and does not install handlers twice.
- Handlers installed automatically.
windowerrorandunhandledrejectionlisteners are attached for you. - Relative
sessionUrlallowed."/bugwatch/session"is resolved against the current page origin at init time.
Reach the singleton from anywhere else without threading it through your app:
import { getBrowserClient } from "@newinstance/bugwatch";
getBrowserClient()?.captureMessage("checkout opened", 30);getBrowserClient() returns undefined before initBrowser has run, so keep the optional chaining.
What is captured automaticallylink
Two things, both at level 50 (error):
| Source | Captured |
|---|---|
window error event | event.error, falling back to event.message |
window unhandledrejection event | event.reason |
Everything else is manual. Errors you catch yourself are invisible to BugWatch until you report them:
try {
await placeOrder(cart);
} catch (err) {
client.captureException(err, { tags: { area: "checkout" }, level: 50 });
showRetryToast();
}
client.captureMessage("payment retried", 40, { tags: { attempt: "2" } });captureException(error, hint?) and captureMessage(message, level?, hint?) both return the event id as a string. The captureException hint accepts level, tags, user, traceId, and spanId; on captureMessage the level is the positional second argument and the hint carries the rest.
User, tags, contexts, breadcrumbslink
All of these attach to every subsequent event from that client. There is one scope per browser tab, so set them right after login and clear them on logout.
client.setUser({ id: "u_912", email: "ada@example.com", username: "ada" });
client.setTag("plan", "pro");
client.setContext("viewport", { w: window.innerWidth, h: window.innerHeight });
client.setRelease("web@1.4.0");
client.addBreadcrumb({
category: "ui",
message: "clicked Checkout",
level: 30,
data: { cartValue: 4200 },
});
client.setUser(null); // on logoutBreadcrumbs are kept in a bounded ring (last 50) and ship with the next event. client.withScope(cb) gives direct access to the scope when you need several changes at once.
Origin allow listlink
Each project has an Allowed Origins list under BugWatch, your project, Settings. Leave it empty and browser ingest is unrestricted. Once you add entries, the ingest call from any other origin gets:
HTTP 403 {"error":"origin_not_allowed"}
That is the intended protection: a token scraped from your page cannot be replayed from an attacker's site. If your own events vanish after you configure the list, check that every origin you serve from is listed (apex plus www, staging, preview deploy domains, and http://localhost:PORT for local dev).
Token refreshlink
The SDK fetches sessionUrl on first send and caches the token. It refreshes when the token is within 30 seconds of expiresAt, and it discards the cached token immediately on a 401 from ingest so the next attempt mints a fresh one. Concurrent sends share a single in flight refresh. If no token can be obtained the batch is dropped and, with debug: true, the reason is printed.
No spans in the browserlink
Span creation requires a projectKey, which only exists server side, so a sessionUrl client records errors and logs but exports no spans. startSpan and withSpan still run: the span objects are real (timed, with real trace and span ids, and withSpan still links captures inside it to the span's trace) but they are never exported, so no browser waterfall is produced. wrapFetch(client) still injects a traceparent header into outgoing requests, so your backend joins the trace the browser started:
import { wrapFetch } from "@newinstance/bugwatch";
const tracedFetch = wrapFetch(client);
await tracedFetch("/api/orders", { method: "POST", body });An existing traceparent on the request is never overwritten.
Complete runnable examplelink
Backend (Express, ESM):
// server.ts
import express from "express";
import { bugWatchBrowserSessionHandler } from "@newinstance/bugwatch/express";
const app = express();
app.get(
"/bugwatch/session",
bugWatchBrowserSessionHandler({ projectKey: process.env.BUGWATCH_KEY! }),
);
app.use(express.static("dist"));
app.listen(3000);Browser entry, framework agnostic:
// src/bugwatch.ts
import { initBrowser } from "@newinstance/bugwatch";
export const bugwatch = initBrowser({
sessionUrl: "/bugwatch/session",
environment: import.meta.env.MODE,
release: import.meta.env.VITE_RELEASE,
debug: import.meta.env.DEV,
});// src/main.ts
import { bugwatch } from "./bugwatch.js";
bugwatch.setUser({ id: "u_912", email: "ada@example.com" });
bugwatch.setTag("plan", "pro");
document.querySelector("#checkout")?.addEventListener("click", async () => {
bugwatch.addBreadcrumb({ category: "ui", message: "clicked Checkout" });
try {
const res = await fetch("/api/orders", { method: "POST" });
if (!res.ok) throw new Error(`order failed: ${res.status}`);
} catch (err) {
bugwatch.captureException(err, { tags: { area: "checkout" } });
}
});
// Verify the pipeline once, then delete:
bugwatch.captureMessage("bugwatch browser smoke test", 50);
void bugwatch.flush();
// Unhandled, captured with no extra code:
setTimeout(() => {
throw new Error("boom from a timer");
}, 0);Load the page, confirm a POST .../ingest/browser returning 202 in DevTools, then check the dashboard.
Legacy pre-0.2.0 flowlink
Still supported, no longer recommended:
import { createClient } from "@newinstance/bugwatch";
import { installBrowserErrorHandlers } from "@newinstance/bugwatch/browser";
const client = createClient({ sessionUrl: "/bugwatch/session" });
installBrowserErrorHandlers(client); // idempotent, returns an uninstall fncreateClient throws on invalid config during module evaluation, which can stop your app from hydrating. initBrowser exists precisely to remove that failure mode. Migration is a one line swap.
Troubleshootinglink
BugWatchResolutionError: "@newinstance/bugwatch/browser" resolved to a type-declaration stub
Your tsconfig.json maps @newinstance/bugwatch/* in compilerOptions.paths to .d.ts files, and a bundler that honours paths (Next.js JsConfigPathsPlugin) rewrote the runtime import to the declaration stub, making every export undefined. Remove those paths entries. Since 0.2.0 the package ships typesVersions, so subpath types resolve even on "moduleResolution": "node", and initBrowser from the package root needs no subpath import at all.
Repeated 401s and no events
The mint endpoint is broken or returning the wrong shape. Fetch it directly in the browser and confirm it answers GET with JSON { token, expiresAt } and a 2xx status. Common causes: mounted as POST only, sitting behind auth middleware that redirects to a login page, returning HTML from a SPA catch all route, or BUGWATCH_KEY unset or revoked on the server. Set debug: true to see session fetch failed: <status> in the console.
403 origin_not_allowed
The page origin is not in the project's Allowed Origins list. See the section above.
Nothing at all in the Network tab
Ad blockers and privacy extensions block requests to third party error reporting hosts. Test in a clean profile or with the extension disabled. If a share of your users blocks it, proxy /ingest/browser through your own domain rather than trying to defeat the blocker.
Only one console warning, then silence
That is by design. initBrowser warns once per distinct message and then runs as a no-op client, so a misconfigured SDK never floods the console or breaks the app. Fix the underlying cause and reload.
Events missing user or tags
setUser, setTag, and setContext must run before the capture. Call them as soon as the session is known, not inside the error handler.