# Effect SDK for Foldkit

Instrument a Foldkit app: Command spans grouped under page loads and navigations, scrubbed Command args, crash reports and slow-phase spans.

The `/foldkit` entry point of `@maple-dev/effect-sdk` instruments a [Foldkit](https://foldkit.dev) app. Foldkit already runs every Command inside an Effect span named after it. On their own those spans are disconnected root traces, one per focus move or scroll. `MapleFoldkit` turns them into one trace per page load or navigation, drops the Commands that did no real work, and adds crash reports and slow-phase spans.

<div class="flex flex-wrap gap-2 mb-8 not-prose">
    <span class="text-[10px] uppercase tracking-wider px-2 py-1 border border-border text-fg-muted">Browsers</span>
    <span class="text-[10px] uppercase tracking-wider px-2 py-1 border border-border text-fg-muted">Foldkit</span>
</div>

> Already installed the SDK? If not, see the [install instructions](/docs/sdks/effect#install). The `/foldkit` entry point ships in `@maple-dev/effect-sdk` 0.12.0 and later. New to Foldkit? Start with its [getting started guide](https://foldkit.dev/get-started).

## Quick start

Create the telemetry once, then wrap the config you pass to `Runtime.makeApplication` with `instrument`:

```typescript
import { MapleFoldkit } from "@maple-dev/effect-sdk/foldkit"
import { Runtime } from "foldkit"
import type { UrlRequest } from "foldkit/navigation"
import type { Url } from "foldkit/url"

const maple = MapleFoldkit.make({
	serviceName: "acme-web",
	ingestKey: "maple_pk_...",
	region: "eu", // omit for the US region
})

Runtime.run(
	Runtime.makeApplication(
		maple.instrument({
			Model,
			init,
			update,
			view,
			resources: ResourcesLive,
			container,
			routing: {
				onUrlRequest: (request: UrlRequest) => Message.ClickedLink({ request }),
				onUrlChange: (url: Url) => Message.ChangedUrl({ url }),
			},
		}),
	),
)
```

Annotate the `routing` callback parameters. TypeScript cannot infer them through `instrument`.

`instrument` merges the telemetry layer into `resources` and wraps `init`, `update`, `routing.onUrlChange`, `crash` and `slow`. Every other field passes through unchanged, and it works the same on a `makeElement` config. Your own `crash.report` and `slow.onSlow` callbacks still run after Maple's.

`MapleFoldkit.make` builds on [`MapleFlush.make`](/docs/sdks/effect-client), so flush on unload, sessions, session replay, privacy and global error capture all work as they do in the browser entry point, and it accepts every one of its options.

## What it records

### Page loads and navigations

The first `init` opens a `pageload <route>` span that starts at the browser's navigation start, not when your JavaScript got there. Every `routing.onUrlChange` opens a `navigate <route>` span. Both carry `url.path`.

The Commands that `init` or the URL change return are parented under that span, and so are the Commands started by their result Messages, and by those Messages' results in turn. `HttpClient` and RPC spans nest inside the Commands and send `traceparent`, so one trace runs from the click in the browser to your backend.

A navigation ends:

- 150ms after its last Command finished, when no other Command started in the meantime.
- 1 second after it started, if no Command started at all.
- After 30 seconds at the latest, or when the page goes away. Both set `maple.navigation.interrupted: true`.

A Command that runs outside any navigation (a Subscription tick, a button click that does not change the URL) is its own root trace.

### Commands that did work

A Command that finished without starting a child span is UI work: a focus move, a scroll, a hover delay. It is dropped. A Command that called an API, an RPC or a traced Effect exports. Failed Commands always export. List a Command in `keepCommands` to export it regardless.

Each exported Command carries `maple.foldkit.message`, the tag of the Message whose `update` returned it. A Submodel Message wrapped by its parent shows as `GotChat > SubmittedComposer`. Only `_tag`s are recorded, never Message fields.

### Scrubbed Command args

Foldkit puts a Command's args on its span. Apps pass message text, invite tokens and API keys through them, so the SDK exports them as `maple.foldkit.arg.<key>` under a default-deny policy:

| `commandArgs`             | Exports                                                                     |
| ------------------------- | --------------------------------------------------------------------------- |
| `"ids"` (default)         | Keys named `id` or ending in `Id`, `Ids`, `_id`, `_ids`; booleans and nulls |
| `{ allow: ["provider"] }` | The `"ids"` set plus the named keys                                         |
| `"all"`                   | Every arg                                                                   |
| `"none"`                  | No args                                                                     |

Any other arg is still present, with the value `[redacted]`, so you can see which args a Command took. Strings are capped at 256 characters and arrays at 20 items. Objects and arrays of objects are left out.

### Crashes

When the Foldkit runtime crashes, the SDK reports an exception named `foldkit.crash` and flushes right away, because nothing else will trigger a flush once the runtime has stopped. It carries:

| Attribute                       | Value                                                  |
| ------------------------------- | ------------------------------------------------------ |
| `maple.exception.source`        | `foldkit.crash`                                        |
| `maple.foldkit.message`         | Tag of the Message that crashed the runtime, or `init` |
| `maple.foldkit.recent_messages` | The last 20 Message tags, oldest first                 |

### Slow phases

An `update` over 16ms, or a `view` or DOM patch over 50ms, becomes a `foldkit.update`, `foldkit.view` or `foldkit.patch` span under the open navigation. Each carries `maple.foldkit.phase`, `maple.foldkit.duration_ms`, `maple.foldkit.threshold_ms` and, where known, `maple.foldkit.message`.

Change the budgets with `slowPhases: { Update: 8 }` (`0` records every run), or turn the spans off with `slowPhases: false`. A `slow.thresholdOverrides` in your Foldkit config wins over `slowPhases`.

## Name navigations by route

Without `routeName`, a navigation span is named after the path: `navigate /services/checkout-api`. That gives every service, trace and dashboard id its own span name. Map the path to your route instead, so the span is `navigate ServiceDetail`:

```typescript
import { Option } from "effect"
import { fromString } from "foldkit/url"

const routeName = (pathname: string): string =>
	Option.match(fromString(`https://app.invalid${pathname}`), {
		onNone: () => pathname,
		onSome: (url) => urlToAppRoute(url)._tag,
	})

