Skip to content
Open
Show file tree
Hide file tree
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
36 changes: 31 additions & 5 deletions packages/astro/src/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createRequire } from 'module';
import * as path from 'path';
import type { VitePlugin } from './cloudflare';
import { sentryCloudflareNodeWarningPlugin, sentryCloudflareVitePlugin } from './cloudflare';
import { SERVER_INIT_ID, SERVER_INIT_MIDDLEWARE_ID, sentryServerInitPlugin } from './serverInit';
import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from './snippets';
import type { SentryOptions } from './types';

Expand Down Expand Up @@ -174,18 +175,17 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
}
}

let pathToServerInit: string | undefined;

if (sdkEnabled.server) {
const pathToServerInit = serverInitPath ? path.resolve(serverInitPath) : findDefaultSdkInitFile('server');
pathToServerInit = serverInitPath ? path.resolve(serverInitPath) : findDefaultSdkInitFile('server');

if (pathToServerInit) {
debug && logger.info(`Using ${pathToServerInit} for server init.`);
// Always inject the server config via `injectScript('page-ssr')`.
// This ensures Sentry.init() runs in dev mode (where the Vite plugin doesn't fire)
// and also serves as the fallback for non-Cloudflare adapters in production.
injectScript('page-ssr', buildSdkInitFileImportSnippet(pathToServerInit));
} else if (!usesCloudflareVitePlugin) {
debug && logger.info('Using default server init.');
injectScript('page-ssr', buildServerSnippet(options || {}));
}

if (isCloudflareWorkers && command !== 'dev') {
Expand Down Expand Up @@ -231,10 +231,36 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
const isSSR = config && (config.output === 'server' || config.output === 'hybrid' || !!config.adapter);
const shouldAddMiddleware = sdkEnabled.server && autoInstrumentation?.requestHandler !== false;

const isNetlify = config?.adapter?.name?.startsWith('@astrojs/netlify');
const initServerInMiddleware = isSSR && shouldAddMiddleware && !isCloudflareWorkers && !isNetlify;

if (sdkEnabled.server && !pathToServerInit && !usesCloudflareVitePlugin) {
debug && logger.info('Using default server init.');

injectScript(
'page-ssr',
initServerInMiddleware ? `import "${SERVER_INIT_ID}";` : buildServerSnippet(options || {}),
);
}

if (isSSR && shouldAddMiddleware) {
if (initServerInMiddleware) {
updateConfig({
vite: {
plugins: [
sentryServerInitPlugin(
pathToServerInit
? buildSdkInitFileImportSnippet(pathToServerInit)
: buildServerSnippet(options || {}),
),
],
},
});
}

addMiddleware({
order: 'pre',
entrypoint: '@sentry/astro/middleware',
entrypoint: initServerInMiddleware ? SERVER_INIT_MIDDLEWARE_ID : '@sentry/astro/middleware',
});
}
},
Expand Down
28 changes: 28 additions & 0 deletions packages/astro/src/integration/serverInit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { VitePlugin } from './cloudflare';

export const SERVER_INIT_ID = 'virtual:sentry-astro/server-init';
export const SERVER_INIT_MIDDLEWARE_ID = 'virtual:sentry-astro/server-init-middleware';

const RESOLVED_ID_PREFIX = '\0';

export function sentryServerInitPlugin(serverInitCode: string): VitePlugin {
return {
name: 'sentry-astro-server-init',

resolveId(id) {
return id === SERVER_INIT_ID || id === SERVER_INIT_MIDDLEWARE_ID ? `${RESOLVED_ID_PREFIX}${id}` : undefined;
},

load(id) {
if (id === `${RESOLVED_ID_PREFIX}${SERVER_INIT_ID}`) {
return serverInitCode;
}

if (id === `${RESOLVED_ID_PREFIX}${SERVER_INIT_MIDDLEWARE_ID}`) {
return `import "${SERVER_INIT_ID}";\nexport { onRequest } from '@sentry/astro/middleware';`;
}

return undefined;
},
};
}
18 changes: 18 additions & 0 deletions packages/astro/test/integration/index.files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,22 @@ describe('sentryAstro integration', () => {
expect.stringMatching(/^import ".*\/sentry.server.config.js"/),
);
});

it('imports the default server init file from the middleware entrypoint', async () => {
const addMiddleware = vi.fn();
const updateConfig = vi.fn();
const setup = sentryAstro({}).hooks['astro:config:setup'] as (params: unknown) => Promise<void>;

await setup({ updateConfig, injectScript, addMiddleware, config: { output: 'server' } });

expect(addMiddleware).toHaveBeenCalledWith({
order: 'pre',
entrypoint: 'virtual:sentry-astro/server-init-middleware',
});

const plugin = updateConfig.mock.calls
.flatMap(([updatedConfig]) => updatedConfig.vite?.plugins ?? [])
.find((vitePlugin: { name?: string }) => vitePlugin?.name === 'sentry-astro-server-init');
expect(plugin.load('\0virtual:sentry-astro/server-init')).toMatch(/^import ".*\/sentry.server.config.js";$/);
});
});
215 changes: 214 additions & 1 deletion packages/astro/test/integration/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,220 @@ describe('sentryAstro integration', () => {
expect(addMiddleware).toHaveBeenCalledTimes(1);
expect(addMiddleware).toHaveBeenCalledWith({
order: 'pre',
entrypoint: '@sentry/astro/middleware',
entrypoint: 'virtual:sentry-astro/server-init-middleware',
});
});

