|
| 1 | +/* eslint-disable @sentry-internal/sdk/no-optional-chaining */ |
| 2 | +import * as fs from 'fs'; |
| 3 | +import * as path from 'path'; |
| 4 | +import type { Plugin } from 'vite'; |
| 5 | + |
| 6 | +export const WRAPPED_MODULE_SUFFIX = '?sentry-auto-wrap'; |
| 7 | + |
| 8 | +export type AutoInstrumentSelection = { |
| 9 | + /** |
| 10 | + * If this flag is `true`, the Sentry plugins will automatically instrument the `load` function of |
| 11 | + * your universal `load` functions declared in your `+page.(js|ts)` and `+layout.(js|ts)` files. |
| 12 | + * |
| 13 | + * @default true |
| 14 | + */ |
| 15 | + load?: boolean; |
| 16 | + |
| 17 | + /** |
| 18 | + * If this flag is `true`, the Sentry plugins will automatically instrument the `load` function of |
| 19 | + * your server-only `load` functions declared in your `+page.server.(js|ts)` |
| 20 | + * and `+layout.server.(js|ts)` files. |
| 21 | + * |
| 22 | + * @default true |
| 23 | + */ |
| 24 | + serverLoad?: boolean; |
| 25 | +}; |
| 26 | + |
| 27 | +type AutoInstrumentPluginOptions = AutoInstrumentSelection & { |
| 28 | + debug: boolean; |
| 29 | +}; |
| 30 | + |
| 31 | +/** |
| 32 | + * Creates a Vite plugin that automatically instruments the parts of the app |
| 33 | + * specified in @param options. This includes |
| 34 | + * |
| 35 | + * - universal `load` functions from `+page.(js|ts)` and `+layout.(js|ts)` files |
| 36 | + * - server-only `load` functions from `+page.server.(js|ts)` and `+layout.server.(js|ts)` files |
| 37 | + * |
| 38 | + * @returns the plugin |
| 39 | + */ |
| 40 | +export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptions): Plugin { |
| 41 | + const { load: shouldWrapLoad, serverLoad: shouldWrapServerLoad, debug } = options; |
| 42 | + |
| 43 | + return { |
| 44 | + name: 'sentry-auto-instrumentation', |
| 45 | + // This plugin needs to run as early as possible, before the SvelteKit plugin virtualizes all paths and ids |
| 46 | + enforce: 'pre', |
| 47 | + |
| 48 | + async load(id) { |
| 49 | + const applyUniversalLoadWrapper = |
| 50 | + shouldWrapLoad && |
| 51 | + /^\+(page|layout)\.(js|ts|mjs|mts)$/.test(path.basename(id)) && |
| 52 | + (await canWrapLoad(id, debug)); |
| 53 | + |
| 54 | + if (applyUniversalLoadWrapper) { |
| 55 | + // eslint-disable-next-line no-console |
| 56 | + debug && console.log(`Wrapping ${id} with Sentry load wrapper`); |
| 57 | + return getWrapperCode('wrapLoadWithSentry', `${id}${WRAPPED_MODULE_SUFFIX}`); |
| 58 | + } |
| 59 | + |
| 60 | + const applyServerLoadWrapper = |
| 61 | + shouldWrapServerLoad && |
| 62 | + /^\+(page|layout)\.server\.(js|ts|mjs|mts)$/.test(path.basename(id)) && |
| 63 | + (await canWrapLoad(id, debug)); |
| 64 | + |
| 65 | + if (applyServerLoadWrapper) { |
| 66 | + // eslint-disable-next-line no-console |
| 67 | + debug && console.log(`Wrapping ${id} with Sentry load wrapper`); |
| 68 | + return getWrapperCode('wrapServerLoadWithSentry', `${id}${WRAPPED_MODULE_SUFFIX}`); |
| 69 | + } |
| 70 | + |
| 71 | + return null; |
| 72 | + }, |
| 73 | + }; |
| 74 | +} |
| 75 | + |
| 76 | +/** |
| 77 | + * We only want to apply our wrapper to files that |
| 78 | + * |
| 79 | + * - Have no Sentry code yet in them. This is to avoid double-wrapping or interferance with custom |
| 80 | + * Sentry calls. |
| 81 | + * - Actually declare a `load` function. The second check of course is not 100% accurate, but it's good enough. |
| 82 | + * Injecting our wrapper into files that don't declare a `load` function would result in a build-time warning |
| 83 | + * because vite/rollup warns if we reference an export from the user's file in our wrapping code that |
| 84 | + * doesn't exist. |
| 85 | + * |
| 86 | + * Exported for testing |
| 87 | + * |
| 88 | + * @returns `true` if we can wrap the given file, `false` otherwise |
| 89 | + */ |
| 90 | +export async function canWrapLoad(id: string, debug: boolean): Promise<boolean> { |
| 91 | + const code = (await fs.promises.readFile(id, 'utf8')).toString(); |
| 92 | + |
| 93 | + const codeWithoutComments = code.replace(/(\/\/.*| ?\/\*[^]*?\*\/)(,?)$/gm, ''); |
| 94 | + |
| 95 | + const hasSentryContent = codeWithoutComments.includes('@sentry/sveltekit'); |
| 96 | + if (hasSentryContent) { |
| 97 | + // eslint-disable-next-line no-console |
| 98 | + debug && console.log(`Skipping wrapping ${id} because it already contains Sentry code`); |
| 99 | + } |
| 100 | + |
| 101 | + const hasLoadDeclaration = /((const|let|var|function)\s+load\s*(=|\())|as\s+load\s*(,|})/gm.test(codeWithoutComments); |
| 102 | + if (!hasLoadDeclaration) { |
| 103 | + // eslint-disable-next-line no-console |
| 104 | + debug && console.log(`Skipping wrapping ${id} because it doesn't declare a \`load\` function`); |
| 105 | + } |
| 106 | + |
| 107 | + return !hasSentryContent && hasLoadDeclaration; |
| 108 | +} |
| 109 | + |
| 110 | +/** |
| 111 | + * Return wrapper code fo the given module id and wrapping function |
| 112 | + */ |
| 113 | +function getWrapperCode( |
| 114 | + wrapperFunction: 'wrapLoadWithSentry' | 'wrapServerLoadWithSentry', |
| 115 | + idWithSuffix: string, |
| 116 | +): string { |
| 117 | + return ( |
| 118 | + `import { ${wrapperFunction} } from "@sentry/sveltekit";` + |
| 119 | + `import * as userModule from ${JSON.stringify(idWithSuffix)};` + |
| 120 | + `export const load = userModule.load ? ${wrapperFunction}(userModule.load) : undefined;` + |
| 121 | + `export * from ${JSON.stringify(idWithSuffix)};` |
| 122 | + ); |
| 123 | +} |
0 commit comments