Skip to content

Commit 9b1f5af

Browse files
chargomeclaude
andauthored
feat(astro)!: Drop support for Astro 3 (#22683)
Raises the minimum supported Astro version to 4. The `astro` peer dependency drops `>=3.x`, and the dev dependency moves to `^4.16.19` — pinned to the new floor so the package keeps typechecking against the oldest supported Astro rather than the newest. Removes the `supportsAddMiddleware` guard, which only existed because `addMiddleware` landed in astro@3.5.0. README, JSDoc, and comments documenting the 3.5.0 conditional are updated; the manual-middleware snippet moves under "Disable Automatic Server Instrumentation", where it's still relevant. `MIGRATION.md` already documents the drop. closes #21462 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 36ad83e commit 9b1f5af

11 files changed

Lines changed: 1318 additions & 1637 deletions

File tree

.github/dependency-review-config.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ allow-ghsas:
2020
- GHSA-v2wj-q39q-566r
2121
# esbuil, used in browser bundler plugin E2E tests
2222
- GHSA-gv7w-rqvm-qjhr
23+
# astro 4, our minimum supported version, kept as a devDependency so the SDK
24+
# keeps typechecking against it.
25+
# Once our minimum supported version is over 6.4.6 these can be removed.
26+
- GHSA-wrwg-2hg8-v723
27+
- GHSA-8hv8-536x-4wqp
28+
- GHSA-2pvr-wf23-7pc7
29+
# sharp, pulled in as an optional dependency of astro 4 (^0.33.3). Only fixed in
30+
# 0.35.0, which is outside that range. Already present on develop via sharp 0.32.6
31+
# and 0.34.5, both of which are affected by the same advisory.
32+
- GHSA-f88m-g3jw-g9cj

packages/astro/README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,7 @@ SENTRY_AUTH_TOKEN="your-token"
6060
### Server Instrumentation
6161

6262
For Astro apps configured for (hybrid) Server Side Rendering (SSR), the Sentry integration will automatically add
63-
middleware to your server to instrument incoming requests **if you're using Astro 3.5.2 or newer**.
64-
65-
If you're using Astro <3.5.2, complete the setup by adding the Sentry middleware to your `src/middleware.js` file:
66-
67-
```javascript
68-
// src/middleware.js
69-
import { sequence } from 'astro:middleware';
70-
import * as Sentry from '@sentry/astro';
71-
72-
export const onRequest = sequence(
73-
Sentry.handleRequest(),
74-
// Add your other handlers after Sentry.handleRequest()
75-
);
76-
```
63+
middleware to your server to instrument incoming requests.
7764

7865
The Sentry middleware enhances the data collected by Sentry on the server side by:
7966

@@ -101,6 +88,19 @@ export default defineConfig({
10188
});
10289
```
10390

91+
If you opt out but still want the middleware, add it manually to your `src/middleware.js` file:
92+
93+
```javascript
94+
// src/middleware.js
95+
import { sequence } from 'astro:middleware';
96+
import * as Sentry from '@sentry/astro';
97+
98+
export const onRequest = sequence(
99+
Sentry.handleRequest(),
100+
// Add your other handlers after Sentry.handleRequest()
101+
);
102+
```
103+
104104
## Configuration
105105

106106
Check out our docs for configuring your SDK setup:

packages/astro/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"access": "public"
5454
},
5555
"peerDependencies": {
56-
"astro": ">=3.x || >=4.0.0-beta || >=7.0.0-beta"
56+
"astro": ">=4.0.0-beta || >=7.0.0-beta"
5757
},
5858
"dependencies": {
5959
"@sentry/browser": "10.67.0",
@@ -63,7 +63,7 @@
6363
"@sentry/bundler-plugins": "10.67.0"
6464
},
6565
"devDependencies": {
66-
"astro": "^3.5.0",
66+
"astro": "^4.16.19",
6767
"vite": "^6.4.3"
6868
},
6969
"scripts": {

packages/astro/src/integration/cloudflare.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import type { AstroConfig } from 'astro';
12
import { builtinModules } from 'module';
2-
import type { Plugin } from 'vite';
3+
4+
// Derived from Astro's own config type rather than imported from `vite` directly: Astro bundles its
5+
// own Vite version, which differs across the Astro majors we support. A plugin typed against any
6+
// single Vite version is not assignable to `updateConfig({ vite: { plugins } })` for the others.
7+
type VitePlugin = Extract<NonNullable<NonNullable<AstroConfig['vite']>['plugins']>[number], { name: string }>;
38

49
// Build a set of all Node.js built-in module names, including both
510
// bare names (e.g. "fs") and "node:" prefixed names (e.g. "node:fs").
@@ -14,7 +19,7 @@ const NODE_BUILTINS = new Set(builtinModules.flatMap(m => [m, `node:${m}`]));
1419
* modules. Vite correctly externalizes them, but warns about it. These warnings are
1520
* harmless since Cloudflare Workers support Node.js built-ins under the `node:` prefix.
1621
*/
17-
export function sentryCloudflareNodeWarningPlugin(): Plugin {
22+
export function sentryCloudflareNodeWarningPlugin(): VitePlugin {
1823
return {
1924
name: 'sentry-astro-cloudflare-suppress-node-warnings',
2025
enforce: 'pre',
@@ -46,7 +51,7 @@ export function sentryCloudflareNodeWarningPlugin(): Plugin {
4651
* - Per-request isolation scopes via `wrapRequestHandler`
4752
* - Trace context propagation
4853
*/
49-
export function sentryCloudflareVitePlugin(): Plugin {
54+
export function sentryCloudflareVitePlugin(): VitePlugin {
5055
return {
5156
name: 'sentry-astro-cloudflare',
5257
enforce: 'post',

packages/astro/src/integration/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,7 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
235235
const isSSR = config && (config.output === 'server' || config.output === 'hybrid' || !!config.adapter);
236236
const shouldAddMiddleware = sdkEnabled.server && autoInstrumentation?.requestHandler !== false;
237237

238-
// Guarding calling the addMiddleware function because it was only introduced in astro@3.5.0
239-
// Users on older versions of astro will need to add the middleware manually.
240-
const supportsAddMiddleware = typeof addMiddleware === 'function';
241-
242-
if (supportsAddMiddleware && isSSR && shouldAddMiddleware) {
238+
if (isSSR && shouldAddMiddleware) {
243239
addMiddleware({
244240
order: 'pre',
245241
entrypoint: '@sentry/astro/middleware',

packages/astro/src/integration/middleware/index.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ type MiddlewareNext = () => Promise<Response>;
44
type MiddlewareHandler = (ctx: unknown, next: MiddlewareNext) => Promise<Response> | Response | Promise<void> | void;
55

66
/**
7-
* This export is used by our integration to automatically add the middleware
8-
* to astro ^3.5.0 projects.
7+
* This export is used by our integration to automatically add the middleware.
98
*
109
* It's not possible to pass options at this moment, so we'll call our middleware
1110
* factory function with the default options. Users can deactivate the automatic
@@ -16,9 +15,9 @@ export const onRequest: MiddlewareHandler = (ctx, next) => {
1615
const middleware = handleRequest();
1716

1817
// `onRequest` deliberately uses framework-agnostic parameter types so the published
19-
// `@sentry/astro/middleware` declaration does not reference Astro-version-specific types
20-
// (e.g. `MiddlewareResponseHandler`, which is absent in some supported Astro versions).
21-
// The handler returned by `handleRequest()` is typed against Astro's own types, so we cast
22-
// back to its expected parameter types here – the runtime shapes are identical.
18+
// `@sentry/astro/middleware` declaration does not reference Astro's own types, which are
19+
// shaped differently across the Astro majors we support. The handler returned by
20+
// `handleRequest()` is typed against Astro's types, so we cast back to its expected
21+
// parameter types here – the runtime shapes are identical.
2322
return middleware(ctx as Parameters<typeof middleware>[0], next as Parameters<typeof middleware>[1]);
2423
};

packages/astro/src/integration/types.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,6 @@ type InstrumentationOptions = {
130130
* - enable distributed tracing between server and client
131131
* - annotate server errors with more information
132132
*
133-
* This middleware will only be added automatically in Astro 3.5.0 and newer.
134-
* For older versions, add the `Sentry.handleRequest` middleware manually
135-
* in your `src/middleware.js` file.
136-
*
137133
* @default true in SSR/hybrid mode, false in SSG/static mode
138134
*/
139135
requestHandler?: boolean;

packages/astro/src/server/middleware.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
winterCGHeadersToDict,
2929
withIsolationScope,
3030
} from '@sentry/node';
31-
import type { APIContext, MiddlewareResponseHandler, RoutePart } from 'astro';
31+
import type { APIContext, MiddlewareHandler, MiddlewareNext, RoutePart } from 'astro';
3232

3333
type MiddlewareOptions = {
3434
/**
@@ -64,7 +64,7 @@ type AstroLocalsWithSentry = Record<string, unknown> & {
6464
__sentry_wrapped__?: boolean;
6565
};
6666

67-
export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseHandler = options => {
67+
export const handleRequest: (options?: MiddlewareOptions) => MiddlewareHandler = options => {
6868
const handlerOptions = {
6969
trackClientIp: false,
7070
...options,
@@ -103,10 +103,7 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH
103103
};
104104
};
105105

106-
async function handleStaticRoute(
107-
ctx: Parameters<MiddlewareResponseHandler>[0],
108-
next: Parameters<MiddlewareResponseHandler>[1],
109-
): Promise<Response> {
106+
async function handleStaticRoute(ctx: APIContext, next: MiddlewareNext): Promise<Response> {
110107
const parametrizedRoute = getParametrizedRoute(ctx);
111108
try {
112109
const originalResponse = await next();
@@ -121,11 +118,7 @@ async function handleStaticRoute(
121118
}
122119
}
123120

124-
async function enhanceHttpServerSpan(
125-
ctx: Parameters<MiddlewareResponseHandler>[0],
126-
next: Parameters<MiddlewareResponseHandler>[1],
127-
rootSpan: Span,
128-
): Promise<Response> {
121+
async function enhanceHttpServerSpan(ctx: APIContext, next: MiddlewareNext, rootSpan: Span): Promise<Response> {
129122
// Make sure we don't accidentally double wrap (e.g. user added middleware and integration auto added it)
130123
const locals = ctx.locals as AstroLocalsWithSentry | undefined;
131124
if (locals?.__sentry_wrapped__) {
@@ -171,8 +164,8 @@ async function enhanceHttpServerSpan(
171164
}
172165

173166
async function instrumentRequestStartHttpServerSpan(
174-
ctx: Parameters<MiddlewareResponseHandler>[0],
175-
next: Parameters<MiddlewareResponseHandler>[1],
167+
ctx: APIContext,
168+
next: MiddlewareNext,
176169
options: MiddlewareOptions,
177170
): Promise<Response> {
178171
// Make sure we don't accidentally double wrap (e.g. user added middleware and integration auto added it)
@@ -393,7 +386,7 @@ function tryDecodeUrl(url: string): string | undefined {
393386
* We can check this by looking at the middleware's `clientAddress` context property because accessing
394387
* this prop in a static route will throw an error which we can conveniently catch.
395388
*/
396-
function checkIsDynamicPageRequest(context: Parameters<MiddlewareResponseHandler>[0]): boolean {
389+
function checkIsDynamicPageRequest(context: APIContext): boolean {
397390
try {
398391
return context.clientAddress != null;
399392
} catch {
@@ -416,9 +409,7 @@ function joinRouteSegments(segments: RoutePart[][]): string {
416409
return `/${parthArray.join('/')}`;
417410
}
418411

419-
function getParametrizedRoute(
420-
ctx: Parameters<MiddlewareResponseHandler>[0] & { routePattern?: string },
421-
): string | undefined {
412+
function getParametrizedRoute(ctx: APIContext & { routePattern?: string }): string | undefined {
422413
try {
423414
// `routePattern` is available after Astro 5
424415
const contextWithRoutePattern = ctx;

packages/astro/test/integration/cloudflare.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const baseConfigHookObject = vi.hoisted(() => ({
3838
logger: { warn: vi.fn(), info: vi.fn(), error: vi.fn() },
3939
injectScript: vi.fn(),
4040
updateConfig: vi.fn(),
41+
addMiddleware: vi.fn(),
4142
}));
4243

4344
describe('Cloudflare Pages vs Workers detection', () => {

packages/astro/test/integration/index.test.ts

Lines changed: 21 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const config = {
2424

2525
const baseConfigHookObject = {
2626
logger: { warn: vi.fn(), info: vi.fn() },
27+
addMiddleware: vi.fn(),
2728
};
2829

2930
describe('sentryAstro integration', () => {
@@ -452,31 +453,28 @@ describe('sentryAstro integration', () => {
452453
);
453454
});
454455

455-
it.each(['server', 'hybrid'])(
456-
'adds middleware by default if in %s mode and `addMiddleware` is available',
457-
async mode => {
458-
const integration = sentryAstro({});
459-
const addMiddleware = vi.fn();
460-
const updateConfig = vi.fn();
461-
const injectScript = vi.fn();
456+
it.each(['server', 'hybrid'])('adds middleware by default if in %s mode', async mode => {
457+
const integration = sentryAstro({});
458+
const addMiddleware = vi.fn();
459+
const updateConfig = vi.fn();
460+
const injectScript = vi.fn();
462461

463-
expect(integration.hooks['astro:config:setup']).toBeDefined();
464-
// @ts-expect-error - the hook exists and we only need to pass what we actually use
465-
await integration.hooks['astro:config:setup']({
466-
// @ts-expect-error - we only need to pass what we actually use
467-
config: { output: mode },
468-
addMiddleware,
469-
updateConfig,
470-
injectScript,
471-
});
462+
expect(integration.hooks['astro:config:setup']).toBeDefined();
463+
// @ts-expect-error - the hook exists and we only need to pass what we actually use
464+
await integration.hooks['astro:config:setup']({
465+
// @ts-expect-error - we only need to pass what we actually use
466+
config: { output: mode },
467+
addMiddleware,
468+
updateConfig,
469+
injectScript,
470+
});
472471

473-
expect(addMiddleware).toHaveBeenCalledTimes(1);
474-
expect(addMiddleware).toHaveBeenCalledWith({
475-
order: 'pre',
476-
entrypoint: '@sentry/astro/middleware',
477-
});
478-
},
479-
);
472+
expect(addMiddleware).toHaveBeenCalledTimes(1);
473+
expect(addMiddleware).toHaveBeenCalledWith({
474+
order: 'pre',
475+
entrypoint: '@sentry/astro/middleware',
476+
});
477+
});
480478

481479
it.each([{ output: 'static' }, { output: undefined }])(
482480
"doesn't add middleware if in static mode (config %s)",
@@ -518,24 +516,6 @@ describe('sentryAstro integration', () => {
518516
expect(addMiddleware).toHaveBeenCalledTimes(0);
519517
});
520518

521-
it("doesn't add middleware (i.e. crash) if `addMiddleware` is N/A", async () => {
522-
const integration = sentryAstro({ autoInstrumentation: { requestHandler: false } });
523-
const updateConfig = vi.fn();
524-
const injectScript = vi.fn();
525-
526-
expect(integration.hooks['astro:config:setup']).toBeDefined();
527-
// @ts-expect-error - the hook exists and we only need to pass what we actually use
528-
await integration.hooks['astro:config:setup']({
529-
// @ts-expect-error - we only need to pass what we actually use
530-
config: { output: 'server' },
531-
updateConfig,
532-
injectScript,
533-
});
534-
535-
expect(updateConfig).toHaveBeenCalledTimes(1);
536-
expect(injectScript).toHaveBeenCalledTimes(2);
537-
});
538-
539519
it("doesn't add middleware if the SDK is disabled", () => {
540520
const integration = sentryAstro({ enabled: false });
541521
const addMiddleware = vi.fn();

0 commit comments

Comments
 (0)