Skip to content

Commit 3c79c4e

Browse files
committed
fix(astro): Run the server init before the middleware so the tunnel works on a cold server
1 parent e1f892c commit 3c79c4e

5 files changed

Lines changed: 73 additions & 11 deletions

File tree

‎dev-packages/e2e-tests/test-applications/astro-7/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"dependencies": {
2020
"@astrojs/node": "^11.1.4",
2121
"@playwright/test": "~1.63.0",
22+
"@sentry/conventions": "0.27.0",
2223
"@sentry-internal/test-utils": "link:../../../test-utils",
2324
"@sentry/astro": "file:../../packed/sentry-astro-packed.tgz",
2425
"astro": "^7.2.8",

‎dev-packages/e2e-tests/test-applications/astro-7/tests/tunnel.test.ts‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
1+
import { URL_PATH } from '@sentry/conventions/attributes';
2+
import { HTTP_SERVER } from '@sentry/conventions/op';
13
import { expect, test } from '@playwright/test';
24
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
35

46
test.skip(!process.env.PUBLIC_E2E_TEST_TUNNEL, 'Only runs in the tunnel variant');
57

8+
const DSN = 'http://public@localhost:3031/1337';
69
const TUNNEL_PATH = /^\/docs\/[a-z0-9]{1,7}\/$/;
710

811
test('answers the managed tunnel under the base path before the app middleware', async ({ page }) => {
12+
// The first request this server sees, so no SSR page has run `Sentry.init` yet.
13+
const coldTunnelResponse = await page.request.post('/docs/qpo967/', {
14+
headers: { 'content-type': 'application/x-sentry-envelope' },
15+
data: `${JSON.stringify({ dsn: DSN })}\n`,
16+
});
17+
expect(coldTunnelResponse.status()).toBe(200);
18+
919
// The variant's auth middleware rejects every other POST.
1020
expect((await page.request.post('/docs/anything/', { data: 'x' })).status()).toBe(401);
1121

1222
const tunnelServerSpanPromise = waitForStreamedSpan(
1323
'astro-7',
1424
span =>
15-
getSpanOp(span) === 'http.server' &&
16-
/\/docs\/[a-z0-9]{1,7}\/?$/.test(String(span.attributes['url.path']?.value ?? '')),
25+
getSpanOp(span) === HTTP_SERVER &&
26+
/\/docs\/[a-z0-9]{1,7}\/?$/.test(String(span.attributes[URL_PATH]?.value ?? '')),
1727
);
1828
const tunnelResponsePromise = page.waitForResponse(
1929
response => response.request().method() === 'POST' && TUNNEL_PATH.test(new URL(response.url()).pathname),
@@ -30,8 +40,7 @@ test('answers the managed tunnel under the base path before the app middleware',
3040
const anchorSpanPromise = waitForStreamedSpan(
3141
'astro-7',
3242
span =>
33-
getSpanOp(span) === 'http.server' &&
34-
String(span.attributes['url.path']?.value ?? '').startsWith('/docs/test-ssr'),
43+
getSpanOp(span) === HTTP_SERVER && String(span.attributes[URL_PATH]?.value ?? '').startsWith('/docs/test-ssr'),
3544
);
3645
await page.request.get('/docs/test-ssr/');
3746

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createRequire } from 'module';
77
import * as path from 'path';
88
import type { VitePlugin } from './cloudflare';
99
import { sentryCloudflareNodeWarningPlugin, sentryCloudflareVitePlugin } from './cloudflare';
10+
import { makeSentryMiddlewareVitePlugin, SENTRY_MIDDLEWARE_MODULE } from './middleware/virtual';
1011
import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from './snippets';
1112
import { getAstroTunnelOptions, makeTunnelOptionsVitePlugin, TUNNEL_OPTIONS_IMPORT } from './tunnel';
1213
import type { SentryOptions } from './types';
@@ -181,18 +182,23 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
181182
}
182183
}
183184

185+
let serverInitSnippet = '';
184186
if (sdkEnabled.server) {
185187
const pathToServerInit = serverInitPath ? path.resolve(serverInitPath) : findDefaultSdkInitFile('server');
186188

187189
if (pathToServerInit) {
188190
debug && logger.info(`Using ${pathToServerInit} for server init.`);
189-
// Always inject the server config via `injectScript('page-ssr')`.
190-
// This ensures Sentry.init() runs in dev mode (where the Vite plugin doesn't fire)
191-
// and also serves as the fallback for non-Cloudflare adapters in production.
192-
injectScript('page-ssr', tunnelOptionsImport + buildSdkInitFileImportSnippet(pathToServerInit));
191+
serverInitSnippet = tunnelOptionsImport + buildSdkInitFileImportSnippet(pathToServerInit);
193192
} else if (!usesCloudflareVitePlugin) {
194193
debug && logger.info('Using default server init.');
195-
injectScript('page-ssr', tunnelOptionsImport + buildServerSnippet(options || {}));
194+
serverInitSnippet = tunnelOptionsImport + buildServerSnippet(options || {});
195+
}
196+
197+
// Always inject the server config via `injectScript('page-ssr')`.
198+
// This ensures Sentry.init() runs in dev mode (where the Vite plugin doesn't fire)
199+
// and also serves as the fallback for non-Cloudflare adapters in production.
200+
if (serverInitSnippet) {
201+
injectScript('page-ssr', serverInitSnippet);
196202
}
197203

198204
if (isCloudflareWorkers && command !== 'dev') {
@@ -239,9 +245,10 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
239245
const shouldAddMiddleware = sdkEnabled.server && autoInstrumentation?.requestHandler !== false;
240246

241247
if (isSSR && shouldAddMiddleware) {
248+
updateConfig({ vite: { plugins: [makeSentryMiddlewareVitePlugin(serverInitSnippet)] } });
242249
addMiddleware({
243250
order: 'pre',
244-
entrypoint: '@sentry/astro/middleware',
251+
entrypoint: SENTRY_MIDDLEWARE_MODULE,
245252
});
246253
}
247254
},
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { VitePlugin } from '../cloudflare';
2+
3+
/**
4+
* The middleware entrypoint the integration registers instead of `@sentry/astro/middleware`.
5+
*/
6+
export const SENTRY_MIDDLEWARE_MODULE = 'virtual:@sentry/astro/middleware';
7+
const RESOLVED_SENTRY_MIDDLEWARE_MODULE = `\0${SENTRY_MIDDLEWARE_MODULE}`;
8+
9+
/**
10+
* Serves the Sentry middleware behind the server init. `page-ssr` scripts only run once an SSR page loads, so without
11+
* this a request that renders no page, like a tunnel request on a cold serverless instance, would find no client.
12+
*/
13+
export function makeSentryMiddlewareVitePlugin(serverInitSnippet: string): VitePlugin {
14+
return {
15+
name: 'sentry-astro-middleware',
16+
resolveId: id => (id === SENTRY_MIDDLEWARE_MODULE ? RESOLVED_SENTRY_MIDDLEWARE_MODULE : null),
17+
load: id =>
18+
id === RESOLVED_SENTRY_MIDDLEWARE_MODULE
19+
? `${serverInitSnippet}\nexport { onRequest } from "@sentry/astro/middleware";\n`
20+
: null,
21+
};
22+
}

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,10 +680,33 @@ describe('sentryAstro integration', () => {
680680
expect(addMiddleware).toHaveBeenCalledTimes(1);
681681
expect(addMiddleware).toHaveBeenCalledWith({
682682
order: 'pre',
683-
entrypoint: '@sentry/astro/middleware',
683+
entrypoint: 'virtual:@sentry/astro/middleware',
684684
});
685685
});
686686

687+
it('runs the server init before the middleware, so requests that render no page find a client', async () => {
688+
const integration = sentryAstro({});
689+
const updateConfig = vi.fn();
690+
const injectScript = vi.fn();
691+
692+
// @ts-expect-error - the hook exists and we only need to pass what we actually use
693+
await integration.hooks['astro:config:setup']({
694+
// @ts-expect-error - we only need to pass what we actually use
695+
config: { output: 'server' },
696+
addMiddleware: vi.fn(),
697+
updateConfig,
698+
injectScript,
699+
});
700+
701+
const plugin = updateConfig.mock.calls
702+
.flatMap(([config]) => config.vite?.plugins ?? [])
703+
.find(plugin => plugin.name === 'sentry-astro-middleware');
704+
const resolvedId = plugin.resolveId('virtual:@sentry/astro/middleware');
705+
const serverInit = injectScript.mock.calls.find(([stage]) => stage === 'page-ssr')?.[1];
706+
707+
expect(plugin.load(resolvedId)).toBe(`${serverInit}\nexport { onRequest } from "@sentry/astro/middleware";\n`);
708+
});
709+
687710
it.each([{ output: 'static' }, { output: undefined }])(
688711
"doesn't add middleware if in static mode (config %s)",
689712
async (config: any) => {

0 commit comments

Comments
 (0)