From 47cb3187f7a248ccfd8776f650107405ad8558a7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 11 Jun 2026 15:37:41 +0200 Subject: [PATCH] astro 7 e2e --- .../test-applications/astro-7/.gitignore | 26 ++ .../astro-7/astro.config.mjs | 24 + .../test-applications/astro-7/package.json | 28 ++ .../astro-7/playwright.config.mjs | 13 + .../astro-7/public/favicon.svg | 9 + .../astro-7/sentry.client.config.js | 22 + .../astro-7/sentry.server.config.js | 9 + .../astro-7/src/assets/astro.svg | 1 + .../astro-7/src/assets/background.svg | 1 + .../astro-7/src/components/Avatar.astro | 5 + .../astro-7/src/components/Welcome.astro | 205 +++++++++ .../test-applications/astro-7/src/fetch.ts | 39 ++ .../astro-7/src/layouts/Layout.astro | 22 + .../src/pages/api/user/[userId].json.js | 8 + .../astro-7/src/pages/blog/[slug].astro | 11 + .../src/pages/catchAll/[...path].astro | 11 + .../src/pages/client-error/index.astro | 7 + .../astro-7/src/pages/endpoint-error/api.ts | 15 + .../src/pages/endpoint-error/index.astro | 9 + .../astro-7/src/pages/index.astro | 23 + .../src/pages/server-island/index.astro | 14 + .../astro-7/src/pages/ssr-error/index.astro | 11 + .../astro-7/src/pages/test-ssr/index.astro | 11 + .../astro-7/src/pages/test-static/index.astro | 11 + .../src/pages/user-page/[userId].astro | 16 + .../src/pages/user-page/settings.astro | 7 + .../astro-7/start-event-proxy.mjs | 6 + .../astro-7/tests/advanced-routing.test.ts | 44 ++ .../astro-7/tests/errors.client.test.ts | 79 ++++ .../astro-7/tests/errors.server.test.ts | 160 +++++++ .../astro-7/tests/tracing.dynamic.test.ts | 411 ++++++++++++++++++ .../tests/tracing.serverIslands.test.ts | 97 +++++ .../astro-7/tests/tracing.static.test.ts | 57 +++ .../test-applications/astro-7/tsconfig.json | 5 + packages/astro/package.json | 2 +- 35 files changed, 1418 insertions(+), 1 deletion(-) create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/astro.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/package.json create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/public/favicon.svg create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/sentry.client.config.js create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/sentry.server.config.js create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/assets/astro.svg create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/assets/background.svg create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/components/Avatar.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/components/Welcome.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/fetch.ts create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/layouts/Layout.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/api/user/[userId].json.js create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/blog/[slug].astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/catchAll/[...path].astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/client-error/index.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/endpoint-error/api.ts create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/endpoint-error/index.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/index.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/server-island/index.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/ssr-error/index.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/test-ssr/index.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/test-static/index.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/user-page/[userId].astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/src/pages/user-page/settings.astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/tests/advanced-routing.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/tests/errors.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/tests/errors.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.dynamic.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.serverIslands.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.static.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/astro-7/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/astro-7/.gitignore b/dev-packages/e2e-tests/test-applications/astro-7/.gitignore new file mode 100644 index 000000000000..560782d47d98 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/.gitignore @@ -0,0 +1,26 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ + +test-results diff --git a/dev-packages/e2e-tests/test-applications/astro-7/astro.config.mjs b/dev-packages/e2e-tests/test-applications/astro-7/astro.config.mjs new file mode 100644 index 000000000000..234a57fca662 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/astro.config.mjs @@ -0,0 +1,24 @@ +import sentry from '@sentry/astro'; +// @ts-check +import { defineConfig } from 'astro/config'; + +import node from '@astrojs/node'; + +// https://astro.build/config +export default defineConfig({ + integrations: [ + sentry({ + debug: true, + sourceMapsUploadOptions: { + enabled: false, + }, + }), + ], + output: 'server', + security: { + allowedDomains: [{ hostname: 'localhost' }], + }, + adapter: node({ + mode: 'standalone', + }), +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7/package.json b/dev-packages/e2e-tests/test-applications/astro-7/package.json new file mode 100644 index 000000000000..aaf618146769 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/package.json @@ -0,0 +1,28 @@ +{ + "name": "astro-7", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro", + "start": "node ./dist/server/entry.mjs", + "test:build": "pnpm install && pnpm build", + "test:assert": "TEST_ENV=production playwright test" + }, + "dependencies": { + "@astrojs/node": "^11.0.0-alpha.0", + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/astro": "file:../../packed/sentry-astro-packed.tgz", + "astro": "beta" + }, + "volta": { + "node": "22.22.0", + "extends": "../../package.json" + }, + "sentryTest": { + "optional": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/astro-7/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/astro-7/playwright.config.mjs new file mode 100644 index 000000000000..ae58e4ff3ddc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/playwright.config.mjs @@ -0,0 +1,13 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const config = getPlaywrightConfig({ + startCommand: 'pnpm start', +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/astro-7/public/favicon.svg b/dev-packages/e2e-tests/test-applications/astro-7/public/favicon.svg new file mode 100644 index 000000000000..f157bd1c5e28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-7/sentry.client.config.js b/dev-packages/e2e-tests/test-applications/astro-7/sentry.client.config.js new file mode 100644 index 000000000000..83573d36d0be --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/sentry.client.config.js @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/astro'; + +Sentry.init({ + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, + environment: 'qa', + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', // proxy server + integrations: [ + Sentry.browserTracingIntegration({ + beforeStartSpan: opts => { + if (opts.name.startsWith('/blog/')) { + return { + ...opts, + name: window.location.pathname, + }; + } + return opts; + }, + }), + ], + debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-7/sentry.server.config.js new file mode 100644 index 000000000000..bc90470cef38 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/sentry.server.config.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/astro'; + +Sentry.init({ + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, + environment: 'qa', + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', // proxy server + debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/assets/astro.svg b/dev-packages/e2e-tests/test-applications/astro-7/src/assets/astro.svg new file mode 100644 index 000000000000..8cf8fb0c7da6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/assets/astro.svg @@ -0,0 +1 @@ + diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/assets/background.svg b/dev-packages/e2e-tests/test-applications/astro-7/src/assets/background.svg new file mode 100644 index 000000000000..4b2be0ac0e47 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/assets/background.svg @@ -0,0 +1 @@ + diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/components/Avatar.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/components/Avatar.astro new file mode 100644 index 000000000000..5611579efaf1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/components/Avatar.astro @@ -0,0 +1,5 @@ +--- + +--- + +User avatar diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/components/Welcome.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/components/Welcome.astro new file mode 100644 index 000000000000..6f862e767574 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/components/Welcome.astro @@ -0,0 +1,205 @@ +--- +import astroLogo from '../assets/astro.svg'; +import background from '../assets/background.svg'; +--- + +
+ +
+
+ Astro Homepage +

