Vue
BugWatch with Vue 3link
@newinstance/bugwatch ships a Vue 3 plugin plus a browser entry point. Together they capture render and lifecycle errors, uncaught window errors, unhandled promise rejections, and anything you report by hand from composables.
npm install @newinstance/bugwatchOne package, no extra installs. The Vue adapter lives at the @newinstance/bugwatch/vue subpath.
Security model: never ship your project keylink
Browser code is public. Your projectKey (<keyId>:<secret>) must never appear in a Vue bundle, an env var prefixed VITE_, or anywhere else the browser can read. Anyone who finds it can ingest arbitrary events under your account.
Instead the browser uses a short lived session token:
- Your backend mounts an endpoint (for example
/bugwatch/session) that callsmintBrowserSession({ projectKey }), or usesbugWatchBrowserSessionHandlerfrom@newinstance/bugwatch/express. That endpoint returns{ token, expiresAt }. - The browser SDK is given
sessionUrl: "/bugwatch/session", fetches the token on first use, caches it, and refreshes it before expiry and on a401. - Events go to
https://api.newinstance.cloud/api/v1/bugwatch/ingest/browserwith anx-bugwatch-sessionheader.
Your secret never leaves your server. Full details are in the Browser Ingest page. If a key ever leaks, rotate it in the dashboard at www.newinstance.cloud under Project settings, API keys.
// WRONG, exposes your secret to every visitor:
const client = createClient({ projectKey: "sk_live_abc123:my-secret" });Step 1: initBrowser in main.tslink
initBrowser is the one call you need in the browser. It guards against SSR, is an idempotent singleton, creates the client, and installs the window error and unhandledrejection handlers. It never throws: bad config or an unreachable session endpoint degrades to a working no-op client plus a single console.warn, so the crash reporter can never crash your app.
import { initBrowser } from "@newinstance/bugwatch";
export const client = initBrowser({
sessionUrl: "/bugwatch/session", // relative URLs resolve against window.location
release: import.meta.env.VITE_RELEASE, // optional but recommended
environment: import.meta.env.MODE,
});Safe at module scope. getBrowserClient() returns the same singleton from anywhere else, so composables and stores do not need to import your main.ts.
Step 2: install the Vue pluginlink
import { createBugWatchVuePlugin } from "@newinstance/bugwatch/vue";
createApp(App).use(createBugWatchVuePlugin(client)).mount("#app");What the plugin does, exactly:
- Hooks
app.config.errorHandler, which is where Vue routes errors thrown in render functions, lifecycle hooks, watchers, and event handlers inside components. - Captures each one at level
50(error) with the tagvueInfoset to Vue's own info string (for example"render function"or"setup function"), then callsclient.flush(). - Chains, does not replace. Any handler already assigned to
app.config.errorHandlerbeforeapp.use(...)is kept and invoked after the capture, with the original(err, instance, info)arguments. Install BugWatch after your own handler and both run. - Wraps its own work in a
try/catchso a reporting failure never propagates into your component tree.
Errors thrown outside a component (a bare promise rejection, a script error) are not seen by errorHandler. Those are covered by the window handlers initBrowser installed, so the pair gives you full coverage.
Capturing handled errors from composableslink
errorHandler only fires for errors Vue actually lets propagate. Anything you catch yourself has to be reported explicitly. Use getBrowserClient() so composables stay decoupled from your entry file.
import { ref } from "vue";
import { getBrowserClient } from "@newinstance/bugwatch";
export function useInvoices() {
const error = ref<string | null>(null);
async function load(customerId: string) {
try {
const res = await fetch(`/api/invoices/${customerId}`);
if (!res.ok) throw new Error(`invoices ${res.status}`);
return await res.json();
} catch (err) {
error.value = "Could not load invoices.";
getBrowserClient()?.captureException(err, {
level: 50,
tags: { composable: "useInvoices", customerId },
});
return [];
}
}
return { load, error };
}The hint argument accepts level, tags, user, traceId, and spanId, all optional. For non error events use client.captureMessage("checkout abandoned", 30).
Identifying userslink
Call setUser once after login and clear it on logout. In the browser there is a single user per tab, so the global setter is correct here (the concurrency warning in the README applies to servers only).
client.setUser({ id: "u_123", email: "alice@acme.com", username: "alice" });
client.setUser(null); // on logoutsetTag(key, value) and setContext(name, object) attach filterable tags and free-form data to every later event.
Breadcrumbslink
Breadcrumbs record what happened before the error. They are kept in a bounded ring of the last 50 on the active scope and render as a timeline on the occurrence view. Router navigations and key user actions are the highest value ones in a Vue app.
router.afterEach((to) => {
client.addBreadcrumb({ category: "navigation", message: to.fullPath });
});
client.addBreadcrumb({
category: "cart",
message: "cart validated",
data: { items: 3 },
});Fields: category, type, level (numeric), message, data (keep it small), and an optional timestamp in ms which defaults to now.
Tracing: no spans in the browserlink
Span creation is server side, that is, projectKey mode. Browser sessionUrl mode captures errors and logs but does not record spans, so do not expect a waterfall from your Vue app.
wrapFetch is still worth using: it injects a traceparent header on every outbound request (never overwriting one you set yourself), so the backend that receives the call joins the same trace and its server side spans and errors line up with the browser error you are looking at.
import { wrapFetch } from "@newinstance/bugwatch";
const tracedFetch = wrapFetch(client);
await tracedFetch("/api/invoices", { method: "POST" });Complete example: src/main.tslink
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { initBrowser, wrapFetch } from "@newinstance/bugwatch";
import { createBugWatchVuePlugin } from "@newinstance/bugwatch/vue";
import App from "./App.vue";
import Home from "./views/Home.vue";
// 1. Init the browser SDK. sessionUrl points at YOUR backend, never a projectKey.
// Never throws, SSR safe, idempotent.
export const client = initBrowser({
sessionUrl: "/bugwatch/session",
release: import.meta.env.VITE_RELEASE,
environment: import.meta.env.MODE,
debug: import.meta.env.DEV, // prints [bugwatch] diagnostics locally
});
// 2. Outbound calls carry traceparent so your API joins the trace.
export const tracedFetch = wrapFetch(client);
const app = createApp(App);
// 3. Your own handler first. The plugin chains to it, it is not replaced.
app.config.errorHandler = (err, _instance, info) => {
console.error("[app]", info, err);
};
// 4. Capture render and lifecycle errors: level 50, tags { vueInfo }, then flush.
app.use(createBugWatchVuePlugin(client));
const router = createRouter({
history: createWebHistory(),
routes: [{ path: "/", component: Home }],
});
// 5. Navigation breadcrumbs give every error a trail.
router.afterEach((to) => {
client.addBreadcrumb({ category: "navigation", message: to.fullPath });
});
// 6. Identity after login, cleared on logout.
export function onLogin(user: { id: string; email: string }) {
client.setUser({ id: user.id, email: user.email });
}
export function onLogout() {
client.setUser(null);
}
app.use(router).mount("#app");Troubleshootinglink
Nothing arrives in the dashboard. Set debug: true and watch the console for [bugwatch] lines. The SDK flushes on a 5 second timer by default, so call await client.flush() to force delivery when testing. Events show up under your project's Issues or Logs tab at www.newinstance.cloud.
The session endpoint returns 401. Confirm /bugwatch/session is deployed and reachable from the app's origin, returns { token, expiresAt }, and that the server side projectKey is valid and not revoked. With debug: true the browser client logs session fetch failures.
BugWatchResolutionError in the console. Something aliased @newinstance/bugwatch/* to .d.ts files in compilerOptions.paths, so runtime exports resolve to a type stub and are undefined. Remove those paths entries. Since v0.2.0 subpath types resolve without aliases, and initBrowser from the package root needs no subpath import at all.
Component errors are not captured. Check that app.use(createBugWatchVuePlugin(client)) runs before mount, and that no code assigns app.config.errorHandler after the plugin is installed, which would drop the chain. Errors from detached async code never reach errorHandler, capture those explicitly.
Events arrive with no user or tags. setUser, setTag, and setContext only affect events captured after them. Call setUser as soon as your auth state resolves, not lazily on the first render that needs it.
Silence in local dev is intentional. Set enabled: false in development if you would rather not send anything, and keep debug: false in production.