> ## Documentation Index
> Fetch the complete documentation index at: https://docs.squasher.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloudflare Workers / Edge

> Set up Squasher error monitoring in Cloudflare Workers, Pages, and other edge runtimes.

## Overview

`@squasher/edge` is Squasher's SDK for **Cloudflare Workers**, **Cloudflare Pages**, and other edge runtimes where Node.js APIs like `process` are not available.

<Note>
  If you're running Node.js on a traditional server or in a container (Railway, Fly, AWS Lambda),
  use [`@squasher/node`](/sdks/node) instead. For Next.js specifically, use
  [`@squasher/nextjs`](/sdks/nextjs).
</Note>

## Install

```bash theme={null}
npm install @squasher/edge
```

## Prerequisites

Your `wrangler.jsonc` must include the `nodejs_compat` compatibility flag (required for `AsyncLocalStorage` and `crypto`):

```jsonc theme={null}
{
  "compatibility_flags": ["nodejs_compat"],
}
```

## Quick Start

Wrap your Worker handler with `withSquasher`. This automatically captures unhandled errors and flushes events via `ctx.waitUntil()`:

```typescript theme={null}
import * as Squasher from "@squasher/edge";

export default Squasher.withSquasher(
  (env) => ({
    apiKey: env.SQUASHER_API_KEY,
    projectId: env.SQUASHER_PROJECT_ID,
    environment: "production",
  }),
  {
    async fetch(request, env, ctx) {
      // Your handler logic — errors are automatically captured
      return new Response("Hello from the edge!");
    },
  },
);
```

That's it. Any unhandled error thrown from your `fetch`, `queue`, or `scheduled` handler is automatically captured, sent to Squasher, and re-thrown.

## How It Works

`withSquasher()` creates a fresh client for **every invocation** (not a long-lived singleton). This matches how Cloudflare Workers operate — each request is an independent execution context.

The lifecycle for each request:

1. `withSquasher` calls your config function with `env` to get the API key and project ID
2. Wraps your handler in a try/catch
3. On error: captures the error with full stack trace and context
4. After the handler returns (or throws): flushes all buffered events via `ctx.waitUntil()`
5. Events are sent to `ingest.squasher.ai` using the global `fetch()` API

If a single invocation captures multiple events, the SDK groups them into as few ingest requests as possible and automatically splits large flushes into smaller requests.

## Manual Capture

Inside a `withSquasher`-wrapped handler, you can manually capture errors and messages:

```typescript theme={null}
import * as Squasher from "@squasher/edge";

export default Squasher.withSquasher(
  (env) => ({
    apiKey: env.SQUASHER_API_KEY,
    projectId: env.SQUASHER_PROJECT_ID,
  }),
  {
    async fetch(request, env, ctx) {
      try {
        const data = await riskyOperation();
        return Response.json(data);
      } catch (error) {
        // Capture but don't crash
        Squasher.captureError(error, { operation: "riskyOperation" });
        return new Response("Something went wrong", { status: 500 });
      }
    },
  },
);
```

### Messages

```typescript theme={null}
Squasher.captureMessage("User signed up", "info");
Squasher.captureMessage("Rate limit approaching", "warning");
```

### Tags and Context

```typescript theme={null}
Squasher.setTag("region", "us-east-1");
Squasher.setTag("worker", "api-gateway");
Squasher.setTags({ version: "1.2.3", tenant: "acme" });

Squasher.setUser({
  id: "user_123",
  email: "user@example.com",
});
```

### Actionable Error Attributes

Edge errors can include AI-triage-friendly attributes. These make the error useful to engineers and agents without requiring them to infer every next step from the stack trace.

```typescript theme={null}
try {
  await forwardWebhook(request);
} catch (error) {
  Squasher.captureError(
    error,
    { operation: "forwardWebhook" },
    {
      attributes: {
        "error.why": "The downstream webhook endpoint rejected the delivery.",
        "error.fix": "Check the customer's webhook URL and retry policy.",
        "error.link": "https://docs.example.com/webhooks/delivery",
        "error.status": "actionable",
      },
    },
  );
  return new Response("Webhook delivery failed", { status: 502 });
}
```

Unhandled errors captured by `withSquasher` are marked fatal when they come from a failed invocation. The SDK also records normalized request attributes such as method, route, status, handler type, duration, and service name for fetch, queue, and scheduled handlers.

