Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions apps/landing/src/content/docs/session-replay/browser-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,71 @@ export const config = {
- `withMapleProxy` keeps a `traceparent` the request already carries, as client navigations do, and only changes responses that go on to a render in your app. Redirects and responses your proxy builds itself pass through unchanged. If a shared cache such as a CDN stores prerendered pages, leave them out of the matcher, or every visitor joins the same trace.
- Server Components can time database and SDK calls with `traced` from `@maple-dev/browser/server`. Pass `isFailure: () => false`: Next.js records an error thrown from a Server Component on its render span, and `redirect()` and `notFound()` work by throwing.

## SvelteKit integration

`@maple-dev/browser/sveltekit` connects SvelteKit's client router (SvelteKit 2.12 or later) to the navigation spans, and `@maple-dev/browser/sveltekit/server` joins the first page load to the server render. Start the page load and report errors from the client hooks:

```ts
// src/hooks.client.ts
import "$lib/maple" // first: your MapleBrowser.init call
import { handleErrorWithMaple, startPageLoad } from "@maple-dev/browser/sveltekit"

export const init = startPageLoad
export const handleError = handleErrorWithMaple() // or handleErrorWithMaple(yourHandleError)
```

Pass the router's hooks from the root layout. They are SvelteKit modules only your app can import:

```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { afterNavigate, beforeNavigate } from "$app/navigation"
import { navigating, page } from "$app/state"
import { traceNavigation } from "@maple-dev/browser/sveltekit"

let { children } = $props()

traceNavigation({ beforeNavigate, afterNavigate, navigating, page })
</script>

{@render children()}
```

Wrap universal `load` functions, in `+page.ts` and `+layout.ts`:

```ts
// src/routes/projects/[id]/+page.ts
import { loadSpan } from "@maple-dev/browser/sveltekit"
import type { PageLoad } from "./$types"

export const load: PageLoad = async ({ fetch, params, route }) =>
loadSpan(`loader ${route.id}`, async () => {
const [project, members] = await Promise.all([
fetch(`/api/projects/${params.id}`),
fetch(`/api/projects/${params.id}/members`),
])
return { project: await project.json(), members: await members.json() }
})
```

With SvelteKit's server tracing on (`experimental.tracing.server`, SvelteKit 2.31 or later), add the `Server-Timing` header to rendered pages:

```ts
// src/hooks.server.ts
import { mapleHandle } from "@maple-dev/browser/sveltekit/server"

export const handle = mapleHandle // or sequence(mapleHandle, yourHandle)
```

- Spans are named after the route id, like `navigate /projects/[id]`, route groups included, the same string SvelteKit puts in `http.route` on its server spans.
- A click while a navigation loads keeps one span, named after where it ends. A cancelled navigation ends right away, as interrupted. A `redirect()` from a load stays in the span, named after the destination.
- Loads SvelteKit preloads when you hover a link run before the click, as their own traces. The navigation span after the click then has no loads under it.
- Hash links, `invalidate()` and shallow routing start no span. A query change is a navigation. A link no route matches loads a new document, whose span is a bare `pageload`.
- `loadSpan` doesn't mark `redirect()` or `error()` below 500 as failures. On the server it only runs your function: SvelteKit's tracing already records each load.
- `handleErrorWithMaple` reports what SvelteKit hands `handleError` as `sveltekit.client_error`, skipping 404s (unknown routes) and errors `loadSpan` already recorded, and returns what your handler returns.
- `mapleHandle` names SvelteKit's `sveltekit.handle.root` span on HTML responses and drops their `ETag`, since a 304 revalidation would reuse the cached header and an earlier trace. Put it first in `sequence`. If a shared cache such as a CDN stores your pages, skip it for those, or every visitor joins the same trace.
- SvelteKit 2.10 and 2.11 have no `$app/state`. Pass `navigating: { get type() { return get(navigatingStore)?.type ?? null } }` and `page: { get route() { return get(pageStore).route } }` instead, with both stores from `$app/stores` and `get` from `svelte/store`.

## Custom events

`track(name, props)` records a product event against the current session. It appears inline in the session transcript next to the clicks and network calls around it, and counts as a [product event](/docs/product-events/overview).
Expand Down
37 changes: 36 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,14 @@
},
"packages/browser": {
// One source module per `exports` subpath; the built `dist/` names don't map back to them.
"entry": ["src/index.ts", "src/server.ts", "src/nextjs/index.ts", "src/nextjs/server.ts"],
"entry": [
"src/index.ts",
"src/server.ts",
"src/nextjs/index.ts",
"src/nextjs/server.ts",
"src/sveltekit/index.ts",
"src/sveltekit/server.ts"
],
"ignoreDependencies": ["rrweb"]
}
},
Expand Down
57 changes: 57 additions & 0 deletions packages/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,63 @@ export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico
- The browser follows the server's sampling decision: a page load under an
unsampled server trace isn't recorded.