+ To get started, open the
src/pages
directory in your project. +

+ +
+
+ + + +

What's New in Astro 5.0?

+

+ From content layers to server islands, click to learn more about the new features and improvements in Astro 5.0 +

+
+
+ + diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/fetch.ts b/dev-packages/e2e-tests/test-applications/astro-7/src/fetch.ts new file mode 100644 index 000000000000..d6951c6bc106 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/fetch.ts @@ -0,0 +1,39 @@ +import type { Fetchable } from 'astro'; +import { astro, FetchState } from 'astro/fetch'; + +// Astro 7 "advanced routing": `src/fetch.ts` lets the app own the request +// pipeline. We use it to verify that Sentry instrumentation still works when +// the user composes the pipeline themselves instead of relying on Astro's +// built-in handler. +// +// - `/fetch-custom` is handled entirely here, bypassing Astro's pipeline. +// The OTel HTTP instrumentation still creates an `http.server` span, but +// Sentry's auto-injected Astro middleware does NOT run (no route enhancement). +// - Every other request flows through Astro's full pipeline via `astro(state)`, +// which still runs Sentry's auto-injected middleware (route parametrization, +// trace meta tag injection, error capture, etc.). +export default { + async fetch(request: Request) { + const url = new URL(request.url); + + if (url.pathname === '/fetch-custom') { + return new Response('handled-by-fetch-entrypoint', { + headers: { + 'content-type': 'text/plain', + 'x-astro-advanced-routing': 'custom', + }, + }); + } + + const state = new FetchState(request); + const response = await astro(state); + + // Tag the response so the e2e test can prove the custom pipeline ran. + try { + response.headers.set('x-astro-advanced-routing', 'pipeline'); + } catch { + // Some responses may have immutable headers; ignore in that case. + } + return response; + }, +} satisfies Fetchable; diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/layouts/Layout.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/layouts/Layout.astro new file mode 100644 index 000000000000..6105f48ffd35 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/layouts/Layout.astro @@ -0,0 +1,22 @@ + + + + + + + + Astro Basics + + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/api/user/[userId].json.js b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/api/user/[userId].json.js new file mode 100644 index 000000000000..481c8979dc89 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/api/user/[userId].json.js @@ -0,0 +1,8 @@ +export function GET({ params }) { + return new Response( + JSON.stringify({ + greeting: `Hello ${params.userId}`, + userId: params.userId, + }), + ); +} diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/blog/[slug].astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/blog/[slug].astro new file mode 100644 index 000000000000..b776fa25c494 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/blog/[slug].astro @@ -0,0 +1,11 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; + +const { slug } = Astro.params; +--- + + +

Blog post: {slug}

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/catchAll/[...path].astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/catchAll/[...path].astro new file mode 100644 index 000000000000..9fe2bdab5c15 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/catchAll/[...path].astro @@ -0,0 +1,11 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; + +const params = Astro.params; +--- + + +

params: {params}

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/client-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/client-error/index.astro new file mode 100644 index 000000000000..492524e2a713 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/client-error/index.astro @@ -0,0 +1,7 @@ +--- +import Layout from '../../layouts/Layout.astro'; +--- + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/endpoint-error/api.ts b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/endpoint-error/api.ts new file mode 100644 index 000000000000..a76accdba010 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/endpoint-error/api.ts @@ -0,0 +1,15 @@ +import type { APIRoute } from 'astro'; + +export const prerender = false; + +export const GET: APIRoute = ({ request, url }) => { + if (url.searchParams.has('error')) { + throw new Error('Endpoint Error'); + } + return new Response( + JSON.stringify({ + search: url.search, + sp: url.searchParams, + }), + ); +}; diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/endpoint-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/endpoint-error/index.astro new file mode 100644 index 000000000000..ecfb0641144e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/endpoint-error/index.astro @@ -0,0 +1,9 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; +--- + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/index.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/index.astro new file mode 100644 index 000000000000..7032437764f8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/index.astro @@ -0,0 +1,23 @@ +--- +import Welcome from '../components/Welcome.astro'; +import Layout from '../layouts/Layout.astro'; + +// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build +// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. +--- + + +
+

Astro E2E Test App

+ +
+
diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/server-island/index.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/server-island/index.astro new file mode 100644 index 000000000000..0e922af4667f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/server-island/index.astro @@ -0,0 +1,14 @@ +--- +import Avatar from '../../components/Avatar.astro'; +import Layout from '../../layouts/Layout.astro'; + +export const prerender = true; +--- + + +

This page is static, except for the avatar which is loaded dynamically from the server

+ + +

Fallback

+
+
diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/ssr-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/ssr-error/index.astro new file mode 100644 index 000000000000..fc42bcbae4f7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/ssr-error/index.astro @@ -0,0 +1,11 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +const a = {} as any; +console.log(a.foo.x); +export const prerender = false; +--- + + +

Page with SSR error

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/test-ssr/index.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/test-ssr/index.astro new file mode 100644 index 000000000000..4531c20c05ad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/test-ssr/index.astro @@ -0,0 +1,11 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; +--- + + +

This is a server page

+ + +
diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/test-static/index.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/test-static/index.astro new file mode 100644 index 000000000000..c0fd701d4a2a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/test-static/index.astro @@ -0,0 +1,11 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = true; +--- + + +

This is a static page

+ + +
diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/user-page/[userId].astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/user-page/[userId].astro new file mode 100644 index 000000000000..8050e386a39f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/user-page/[userId].astro @@ -0,0 +1,16 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; + +const { userId } = Astro.params; + +const response = await fetch(Astro.url.origin + `/api/user/${userId}.json`); +const data = await response.json(); +--- + + +

{data.greeting}

+ +

data: {JSON.stringify(data)}

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-7/src/pages/user-page/settings.astro b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/user-page/settings.astro new file mode 100644 index 000000000000..8260e632c07b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/src/pages/user-page/settings.astro @@ -0,0 +1,7 @@ +--- +import Layout from '../../layouts/Layout.astro'; +--- + + +

User Settings

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-7/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/astro-7/start-event-proxy.mjs new file mode 100644 index 000000000000..9dca8e6f667e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'astro-7', +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7/tests/advanced-routing.test.ts b/dev-packages/e2e-tests/test-applications/astro-7/tests/advanced-routing.test.ts new file mode 100644 index 000000000000..0b94323e233e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/tests/advanced-routing.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +// Astro 7 "advanced routing": the app owns the request pipeline via `src/fetch.ts`. +// These tests verify Sentry behaves correctly both when the app delegates to +// Astro's full pipeline and when it short-circuits a request entirely. +test.describe('astro 7 advanced routing (src/fetch.ts)', () => { + test("handles a fully custom route outside Astro's pipeline", async ({ request }) => { + const response = await request.get('/fetch-custom'); + + expect(response.status()).toBe(200); + expect(await response.text()).toBe('handled-by-fetch-entrypoint'); + // The custom branch in `src/fetch.ts` ran (Astro's pipeline was bypassed). + expect(response.headers()['x-astro-advanced-routing']).toBe('custom'); + }); + + test('Sentry middleware still parametrizes routes when run through the full pipeline', async ({ page }) => { + // Hit a dynamic route so we exercise the actual URL -> route parametrization + // (`/user-page/myUsername123` -> `/user-page/[userId]`), not just a static route name. + const serverPageRequestTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction === 'GET /user-page/[userId]'; + }); + + const response = await page.goto('/user-page/myUsername123'); + + // Proves the request flowed through our custom `astro(state)` pipeline wrapper. + expect(response?.headers()['x-astro-advanced-routing']).toBe('pipeline'); + + const serverPageRequestTxn = await serverPageRequestTxnPromise; + + // The parametrized transaction name proves Sentry's auto-injected middleware + // ran inside the user-owned pipeline AND resolved the dynamic segment to + // `[userId]` via Astro's route manifest (rather than leaving the raw URL). + expect(serverPageRequestTxn.transaction).toBe('GET /user-page/[userId]'); + expect(serverPageRequestTxn.contexts?.trace).toMatchObject({ + op: 'http.server', + origin: 'auto.http.astro', + data: expect.objectContaining({ + 'sentry.source': 'route', + url: expect.stringContaining('/user-page/myUsername123'), + }), + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/astro-7/tests/errors.client.test.ts new file mode 100644 index 000000000000..6c5e9dd3d2b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/tests/errors.client.test.ts @@ -0,0 +1,79 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', () => { + test('captures error thrown on click', async ({ page }) => { + const errorEventPromise = waitForError('astro-7', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'client error'; + }); + + await page.goto('/client-error'); + + await page.getByText('Throw Error').click(); + + const errorEvent = await errorEventPromise; + + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + colno: expect.any(Number), + lineno: expect.any(Number), + filename: expect.stringContaining('/client-error'), + function: 'HTMLButtonElement.onclick', + in_app: true, + }), + ); + + expect(errorEvent).toMatchObject({ + exception: { + values: [ + { + mechanism: { + handled: false, + type: 'auto.browser.global_handlers.onerror', + }, + type: 'Error', + value: 'client error', + stacktrace: expect.any(Object), // detailed check above + }, + ], + }, + level: 'error', + platform: 'javascript', + request: { + url: expect.stringContaining('/client-error'), + headers: { + 'User-Agent': expect.any(String), + }, + }, + event_id: expect.stringMatching(/[a-f0-9]{32}/), + timestamp: expect.any(Number), + sdk: { + integrations: expect.arrayContaining([ + 'InboundFilters', + 'FunctionToString', + 'BrowserApiErrors', + 'Breadcrumbs', + 'GlobalHandlers', + 'LinkedErrors', + 'Dedupe', + 'HttpContext', + 'BrowserSession', + 'BrowserTracing', + ]), + name: 'sentry.javascript.astro', + version: expect.any(String), + packages: expect.any(Array), + }, + transaction: '/client-error', + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + environment: 'qa', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/astro-7/tests/errors.server.test.ts new file mode 100644 index 000000000000..5ba69676aba1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/tests/errors.server.test.ts @@ -0,0 +1,160 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', () => { + test('captures SSR error', async ({ page }) => { + const errorEventPromise = waitForError('astro-7', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === "Cannot read properties of undefined (reading 'x')"; + }); + + const transactionEventPromise = waitForTransaction('astro-7', transactionEvent => { + return transactionEvent.transaction === 'GET /ssr-error'; + }); + + // This page returns an error status code, so we need to catch the navigation error + await page.goto('/ssr-error').catch(() => { + // Expected to fail with net::ERR_HTTP_RESPONSE_CODE_FAILURE in newer Chromium versions + }); + + const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toMatchObject({ + transaction: 'GET /ssr-error', + spans: [], + }); + + const traceId = transactionEvent.contexts?.trace?.trace_id; + const spanId = transactionEvent.contexts?.trace?.span_id; + + expect(traceId).toMatch(/[a-f0-9]{32}/); + expect(spanId).toMatch(/[a-f0-9]{16}/); + expect(transactionEvent.contexts?.trace?.parent_span_id).toBeUndefined(); + + expect(errorEvent).toMatchObject({ + contexts: { + app: expect.any(Object), + cloud_resource: expect.any(Object), + culture: expect.any(Object), + device: expect.any(Object), + os: expect.any(Object), + runtime: expect.any(Object), + trace: { + span_id: spanId, + trace_id: traceId, + }, + }, + environment: 'qa', + event_id: expect.stringMatching(/[a-f0-9]{32}/), + exception: { + values: [ + { + mechanism: { + handled: false, + type: 'auto.middleware.astro', + }, + stacktrace: expect.any(Object), + type: 'TypeError', + value: "Cannot read properties of undefined (reading 'x')", + }, + ], + }, + platform: 'node', + request: { + cookies: {}, + headers: expect.objectContaining({ + // demonstrates that requestData integration is getting data + host: 'localhost:3030', + 'user-agent': expect.any(String), + }), + method: 'GET', + url: expect.stringContaining('/ssr-error'), + }, + sdk: { + integrations: expect.any(Array), + name: 'sentry.javascript.astro', + packages: expect.any(Array), + version: expect.any(String), + }, + server_name: expect.any(String), + timestamp: expect.any(Number), + transaction: 'GET /ssr-error', + }); + }); + + test('captures endpoint error', async ({ page }) => { + const errorEventPromise = waitForError('astro-7', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Endpoint Error'; + }); + const transactionEventApiPromise = waitForTransaction('astro-7', transactionEvent => { + return transactionEvent.transaction === 'GET /endpoint-error/api'; + }); + const transactionEventEndpointPromise = waitForTransaction('astro-7', transactionEvent => { + return transactionEvent.transaction === 'GET /endpoint-error'; + }); + + await page.goto('/endpoint-error'); + await page.getByText('Get Data').click(); + + const errorEvent = await errorEventPromise; + const transactionEventApi = await transactionEventApiPromise; + const transactionEventEndpoint = await transactionEventEndpointPromise; + + expect(transactionEventEndpoint).toMatchObject({ + transaction: 'GET /endpoint-error', + spans: [], + }); + + const traceId = transactionEventEndpoint.contexts?.trace?.trace_id; + const endpointSpanId = transactionEventApi.contexts?.trace?.span_id; + + expect(traceId).toMatch(/[a-f0-9]{32}/); + expect(endpointSpanId).toMatch(/[a-f0-9]{16}/); + + expect(transactionEventApi).toMatchObject({ + transaction: 'GET /endpoint-error/api', + spans: [], + }); + + const spanId = transactionEventApi.contexts?.trace?.span_id; + const parentSpanId = transactionEventApi.contexts?.trace?.parent_span_id; + + expect(spanId).toMatch(/[a-f0-9]{16}/); + expect(parentSpanId).toMatch(/[a-f0-9]{16}/); + expect(parentSpanId).not.toEqual(endpointSpanId); + + expect(errorEvent).toMatchObject({ + contexts: { + trace: { + parent_span_id: parentSpanId, + span_id: spanId, + trace_id: traceId, + }, + }, + exception: { + values: [ + { + mechanism: { + handled: false, + type: 'auto.middleware.astro', + }, + stacktrace: expect.any(Object), + type: 'Error', + value: 'Endpoint Error', + }, + ], + }, + platform: 'node', + request: { + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + }), + method: 'GET', + query_string: 'error=1', + url: expect.stringContaining('endpoint-error/api?error=1'), + }, + transaction: 'GET /endpoint-error/api', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.dynamic.test.ts new file mode 100644 index 000000000000..0806d939b529 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.dynamic.test.ts @@ -0,0 +1,411 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('tracing in dynamically rendered (ssr) routes', () => { + test('sends server and client pageload spans with the same trace id', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction === '/test-ssr'; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction === 'GET /test-ssr'; + }); + + await page.goto('/test-ssr'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + + const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; + const clientPageloadParentSpanId = clientPageloadTxn.contexts?.trace?.parent_span_id; + + const serverPageRequestTraceId = serverPageRequestTxn.contexts?.trace?.trace_id; + const serverPageloadSpanId = serverPageRequestTxn.contexts?.trace?.span_id; + + expect(clientPageloadTraceId).toEqual(serverPageRequestTraceId); + expect(clientPageloadParentSpanId).toEqual(serverPageloadSpanId); + + expect(clientPageloadTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', + }), + op: 'pageload', + origin: 'auto.pageload.astro', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + environment: 'qa', + event_id: expect.stringMatching(/[a-f0-9]{32}/), + measurements: expect.any(Object), + platform: 'javascript', + request: expect.any(Object), + sdk: { + integrations: expect.any(Array), + name: 'sentry.javascript.astro', + packages: expect.any(Array), + version: expect.any(String), + }, + spans: expect.any(Array), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/test-ssr', + transaction_info: { + source: 'route', + }, + type: 'transaction', + }); + + expect(serverPageRequestTxn).toMatchObject({ + contexts: { + app: expect.any(Object), + cloud_resource: expect.any(Object), + culture: expect.any(Object), + device: expect.any(Object), + os: expect.any(Object), + otel: expect.any(Object), + runtime: expect.any(Object), + trace: { + data: { + 'http.response.status_code': 200, + method: 'GET', + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + url: expect.stringContaining('/test-ssr'), + 'http.request.header.accept': expect.any(String), + 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd', + 'http.request.header.accept_language': 'en-US', + 'http.request.header.sec_fetch_mode': 'navigate', + 'http.request.header.user_agent': expect.any(String), + }, + op: 'http.server', + origin: 'auto.http.astro', + status: 'ok', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + environment: 'qa', + event_id: expect.stringMatching(/[a-f0-9]{32}/), + platform: 'node', + request: { + cookies: {}, + headers: expect.objectContaining({ + // demonstrates that request data integration can extract headers + accept: expect.any(String), + 'accept-encoding': expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + url: expect.stringContaining('/test-ssr'), + }, + sdk: { + integrations: expect.any(Array), + name: 'sentry.javascript.astro', + packages: expect.any(Array), + version: expect.any(String), + }, + server_name: expect.any(String), + spans: expect.any(Array), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /test-ssr', + transaction_info: { + source: 'route', + }, + type: 'transaction', + }); + }); +}); + +test.describe('nested SSR routes (client, server, server request)', () => { + /** The user-page route fetches from an endpoint and creates a deeply nested span structure: + * pageload — /user-page/myUsername123 + * ├── browser.** — multiple browser spans + * └── browser.request — /user-page/myUsername123 + * └── http.server — GET /user-page/[userId] (SSR page request) + * └── http.client — GET /api/user/myUsername123.json (executing fetch call from SSR page - span) + * └── http.server — GET /api/user/myUsername123.json (server request) + */ + test('sends connected server and client pageload and request spans with the same trace id', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('/user-page/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; + }); + + const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /api/user/') ?? false; + }); + + await page.goto('/user-page/myUsername123'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise; + const serverRequestHTTPClientSpan = serverPageRequestTxn.spans?.find( + span => span.op === 'http.client' && span.description?.includes('/api/user/'), + ); + + const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; + + // Verify all spans have the same trace ID + expect(clientPageloadTraceId).toEqual(serverPageRequestTxn.contexts?.trace?.trace_id); + expect(clientPageloadTraceId).toEqual(serverHTTPServerRequestTxn.contexts?.trace?.trace_id); + expect(clientPageloadTraceId).toEqual(serverRequestHTTPClientSpan?.trace_id); + + // serverPageRequest has no parent (root span) + expect(serverPageRequestTxn.contexts?.trace?.parent_span_id).toBeUndefined(); + + // clientPageload's parent and serverRequestHTTPClient's parent is serverPageRequest + const serverPageRequestSpanId = serverPageRequestTxn.contexts?.trace?.span_id; + expect(clientPageloadTxn.contexts?.trace?.parent_span_id).toEqual(serverPageRequestSpanId); + expect(serverRequestHTTPClientSpan?.parent_span_id).toEqual(serverPageRequestSpanId); + + // serverHTTPServerRequest's parent is serverRequestHTTPClient + expect(serverHTTPServerRequestTxn.contexts?.trace?.parent_span_id).toEqual(serverRequestHTTPClientSpan?.span_id); + }); + + test('sends parametrized pageload, server and API request transaction names', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('/user-page/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; + }); + + const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /api/user/') ?? false; + }); + + await page.goto('/user-page/myUsername123'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise; + + const serverRequestHTTPClientSpan = serverPageRequestTxn.spans?.find( + span => span.op === 'http.client' && span.description?.includes('/api/user/'), + ); + + const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content'); + expect(routeNameMetaContent).toBe('%2Fuser-page%2F%5BuserId%5D'); + + // Client pageload transaction - actual URL with pageload operation + expect(clientPageloadTxn).toMatchObject({ + transaction: '/user-page/[userId]', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.astro', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', + }, + }, + }, + }); + + // Server page request transaction - parametrized transaction name with actual URL in data + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /user-page/[userId]', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.astro', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + url: expect.stringContaining('/user-page/myUsername123'), + 'http.request.header.accept': expect.any(String), + 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd', + 'http.request.header.accept_language': 'en-US', + 'http.request.header.sec_fetch_mode': 'navigate', + 'http.request.header.user_agent': expect.any(String), + }, + }, + }, + request: { url: expect.stringContaining('/user-page/myUsername123') }, + }); + + // HTTP client span - actual API URL with client operation + expect(serverRequestHTTPClientSpan).toMatchObject({ + op: 'http.client', + origin: 'auto.http.otel.node_fetch', + description: 'GET http://localhost:3030/api/user/myUsername123.json', // http.client does not need to be parametrized + data: { + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.otel.node_fetch', + 'url.full': expect.stringContaining('/api/user/myUsername123.json'), + 'url.path': '/api/user/myUsername123.json', + url: expect.stringContaining('/api/user/myUsername123.json'), + }, + }); + + // Server HTTP request transaction + expect(serverHTTPServerRequestTxn).toMatchObject({ + transaction: 'GET /api/user/[userId].json', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.astro', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + url: expect.stringContaining('/api/user/myUsername123.json'), + 'http.request.header.accept': expect.any(String), + 'http.request.header.accept_encoding': 'gzip, deflate', + 'http.request.header.accept_language': '*', + 'http.request.header.sec_fetch_mode': 'cors', + 'http.request.header.user_agent': expect.any(String), + }, + }, + }, + request: { url: expect.stringContaining('/api/user/myUsername123.json') }, + }); + }); + + test('sends parametrized pageload and server transaction names for catch-all routes', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('/catchAll/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /catchAll/') ?? false; + }); + + await page.goto('/catchAll/hell0/whatever-do'); + + const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content'); + expect(routeNameMetaContent).toBe('%2FcatchAll%2F%5B...path%5D'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + + expect(clientPageloadTxn).toMatchObject({ + transaction: '/catchAll/[...path]', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.astro', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', + }, + }, + }, + }); + + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /catchAll/[...path]', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.astro', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + url: expect.stringContaining('/catchAll/hell0/whatever-do'), + 'http.request.header.accept': expect.any(String), + 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd', + 'http.request.header.accept_language': 'en-US', + 'http.request.header.sec_fetch_mode': 'navigate', + 'http.request.header.user_agent': expect.any(String), + }, + }, + }, + request: { url: expect.stringContaining('/catchAll/hell0/whatever-do') }, + }); + }); +}); + +// Case for `user-page/[id]` vs. `user-page/settings` static routes +test.describe('parametrized vs static paths', () => { + test('should use static route name for static route in parametrized path', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('/user-page/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; + }); + + await page.goto('/user-page/settings'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + + expect(clientPageloadTxn).toMatchObject({ + transaction: '/user-page/settings', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.astro', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', + }, + }, + }, + }); + + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /user-page/settings', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.astro', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + url: expect.stringContaining('/user-page/settings'), + 'http.request.header.accept': expect.any(String), + 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd', + 'http.request.header.accept_language': 'en-US', + 'http.request.header.sec_fetch_mode': 'navigate', + 'http.request.header.user_agent': expect.any(String), + }, + }, + }, + request: { url: expect.stringContaining('/user-page/settings') }, + }); + }); + + test('allows for span name override via beforeStartSpan', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction?.startsWith('/blog/') ?? false; + }); + + await page.goto('/blog/my-post'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + expect(clientPageloadTxn).toMatchObject({ + transaction: '/blog/my-post', + transaction_info: { source: 'custom' }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.serverIslands.test.ts new file mode 100644 index 000000000000..a8ddef968918 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.serverIslands.test.ts @@ -0,0 +1,97 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('tracing in static routes with server islands', () => { + test('only sends client pageload transaction and server island endpoint transaction', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent.transaction === '/server-island'; + }); + + const serverIslandEndpointTxnPromise = waitForTransaction('astro-7', evt => { + return evt.transaction === 'GET /_server-islands/[name]'; + }); + + await page.goto('/server-island'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; + const clientPageloadParentSpanId = clientPageloadTxn.contexts?.trace?.parent_span_id; + + const sentryTraceMetaTags = await page.locator('meta[name="sentry-trace"]').count(); + expect(sentryTraceMetaTags).toBe(0); + + const baggageMetaTags = await page.locator('meta[name="baggage"]').count(); + expect(baggageMetaTags).toBe(0); + + expect(clientPageloadTraceId).toMatch(/[a-f0-9]{32}/); + expect(clientPageloadParentSpanId).toBeUndefined(); + + expect(clientPageloadTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', + }), + op: 'pageload', + origin: 'auto.pageload.astro', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: clientPageloadTraceId, + }, + }, + platform: 'javascript', + transaction: '/server-island', + transaction_info: { + source: 'route', + }, + type: 'transaction', + }); + + const pageloadSpans = clientPageloadTxn.spans; + + // pageload transaction contains a resource link span for the preloaded server island request + expect(pageloadSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + op: 'resource.link', + origin: 'auto.resource.browser.metrics', + description: expect.stringMatching(/\/_server-islands\/Avatar.*$/), + }), + ]), + ); + + const serverIslandEndpointTxn = await serverIslandEndpointTxnPromise; + + expect(serverIslandEndpointTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + 'http.request.header.accept': expect.any(String), + 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd', + 'http.request.header.accept_language': 'en-US', + 'http.request.header.sec_fetch_mode': 'cors', + 'http.request.header.user_agent': expect.any(String), + }), + op: 'http.server', + origin: 'auto.http.astro', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + transaction: 'GET /_server-islands/[name]', + }); + + const serverIslandEndpointTraceId = serverIslandEndpointTxn.contexts?.trace?.trace_id; + + // unfortunately, the server island trace id is not the same as the client pageload trace id + // this is because the server island endpoint request is made as a resource link request, + // meaning our fetch instrumentation can't attach headers to the request :( + expect(serverIslandEndpointTraceId).not.toBe(clientPageloadTraceId); + + await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.static.test.ts new file mode 100644 index 000000000000..a11221e03f68 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.static.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('tracing in static/pre-rendered routes', () => { + test('only sends client pageload span with traceId from pre-rendered tags', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-7', txnEvent => { + return txnEvent?.transaction === '/test-static'; + }); + + waitForTransaction('astro-7', evt => { + if (evt.platform !== 'javascript') { + throw new Error('Server transaction should not be sent'); + } + return false; + }); + + await page.goto('/test-static'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + + const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; + const clientPageloadParentSpanId = clientPageloadTxn.contexts?.trace?.parent_span_id; + + const sentryTraceMetaTags = await page.locator('meta[name="sentry-trace"]').count(); + expect(sentryTraceMetaTags).toBe(0); + + const baggageMetaTags = await page.locator('meta[name="baggage"]').count(); + expect(baggageMetaTags).toBe(0); + + expect(clientPageloadTraceId).toMatch(/[a-f0-9]{32}/); + expect(clientPageloadParentSpanId).toBeUndefined(); + + expect(clientPageloadTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', + }), + op: 'pageload', + origin: 'auto.pageload.astro', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + platform: 'javascript', + transaction: '/test-static', + transaction_info: { + source: 'route', + }, + type: 'transaction', + }); + + await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7/tsconfig.json b/dev-packages/e2e-tests/test-applications/astro-7/tsconfig.json new file mode 100644 index 000000000000..8bf91d3bb997 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/packages/astro/package.json b/packages/astro/package.json index 4539479195a0..d039fda261d1 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -53,7 +53,7 @@ "access": "public" }, "peerDependencies": { - "astro": ">=3.x || >=4.0.0-beta" + "astro": ">=3.x || >=4.0.0-beta || >=7.0.0-beta" }, "dependencies": { "@sentry/browser": "10.57.0",