Skip to content

feat(browser): Astro integration - #1134

Open
JeremyFunk wants to merge 2 commits into
feat/browser-server-nextjsfrom
feat/browser-astro
Open

JeremyFunk wants to merge 2 commits into
feat/browser-server-nextjsfrom
feat/browser-astro

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

An Astro integration for @maple-dev/browser, replacing the glue the Astro frontend guide has customers copy (tracing.ts, the layout script, data-route={Astro.routePattern}, src/middleware.ts). Stacked on #1128.

// astro.config.mjs
import maple from "@maple-dev/browser/astro"
export default defineConfig({ integrations: [maple()] })

Plus the customer's own MapleBrowser.init() script in the base layout. Nothing else goes in the layouts.

Export Purpose
@maple-dev/browser/astro → maple(): AstroIntegration (default) addMiddleware (order pre) + injectScript("page", …): wires the two entries below into every page
@maple-dev/browser/astro/client → traceAstroNavigation(): void Page loads and <ClientRouter /> navigations, astro:hydration-error reporting. The integration calls it; without the integration, call it from the layout script
@maple-dev/browser/astro/middleware → onRequest: MiddlewareHandler Writes data-route on each page's <html> (at build time for prerendered pages), ssr <route> span + Server-Timing for on-demand pages. Composes with sequence()

astro is an optional peer (*), a dev dep for types and tests (same 7.3.5 as apps/landing). /astro/middleware depends on @opentelemetry/api only. The main entry is unchanged: eager 37.42 kB, first-party 14.30 kB, same as the base branch.

