diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/astro.config.mjs b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/astro.config.mjs index bca1420e1124..3eecaa7d420b 100644 --- a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/astro.config.mjs +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/astro.config.mjs @@ -1,6 +1,5 @@ import node from '@astrojs/node'; import sentry from '@sentry/astro'; -import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/vite'; // @ts-check import { defineConfig } from 'astro/config'; @@ -18,9 +17,4 @@ export default defineConfig({ adapter: node({ mode: 'standalone', }), - vite: { - // Run the orchestrion code transform on the Vite SSR bundle so instrumented - // DB drivers get `diagnostics_channel` publishers injected at build time. - plugins: [sentryOrchestrionPlugin()], - }, }); diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/package.json b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/package.json index 5d39c4e4b7c8..cfa13769990a 100644 --- a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/package.json @@ -18,7 +18,6 @@ "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/astro": "file:../../packed/sentry-astro-packed.tgz", "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz", "astro": "^7.0.6", "ioredis": "5.10.1", "mysql": "^2.18.1" diff --git a/packages/astro/src/integration/cloudflare.ts b/packages/astro/src/integration/cloudflare.ts index 2d6d02ba3ed0..93311a735248 100644 --- a/packages/astro/src/integration/cloudflare.ts +++ b/packages/astro/src/integration/cloudflare.ts @@ -4,7 +4,7 @@ import { builtinModules } from 'module'; // Derived from Astro's own config type rather than imported from `vite` directly: Astro bundles its // own Vite version, which differs across the Astro majors we support. A plugin typed against any // single Vite version is not assignable to `updateConfig({ vite: { plugins } })` for the others. -type VitePlugin = Extract['plugins']>[number], { name: string }>; +export type VitePlugin = Extract['plugins']>[number], { name: string }>; // Build a set of all Node.js built-in module names, including both // bare names (e.g. "fs") and "node:" prefixed names (e.g. "node:fs"). diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 7845308a7536..32593a0d2c32 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -1,8 +1,10 @@ import { sentryVitePlugin } from '@sentry/bundler-plugins/vite'; +import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/vite'; import type { AstroConfig, AstroIntegration, AstroIntegrationLogger } from 'astro'; import * as fs from 'fs'; import { createRequire } from 'module'; import * as path from 'path'; +import type { VitePlugin } from './cloudflare'; import { sentryCloudflareNodeWarningPlugin, sentryCloudflareVitePlugin } from './cloudflare'; import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from './snippets'; import type { SentryOptions } from './types'; @@ -34,6 +36,7 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { sourcemaps, // todo(v11): Extract `release` build time option here - cannot be done currently, because it conflicts with the `DeprecatedRuntimeOptions` type // release, + buildTimeInstrumentation, bundleSizeOptimizations, applicationKey, unstable_sentryVitePluginOptions, @@ -167,6 +170,19 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { const isCloudflare = config?.adapter?.name?.startsWith('@astrojs/cloudflare'); const isCloudflareWorkers = isCloudflare && !isCloudflarePages(); + // Wire up the orchestrion code transform so instrumented server-side dependencies (e.g. + // `mysql`, `ioredis`) get `diagnostics_channel` publishers injected into the SSR bundle at + // build time, with no manual plugin setup. The plugin opts out internally when + // `buildTimeInstrumentation` is `false`. + // TODO: Cloudflare/workerd needs different wiring — skipped for now. + if (sdkEnabled.server && !isCloudflare) { + updateConfig({ + vite: { + plugins: [sentryOrchestrionPlugin({ buildTimeInstrumentation }) as VitePlugin], + }, + }); + } + if (isCloudflare) { try { const _require = createRequire(`${process.cwd()}/`); diff --git a/packages/astro/test/integration/index.test.ts b/packages/astro/test/integration/index.test.ts index 7012140f92bf..68ef1dd5e33a 100644 --- a/packages/astro/test/integration/index.test.ts +++ b/packages/astro/test/integration/index.test.ts @@ -10,6 +10,26 @@ vi.mock('@sentry/bundler-plugins/vite', () => ({ sentryVitePlugin: vi.fn(args => sentryVitePluginSpy(args)), })); +// Stub the orchestrion plugin so these stay pure wiring tests (no apm code transformer pulled in). +// Mirror the real plugin's contract: `buildTimeInstrumentation: false` yields the inert variant. +const orchestrionVite = vi.fn((options?: { buildTimeInstrumentation?: boolean }) => ({ + name: options?.buildTimeInstrumentation === false ? 'sentry-orchestrion-disabled' : 'sentry-orchestrion-vite', +})); +vi.mock('@sentry/server-utils/orchestrion/vite', () => ({ + sentryOrchestrionPlugin: (options?: { buildTimeInstrumentation?: boolean }) => orchestrionVite(options), +})); + +// The cloudflare adapter path resolves `@sentry/cloudflare` via `createRequire` and calls +// `process.exit(1)` when it's missing. Stub the resolver so it always "finds" the package, +// keeping these tests hermetic regardless of what's installed in `node_modules`. +vi.mock('module', async requireActual => { + const actual = await requireActual(); + return { + ...actual, + createRequire: () => ({ resolve: () => '@sentry/cloudflare' }), + }; +}); + process.env = { ...process.env, SENTRY_AUTH_TOKEN: 'my-token', @@ -23,7 +43,7 @@ const config = { } as AstroConfig; const baseConfigHookObject = { - logger: { warn: vi.fn(), info: vi.fn() }, + logger: { warn: vi.fn(), info: vi.fn(), error: vi.fn() }, addMiddleware: vi.fn(), }; @@ -46,7 +66,8 @@ describe('sentryAstro integration', () => { // @ts-expect-error - the hook exists and we only need to pass what we actually use await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config }); - expect(updateConfig).toHaveBeenCalledTimes(1); + // one call for the sourcemaps vite plugin, one for the orchestrion plugin + expect(updateConfig).toHaveBeenCalledTimes(2); expect(updateConfig).toHaveBeenCalledWith({ vite: { build: { @@ -55,6 +76,11 @@ describe('sentryAstro integration', () => { plugins: ['sentryVitePlugin'], }, }); + expect(updateConfig).toHaveBeenCalledWith({ + vite: { + plugins: [{ name: 'sentry-orchestrion-vite' }], + }, + }); expect(sentryVitePluginSpy).toHaveBeenCalledTimes(1); expect(sentryVitePluginSpy).toHaveBeenCalledWith( @@ -294,7 +320,13 @@ describe('sentryAstro integration', () => { // @ts-expect-error - the hook exists and we only need to pass what we actually use await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config }); - expect(updateConfig).toHaveBeenCalledTimes(0); + // only the orchestrion plugin is wired, no sourcemaps plugin + expect(updateConfig).toHaveBeenCalledTimes(1); + expect(updateConfig).toHaveBeenCalledWith({ + vite: { + plugins: [{ name: 'sentry-orchestrion-vite' }], + }, + }); expect(sentryVitePluginSpy).toHaveBeenCalledTimes(0); }); @@ -307,7 +339,13 @@ describe('sentryAstro integration', () => { // @ts-expect-error - the hook exists and we only need to pass what we actually use await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config }); - expect(updateConfig).toHaveBeenCalledTimes(0); + // only the orchestrion plugin is wired, no sourcemaps plugin + expect(updateConfig).toHaveBeenCalledTimes(1); + expect(updateConfig).toHaveBeenCalledWith({ + vite: { + plugins: [{ name: 'sentry-orchestrion-vite' }], + }, + }); expect(sentryVitePluginSpy).toHaveBeenCalledTimes(0); }); @@ -318,11 +356,12 @@ describe('sentryAstro integration', () => { // @ts-expect-error - the hook exists and we only need to pass what we actually use await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config }); - expect(updateConfig).toHaveBeenCalledTimes(1); + // one call for the sourcemaps vite plugin, one for the orchestrion plugin + expect(updateConfig).toHaveBeenCalledTimes(2); expect(sentryVitePluginSpy).toHaveBeenCalledTimes(1); }); - it("doesn't add the Vite plugin in dev mode", async () => { + it("doesn't add the sourcemaps Vite plugin in dev mode", async () => { const integration = sentryAstro({ sourceMapsUploadOptions: { enabled: true }, }); @@ -337,7 +376,13 @@ describe('sentryAstro integration', () => { command: 'dev', }); - expect(updateConfig).toHaveBeenCalledTimes(0); + // the sourcemaps plugin is skipped in dev, but the orchestrion plugin is still wired + expect(updateConfig).toHaveBeenCalledTimes(1); + expect(updateConfig).toHaveBeenCalledWith({ + vite: { + plugins: [{ name: 'sentry-orchestrion-vite' }], + }, + }); expect(sentryVitePluginSpy).toHaveBeenCalledTimes(0); }); @@ -348,12 +393,78 @@ describe('sentryAstro integration', () => { expect(integration.hooks['astro:config:setup']).toBeDefined(); // @ts-expect-error - the hook exists and we only need to pass what we actually use - await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config }); + await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config }); + // neither the sourcemaps nor the orchestrion plugin should be wired expect(updateConfig).toHaveBeenCalledTimes(0); + expect(orchestrionVite).not.toHaveBeenCalled(); expect(sentryVitePluginSpy).toHaveBeenCalledTimes(0); }); + it('adds the orchestrion plugin by default', async () => { + const integration = sentryAstro({}); + + expect(integration.hooks['astro:config:setup']).toBeDefined(); + // @ts-expect-error - the hook exists and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config }); + + expect(orchestrionVite).toHaveBeenCalledWith({ buildTimeInstrumentation: undefined }); + expect(updateConfig).toHaveBeenCalledWith({ + vite: { + plugins: [{ name: 'sentry-orchestrion-vite' }], + }, + }); + }); + + it('adds an inert orchestrion plugin when `buildTimeInstrumentation` is `false`', async () => { + const integration = sentryAstro({ buildTimeInstrumentation: false }); + + expect(integration.hooks['astro:config:setup']).toBeDefined(); + // @ts-expect-error - the hook exists and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config }); + + expect(orchestrionVite).toHaveBeenCalledWith({ buildTimeInstrumentation: false }); + expect(updateConfig).toHaveBeenCalledWith({ + vite: { + plugins: [{ name: 'sentry-orchestrion-disabled' }], + }, + }); + }); + + it("doesn't add the orchestrion plugin for the cloudflare adapter", async () => { + const integration = sentryAstro({}); + + const cloudflareConfig = { ...config, adapter: { name: '@astrojs/cloudflare' } } as AstroConfig; + + expect(integration.hooks['astro:config:setup']).toBeDefined(); + // @ts-expect-error - the hook exists and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ + ...baseConfigHookObject, + updateConfig, + injectScript, + config: cloudflareConfig, + }); + + expect(orchestrionVite).not.toHaveBeenCalled(); + expect(updateConfig).not.toHaveBeenCalledWith({ + vite: { + plugins: [{ name: 'sentry-orchestrion-vite' }], + }, + }); + }); + + it("doesn't warn about deprecated options when `buildTimeInstrumentation` is set", async () => { + const integration = sentryAstro({ buildTimeInstrumentation: false }); + + const logger = { warn: vi.fn(), info: vi.fn() }; + + expect(integration.hooks['astro:config:setup']).toBeDefined(); + // @ts-expect-error - the hook exists and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config, logger }); + + expect(logger.warn).not.toHaveBeenCalledWith(expect.stringContaining('buildTimeInstrumentation')); + }); + it.each([{}, { enabled: true }])('injects client and server init scripts', async options => { const integration = sentryAstro(options);