Cloudflare integration

Cloudflare Pages and Workers monitoring

BugOtter imports your Cloudflare Pages projects and Workers on custom domains with a read-only token it uses once. Uptime checks start on each domain from three regions. A Worker route or Pages Function serves analytics from your own domain, and @sentry/cloudflare sends Worker errors to BugOtter.

  • Used onceimport token, never stored
  • 2read permissions, nothing else
  • 3 regionscheck each domain
The BugOtter otter plugging in a cable

Import Pages projects and Workers

  1. Create a custom API token

    Open dash.cloudflare.com/profile/api-tokens → Create token → Custom token. Add Account · Cloudflare Pages · Read and Account · Workers Scripts · Read. Pick the accounts it may see.
  2. Paste it in BugOtter

    Click Import from Cloudflare. You get every Pages project, with its custom domain when it has one (otherwise its pages.dev address), and every Worker on a Custom Domain.
  3. Pick what to watch

    Checks start right away from US East, Europe and US West, with an outage only when two regions agree. SSL and domain expiry are watched too. The token isn't stored.
BugOtter uptime for one app: 30 days of daily bars with two amber days, the response time chart, and the latest check from US East, Europe and US West
  1. 30 days at a glance. Amber marks a day with some downtime.
  2. Response time, median and p95, with the slowest region named.
  3. Three regions. An outage only counts when two agree.
Uptime for one app, from three regions.

Analytics past ad blockers, the Cloudflare way

Ad blockers drop 20–40% of visitors for most analytics. On Cloudflare, a few lines of Worker code serve BugOtter from your own domain. Pages' _redirects can't proxy to another domain, so it's a Function there too.

One small helper does the forwarding for both cases. Put it in src/ for a Worker, or at the project root next to functions/ for Pages:

bo-proxy.ts
// bo-proxy.ts: forwards three paths to BugOtter, nothing else
const BO = "https://bugotter.com";

export async function forward(request: Request, path: string, key: string) {
  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[path];
  if (!url) return new Response("Not found", { status: 404 });
  // Only what BugOtter checks. Your site's cookies stay on your site.
  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);
  }
  return fetch(url, {
    method: request.method,
    headers,
    body: request.method === "POST" ? await request.text() : undefined,
  });
}

Workers, or any site behind Cloudflare

