SvelteKit integration

SvelteKit analytics, error tracking and uptime

BugOtter works with SvelteKit on any adapter. One script tag in src/app.html gives you cookieless analytics and browser errors. @sentry/sveltekit, pointed at your BugOtter DSN, catches errors in load functions, actions and hooks. Uptime and a status page start when you add the URL.

  • 1 tagin src/app.html
  • Any adapterNode, Vercel, Netlify, Cloudflare
  • 3 regionsuptime checks

What BugOtter gives a SvelteKit app

  • Analytics with no banner

    One tag in the page shell. Client-side navigation counts, and the script loads once.
  • Errors from load and actions

    Two server hooks with @sentry/sveltekit. The script covers the browser.
  • Past ad blockers

    A small +server.ts route forwards three paths from your own domain.
  1. Script in src/app.html

    Or let the CLI add it. Details
    Terminalnpx bugotter init --token bo_…
  2. Proxy route

    Optional, on a server adapter. Details
  3. Server errors

    Two hooks, your BugOtter DSN. Details
    Terminal: npm install @sentry/sveltekit
  4. Upload source maps

    From your adapter's output folder. Details
  5. Uptime and deploys

    A health route and deploy markers. Details
    Last deploy step: npx bugotter deploy

Analytics: one line in src/app.html

  1. Add the script to the page shell

    src/app.html is the HTML every page starts from, so it's the one place to add the tag. npx bugotter init --token bo_… finds it and adds the tag for you, and writes your keys to .env.local.

    src/app.html
    <!-- src/app.html -->
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <script defer src="https://bugotter.com/bo.js" data-app="YOUR_APP_ID"></script>
        %sveltekit.head%
      </head>
      <body data-sveltekit-preload-data="hover">
        <div style="display: contents">%sveltekit.body%</div>
      </body>
    </html>
  2. Client-side navigation is counted

    SvelteKit's router moves between pages with history.pushState. The script follows it, so each new path is a page view with its own time on page, scroll depth and Web Vitals. Keep signed-in pages out with data-exclude="/app,/account" on the tag.

  3. Goals from any component

    Call window.bo?.track() in an event handler. Form submissions, including use:enhance forms, file downloads and outbound links are counted with no code.

    src/routes/pricing/+page.svelte
    <!-- src/routes/pricing/+page.svelte -->
    <script lang="ts">
      let plan = $state("pro");
    </script>
    
    <button onclick={() => window.bo?.track("upgrade_click", { plan })}>
      Upgrade
    </button>

    Add the type to src/app.d.ts (it already has a declare global block):

    src/app.d.ts
    declare global {
      interface Window {
        bo?: {
          track(name: string, props?: Record<string, string | number | boolean>): void;
          visitorId(): string | null;
        };
      }
    }
    export {};
BugOtter visitors chart for one site: daily bars, the previous period as a dashed line, and markers for big payments and an error spike
  1. Payments on the chart. A green marker shows the day a big payment came in.
  2. Error spikes, too. A red marker lines up a bad day with the error behind it.
  3. Last period, dashed, so a drop or a launch spike is obvious at a glance.
  4. Bots kept out of every number, with the count one click away.
One app's visitors in the live demo, with payments, error spikes and the last period on the same chart.

Get past ad blockers with a +server.ts route

A small endpoint on your own domain forwards three paths to BugOtter, and nothing else.

20–40%

of visitors are dropped by ad blockers for most analytics

One route file brings them back.
src/routes/_bo/[...path]/+server.ts
// src/routes/_bo/[...path]/+server.ts
import { env } from "$env/dynamic/private";
import type { RequestHandler } from "./$types";

const BO = "https://bugotter.com";

export const fallback: RequestHandler = async ({ params, request }) => {
  const key = env.BUGOTTER_PROXY_KEY;
  const target: Record<string, string> = {
    "bo.js": `${BO}/bo.js`,
    "api/collect": `${BO}/api/collect?via=${key}`,
    "api/errors": `${BO}/api/errors?via=${key}`,
  };
  const url = target[params.path];
  if (!url) return new Response("Not found", { status: 404 });

  // Pass on what BugOtter checks: who sent it, from which page, in which browser.
  const headers = new Headers();
  for (const h of ["content-type", "user-agent", "origin", "referer"]) {
    const v = request.headers.get(h);
    if (v) headers.set(h, v);
  }
  const res = await fetch(url, {
    method: request.method,
    headers,
    body: request.method === "POST" ? await request.text() : undefined,
  });
  return new Response(res.body, {
    status: res.status,
    headers: {
      "content-type": res.headers.get("content-type") ?? "text/plain",
      "cache-control": res.headers.get("cache-control") ?? "no-store",
    },
  });
};

Add BUGOTTER_PROXY_KEY to your environment, then change the tag in app.html to src="/_bo/bo.js".

  • The endpoint needs a server adapter (Node, Vercel, Netlify or Cloudflare).
  • If your handle hook redirects signed-out visitors, let paths under /_bo/ through.

Check it once: curl -sI https://yourapp.com/_bo/bo.js should answer 200. Your proxy key is on the app's Traffic tab in BugOtter. Keep the ?via= part: it proves visits came through your proxy, and it only lives in server config, never in the page.

Errors: @sentry/sveltekit with your BugOtter DSN

Browser errors already arrive from the script. For the server side, use Sentry's SvelteKit SDK and change only where it sends. You don't need a Sentry account, the wizard or the Sentry Vite plugin.

  1. Install the SDK

    terminal
    npm install @sentry/sveltekit
  2. Turn on server instrumentation and init

    svelte.config.js
    // svelte.config.js (SvelteKit 2.31 or newer)
    const config = {
      kit: {
        adapter: adapter(),
        experimental: { instrumentation: { server: true } },
      },
    };

    Then init the SDK with your DSN, in its own file next to hooks.server.ts:

    src/instrumentation.server.ts
    // src/instrumentation.server.ts
    import * as Sentry from "@sentry/sveltekit";
    
    Sentry.init({
      dsn: "YOUR_BUGOTTER_DSN", // from your app's page; it can only send errors
      dataCollection: { userInfo: false, cookies: false, httpBodies: [], databaseQueryData: false },
    });

    On SvelteKit older than 2.31, put the same Sentry.init call at the top of src/hooks.server.ts instead. The dataCollection line is for SDK v11; on v10 use sendDefaultPii: false.

  3. Catch errors in hooks

    handleErrorWithSentry reports unexpected errors from load functions, form actions and endpoints. Don't add a hooks.client.ts init: the script covers the browser.

    src/hooks.server.ts
    // src/hooks.server.ts
    import * as Sentry from "@sentry/sveltekit";
    import { sequence } from "@sveltejs/kit/hooks";
    
    export const handleError = Sentry.handleErrorWithSentry();
    export const handle = sequence(Sentry.sentryHandle() /*, your own handle */);

Kept

  • Exceptions and captureMessage events, grouped into issues
  • Release, environment, the request path and runtime
  • Navigation and failed-request breadcrumbs
  • Your user id, hashed (never stored raw)

Accepted, then dropped

  • Performance traces and spans
  • Session replays and profiles
  • Sessions, logs and metrics
  • Console breadcrumbs and click text

On adapter-cloudflare, Sentry's SvelteKit SDK needs its extra Cloudflare setup from Sentry's docs. The Node, Vercel and Netlify adapters work as shown.

A BugOtter error page: a ChunkLoadError on /pricing with a plain summary, Fix with agent, events and users, 14-day bars and the source-mapped line
  1. A plain summary written from the evidence: page, browser, release and visits.
  2. Fix with agent hands the whole issue to Claude Code, Cursor or Codex.
  3. Where your code broke, mapped back to your file and line with your source maps.
One error in the live demo: a plain summary, who it hit, and the line in your code.

Source maps from your adapter's output

In vite.config.ts, set build: { sourcemap: "hidden" }, so maps are written without a link to them in your scripts.

Then upload them after vite build from the folder your adapter deploys. Uploading from there matters: --delete then removes the maps from what actually goes live.