### Breadcrumbs

```typescript theme={null}
Squasher.addBreadcrumb({
  category: "http",
  message: "GET /api/users",
  data: { status: 200 },
});
```

## Tracing, Logs, and Metrics

`@squasher/edge` exposes a small OpenTelemetry-compatible API for spans, structured logs, and metrics. Same shape as standard OTel — pure OTLP wire format under the hood, so no vendor lock-in.

The active span is tracked through `AsyncLocalStorage`, so logs and metrics emitted inside a span automatically attach to it. Nested `span()` calls auto-parent.

### `span(name, fn)` — one-line spans

```typescript theme={null}
import { span } from "@squasher/edge";

const rows = await span("db.query", () => db.query("SELECT ..."));
```

With kind and attributes:

```typescript theme={null}
await span(
  "db.insert",
  { kind: "client", attrs: { "db.system": "database", rows: rows.length } },
  () => db.insertRows(rows),
);
```

If the function throws, the span is marked `status: "error"`, the error is captured with stack, and the error is re-thrown unchanged.

### `startSpan(name, opts?)` — manual lifecycle

Use when the start and end are not in the same scope (e.g., across an event emitter):

```typescript theme={null}
import { startSpan } from "@squasher/edge";

const handle = startSpan("upload.stream", { kind: "client" });
try {
  await streamUpload();
  handle.setAttr("bytes", totalBytes);
} catch (err) {
  handle.setStatus("error", err);
  throw err;
} finally {
  handle.end();
}
```

### `getActiveSpan()` — read current trace context

```typescript theme={null}
import { getActiveSpan, span } from "@squasher/edge";

await span("external.fetch", async () => {
  const active = getActiveSpan();
  const headers = new Headers();
  if (active) {
    headers.set("traceparent", `00-${active.traceId}-${active.spanId}-01`);
  }
  return fetch("https://api.example.com/data", { headers });
});
```

W3C `traceparent` propagates the trace across process boundaries. The receiving service reconstructs the parent and continues the same trace.

### `log.{debug, info, warn, error}(message, attrs?)`

```typescript theme={null}
import { log } from "@squasher/edge";

log.info("served request", { userId, rows: data.length });
log.warn("rate limit approaching", { remaining: 5 });
log.error(new Error("payment failed"), { userId, amount });
```

`log.error` accepts an `Error` directly and captures the stack. Logs emitted inside a span auto-correlate via `trace.id` and `span.id` attributes.

### `recordMetric(name, value, opts?)`

```typescript theme={null}
import { recordMetric } from "@squasher/edge";

recordMetric("cache.hit.rate", 0.94, { unit: "1" });
recordMetric("api.requests", 1, { type: "counter", attrs: { route: "/users" } });
recordMetric("queue.depth", 42, { unit: "{messages}" });
```

`type` defaults to `"gauge"`. Metrics inside a span auto-attach the trace context.

### `runWithSquasher(config, fn)` — Durable Object scope

`withSquasher` only covers your top-level handler. If you call `span()` / `log` / `recordMetric` from a **Durable Object method** (or any code path that runs outside the handler), wrap it with `runWithSquasher`:

```typescript theme={null}
import { runWithSquasher, span } from "@squasher/edge";

export class CacheWarmer {
  constructor(
    private state: DurableObjectState,
    private env: Env,
  ) {}

  async alarm() {
    await runWithSquasher(
      {
        apiKey: this.env.SQUASHER_API_KEY,
        projectId: this.env.SQUASHER_PROJECT_ID,
      },
      async () => {
        await span("batcher.flush", () => this.flushBatch());
      },
    );
  }
}
```

Without this, calls outside the handler-scoped client are no-ops.

### Cross-process traces

Inject `traceparent` on outbound HTTP, then on the receiving end pass `parent` to `span()`:

```typescript theme={null}
// Sender
const active = getActiveSpan();
const headers = active ? { traceparent: `00-${active.traceId}-${active.spanId}-01` } : {};

// Receiver
const traceparent = request.headers.get("traceparent");
const parts = traceparent?.split("-");
const parent =
  parts && parts.length >= 4 && parts[1] && parts[2]
    ? { traceId: parts[1], spanId: parts[2] }
    : undefined;
await span("downstream.work", { kind: "server", parent }, () => doWork());
```

## Supported Handlers

`withSquasher` wraps all Cloudflare Worker handler types:

| Handler     | Captured Context          |
| ----------- | ------------------------- |
| `fetch`     | URL, HTTP method          |
| `queue`     | Queue name, message count |
| `scheduled` | Cron expression           |

```typescript theme={null}
export default Squasher.withSquasher(
  (env) => ({ apiKey: env.SQUASHER_API_KEY, projectId: env.SQUASHER_PROJECT_ID }),
  {
    async fetch(request, env, ctx) {
      return new Response("OK");
    },
    async queue(batch, env, ctx) {
      for (const msg of batch.messages) {
        await processMessage(msg);
      }
    },
    async scheduled(event, env, ctx) {
      await runCleanupJob();
    },
  },
);
```

## Configuration

| Option           | Type       | Default                      | Description                           |
| ---------------- | ---------- | ---------------------------- | ------------------------------------- |
| `apiKey`         | `string`   | *required*                   | Your Squasher API key (`sq_pk_...`)   |
| `projectId`      | `string`   | *required*                   | Your Squasher project ID              |
| `endpoint`       | `string`   | `https://ingest.squasher.ai` | Ingestion endpoint                    |
| `environment`    | `string`   | —                            | Environment tag (production, staging) |
| `release`        | `string`   | —                            | Release/version tag                   |
| `debug`          | `boolean`  | `false`                      | Enable debug logging                  |
| `sampleRate`     | `number`   | `1`                          | Sampling rate 0–1                     |
| `beforeSend`     | `function` | —                            | Modify or drop events before sending  |
| `maxBreadcrumbs` | `number`   | `50`                         | Max breadcrumbs to retain             |

## Tracing API Reference

| Export                                     | Purpose                                                                   |
| ------------------------------------------ | ------------------------------------------------------------------------- |
| `span(name, fn)` / `span(name, opts, fn)`  | Wrap an async fn in a span. Auto-status, auto-parent.                     |
| `startSpan(name, opts?)`                   | Manual lifecycle. Returns `{ end, setAttr, setStatus, traceId, spanId }`. |
| `getActiveSpan()`                          | Read `{ traceId, spanId }` of the active span (or `undefined`).           |
| `log.{debug,info,warn,error}(msg, attrs?)` | Structured logs. Auto-correlates to active span.                          |
| `recordMetric(name, value, opts?)`         | OTLP gauge / counter. `opts.type`, `opts.unit`, `opts.attrs`.             |
| `withSquasher(configFn, handlers)`         | Wrap a Worker `fetch`/`queue`/`scheduled` handler (default).              |
| `runWithSquasher(config, fn)`              | Establish SDK scope outside a handler (e.g. inside a Durable Object).     |
| `createTraceId()` / `createSpanId()`       | Mint OTLP-compliant IDs (16 / 8 bytes hex).                               |
| `flush()`                                  | Force-flush pending events on the current client.                         |

## Environment Variables

Set your API key and project ID as Worker secrets:

```bash theme={null}
npx wrangler secret put SQUASHER_API_KEY
npx wrangler secret put SQUASHER_PROJECT_ID
```

## Differences from `@squasher/node`

| Feature        | `@squasher/node`                  | `@squasher/edge`                |
| -------------- | --------------------------------- | ------------------------------- |
| Runtime        | Node.js                           | Cloudflare Workers / edge       |
| Init pattern   | `init()` singleton                | `withSquasher()` per-request    |
| Error handlers | `process.on('uncaughtException')` | Handler try/catch wrapper       |
| Flushing       | `setInterval` timer               | `ctx.waitUntil()`               |
| Transport      | `fetch()`                         | `fetch()`                       |
| Lifecycle      | Long-lived process                | Per-invocation create + dispose |

## Known Limitations

* **Span durations show 0ms**: In Cloudflare Workers, `performance.now()` and `Date.now()` only advance after I/O. CPU-bound operations show zero duration.
* **30 events per invocation**: Hard buffer cap to prevent unbounded memory usage in short-lived Workers.
* **Retry backoff is shorter**: Max 3s backoff (vs 30s in Node SDK) to stay within Workers' wall-clock limits.

## Agent handoff

```text theme={null}
Install @squasher/edge for edge runtimes. Wrap the top-level handler with withSquasher, read apiKey and projectId from bindings or environment variables, use runWithSquasher for code outside the handler, and verify with one thrown test error.
```