Add a Worker on the route example.com/_bo/*. Routes need the hostname proxied by Cloudflare (the orange cloud), and the site behind it can be anything: a Worker, a server, or another host.

If your site already is a Worker, call forward() from its own fetch for paths under /_bo/ instead.

src/index.ts
// src/index.ts: a small Worker on the route example.com/_bo/*
import { forward } from "./bo-proxy";

export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname.replace(/^\/_bo\//, "");
    return forward(request, path, env.BUGOTTER_PROXY_KEY);
  },
} satisfies ExportedHandler<{ BUGOTTER_PROXY_KEY: string }>;
wrangler.jsonc
// wrangler.jsonc
{
  "name": "bo-proxy",
  "main": "src/index.ts",
  "compatibility_date": "2026-11-04",
  "routes": [{ "pattern": "example.com/_bo/*", "zone_name": "example.com" }]
}
// then: npx wrangler secret put BUGOTTER_PROXY_KEY

Cloudflare Pages

Add a Pages Function. A [[path]] file name matches everything under /_bo/. Set BUGOTTER_PROXY_KEY in the project's variables and secrets.

functions/_bo/[[path]].ts
// functions/_bo/[[path]].ts: a Pages Function for /_bo/*
import { forward } from "../../bo-proxy";

export const onRequest: PagesFunction<{ BUGOTTER_PROXY_KEY: string }> = ({ request, params, env }) =>
  forward(request, (params.path as string[]).join("/"), env.BUGOTTER_PROXY_KEY);

Then change the script tag to src="/_bo/bo.js".

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.

BugOtter's visitors chart for the last 7 days, with the previous period as a dashed line and markers for big payments and an error spike
  1. Visitors, against the period before (the dashed line).
  2. Markers for big payments, error spikes and deploys.
  3. Bots kept out of the count.
Traffic for one site, counted past ad blockers.

Errors from Workers and Pages Functions

Browser errors come from the script. For Worker code, use Sentry's Cloudflare SDK with your BugOtter DSN. You don't need a Sentry account.

Store the DSN with npx wrangler secret put BUGOTTER_DSN or as a variable.

src/index.ts (Workers)
// src/index.ts
import * as Sentry from "@sentry/cloudflare";

export default Sentry.withSentry(
  (env: Env) => ({
    dsn: env.BUGOTTER_DSN,
    dataCollection: { userInfo: false, cookies: false, httpBodies: [], databaseQueryData: false },
  }),
  {
    async fetch(request, env, ctx) {
      return handle(request, env, ctx); // your Worker, unchanged
    },
  } satisfies ExportedHandler<Env>,
);

// wrangler.jsonc needs: "compatibility_flags": ["nodejs_compat"]
// and a compatibility_date of 2024-09-23 or later
functions/_middleware.ts (Pages)
// functions/_middleware.ts
import * as Sentry from "@sentry/cloudflare";

export const onRequest = [
  // Sentry first, then any middleware of your own
  Sentry.sentryPagesPlugin((context) => ({ dsn: context.env.BUGOTTER_DSN })),
];

The dataCollection line is for SDK v11. Leave out tracesSampleRate: BugOtter keeps errors, not traces, so spans are accepted and dropped.

Running SvelteKit, Astro or Nuxt on Cloudflare? Their Sentry SDKs need this Cloudflare SDK underneath. See each framework page.

Source maps for browser bundles: add the upload to the build command, like npm run build && npx bugotter sourcemaps dist --delete. On Pages, the CLI tags them with CF_PAGES_COMMIT_SHA by itself. With Workers Builds, pass --release $WORKERS_CI_COMMIT_SHA. More in the source maps guide.

Mark deploys from your build

BugOtter doesn't read deploys from Cloudflare by itself yet

So mark them from the build with npx bugotter deploy. The CLI needs BUGOTTER_PROJECT and BUGOTTER_KEY (the upload key: keep it a secret, not a plain variable).
  • Pages: end the build command with && npx bugotter deploy. It marks the deploy when the build finishes, a minute or so before it's live.
  • Workers Builds: make the deploy command npx wrangler deploy && npx bugotter deploy --version $WORKERS_CI_COMMIT_SHA.
  • GitHub Actions: run npx bugotter deploy after the deploy step, or point a GitHub Deployment statuses webhook at your app's deploy URL.

Issues then say “started 4 min after deploy c0ffee1”, and release health compares each deploy with the one before.

BugOtter's deploy check for v1.8.1 saying it looks fine, above the error inbox with a new ChunkLoadError seen 9 times by 1 person
  1. Deploy check. After each Cloudflare deploy: new errors, error rate, speed and sales against the day before.
  2. New errors name their release, so you know which change to look at.
  3. Who it hit: events and people, per issue.
Each marked deploy gets a check, and each issue names the release it started in.

A status page for your Cloudflare app

Cloudflare's status page covers Cloudflare. Yours should cover your app: the home page, an API path your app can't work without, and sign-in.

Add each as a check with an expected status, word or JSON value, so an error page that answers 200 still fails. Then:

  • Turn on the status page at bugotter.com/status/your-app: uptime, incidents and your updates, never errors or revenue.
  • Pro can serve it from your own domain, like status.yourapp.com. Add the records it shows in your Cloudflare DNS.
  • People can subscribe to updates, and a badge shows your uptime in the README.
  • Cron Triggers can call a heartbeat URL when they finish, so a run that's late or fails alerts you.
A BugOtter public status page: all systems operational, uptime bars for Website, Docs and API, and a resolved partial outage with its updates
  1. One line for your users: all good, or what isn't.
  2. Uptime per check, day by day.
  3. Incidents and your updates. Never errors, logs or revenue.
A public status page from the live demo.

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 the BugOtter ad-blocker proxy to this Cloudflare project: a Pages Function at functions/_bo/[[path]].ts, or a route in our Worker, then switch the script to /_bo/bo.js.

  • Wrap this Worker with @sentry/cloudflare using my BugOtter DSN, server errors only.

  • Using BugOtter, list this app's errors since the last deploy and fix the newest 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 keep my Cloudflare token?

No. It's used once, to list your Pages projects and Workers while you pick, and never stored. It only needs two read permissions, and you can delete it in Cloudflare right after the import.

Why isn't my Worker in the import list?

The import lists Workers on Custom Domains. Workers that answer on a route of a zone, or only on workers.dev, aren't in that list: add their URLs by hand. Checks work the same.

Can I proxy with Cloudflare Pages _redirects?

No. Cloudflare's _redirects can rewrite within your own site but can't proxy to another domain. A Pages Function at functions/_bo/[[path]].ts does the same job in a few lines.

Do Cloudflare deploys show up by themselves?

Not yet. BugOtter reads deploys from Vercel automatically, but not from Cloudflare. Add npx bugotter deploy to the build or deploy command, or send a GitHub deployment webhook if you deploy from GitHub Actions.

My uptime checks get a Cloudflare challenge. What now?

Bot Fight Mode or a WAF rule can challenge outside checkers, which then look like outages. BugOtter's checks always send the user agent BugOtter/1.0 (+https://bugotter.com/bot). Add a WAF skip rule for it, or send a header the rule lets through: each check can carry its own headers, such as a Cloudflare Access service token.

Are Worker stack traces source-mapped?

Browser stacks are, from the maps you upload. Worker stacks are shown as the SDK sends them. Wrangler's upload_source_maps setting is for Cloudflare's own dashboard, not BugOtter.

Sources

Checked Nov 4, 2026 against Cloudflare's docs on API token permissions, Workers routes, Pages redirects, Pages Functions routing and Workers Builds, and Sentry's Cloudflare guide. BugOtter isn't affiliated with Cloudflare or Sentry.

Watch every app you ship, from one calm list.

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