# Vue

`@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.

```sh
npm install @newinstance/bugwatch
```

One package, no extra installs. The Vue adapter lives at the `@newinstance/bugwatch/vue` subpath.

## Security model: never ship your project key

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**:

1. Your backend mounts an endpoint (for example `/bugwatch/session`) that calls `mintBrowserSession({ projectKey })`, or uses `bugWatchBrowserSessionHandler` from `@newinstance/bugwatch/express`. That endpoint returns `{ token, expiresAt }`.
2. The browser SDK is given `sessionUrl: "/bugwatch/session"`, fetches the token on first use, caches it, and refreshes it before expiry and on a `401`.
3. Events go to `https://api.newinstance.cloud/api/v1/bugwatch/ingest/browser` with an `x-bugwatch-session` header.

Your secret never leaves your server. Full details are in the [Browser Ingest](/browser-ingest) page. If a key ever leaks, rotate it in the dashboard at [www.newinstance.cloud](https://www.newinstance.cloud) under Project settings, API keys.

```ts
// WRONG, exposes your secret to every visitor:
const client = createClient({ projectKey: "sk_live_abc123:my-secret" });
```

## Step 1: initBrowser in main.ts

`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.

```ts
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 plugin

```ts
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 tag `vueInfo` set to Vue's own info string (for example `"render function"` or `"setup function"`), then calls `client.flush()`.
- **Chains, does not replace.** Any handler already assigned to `app.config.errorHandler` before `app.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`/`catch` so 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 composables

`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.

```ts
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 users

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).

```ts
client.setUser({ id: "u_123", email: "alice@acme.com", username: "alice" });
client.setUser(null); // on logout
```

`setTag(key, value)` and `setContext(name, object)` attach filterable tags and free-form data to every later event.

## Breadcrumbs

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.

```ts
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 browser

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.

```ts
import { wrapFetch } from "@newinstance/bugwatch";
const tracedFetch = wrapFetch(client);
await tracedFetch("/api/invoices", { method: "POST" });
```

## Complete example: src/main.ts

```ts
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");
```

## Troubleshooting

**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](https://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.