const maple = MapleFoldkit.make({ serviceName: "acme-web", ingestKey: "maple_pk_...", routeName })
```

The full path stays on the span as `url.path`.

## Sampling

- `navigationSampleRate` traces a share of navigations, `1` by default. The decision is made once per navigation, and every Command and slow phase under it follows it.
- `sampleCommands` sets rates for chatty Commands outside a navigation, such as `{ SendTypingHeartbeat: 0.01 }`. Under a navigation, a Command always follows the navigation's decision.

## Configuration

`MapleFoldkit.make()` accepts every [`MapleFlush.make` option](/docs/sdks/effect#configuration-reference) (`serviceName`, `ingestKey`, `region`, `endpoint`, `replay`, `privacy`, the span options, `autoFlushInterval`, ...) plus these:

| Option                 | Type                                    | Default                               | Description                                                                    |
| ---------------------- | --------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------ |
| `commandArgs`          | `"ids" \| "all" \| "none" \| { allow }` | `"ids"`                               | Which Command args export. See [Scrubbed Command args](#scrubbed-command-args) |
| `keepCommands`         | `string[]`                              | `[]`                                  | Commands exported even without a child span                                    |
| `sampleCommands`       | `Record<string, number>`                | `{}`                                  | Per-Command sample rates outside a navigation                                  |
| `navigationSampleRate` | `number`                                | `1`                                   | Share of navigations traced                                                    |
| `routeName`            | `(pathname: string) => string`          | the path                              | Navigation span name suffix, e.g. a route name or template                     |
| `slowPhases`           | `false \| { Update?, View?, Patch? }`   | `{ Update: 16, View: 50, Patch: 50 }` | Slow-phase budgets in ms, or `false` to turn them off                          |

The returned object has `instrument`, plus the `layer`, `flush()` and `dispose()` of [`MapleFlush.make`](/docs/sdks/effect-client). `dispose()` also ends any open navigation so it goes out with the final flush.

## Verify

1. Load a page of your app and click through to another route.
2. In Maple, open **Explore → Traces** and filter on your service name. You should see a `pageload <route>` trace and a `navigate <route>` trace, with the Commands that fetched data nested inside them and your backend's spans below those.
3. Look for Commands such as focus or scroll moves. They should not be there.

## Troubleshooting

- **Commands show up as separate root traces.** Only Commands caused by a page load or a URL change join a navigation. Check that URL changes go through `routing.onUrlChange`. Commands started by Subscriptions or by clicks that keep the URL are root traces by design.
- **A Command you need is missing.** It finished without starting a child span, so it was treated as UI work. Add it to `keepCommands`.
- **An arg shows `[redacted]`.** Its key is not an id. Add it to `commandArgs: { allow: [...] }` once you are sure it holds no user content.
- **A span name per id, like `navigate /traces/4bf92f...`.** Set `routeName`.
- **TypeScript errors on the `routing` callbacks.** Annotate their parameters, as in the quick start.

For key, region and CORS problems, see the [browser troubleshooting](/docs/sdks/effect-client#troubleshooting).

## Next steps

- [Effect SDK in the browser](/docs/sdks/effect-client)
- [Foldkit documentation](https://foldkit.dev)
- [Session replays](/docs/session-replay/replays)
- [Explore traces](/docs/explore/traces)
- [OpenTelemetry conventions](/docs/concepts/otel-conventions)
