diff --git a/dev-packages/e2e-tests/test-applications/astro-5/.gitignore b/dev-packages/e2e-tests/test-applications/astro-5/.gitignore new file mode 100644 index 000000000000..560782d47d98 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/.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-5/.npmrc b/dev-packages/e2e-tests/test-applications/astro-5/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/astro-5/README.md b/dev-packages/e2e-tests/test-applications/astro-5/README.md new file mode 100644 index 000000000000..ff19a3e7ece8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/README.md @@ -0,0 +1,48 @@ +# Astro Starter Kit: Basics + +```sh +npm create astro@latest -- --template basics +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) +[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) + +> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! + +![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) + +## 🚀 Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +```text +/ +├── public/ +│ └── favicon.svg +├── src/ +│ ├── layouts/ +│ │ └── Layout.astro +│ └── pages/ +│ └── index.astro +└── package.json +``` + +To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## 👀 Want to learn more? + +Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/dev-packages/e2e-tests/test-applications/astro-5/astro.config.mjs b/dev-packages/e2e-tests/test-applications/astro-5/astro.config.mjs new file mode 100644 index 000000000000..f38dac6171bf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/astro.config.mjs @@ -0,0 +1,21 @@ +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', + adapter: node({ + mode: 'standalone', + }), +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/package.json b/dev-packages/e2e-tests/test-applications/astro-5/package.json new file mode 100644 index 000000000000..a41a919b0283 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/package.json @@ -0,0 +1,21 @@ +{ + "name": "astro-5", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "TEST_ENV=production playwright test" + }, + "dependencies": { + "@astrojs/internal-helpers": "^0.4.2", + "@astrojs/node": "^9.0.0", + "@playwright/test": "^1.46.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/astro": "^8.42.0", + "astro": "^5.0.3" + } +} diff --git a/dev-packages/e2e-tests/test-applications/astro-5/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/astro-5/playwright.config.mjs new file mode 100644 index 000000000000..cd6ed611fb4a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/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: 'node ./dist/server/entry.mjs', +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/astro-5/public/favicon.svg b/dev-packages/e2e-tests/test-applications/astro-5/public/favicon.svg new file mode 100644 index 000000000000..f157bd1c5e28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/sentry.client.config.js b/dev-packages/e2e-tests/test-applications/astro-5/sentry.client.config.js new file mode 100644 index 000000000000..7bb40f0c60d4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/sentry.client.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 + integrations: [Sentry.browserTracingIntegration()], +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js new file mode 100644 index 000000000000..2b79ec0ed337 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js @@ -0,0 +1,8 @@ +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 +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/assets/astro.svg b/dev-packages/e2e-tests/test-applications/astro-5/src/assets/astro.svg new file mode 100644 index 000000000000..8cf8fb0c7da6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/assets/astro.svg @@ -0,0 +1 @@ + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/assets/background.svg b/dev-packages/e2e-tests/test-applications/astro-5/src/assets/background.svg new file mode 100644 index 000000000000..4b2be0ac0e47 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/assets/background.svg @@ -0,0 +1 @@ + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/components/Avatar.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/components/Avatar.astro new file mode 100644 index 000000000000..09a539f14e64 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/components/Avatar.astro @@ -0,0 +1,3 @@ +--- +--- +User avatar diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/components/Welcome.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/components/Welcome.astro new file mode 100644 index 000000000000..6b7b9c70e869 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/components/Welcome.astro @@ -0,0 +1,209 @@ +--- +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-5/src/layouts/Layout.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/layouts/Layout.astro new file mode 100644 index 000000000000..e455c6106729 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/layouts/Layout.astro @@ -0,0 +1,22 @@ + + + + + + + + Astro Basics + + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/client-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/client-error/index.astro new file mode 100644 index 000000000000..facd6f077a6e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/client-error/index.astro @@ -0,0 +1,11 @@ +--- +import Layout from "../../layouts/Layout.astro"; +--- + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/endpoint-error/api.ts b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/endpoint-error/api.ts new file mode 100644 index 000000000000..a76accdba010 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/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-5/src/pages/endpoint-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/endpoint-error/index.astro new file mode 100644 index 000000000000..f025c76f8365 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/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-5/src/pages/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/index.astro new file mode 100644 index 000000000000..457d94f43457 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/index.astro @@ -0,0 +1,21 @@ +--- +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-5/src/pages/server-island/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/server-island/index.astro new file mode 100644 index 000000000000..d0544ac4f32f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/server-island/index.astro @@ -0,0 +1,16 @@ +--- +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-5/src/pages/ssr-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/ssr-error/index.astro new file mode 100644 index 000000000000..4ecb7466de70 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/ssr-error/index.astro @@ -0,0 +1,13 @@ +--- +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-5/src/pages/test-ssr/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/test-ssr/index.astro new file mode 100644 index 000000000000..58f5d80198d7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/test-ssr/index.astro @@ -0,0 +1,15 @@ +--- +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-5/src/pages/test-static/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/test-static/index.astro new file mode 100644 index 000000000000..f71bf00c9adf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/test-static/index.astro @@ -0,0 +1,15 @@ +--- +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-5/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/astro-5/start-event-proxy.mjs new file mode 100644 index 000000000000..875a9a2afac1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'astro-5', +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.client.test.ts new file mode 100644 index 000000000000..22572d009202 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.client.test.ts @@ -0,0 +1,80 @@ +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-5', 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: '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}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + environment: 'qa', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts new file mode 100644 index 000000000000..d6a9514da1d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts @@ -0,0 +1,164 @@ +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-5', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === "Cannot read properties of undefined (reading 'x')"; + }); + + const transactionEventPromise = waitForTransaction('astro-5', transactionEvent => { + return transactionEvent.transaction === 'GET /ssr-error'; + }); + + await page.goto('/ssr-error'); + + 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: { + data: { + function: 'astroMiddleware', + }, + handled: false, + type: '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-5', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Endpoint Error'; + }); + const transactionEventApiPromise = waitForTransaction('astro-5', transactionEvent => { + return transactionEvent.transaction === 'GET /endpoint-error/api'; + }); + const transactionEventEndpointPromise = waitForTransaction('astro-5', 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}/); + // TODO: This is incorrect, for whatever reason, it should be the endpointSpanId ideally + 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: { + data: { + function: 'astroMiddleware', + }, + handled: false, + type: '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-5/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts new file mode 100644 index 000000000000..8c0e2c0c8850 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts @@ -0,0 +1,123 @@ +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-5', txnEvent => { + return txnEvent?.transaction === '/test-ssr'; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-5', 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.browser', + 'sentry.sample_rate': 1, + 'sentry.source': 'url', + }), + op: 'pageload', + origin: 'auto.pageload.browser', + 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: 'url', + }, + type: 'transaction', + }); + + expect(serverPageRequestTxn).toMatchObject({ + breadcrumbs: expect.any(Array), + 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'), + }, + 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', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts new file mode 100644 index 000000000000..a6b288f4de71 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts @@ -0,0 +1,99 @@ +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-5', txnEvent => { + return txnEvent?.transaction === '/server-island'; + }); + + const serverIslandEndpointTxnPromise = waitForTransaction('astro-5', evt => { + return !!evt.transaction?.startsWith('GET /_server-islands'); + }); + + 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 sentryTraceMetaTagContent = await page.locator('meta[name="sentry-trace"]').getAttribute('content'); + const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content'); + + const [metaTraceId, metaParentSpanId, metaSampled] = sentryTraceMetaTagContent?.split('-') || []; + + expect(clientPageloadTraceId).toMatch(/[a-f0-9]{32}/); + expect(clientPageloadParentSpanId).toMatch(/[a-f0-9]{16}/); + expect(metaSampled).toBe('1'); + + expect(clientPageloadTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.sample_rate': 1, + 'sentry.source': 'url', + }), + op: 'pageload', + origin: 'auto.pageload.browser', + parent_span_id: metaParentSpanId, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: metaTraceId, + }, + }, + platform: 'javascript', + transaction: '/server-island', + transaction_info: { + source: 'url', + }, + 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.*$/), + }), + ]), + ); + + expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Fserver-island%2F'); // URL-encoded for 'GET /test-static/' + expect(baggageMetaTagContent).toContain('sentry-sampled=true'); + + const serverIslandEndpointTxn = await serverIslandEndpointTxnPromise; + + expect(serverIslandEndpointTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }), + 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-5/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts new file mode 100644 index 000000000000..9c202da53542 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts @@ -0,0 +1,62 @@ +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-5', txnEvent => { + return txnEvent?.transaction === '/test-static'; + }); + + waitForTransaction('astro-5', 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 sentryTraceMetaTagContent = await page.locator('meta[name="sentry-trace"]').getAttribute('content'); + const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content'); + + const [metaTraceId, metaParentSpanId, metaSampled] = sentryTraceMetaTagContent?.split('-') || []; + + expect(clientPageloadTraceId).toMatch(/[a-f0-9]{32}/); + expect(clientPageloadParentSpanId).toMatch(/[a-f0-9]{16}/); + expect(metaSampled).toBe('1'); + + expect(clientPageloadTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.sample_rate': 1, + 'sentry.source': 'url', + }), + op: 'pageload', + origin: 'auto.pageload.browser', + parent_span_id: metaParentSpanId, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: metaTraceId, + }, + }, + platform: 'javascript', + transaction: '/test-static', + transaction_info: { + source: 'url', + }, + type: 'transaction', + }); + + expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Ftest-static%2F'); // URL-encoded for 'GET /test-static/' + expect(baggageMetaTagContent).toContain('sentry-sampled=true'); + + await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tsconfig.json b/dev-packages/e2e-tests/test-applications/astro-5/tsconfig.json new file mode 100644 index 000000000000..8bf91d3bb997 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/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 48d2424414ba..a678100bbd90 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -4,22 +4,14 @@ "description": "Official Sentry SDK for Astro", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", - "keywords": [ - "withastro", - "astro-component", - "astro-integration", - "sentry", - "apm" - ], + "keywords": ["withastro", "astro-component", "astro-integration", "sentry", "apm"], "author": "Sentry", "license": "MIT", "engines": { "node": ">=18.14.1" }, "type": "module", - "files": [ - "/build" - ], + "files": ["/build"], "main": "build/cjs/index.client.js", "module": "build/esm/index.server.js", "browser": "build/esm/index.client.js", @@ -53,7 +45,7 @@ "access": "public" }, "peerDependencies": { - "astro": ">=3.x || >=4.0.0-beta" + "astro": ">=3.x || >=4.0.0-beta || >=5.x" }, "dependencies": { "@sentry/browser": "8.42.0",