AdapterUpload from
adapter-nodebuild/client
adapter-static, adapter-netlifybuild
adapter-vercel.vercel/output/static
adapter-cloudflare.svelte-kit/cloudflare
package.json (adapter-node)
"build": "vite build && npx bugotter sourcemaps build/client --delete"

Paths come out as /_app/immutable/…, exactly as the browser loads them. The CLI needs BUGOTTER_PROJECT and BUGOTTER_KEY in the build environment. See the source maps guide for releases.

Uptime, a status page and deploys

3 regions

check your app from the moment you add the URL

Add a second check on an endpoint that touches your database, and expect ok to be true in the JSON:

src/routes/api/health/+server.ts
// src/routes/api/health/+server.ts
import { json } from "@sveltejs/kit";
import { db } from "$lib/server/db";

export async function GET() {
  await db.execute("select 1"); // prove the database answers, not just the server
  return json({ ok: true });
}

Deploys: issues name the deploy they started after.

  • Vercel: tracked by themselves once connected.
  • Netlify: add your app's deploy webhook as a Deploy succeeded notification.
  • A Node server or Cloudflare: add npx bugotter deploy as the last deploy step.

Status page: at bugotter.com/status/your-app, with incidents you can post updates to and a README badge. Pro can use your own domain.

BugOtter uptime for one app: 30 days of daily bars with two amber days, a response time chart, and the latest check from US East, Europe and US West
  1. Every day at a glance. Amber means a short outage that day.
  2. Response time, median and p95, and the slowest region.
  3. Three regions check your URL. An outage only counts when two agree.
Thirty days of uptime in the live demo, with response times and the latest check from each region.

Or ask your coding agent

Connect BugOtter to Claude Code, Cursor or Codex once over MCP. It signs in through your browser, read-only unless you allow changes. Then paste one line.

Claude Code, once
claude mcp add --transport http --scope user bugotter https://bugotter.com/api/mcp

Then ask, in plain words:

  • Add BugOtter to this SvelteKit app: the script in src/app.html, @sentry/sveltekit server hooks with my BugOtter DSN, and source map upload for our adapter.

  • Add the BugOtter ad-blocker proxy as src/routes/_bo/[...path]/+server.ts and switch the script to /_bo/bo.js.

  • Using BugOtter, show the errors from our load functions this week and fix the most frequent one.

Cursor, VS Code, Codex and the rest have their own one-step setup at Docs: your agent (MCP). Every setup screen in BugOtter also has a Copy prompt for your agent button with your keys filled in.

Questions

Does BugOtter count SvelteKit client-side navigation?

Yes. SvelteKit's router changes pages with history.pushState, which the script listens for, along with the back button. Each new path is one page view. goto() with replaceState: true replaces the entry instead, so it isn't counted as a new page view.

Will the script run again on every navigation?

No. src/app.html is the shell SvelteKit sends once. The script in it runs on the first load and then follows client-side navigation itself.

Can a SvelteKit route start with an underscore?

Yes. In SvelteKit only files starting with + define routes, and a folder named _bo is an ordinary path segment, so src/routes/_bo/[...path]/+server.ts answers /_bo/bo.js and /_bo/api/collect.

My site uses adapter-static. Can I still proxy?

Not with a +server.ts route: on a static site there's no server to run it. Use your host's rewrites instead (Netlify, Vercel or a Cloudflare Pages Function). Everything else on this page works the same.

Do I need the Sentry Vite plugin?

No. sentrySvelteKit() in vite.config uploads source maps to Sentry and adds tracing to load functions. BugOtter takes source maps from its own CLI, and server errors are caught by the two hooks. If you already have the plugin, set autoUploadSourceMaps: false.

Where is the status page?

At bugotter.com/status/your-app once you turn it on. It shows uptime and incidents only, never errors or revenue. Pro can serve it from your own domain.

Sources

Checked Nov 4, 2026 against SvelteKit's docs on project structure, routing and hooks, and Sentry's SvelteKit manual setup. BugOtter isn't affiliated with Sentry or Svelte.

Watch every app you ship, from one calm list.

14 days of everything, no card needed. Plans from $10 a month, billed yearly.