describe('server init in the middleware', () => {
const setupWithMiddleware = async (options: SentryOptions, astroConfig: Record<string, unknown>) => {
const addMiddleware = vi.fn();
const updateConfig = vi.fn();
const injectScript = vi.fn();
const setup = sentryAstro(options).hooks['astro:config:setup'] as (params: unknown) => Promise<void>;

await setup({
config: astroConfig,
addMiddleware,
updateConfig,
injectScript,
logger: baseConfigHookObject.logger,
});

const plugin = updateConfig.mock.calls
.flatMap(([updatedConfig]) => updatedConfig.vite?.plugins ?? [])
.find((vitePlugin: { name?: string }) => vitePlugin?.name === 'sentry-astro-server-init');

return { addMiddleware, injectScript, plugin };
};

it('adds a middleware entrypoint that imports the init file before the Sentry middleware', async () => {
const { addMiddleware, injectScript, plugin } = await setupWithMiddleware(
{ serverInitPath: 'my-server-init-path.js' },
{ output: 'server' },
);

expect(addMiddleware).toHaveBeenCalledTimes(1);
expect(addMiddleware).toHaveBeenCalledWith({
order: 'pre',
entrypoint: 'virtual:sentry-astro/server-init-middleware',
});

expect(plugin).toBeDefined();
expect(plugin.resolveId('virtual:sentry-astro/server-init-middleware')).toBe(
'\0virtual:sentry-astro/server-init-middleware',
);
expect(plugin.resolveId('virtual:sentry-astro/server-init')).toBe('\0virtual:sentry-astro/server-init');
expect(plugin.resolveId('@sentry/astro/middleware')).toBeUndefined();
expect(plugin.load('\0virtual:sentry-astro/server-init-middleware')).toBe(
'import "virtual:sentry-astro/server-init";\nexport { onRequest } from \'@sentry/astro/middleware\';',
);
expect(plugin.load('\0virtual:sentry-astro/server-init')).toMatch(/^import ".*\/my-server-init-path.js";$/);
expect(plugin.load('some-other-module')).toBeUndefined();

expect(injectScript).toHaveBeenCalledWith(
'page-ssr',
expect.stringMatching(/^import ".*\/my-server-init-path.js"/),
);
});

it('keeps the default middleware entrypoint for the cloudflare workers adapter', async () => {
const { addMiddleware, plugin } = await setupWithMiddleware(
{ serverInitPath: 'my-server-init-path.js' },
{ output: 'server', adapter: { name: '@astrojs/cloudflare' } },
);

expect(addMiddleware).toHaveBeenCalledWith({ order: 'pre', entrypoint: '@sentry/astro/middleware' });
expect(plugin).toBeUndefined();
});

it("doesn't add the middleware entrypoint if the middleware is disabled by users", async () => {
const { addMiddleware, plugin } = await setupWithMiddleware(
{ serverInitPath: 'my-server-init-path.js', autoInstrumentation: { requestHandler: false } },
{ output: 'server' },
);

expect(addMiddleware).not.toHaveBeenCalled();
expect(plugin).toBeUndefined();
});

it.each(['server', 'hybrid'])('uses the init middleware entrypoint in %s mode', async output => {
const { addMiddleware, plugin } = await setupWithMiddleware(
{ serverInitPath: 'my-server-init-path.js' },
{ output },
);

expect(addMiddleware).toHaveBeenCalledWith({
order: 'pre',
entrypoint: 'virtual:sentry-astro/server-init-middleware',
});
expect(plugin).toBeDefined();
});

it("doesn't add the middleware entrypoint if the server SDK is disabled", async () => {
const { addMiddleware, plugin } = await setupWithMiddleware(
{ serverInitPath: 'my-server-init-path.js', enabled: { client: true, server: false } },
{ output: 'server' },
);

expect(addMiddleware).not.toHaveBeenCalled();
expect(plugin).toBeUndefined();
});

it('initializes the SDK with the default snippet once, from a module shared by page-ssr and the middleware', async () => {
const { addMiddleware, injectScript, plugin } = await setupWithMiddleware({}, { output: 'server' });

expect(addMiddleware).toHaveBeenCalledWith({
order: 'pre',
entrypoint: 'virtual:sentry-astro/server-init-middleware',
});
expect(injectScript.mock.calls.filter(([target]) => target === 'page-ssr')).toHaveLength(1);
expect(injectScript).toHaveBeenCalledWith('page-ssr', 'import "virtual:sentry-astro/server-init";');

const initCode: string = plugin.load('\0virtual:sentry-astro/server-init');
const middlewareCode: string = plugin.load('\0virtual:sentry-astro/server-init-middleware');
expect(initCode.match(/Sentry\.init\(/g)).toHaveLength(1);
expect(middlewareCode).not.toContain('Sentry.init(');
expect(middlewareCode).toContain('import "virtual:sentry-astro/server-init";');
});

it('does not put a Sentry.init into the virtual modules when a server init file is used', async () => {
const { plugin } = await setupWithMiddleware({ serverInitPath: 'my-server-init-path.js' }, { output: 'server' });

expect(plugin.load('\0virtual:sentry-astro/server-init')).not.toContain('Sentry.init(');
expect(plugin.load('\0virtual:sentry-astro/server-init-middleware')).not.toContain('Sentry.init(');
});

it('passes the build options to the default snippet in the virtual module', async () => {
const { plugin } = await setupWithMiddleware({ debug: true, release: { name: '1.0.0' } }, { output: 'server' });

const code: string = plugin.load('\0virtual:sentry-astro/server-init');
expect(code).toContain('release: "1.0.0"');
expect(code).toContain('debug: true');
});

it('keeps the default snippet in page-ssr for the cloudflare workers adapter', async () => {
const { addMiddleware, injectScript, plugin } = await setupWithMiddleware(
{},
{ output: 'server', adapter: { name: '@astrojs/cloudflare' } },
);

expect(addMiddleware).toHaveBeenCalledWith({ order: 'pre', entrypoint: '@sentry/astro/middleware' });
expect(plugin).toBeUndefined();
expect(injectScript).toHaveBeenCalledWith('page-ssr', expect.stringContaining('Sentry.init'));
});

it.each([
['a server init file', { serverInitPath: 'my-server-init-path.js' }],
['the default snippet', {}],
])('uses the init middleware entrypoint for the cloudflare pages adapter with %s', async (_, options) => {
wranglerPagesConfig = '{ "pages_build_output_dir": "./dist" }';

try {
const { addMiddleware, injectScript, plugin } = await setupWithMiddleware(options, {
output: 'server',
adapter: { name: '@astrojs/cloudflare' },
});

expect(addMiddleware).toHaveBeenCalledWith({
order: 'pre',
entrypoint: 'virtual:sentry-astro/server-init-middleware',
});
expect(plugin).toBeDefined();
expect(injectScript).toHaveBeenCalledWith(
'page-ssr',
'serverInitPath' in options
? expect.stringMatching(/^import ".*\/my-server-init-path.js"/)
: 'import "virtual:sentry-astro/server-init";',
);
} finally {
wranglerPagesConfig = undefined;
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Conditional assertion in parameterized test

Low Severity

The Cloudflare Pages it.each still picks the expected page-ssr script with a ternary on serverInitPath. Nearby Netlify cases already put that expected script in the table. This was flagged because the review rules ask to split conditional paths instead of branching inside one test.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit aa825c3. Configure here.


it.each([
['a server init file', { serverInitPath: 'my-server-init-path.js' }, /^import ".*\/my-server-init-path.js";$/],
['the default snippet', {}, /^import \* as Sentry from "@sentry\/astro";[\s\S]*Sentry\.init\(/],
])('keeps the previous setup for the netlify adapter with %s', async (_, options, pageSsrScript) => {
const { addMiddleware, injectScript, plugin } = await setupWithMiddleware(options, {
output: 'server',
adapter: { name: '@astrojs/netlify' },
});

expect(addMiddleware).toHaveBeenCalledWith({ order: 'pre', entrypoint: '@sentry/astro/middleware' });
expect(plugin).toBeUndefined();
expect(injectScript).toHaveBeenCalledWith('page-ssr', expect.stringMatching(pageSsrScript));
});

it('uses the init middleware entrypoint when only an adapter is configured', async () => {
const { addMiddleware, plugin } = await setupWithMiddleware(
{ serverInitPath: 'my-server-init-path.js' },
{ adapter: { name: '@astrojs/node' } },
);

expect(addMiddleware).toHaveBeenCalledWith({
order: 'pre',
entrypoint: 'virtual:sentry-astro/server-init-middleware',
});
expect(plugin).toBeDefined();
});

it('keeps the default snippet in page-ssr if the middleware is disabled by users', async () => {
const { addMiddleware, injectScript, plugin } = await setupWithMiddleware(
{ autoInstrumentation: { requestHandler: false } },
{ output: 'server' },
);

expect(addMiddleware).not.toHaveBeenCalled();
expect(plugin).toBeUndefined();
expect(injectScript).toHaveBeenCalledWith('page-ssr', expect.stringContaining('Sentry.init'));
});

it('keeps the default snippet in page-ssr in static mode', async () => {
const { addMiddleware, injectScript, plugin } = await setupWithMiddleware({}, {});

expect(addMiddleware).not.toHaveBeenCalled();
expect(plugin).toBeUndefined();
expect(injectScript).toHaveBeenCalledWith('page-ssr', expect.stringContaining('Sentry.init'));
});
});

Expand Down
Loading