From 80e841cdfa083deeb76fdf7e8c2cd9df497e0d4d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 23 May 2023 18:20:27 +0200 Subject: [PATCH 1/9] feat(sveltekit): Add source maps support for Vercel (lambda) --- packages/sveltekit/.eslintrc.js | 4 ++ packages/sveltekit/src/server/utils.ts | 28 ++++++++++++-- .../sveltekit/src/vite/sentryVitePlugins.ts | 1 + packages/sveltekit/src/vite/sourceMaps.ts | 33 ++++++++++++----- packages/sveltekit/src/vite/svelteConfig.ts | 31 +++++++++++++--- packages/sveltekit/test/server/utils.test.ts | 37 ++++++++++++++++++- .../test/vite/sentrySvelteKitPlugins.test.ts | 2 + .../sveltekit/test/vite/sourceMaps.test.ts | 11 +++++- .../sveltekit/test/vite/svelteConfig.test.ts | 32 ++++++++++++++-- 9 files changed, 154 insertions(+), 25 deletions(-) diff --git a/packages/sveltekit/.eslintrc.js b/packages/sveltekit/.eslintrc.js index c3ca0faa363b..277b646bb85f 100644 --- a/packages/sveltekit/.eslintrc.js +++ b/packages/sveltekit/.eslintrc.js @@ -17,6 +17,10 @@ module.exports = { project: ['tsconfig.test.json'], }, }, + { + files: ['./src/vite/**', './src/server/**'], + '@sentry-internal/sdk/no-optional-chaining': 'off', + }, ], extends: ['../../.eslintrc.js'], }; diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts index 17fc855ebc16..67023a0a4b37 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server/utils.ts @@ -1,9 +1,23 @@ import type { DynamicSamplingContext, StackFrame, TraceparentData } from '@sentry/types'; -import { baggageHeaderToDynamicSamplingContext, basename, extractTraceparentData } from '@sentry/utils'; +import { + baggageHeaderToDynamicSamplingContext, + basename, + escapeStringForRegex, + extractTraceparentData, +} from '@sentry/utils'; import type { RequestEvent } from '@sveltejs/kit'; import { WRAPPED_MODULE_SUFFIX } from '../vite/autoInstrument'; +/** + * Extend the `global` type with custom properties that are + * injected by the SvelteKit SDK at build time. + * @see packages/sveltekit/src/vite/sourcemaps.ts + */ +export type GlobalWithSentryValues = typeof globalThis & { + __sentry_sveltekit_output_dir?: string; +}; + /** * Takes a request event and extracts traceparent and DSC data * from the `sentry-trace` and `baggage` DSC headers. @@ -35,7 +49,8 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame { if (!frame.filename) { return frame; } - + const globalWithSentryValues: GlobalWithSentryValues = globalThis; + const svelteKitBuildOutDir = globalWithSentryValues.__sentry_sveltekit_output_dir; const prefix = 'app:///'; // Check if the frame filename begins with `/` or a Windows-style prefix such as `C:\` @@ -48,8 +63,13 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame { .replace(/\\/g, '/') // replace all `\\` instances with `/` : frame.filename; - const base = basename(filename); - frame.filename = `${prefix}${base}`; + let strippedFilename; + if (svelteKitBuildOutDir) { + strippedFilename = filename.replace(new RegExp(`^.*${escapeStringForRegex(svelteKitBuildOutDir)}\/server\/`), ''); + } else { + strippedFilename = basename(filename); + } + frame.filename = `${prefix}${strippedFilename}`; } delete frame.module; diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts index e78a25adea10..60852424ded2 100644 --- a/packages/sveltekit/src/vite/sentryVitePlugins.ts +++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts @@ -103,6 +103,7 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} const pluginOptions = { ...mergedOptions.sourceMapsUploadOptions, debug: mergedOptions.debug, // override the plugin's debug flag with the one from the top-level options + adapter: mergedOptions.adapter, }; sentryPlugins.push(await makeCustomSentryVitePlugin(pluginOptions)); } diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 8c7662a93813..2613a86fc1f0 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -11,7 +11,8 @@ import * as sorcery from 'sorcery'; import type { Plugin } from 'vite'; import { WRAPPED_MODULE_SUFFIX } from './autoInstrument'; -import { getAdapterOutputDir, loadSvelteConfig } from './svelteConfig'; +import type { SupportedSvelteKitAdapters } from './detectAdapter'; +import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from './svelteConfig'; // sorcery has no types, so these are some basic type definitions: type Chain = { @@ -26,6 +27,10 @@ type SentryVitePluginOptionsOptionalInclude = Omit { +export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePluginOptions): Promise { const svelteConfig = await loadSvelteConfig(); - const outputDir = await getAdapterOutputDir(svelteConfig); + const usedAdapter = options?.adapter || 'other'; + const outputDir = await getAdapterOutputDir(svelteConfig, usedAdapter); const hasSentryProperties = fs.existsSync(path.resolve(process.cwd(), 'sentry.properties')); const defaultPluginOptions: SentryVitePluginOptions = { - include: [ - { paths: [`${outputDir}/client`] }, - { paths: [`${outputDir}/server/chunks`] }, - { paths: [`${outputDir}/server`], ignore: ['chunks/**'] }, - ], + include: [{ paths: [`${outputDir}/client`] }, { paths: [`${outputDir}/server`] }], configFile: hasSentryProperties ? 'sentry.properties' : undefined, release, }; @@ -74,6 +76,8 @@ export async function makeCustomSentryVitePlugin(options?: SentryVitePluginOptio let isSSRBuild = true; + const serverHooksFile = getHooksFileName(svelteConfig, 'server'); + const customPlugin: Plugin = { name: 'sentry-upload-source-maps', apply: 'build', // only apply this plugin at build time @@ -84,7 +88,6 @@ export async function makeCustomSentryVitePlugin(options?: SentryVitePluginOptio buildStart, resolveId, renderChunk, - transform, // Modify the config to generate source maps config: config => { @@ -109,6 +112,18 @@ export async function makeCustomSentryVitePlugin(options?: SentryVitePluginOptio } }, + transform: async (code, id) => { + let modifiedCode = code; + const isServerHooksFile = new RegExp(`\/${escapeStringForRegex(serverHooksFile)}(\.(js|ts|mjs|mts))?`).test(id); + + if (isServerHooksFile) { + let injectedCode = `global.__sentry_sveltekit_output_dir = "${outputDir || 'undefined'}";\n`; + modifiedCode = `${code}\n${injectedCode}`; + } + // @ts-ignore - this hook exists on the plugin! + return sentryPlugin.transform(modifiedCode, id); + }, + // We need to start uploading source maps later than in the original plugin // because SvelteKit is invoking the adapter at closeBundle. // This means that we need to wait until the adapter is done before we start uploading. diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts index 702e29cb9c3f..47c0eb88c5b1 100644 --- a/packages/sveltekit/src/vite/svelteConfig.ts +++ b/packages/sveltekit/src/vite/svelteConfig.ts @@ -4,6 +4,7 @@ import type { Builder, Config } from '@sveltejs/kit'; import * as fs from 'fs'; import * as path from 'path'; import * as url from 'url'; +import { SupportedSvelteKitAdapters } from './detectAdapter'; /** * Imports the svelte.config.js file and returns the config object. @@ -35,20 +36,38 @@ export async function loadSvelteConfig(): Promise { } } +/** + * Reads a custom hooks directory from the SvelteKit config. In case no custom hooks + * directory is specified, the default directory is returned. + */ +export function getHooksFileName(svelteConfig: Config, hookType: 'client' | 'server') { + return svelteConfig.kit?.files?.hooks?.[hookType] || `src/hooks.${hookType}`; +} + /** * Attempts to read a custom output directory that can be specidied in the options * of a SvelteKit adapter. If no custom output directory is specified, the default * directory is returned. - * - * To get the directory, we have to apply a hack and call the adapter's adapt method + */ +export async function getAdapterOutputDir(svelteConfig: Config, adapter: SupportedSvelteKitAdapters): Promise { + if (adapter === 'node') { + return await getNodeAdapterOutputDir(svelteConfig); + } + + // Auto and Vercel adapters simply use config.kit.outDir + // Let's also use this directory for the 'other' case + return path.join(svelteConfig.kit?.outDir || '.svelte-kit', 'output'); +} + +/** + * To get the Node adapter output directory, we have to apply a hack and call the adapter's adapt method * with a custom adapter `Builder` that only calls the `writeClient` method. * This method is the first method that is called with the output directory. * Once we obtained the output directory, we throw an error to exit the adapter. * * see: https://github.com/sveltejs/kit/blob/master/packages/adapter-node/index.js#L17 - * */ -export async function getAdapterOutputDir(svelteConfig: Config): Promise { +async function getNodeAdapterOutputDir(svelteConfig: Config): Promise { // 'build' is the default output dir for the node adapter let outputDir = 'build'; @@ -56,7 +75,7 @@ export async function getAdapterOutputDir(svelteConfig: Config): Promise return outputDir; } - const adapter = svelteConfig.kit.adapter; + const nodeAdapter = svelteConfig.kit.adapter; const adapterBuilder: Builder = { writeClient(dest: string) { @@ -85,7 +104,7 @@ export async function getAdapterOutputDir(svelteConfig: Config): Promise }; try { - await adapter.adapt(adapterBuilder); + await nodeAdapter.adapt(adapterBuilder); } catch (_) { // We expect the adapter to throw in writeClient! } diff --git a/packages/sveltekit/test/server/utils.test.ts b/packages/sveltekit/test/server/utils.test.ts index 179cc6682d85..4ea1b3a22f10 100644 --- a/packages/sveltekit/test/server/utils.test.ts +++ b/packages/sveltekit/test/server/utils.test.ts @@ -1,7 +1,8 @@ import { RewriteFrames } from '@sentry/integrations'; import type { StackFrame } from '@sentry/types'; +import { basename } from '@sentry/utils'; -import { getTracePropagationData, rewriteFramesIteratee } from '../../src/server/utils'; +import { getTracePropagationData, GlobalWithSentryValues, rewriteFramesIteratee } from '../../src/server/utils'; const MOCK_REQUEST_EVENT: any = { request: { @@ -69,7 +70,7 @@ describe('rewriteFramesIteratee', () => { expect(result).not.toHaveProperty('module'); }); - it('does the same filename modification as the default RewriteFrames iteratee', () => { + it('does the same filename modification as the default RewriteFrames iteratee if no output dir is available', () => { const frame: StackFrame = { filename: '/some/path/to/server/chunks/3-ab34d22f.js', lineno: 1, @@ -94,4 +95,36 @@ describe('rewriteFramesIteratee', () => { expect(result).toStrictEqual(defaultResult); }); + + it.each([ + ['adapter-node', 'build', '/absolute/path/to/build/server/chunks/3-ab34d22f.js', 'app:///chunks/3-ab34d22f.js'], + [ + 'adapter-auto', + '.svelte-kit/output', + '/absolute/path/to/.svelte-kit/output/server/entries/pages/page.ts.js', + 'app:///entries/pages/page.ts.js', + ], + ])( + 'removes the absolut path to the server output dir, if the output dir is available (%s)', + (_, outputDir, frameFilename, modifiedFilename) => { + (global as GlobalWithSentryValues).__sentry_sveltekit_output_dir = outputDir; + + const frame: StackFrame = { + filename: frameFilename, + lineno: 1, + colno: 1, + module: basename(frameFilename), + }; + + const result = rewriteFramesIteratee({ ...frame }); + + expect(result).toStrictEqual({ + filename: modifiedFilename, + lineno: 1, + colno: 1, + }); + + delete (global as GlobalWithSentryValues).__sentry_sveltekit_output_dir; + }, + ); }); diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts index 026d347d777d..923005b2e9f9 100644 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts @@ -72,6 +72,7 @@ describe('sentrySvelteKit()', () => { ignore: ['bar.js'], }, autoInstrument: false, + adapter: 'vercel', }); const plugin = plugins[0]; @@ -80,6 +81,7 @@ describe('sentrySvelteKit()', () => { debug: true, ignore: ['bar.js'], include: ['foo.js'], + adapter: 'vercel', }); }); diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index 7d412811b92e..2a55ac547674 100644 --- a/packages/sveltekit/test/vite/sourceMaps.test.ts +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -6,7 +6,7 @@ const mockedSentryVitePlugin = { buildStart: vi.fn(), resolveId: vi.fn(), renderChunk: vi.fn(), - transform: vi.fn(), + transform: vi.fn().mockImplementation((code: string, _id: string) => code), writeBundle: vi.fn(), }; @@ -54,6 +54,15 @@ describe('makeCustomSentryVitePlugin()', () => { }); }); + it('injects the output dir into the server hooks file', async () => { + const plugin = await makeCustomSentryVitePlugin(); + // @ts-ignore this function exists! + const transformedCode = await plugin.transform('foo', '/src/hooks.server.ts'); + const expectedtransformedCode = 'foo\nglobal.__sentry_sveltekit_output_dir = ".svelte-kit/output";\n'; + expect(mockedSentryVitePlugin.transform).toHaveBeenCalledWith(expectedtransformedCode, '/src/hooks.server.ts'); + expect(transformedCode).toEqual(expectedtransformedCode); + }); + it('uploads source maps during the SSR build', async () => { const plugin = await makeCustomSentryVitePlugin(); // @ts-ignore this function exists! diff --git a/packages/sveltekit/test/vite/svelteConfig.test.ts b/packages/sveltekit/test/vite/svelteConfig.test.ts index 73f624c8b1be..5908854e577b 100644 --- a/packages/sveltekit/test/vite/svelteConfig.test.ts +++ b/packages/sveltekit/test/vite/svelteConfig.test.ts @@ -1,6 +1,7 @@ import { vi } from 'vitest'; +import { SupportedSvelteKitAdapters } from '../../src/vite/detectAdapter'; -import { getAdapterOutputDir, loadSvelteConfig } from '../../src/vite/svelteConfig'; +import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from '../../src/vite/svelteConfig'; let existsFile; @@ -62,8 +63,33 @@ describe('getAdapterOutputDir', () => { }, }; - it('returns the output directory of the adapter', async () => { - const outputDir = await getAdapterOutputDir({ kit: { adapter: mockedAdapter } }); + it('returns the output directory of the Node adapter', async () => { + const outputDir = await getAdapterOutputDir({ kit: { adapter: mockedAdapter } }, 'node'); expect(outputDir).toEqual('customBuildDir'); }); + + it.each(['vercel', 'auto', 'other'] as SupportedSvelteKitAdapters[])( + 'returns the config.kit.outdir directory for adapter-%s', + async adapter => { + const outputDir = await getAdapterOutputDir({ kit: { outDir: 'customOutDir' } }, adapter); + expect(outputDir).toEqual('customOutDir/output'); + }, + ); + + it('falls back to the default out dir for all other adapters if outdir is not specified in the config', async () => { + const outputDir = await getAdapterOutputDir({ kit: {} }, 'vercel'); + expect(outputDir).toEqual('.svelte-kit/output'); + }); +}); + +describe('getHooksFileName', () => { + it('returns the default hooks file name if no custom hooks file is specified', () => { + const hooksFileName = getHooksFileName({}, 'server'); + expect(hooksFileName).toEqual('src/hooks.server'); + }); + + it('returns the custom hooks file name if specified in the config', () => { + const hooksFileName = getHooksFileName({ kit: { files: { hooks: { server: 'serverhooks' } } } }, 'server'); + expect(hooksFileName).toEqual('serverhooks'); + }); }); From 21d63cca728ae7dd97ae2b9da3ed3764845a8eea Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 30 May 2023 17:23:45 +0200 Subject: [PATCH 2/9] remove escape char from regex --- packages/sveltekit/src/vite/sourceMaps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 2613a86fc1f0..65fbc5ca105e 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -114,7 +114,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi transform: async (code, id) => { let modifiedCode = code; - const isServerHooksFile = new RegExp(`\/${escapeStringForRegex(serverHooksFile)}(\.(js|ts|mjs|mts))?`).test(id); + const isServerHooksFile = new RegExp(`\/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id); if (isServerHooksFile) { let injectedCode = `global.__sentry_sveltekit_output_dir = "${outputDir || 'undefined'}";\n`; From f0445e700bf9adacc02ede81f374d8cf9e48b73b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 30 May 2023 17:58:54 +0200 Subject: [PATCH 3/9] fix various linter errors --- packages/sveltekit/.eslintrc.js | 6 ++++-- packages/sveltekit/src/server/utils.ts | 2 +- packages/sveltekit/src/vite/sourceMaps.ts | 6 +++--- packages/sveltekit/src/vite/svelteConfig.ts | 5 +++-- packages/sveltekit/test/server/utils.test.ts | 7 ++++--- packages/sveltekit/test/vite/svelteConfig.test.ts | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/sveltekit/.eslintrc.js b/packages/sveltekit/.eslintrc.js index 277b646bb85f..4a8474637698 100644 --- a/packages/sveltekit/.eslintrc.js +++ b/packages/sveltekit/.eslintrc.js @@ -18,8 +18,10 @@ module.exports = { }, }, { - files: ['./src/vite/**', './src/server/**'], - '@sentry-internal/sdk/no-optional-chaining': 'off', + files: ['src/vite/**', 'src/server/**'], + rules: { + '@sentry-internal/sdk/no-optional-chaining': 'off', + }, }, ], extends: ['../../.eslintrc.js'], diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts index 67023a0a4b37..d568260054ff 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server/utils.ts @@ -65,7 +65,7 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame { let strippedFilename; if (svelteKitBuildOutDir) { - strippedFilename = filename.replace(new RegExp(`^.*${escapeStringForRegex(svelteKitBuildOutDir)}\/server\/`), ''); + strippedFilename = filename.replace(new RegExp(`^.*${escapeStringForRegex(svelteKitBuildOutDir)}/server/`), ''); } else { strippedFilename = basename(filename); } diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 65fbc5ca105e..ba11be6cdb5e 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -72,7 +72,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi const sentryPlugin: Plugin = sentryVitePlugin(mergedOptions); const { debug } = mergedOptions; - const { buildStart, resolveId, transform, renderChunk } = sentryPlugin; + const { buildStart, resolveId, renderChunk } = sentryPlugin; let isSSRBuild = true; @@ -114,10 +114,10 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi transform: async (code, id) => { let modifiedCode = code; - const isServerHooksFile = new RegExp(`\/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id); + const isServerHooksFile = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id); if (isServerHooksFile) { - let injectedCode = `global.__sentry_sveltekit_output_dir = "${outputDir || 'undefined'}";\n`; + const injectedCode = `global.__sentry_sveltekit_output_dir = "${outputDir || 'undefined'}";\n`; modifiedCode = `${code}\n${injectedCode}`; } // @ts-ignore - this hook exists on the plugin! diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts index 47c0eb88c5b1..07c701e912f1 100644 --- a/packages/sveltekit/src/vite/svelteConfig.ts +++ b/packages/sveltekit/src/vite/svelteConfig.ts @@ -4,7 +4,8 @@ import type { Builder, Config } from '@sveltejs/kit'; import * as fs from 'fs'; import * as path from 'path'; import * as url from 'url'; -import { SupportedSvelteKitAdapters } from './detectAdapter'; + +import type { SupportedSvelteKitAdapters } from './detectAdapter'; /** * Imports the svelte.config.js file and returns the config object. @@ -40,7 +41,7 @@ export async function loadSvelteConfig(): Promise { * Reads a custom hooks directory from the SvelteKit config. In case no custom hooks * directory is specified, the default directory is returned. */ -export function getHooksFileName(svelteConfig: Config, hookType: 'client' | 'server') { +export function getHooksFileName(svelteConfig: Config, hookType: 'client' | 'server'): string { return svelteConfig.kit?.files?.hooks?.[hookType] || `src/hooks.${hookType}`; } diff --git a/packages/sveltekit/test/server/utils.test.ts b/packages/sveltekit/test/server/utils.test.ts index 4ea1b3a22f10..2fd9b0492013 100644 --- a/packages/sveltekit/test/server/utils.test.ts +++ b/packages/sveltekit/test/server/utils.test.ts @@ -2,7 +2,8 @@ import { RewriteFrames } from '@sentry/integrations'; import type { StackFrame } from '@sentry/types'; import { basename } from '@sentry/utils'; -import { getTracePropagationData, GlobalWithSentryValues, rewriteFramesIteratee } from '../../src/server/utils'; +import type { GlobalWithSentryValues } from '../../src/server/utils'; +import { getTracePropagationData, rewriteFramesIteratee } from '../../src/server/utils'; const MOCK_REQUEST_EVENT: any = { request: { @@ -107,7 +108,7 @@ describe('rewriteFramesIteratee', () => { ])( 'removes the absolut path to the server output dir, if the output dir is available (%s)', (_, outputDir, frameFilename, modifiedFilename) => { - (global as GlobalWithSentryValues).__sentry_sveltekit_output_dir = outputDir; + (globalThis as GlobalWithSentryValues).__sentry_sveltekit_output_dir = outputDir; const frame: StackFrame = { filename: frameFilename, @@ -124,7 +125,7 @@ describe('rewriteFramesIteratee', () => { colno: 1, }); - delete (global as GlobalWithSentryValues).__sentry_sveltekit_output_dir; + delete (globalThis as GlobalWithSentryValues).__sentry_sveltekit_output_dir; }, ); }); diff --git a/packages/sveltekit/test/vite/svelteConfig.test.ts b/packages/sveltekit/test/vite/svelteConfig.test.ts index 5908854e577b..5f079deb7c1a 100644 --- a/packages/sveltekit/test/vite/svelteConfig.test.ts +++ b/packages/sveltekit/test/vite/svelteConfig.test.ts @@ -1,6 +1,6 @@ import { vi } from 'vitest'; -import { SupportedSvelteKitAdapters } from '../../src/vite/detectAdapter'; +import type { SupportedSvelteKitAdapters } from '../../src/vite/detectAdapter'; import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from '../../src/vite/svelteConfig'; let existsFile; From 7b96613a44ecf33808bd320dc2e39cd31308ed6e Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 30 May 2023 18:21:08 +0200 Subject: [PATCH 4/9] make globalSentryValues type safe --- packages/sveltekit/src/server/utils.ts | 8 +++++--- packages/sveltekit/src/vite/sourceMaps.ts | 11 +++++++++-- packages/sveltekit/test/vite/sourceMaps.test.ts | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts index d568260054ff..3148a7cb34a6 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server/utils.ts @@ -9,14 +9,16 @@ import type { RequestEvent } from '@sveltejs/kit'; import { WRAPPED_MODULE_SUFFIX } from '../vite/autoInstrument'; +export type GlobalSentryValues = { + __sentry_sveltekit_output_dir?: string; +}; + /** * Extend the `global` type with custom properties that are * injected by the SvelteKit SDK at build time. * @see packages/sveltekit/src/vite/sourcemaps.ts */ -export type GlobalWithSentryValues = typeof globalThis & { - __sentry_sveltekit_output_dir?: string; -}; +export type GlobalWithSentryValues = typeof globalThis & GlobalSentryValues; /** * Takes a request event and extracts traceparent and DSC data diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index ba11be6cdb5e..ef6ad712b1e0 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -10,6 +10,7 @@ import * as path from 'path'; import * as sorcery from 'sorcery'; import type { Plugin } from 'vite'; +import type { GlobalSentryValues } from '../server/utils'; import { WRAPPED_MODULE_SUFFIX } from './autoInstrument'; import type { SupportedSvelteKitAdapters } from './detectAdapter'; import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from './svelteConfig'; @@ -78,6 +79,10 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi const serverHooksFile = getHooksFileName(svelteConfig, 'server'); + const globalSentryValues: GlobalSentryValues = { + __sentry_sveltekit_output_dir: outputDir, + }; + const customPlugin: Plugin = { name: 'sentry-upload-source-maps', apply: 'build', // only apply this plugin at build time @@ -117,8 +122,10 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi const isServerHooksFile = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id); if (isServerHooksFile) { - const injectedCode = `global.__sentry_sveltekit_output_dir = "${outputDir || 'undefined'}";\n`; - modifiedCode = `${code}\n${injectedCode}`; + const injectedCode = Object.entries(globalSentryValues) + .map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`) + .join('\n'); + modifiedCode = `${code}\n${injectedCode}\n`; } // @ts-ignore - this hook exists on the plugin! return sentryPlugin.transform(modifiedCode, id); diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index 2a55ac547674..62db9a2ebb49 100644 --- a/packages/sveltekit/test/vite/sourceMaps.test.ts +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -58,7 +58,7 @@ describe('makeCustomSentryVitePlugin()', () => { const plugin = await makeCustomSentryVitePlugin(); // @ts-ignore this function exists! const transformedCode = await plugin.transform('foo', '/src/hooks.server.ts'); - const expectedtransformedCode = 'foo\nglobal.__sentry_sveltekit_output_dir = ".svelte-kit/output";\n'; + const expectedtransformedCode = 'foo\nglobalThis["__sentry_sveltekit_output_dir"] = ".svelte-kit/output";\n'; expect(mockedSentryVitePlugin.transform).toHaveBeenCalledWith(expectedtransformedCode, '/src/hooks.server.ts'); expect(transformedCode).toEqual(expectedtransformedCode); }); From 6db9d10d47a85df3a70ec9af4d0fa4dbb0506737 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 31 May 2023 15:25:54 +0200 Subject: [PATCH 5/9] use GLOBAL from utils --- packages/sveltekit/src/server/utils.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts index 3148a7cb34a6..419813af1244 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server/utils.ts @@ -1,9 +1,12 @@ import type { DynamicSamplingContext, StackFrame, TraceparentData } from '@sentry/types'; +import type { InternalGlobal } from '@sentry/utils'; import { baggageHeaderToDynamicSamplingContext, basename, escapeStringForRegex, extractTraceparentData, + GLOBAL_OBJ, + join, } from '@sentry/utils'; import type { RequestEvent } from '@sveltejs/kit'; @@ -18,7 +21,7 @@ export type GlobalSentryValues = { * injected by the SvelteKit SDK at build time. * @see packages/sveltekit/src/vite/sourcemaps.ts */ -export type GlobalWithSentryValues = typeof globalThis & GlobalSentryValues; +export type GlobalWithSentryValues = InternalGlobal & GlobalSentryValues; /** * Takes a request event and extracts traceparent and DSC data @@ -51,7 +54,7 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame { if (!frame.filename) { return frame; } - const globalWithSentryValues: GlobalWithSentryValues = globalThis; + const globalWithSentryValues: GlobalWithSentryValues = GLOBAL_OBJ; const svelteKitBuildOutDir = globalWithSentryValues.__sentry_sveltekit_output_dir; const prefix = 'app:///'; @@ -67,7 +70,10 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame { let strippedFilename; if (svelteKitBuildOutDir) { - strippedFilename = filename.replace(new RegExp(`^.*${escapeStringForRegex(svelteKitBuildOutDir)}/server/`), ''); + strippedFilename = filename.replace( + new RegExp(`^.*${escapeStringForRegex(join(svelteKitBuildOutDir, 'server'))}`), + '', + ); } else { strippedFilename = basename(filename); } From 023151a9282d040e28aa1d54262dd4d6ede29f3b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 31 May 2023 17:22:04 +0200 Subject: [PATCH 6/9] move to virtual injection file --- packages/sveltekit/src/server/utils.ts | 15 +------ .../sveltekit/src/vite/injectGlobalValues.ts | 41 +++++++++++++++++++ packages/sveltekit/src/vite/sourceMaps.ts | 33 +++++++++++---- .../test/vite/injectGlobalValues.test.ts | 34 +++++++++++++++ .../sveltekit/test/vite/sourceMaps.test.ts | 2 +- 5 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 packages/sveltekit/src/vite/injectGlobalValues.ts create mode 100644 packages/sveltekit/test/vite/injectGlobalValues.test.ts diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts index 419813af1244..6e130f13a514 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server/utils.ts @@ -1,5 +1,4 @@ import type { DynamicSamplingContext, StackFrame, TraceparentData } from '@sentry/types'; -import type { InternalGlobal } from '@sentry/utils'; import { baggageHeaderToDynamicSamplingContext, basename, @@ -11,17 +10,7 @@ import { import type { RequestEvent } from '@sveltejs/kit'; import { WRAPPED_MODULE_SUFFIX } from '../vite/autoInstrument'; - -export type GlobalSentryValues = { - __sentry_sveltekit_output_dir?: string; -}; - -/** - * Extend the `global` type with custom properties that are - * injected by the SvelteKit SDK at build time. - * @see packages/sveltekit/src/vite/sourcemaps.ts - */ -export type GlobalWithSentryValues = InternalGlobal & GlobalSentryValues; +import type { GlobalWithSentryValues } from '../vite/injectGLobalValues'; /** * Takes a request event and extracts traceparent and DSC data @@ -71,7 +60,7 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame { let strippedFilename; if (svelteKitBuildOutDir) { strippedFilename = filename.replace( - new RegExp(`^.*${escapeStringForRegex(join(svelteKitBuildOutDir, 'server'))}`), + new RegExp(`^.*${escapeStringForRegex(join(svelteKitBuildOutDir, 'server'))}/`), '', ); } else { diff --git a/packages/sveltekit/src/vite/injectGlobalValues.ts b/packages/sveltekit/src/vite/injectGlobalValues.ts new file mode 100644 index 000000000000..14a39d99bceb --- /dev/null +++ b/packages/sveltekit/src/vite/injectGlobalValues.ts @@ -0,0 +1,41 @@ +import type { InternalGlobal } from '@sentry/utils'; + +export type GlobalSentryValues = { + __sentry_sveltekit_output_dir?: string; +}; + +/** + * Extend the `global` type with custom properties that are + * injected by the SvelteKit SDK at build time. + * @see packages/sveltekit/src/vite/sourcemaps.ts + */ +export type GlobalWithSentryValues = InternalGlobal & GlobalSentryValues; + +export const VIRTUAL_GLOBAL_VALUES_FILE = '\0sentry-inject-global-values-file'; + +/** + * @returns code that injects @param globalSentryValues into the global scope. + */ +export function getGlobalValueInjectionCode(globalSentryValues: GlobalSentryValues): string { + if (Object.keys(globalSentryValues).length === 0) { + return ''; + } + + const sentryGlobal = '_global'; + + const globalCode = `var ${sentryGlobal} = + typeof window !== 'undefined' ? + window : + typeof globalThis !== 'undefined' ? + globalThis : + typeof global !== 'undefined' ? + global : + typeof self !== 'undefined' ? + self : + {};`; + const injectedValuesCode = Object.entries(globalSentryValues) + .map(([key, value]) => `${sentryGlobal}["${key}"] = ${JSON.stringify(value)};`) + .join('\n'); + + return `${globalCode}\n${injectedValuesCode}\n`; +} diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index ef6ad712b1e0..aca1b1002a24 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -10,9 +10,10 @@ import * as path from 'path'; import * as sorcery from 'sorcery'; import type { Plugin } from 'vite'; -import type { GlobalSentryValues } from '../server/utils'; import { WRAPPED_MODULE_SUFFIX } from './autoInstrument'; import type { SupportedSvelteKitAdapters } from './detectAdapter'; +import type { GlobalSentryValues } from './injectGLobalValues'; +import { getGlobalValueInjectionCode, VIRTUAL_GLOBAL_VALUES_FILE } from './injectGLobalValues'; import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from './svelteConfig'; // sorcery has no types, so these are some basic type definitions: @@ -73,7 +74,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi const sentryPlugin: Plugin = sentryVitePlugin(mergedOptions); const { debug } = mergedOptions; - const { buildStart, resolveId, renderChunk } = sentryPlugin; + const { buildStart, renderChunk } = sentryPlugin; let isSSRBuild = true; @@ -91,7 +92,6 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi // These hooks are copied from the original Sentry Vite plugin. // They're mostly responsible for options parsing and release injection. buildStart, - resolveId, renderChunk, // Modify the config to generate source maps @@ -107,6 +107,27 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi }; }, + resolveId: (id, _importer, _ref) => { + if (id === VIRTUAL_GLOBAL_VALUES_FILE) { + return { + id: VIRTUAL_GLOBAL_VALUES_FILE, + external: false, + moduleSideEffects: true, + }; + } + // @ts-ignore - this hook exists on the plugin! + return sentryPlugin.resolveId(id, _importer, _ref); + }, + + load: id => { + if (id === VIRTUAL_GLOBAL_VALUES_FILE) { + return { + code: getGlobalValueInjectionCode(globalSentryValues), + }; + } + return null; + }, + configResolved: config => { // The SvelteKit plugins trigger additional builds within the main (SSR) build. // We just need a mechanism to upload source maps only once. @@ -122,10 +143,8 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi const isServerHooksFile = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id); if (isServerHooksFile) { - const injectedCode = Object.entries(globalSentryValues) - .map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`) - .join('\n'); - modifiedCode = `${code}\n${injectedCode}\n`; + const globalValuesImport = `; import "${VIRTUAL_GLOBAL_VALUES_FILE}";`; + modifiedCode = `${code}\n${globalValuesImport}\n`; } // @ts-ignore - this hook exists on the plugin! return sentryPlugin.transform(modifiedCode, id); diff --git a/packages/sveltekit/test/vite/injectGlobalValues.test.ts b/packages/sveltekit/test/vite/injectGlobalValues.test.ts new file mode 100644 index 000000000000..0fe6b07dc989 --- /dev/null +++ b/packages/sveltekit/test/vite/injectGlobalValues.test.ts @@ -0,0 +1,34 @@ +import { getGlobalValueInjectionCode } from '../../src/vite/injectGlobalValues'; + +describe('getGlobalValueInjectionCode', () => { + it('returns code that injects values into the global object', () => { + const injectionCode = getGlobalValueInjectionCode({ + // @ts-ignore - just want to test this with multiple values + something: 'else', + __sentry_sveltekit_output_dir: '.svelte-kit/output', + }); + expect(injectionCode).toEqual(`var _global = + typeof window !== 'undefined' ? + window : + typeof globalThis !== 'undefined' ? + globalThis : + typeof global !== 'undefined' ? + global : + typeof self !== 'undefined' ? + self : + {}; +_global["something"] = "else"; +_global["__sentry_sveltekit_output_dir"] = ".svelte-kit/output"; +`); + + // Check that the code above is in fact valid and works as expected + // The return value of eval here is the value of the last expression in the code + expect(eval(`${injectionCode}`)).toEqual('.svelte-kit/output'); + + delete globalThis.__sentry_sveltekit_output_dir; + }); + + it('returns empty string if no values are passed', () => { + expect(getGlobalValueInjectionCode({})).toEqual(''); + }); +}); diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index 62db9a2ebb49..9d565aceab58 100644 --- a/packages/sveltekit/test/vite/sourceMaps.test.ts +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -58,7 +58,7 @@ describe('makeCustomSentryVitePlugin()', () => { const plugin = await makeCustomSentryVitePlugin(); // @ts-ignore this function exists! const transformedCode = await plugin.transform('foo', '/src/hooks.server.ts'); - const expectedtransformedCode = 'foo\nglobalThis["__sentry_sveltekit_output_dir"] = ".svelte-kit/output";\n'; + const expectedtransformedCode = 'foo\n; import "\0sentry-inject-global-values-file";\n'; expect(mockedSentryVitePlugin.transform).toHaveBeenCalledWith(expectedtransformedCode, '/src/hooks.server.ts'); expect(transformedCode).toEqual(expectedtransformedCode); }); From 67afed41baecdb917982b538fee7b46253e69fe5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 31 May 2023 17:30:36 +0200 Subject: [PATCH 7/9] typo --- packages/sveltekit/src/vite/injectGlobalValues.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sveltekit/src/vite/injectGlobalValues.ts b/packages/sveltekit/src/vite/injectGlobalValues.ts index 14a39d99bceb..d0f6424a338d 100644 --- a/packages/sveltekit/src/vite/injectGlobalValues.ts +++ b/packages/sveltekit/src/vite/injectGlobalValues.ts @@ -14,7 +14,7 @@ export type GlobalWithSentryValues = InternalGlobal & GlobalSentryValues; export const VIRTUAL_GLOBAL_VALUES_FILE = '\0sentry-inject-global-values-file'; /** - * @returns code that injects @param globalSentryValues into the global scope. + * @returns code that injects @param globalSentryValues into the global object. */ export function getGlobalValueInjectionCode(globalSentryValues: GlobalSentryValues): string { if (Object.keys(globalSentryValues).length === 0) { From 197791f576e9d54e5f69df20c9c38fc6f7ba408b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 31 May 2023 17:45:37 +0200 Subject: [PATCH 8/9] simplify include entry --- packages/sveltekit/src/vite/sourceMaps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index aca1b1002a24..bf842882488d 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -61,7 +61,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi const hasSentryProperties = fs.existsSync(path.resolve(process.cwd(), 'sentry.properties')); const defaultPluginOptions: SentryVitePluginOptions = { - include: [{ paths: [`${outputDir}/client`] }, { paths: [`${outputDir}/server`] }], + include: [`${outputDir}/client`, `${outputDir}/server`], configFile: hasSentryProperties ? 'sentry.properties' : undefined, release, }; From 568f9e7d73c8b1e6b425a1dd960b9ae8ae13e1c1 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 1 Jun 2023 12:49:41 +0200 Subject: [PATCH 9/9] fix typo --- packages/sveltekit/src/server/utils.ts | 2 +- packages/sveltekit/src/vite/sourceMaps.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts index 6e130f13a514..8ef6acced314 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server/utils.ts @@ -10,7 +10,7 @@ import { import type { RequestEvent } from '@sveltejs/kit'; import { WRAPPED_MODULE_SUFFIX } from '../vite/autoInstrument'; -import type { GlobalWithSentryValues } from '../vite/injectGLobalValues'; +import type { GlobalWithSentryValues } from '../vite/injectGlobalValues'; /** * Takes a request event and extracts traceparent and DSC data diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index bf842882488d..6f2b7086786a 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -12,8 +12,8 @@ import type { Plugin } from 'vite'; import { WRAPPED_MODULE_SUFFIX } from './autoInstrument'; import type { SupportedSvelteKitAdapters } from './detectAdapter'; -import type { GlobalSentryValues } from './injectGLobalValues'; -import { getGlobalValueInjectionCode, VIRTUAL_GLOBAL_VALUES_FILE } from './injectGLobalValues'; +import type { GlobalSentryValues } from './injectGlobalValues'; +import { getGlobalValueInjectionCode, VIRTUAL_GLOBAL_VALUES_FILE } from './injectGlobalValues'; import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from './svelteConfig'; // sorcery has no types, so these are some basic type definitions: