Skip to content

feat(astro): Add Astro 5 support #14613

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 10, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions dev-packages/e2e-tests/test-applications/astro-5/.gitignore
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions dev-packages/e2e-tests/test-applications/astro-5/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@sentry:registry=http://127.0.0.1:4873
@sentry-internal:registry=http://127.0.0.1:4873
48 changes: 48 additions & 0 deletions dev-packages/e2e-tests/test-applications/astro-5/README.md
Original file line number Diff line number Diff line change
@@ -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).
21 changes: 21 additions & 0 deletions dev-packages/e2e-tests/test-applications/astro-5/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -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',
}),
});
21 changes: 21 additions & 0 deletions dev-packages/e2e-tests/test-applications/astro-5/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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()],
});
Original file line number Diff line number Diff line change
@@ -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
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
---
<img alt="User avatar" src="/favicon.svg" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---

<div id="container">
<img id="background" src={background.src} alt="" fetchpriority="high" />
<main>
<section id="hero">
<a href="https://astro.build"
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
>
<h1>
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
</h1>
<section id="links">
<a class="button" href="https://docs.astro.build">Read our docs</a>
<a href="https://astro.build/chat"
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
><path
fill="currentColor"
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
></path></svg
>
</a>
</section>
</section>
</main>

<a href="https://astro.build/blog/astro-5/" id="news" class="box">
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
><path
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
fill="#111827"></path></svg
>
<h2>What's New in Astro 5.0?</h2>
<p>
From content layers to server islands, click to learn more about the new features and
improvements in Astro 5.0
</p>
</a>
</div>

<style>
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: blur(100px);
}

#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
height: 100%;
}

main {
height: 100%;
display: flex;
justify-content: center;
}

#hero {
display: flex;
align-items: start;
flex-direction: column;
justify-content: center;
padding: 16px;
}

h1 {
font-size: 22px;
margin-top: 0.25em;
}

#links {
display: flex;
gap: 16px;
}

#links a {
display: flex;
align-items: center;
padding: 10px 12px;
color: #111827;
text-decoration: none;
transition: color 0.2s;
}

#links a:hover {
color: rgb(78, 80, 86);
}

#links a svg {
height: 1em;
margin-left: 8px;
}

#links a.button {
color: white;
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
border-radius: 10px;
}

#links a.button:hover {
color: rgb(230, 230, 230);
box-shadow: none;
}

pre {
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas,
'DejaVu Sans Mono', monospace;
font-weight: normal;
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}

h2 {
margin: 0 0 1em;
font-weight: normal;
color: #111827;
font-size: 20px;
}

p {
color: #4b5563;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.006em;
margin: 0;
}

code {
display: inline-block;
background:
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
border-radius: 8px;
border: 1px solid transparent;
padding: 6px 8px;
}

.box {
padding: 16px;
background: rgba(255, 255, 255, 1);
border-radius: 16px;
border: 1px solid white;
}

#news {
position: absolute;
bottom: 16px;
right: 16px;
max-width: 300px;
text-decoration: none;
transition: background 0.2s;
backdrop-filter: blur(50px);
}

#news:hover {
background: rgba(255, 255, 255, 0.55);
}

@media screen and (max-height: 368px) {
#news {
display: none;
}
}

@media screen and (max-width: 768px) {
#container {
display: flex;
flex-direction: column;
}

#hero {
display: block;
padding-top: 10%;
}

#links {
flex-wrap: wrap;
}

#links a.button {
padding: 14px 18px;
}

#news {
right: 16px;
left: 16px;
bottom: 2.5rem;
max-width: 100%;
}

h1 {
line-height: 1.5;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>Astro Basics</title>
</head>
<body>
<slot />
</body>
</html>

<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
import Layout from "../../layouts/Layout.astro";
---

<Layout title="Client Error">

<button id="#errBtn" onclick="throw new Error('client error')">
Throw Error
</button>

</Layout>
Original file line number Diff line number Diff line change
@@ -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,
}),
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
import Layout from "../../layouts/Layout.astro";
export const prerender = false;
---

<Layout title="Endpoint Error">
<button onclick="fetch('endpoint-error/api?error=1')">Get Data</button>
</Layout>
Original file line number Diff line number Diff line change
@@ -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.
---

<Layout title="Welcome to Astro.">
<main>
<h1>Astro E2E Test App</h1>
<ul role="list" style="display: flex; flex-direction: column;">
<a href="/ssr-error">SSR Error</a>
<a href="/endpoint-error">Endpoint Error</a>
<a href="/client-error">Client Error</a>
<a href="/test-ssr">SSR page</a>
<a href="/test-static" title="static page">Static Page</a>
<a href="/server-island">Server Island</a>
</ul>
</main>
</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
import Avatar from '../../components/Avatar.astro';
import Layout from '../../layouts/Layout.astro';
export const prerender = true;
---
<Layout title="Server Island Page">

<p>This page is static, except for the avatar which is loaded dynamically from the server</p>

<Avatar server:defer>
<p slot="fallback">Fallback</p>
</Avatar>

</Layout>

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
import Layout from "../../layouts/Layout.astro";
const a = {} as any;
console.log(a.foo.x);
export const prerender = false;
---

<Layout title="SSR Error">

<h1>Page with SSR error</h1>

</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
import Layout from "../../layouts/Layout.astro"
export const prerender = false
---

<Layout title="Dynamic SSR page">

<h1>
This is a server page
</h1>

<button onclick="fetch('/test-server')">fetch</button>

</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
import Layout from "../../layouts/Layout.astro";
export const prerender = true;
---

<Layout title="Static Page">

<h1>
This is a static page
</h1>

<button onclick="fetch('/test-static')">fetch</button>

</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'astro-5',
});
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
Original file line number Diff line number Diff line change
@@ -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
});
});
Original file line number Diff line number Diff line change
@@ -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 <meta> 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
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
14 changes: 3 additions & 11 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
@@ -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"],
Comment on lines -8 to +14
Copy link
Member Author

Choose a reason for hiding this comment

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

these are just auto-format changes

"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",