## SvelteKit

`@maple-dev/browser/sveltekit` wires SvelteKit's client router (2.12+) to the
navigation spans:

```ts
// src/hooks.client.ts
import "$lib/maple" // your MapleBrowser.init call, first
import { handleErrorWithMaple, startPageLoad } from "@maple-dev/browser/sveltekit"

export const init = startPageLoad // the page load
export const handleError = handleErrorWithMaple() // or handleErrorWithMaple(yourHandleError)
```

```svelte
<!-- src/routes/+layout.svelte: the root layout -->
<script lang="ts">
import { afterNavigate, beforeNavigate } from "$app/navigation"
import { navigating, page } from "$app/state"
import { traceNavigation } from "@maple-dev/browser/sveltekit"

let { children } = $props()
traceNavigation({ beforeNavigate, afterNavigate, navigating, page })
</script>

{@render children()}
```

```ts
// src/routes/projects/[id]/+page.ts: universal loads
import { loadSpan } from "@maple-dev/browser/sveltekit"
import type { PageLoad } from "./$types"

export const load: PageLoad = async ({ fetch, params, route }) =>
loadSpan(`loader ${route.id}`, async () => (await fetch(`/api/projects/${params.id}`)).json())
```

```ts
// src/hooks.server.ts: joins the page load to SvelteKit's server trace (2.31+)
import { mapleHandle } from "@maple-dev/browser/sveltekit/server"

export const handle = mapleHandle // or sequence(mapleHandle, yourHandle)
```

- Spans are named after the route id, like `navigate /projects/[id]`. A click
while a navigation loads, or a `redirect()`, stays in one span named after
where it ends; a cancelled navigation ends right away, as interrupted. Hash
links start no span.
- The `$app/*` modules are passed in because only SvelteKit's Vite plugin
resolves them.
- `loadSpan` skips `redirect()` and `error()` below 500, and only runs `fn` on
the server, where SvelteKit spans loads itself. `handleErrorWithMaple` skips
404s and errors `loadSpan` recorded.
- `mapleHandle` adds `Server-Timing` to HTML responses and drops their `ETag`
(a 304 would reuse the cached header). Skip pages a shared cache (CDN) stores,
or every visitor joins one trace.

## Linking a marketing site to your app

The visitor id lives in localStorage **and** a cookie scoped to your registered
Expand Down
13 changes: 13 additions & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@
"./nextjs/server": {
"types": "./dist/nextjs-server.d.mts",
"import": "./dist/nextjs-server.mjs"
},
"./sveltekit": {
"types": "./dist/sveltekit.d.mts",
"import": "./dist/sveltekit.mjs"
},
"./sveltekit/server": {
"types": "./dist/sveltekit-server.d.mts",
"import": "./dist/sveltekit-server.mjs"
}
},
"publishConfig": {
Expand All @@ -67,6 +75,7 @@
"devDependencies": {
"@maple/browser-session": "workspace:*",
"@opentelemetry/context-async-hooks": "^2.11.0",
"@sveltejs/kit": "^2.70.3",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@vitest/browser-playwright": "catalog:",
Expand All @@ -79,10 +88,14 @@
"vitest": "catalog:"
},
"peerDependencies": {
"@sveltejs/kit": "*",
"next": "*",
"react": "*"
},
"peerDependenciesMeta": {
"@sveltejs/kit": {
"optional": true
},
"next": {
"optional": true
},
Expand Down
Loading
Loading