Why an integration

  • Route names without touching the layouts. The middleware writes Astro.routePattern onto the <html> start tag as the HTML streams past. Only the bytes up to the end of that tag are held back. Prerendered pages get it at build time: Astro runs middleware for them and writes the response it returns. A page that already has data-route (the guide's setup) keeps its own, and fragments (server islands, partials) pass through. After a rewrite (Astro.rewrite), the page that rendered wins, because Astro runs the middleware again for it.
  • One navigation script per page. injectScript("page") is one module, once per page, and <ClientRouter /> doesn't run it again after a swap. This removes the guide's "two layouts, two scripts, the second ends the first as interrupted" pitfall. traceAstroNavigation is also idempotent.
  • init stays in user code. Passing init options through the integration isn't safe. privacy.sanitizeUrl is a function, which can't be serialized into the injected script. import.meta.env.PUBLIC_* isn't what astro.config.mjs sees. The integration's script can land before the user's (it renders at the end of <head>), so traceAstroNavigation starts the page load at DOMContentLoaded, after every module script, init included, has run.

Behaviour vs the guide

Same span names, parents and edge cases: pageload <route> per document ended at load; navigate <route> from astro:before-preparation to astro:page-load, with a load page span and the page request under it; an aborted navigation is navigate interrupted; a full-page-load fallback is navigate with the generic name; redirects are named after the destination; hash links start no span; astro.hydration_error for island code that fails to load; ssr <routePattern> on the server, ending when streaming starts, with Server-Timing on HTML only, skipping prerendered pages and route-cached ones.

Changes:

  • Fixed: a click before the window's load event. The guide ended the in-flight navigate span at load, and at the astro:page-load the router fires for that same load, named after the old page. E2E: navigate /heavy 608 ms (old) vs navigate /slow-server 1542 ms (new). Now the load listener only ends the document's page load, and astro:page-load only ends a navigation whose page was swapped in.
  • Fixed: a stale astro:page-load. The page load of a swap that a newer navigation overtook no longer ends the newer one.
  • Server-Timing is skipped on more cacheable responses, since the customer can't edit packaged middleware: Astro's route cache (maxAge or swr), Cache-Control with public, s-maxage or max-age > 0, CDN-Cache-Control and vendor variants, Surrogate-Control (all unless no-store/private).
  • Response headers are copied rather than mutated, so a fetch() response with immutable headers doesn't throw. Set-Cookie and the status are kept. content-length is dropped when the body changes, and compressed bodies aren't read.

Verification

  • packages/browser: typecheck, 182 tests (node + Chromium), build, size budget. New tests:
    • html.test.ts: tag lookup, chunk splits, multibyte text, escaping, lookahead cap.
    • middleware.test.ts: the real sequence() from astro/middleware, streaming, rewrite re-run, cache rules, errors, Astro 4 and 6 contexts.
    • client.browser.test.ts: Astro's real doPreparation / TransitionBeforeSwapEvent / onPageLoad from astro/virtual-modules/transitions-events.js: interrupted, fallback, load race, stale page-load, DOMContentLoaded ordering, idempotency, hydration error.
  • oxlint + oxfmt on changed files; astro check on the e2e app.
  • End to end: the Astro 7.3.5 test app instrumented with the old guide code (@astrojs/node, prerendered pages plus prerender = false pages, Node SDK preloaded), switched to the packed tarball with integrations: [maple()] and only the init script left. Production build + node --import, Playwright over 21 scenarios, both builds (MPA and PUBLIC_CLIENT_ROUTER=1). Spans match the old run:
Scenario MPA ClientRouter
H1 pageload named by route, url.path concrete = =
H2 on-demand pageload child of ssr /server, new trace on reload, no header on prerendered/assets/404/redirect = =
H2b frontmatter error: ssr span Error + exception, no header = (status message now set) = (same)
H3 click → navigate /projects/[id] > load page > GET > server = (pageload) =
H4 param change = =
H5 query = navigation, hash = no span = =
H6 /old redirect = =
H7/H7b/H7c interrupted (client-slow and server-slow targets) = =
H8 back/forward = =
H9 not found (/404 template), no error spans = =
H10/H10b loader error, caught error: one exception = =
H11/H11b/H11c render error, boundary, astro.hydration_error = =
H12/H13 uncaught click error, backend 500: once each = =
H14 propagation: first-party gets traceparent, third party none = =
H16 prefetch on hover: own traces = =
H17 one span per click = =

(= means the normalized span tree is identical. The only other differences: one third-party and one slow fetch span that the old run dropped at page exit, and probe noise.)

Extra scenarios on the new build:

  • Click during the load race: fixed (above).
  • Astro.rewrite("/server"): data-route="/server", ssr /rewritten > ssr /server > pageload /server.
  • Server island: fragment untouched, ssr /_server-islands/[name].
  • Streaming page with a 1.5 s component: TTFB 2 ms, total 1.5 s, Set-Cookie kept.
  • Pure static build (no adapter) under astro preview: H1/H3–H9 correct.
  • astro dev: stamped and traced.
  • Manual wiring without the integration (export { onRequest } via sequence(maple, own) + traceAstroNavigation()): H2, H3, H7b, rewrite, load race all correct, and the user middleware's header is kept.

Known limits

  • Astro 5 or later (routePattern, isPrerendered). On Astro 4 the middleware passes everything through. astro:hydration-error needs 6.3, and the route-cache check needs 7.
  • The ssr span ends when streaming starts, as in the guide. Components rendered after that nest under it but run past its end, and an error thrown mid-stream isn't recorded.
  • A page whose document doesn't start with <html> (no doctype/comment preamble then <html> within 16 KiB) gets no route, and its spans keep the generic name.
  • The integration's script loads the SDK chunk on every page, including pages without the init script. To trace only some pages, wire it by hand.
  • A rewrite runs the middleware twice, so the response carries two Server-Timing traceparents (inner first). The browser joins the inner ssr span, in the same trace.
  • A navigation that another astro:before-preparation listener cancels, or whose custom loader throws, ends as interrupted at the next navigation or page exit (as in the guide).

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Devin Review

@coderabbitai

coderabbitai Bot commented Sep 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 20340c4f-340f-4889-a303-521942af5b0f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 3/5 · needs attention
quality 100/100 · no findings · tests covered · risk medium

Warning

This review ended early; what follows is what it established.

Adds the @maple-dev/browser/astro integration: middleware that stamps data-route on each page's <html> and opens an ssr <route> span (with Server-Timing traceparent), plus a client script tracing page loads, <ClientRouter /> navigations and hydration errors. Contained and consistent with the Next.js entry; safe to merge.

  • maple() integration wires an Astro middleware and an injected page script
  • stampRoute writes data-route on the first <html> start tag as HTML streams
  • onRequest opens an ssr <route> span and appends a traceparent Server-Timing entry
  • traceAstroNavigation spans page loads, <ClientRouter /> navigations and hydration errors

d6d6e66 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +9 to +10
const HTML_TAG = /^<html(?=[\s>])[^>]*>/i
const HAS_ROUTE = /\sdata-route[\s=>]/i

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Quoted attributes corrupt route stamping

When an HTML attribute contains > before an existing route, HTML_TAG misses that route and inserts a duplicate. When attribute text contains data-route=, HAS_ROUTE suppresses stamping entirely. Page spans then use the wrong route or keep generic names.

Learn more

The route stamper scans the opening <html> tag before adding the route template used to name browser spans. Its regular expression treats the first > as the end of the tag even when that character appears inside a quoted attribute. Its route check also searches raw tag text rather than parsed attributes, so quoted attribute values can impersonate data-route. Both cases make the emitted markup disagree with the intended route selection.

Example: With <html data-note="a > b" data-route="/custom">, the matcher stops at the quoted > and inserts a second data-route before /custom. With <html data-note=" data-route=/fake">, it sees no actual route attribute but adds none.

Recommended fix: Parse the <html> opening tag with a quote-aware scanner, including the lookahead across stream chunks. Check only actual attribute names for data-route, then insert the route only when it is absent.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant