Skip to content

Commit 57007c9

Browse files
committed
feat(sveltekit): Auto-wrap universal and server load functions
1 parent 15d9102 commit 57007c9

13 files changed

+487
-28
lines changed

packages/sveltekit/.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ module.exports = {
1919
},
2020
],
2121
extends: ['../../.eslintrc.js'],
22+
ignorePatterns: ['scripts/**/*', 'src/vite/templates/**/*'],
2223
};

packages/sveltekit/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,23 @@
2929
"@sentry/utils": "7.49.0",
3030
"@sentry/vite-plugin": "^0.6.0",
3131
"magic-string": "^0.30.0",
32+
"rollup": "^3.20.2",
3233
"sorcery": "0.11.0"
3334
},
3435
"devDependencies": {
3536
"@sveltejs/kit": "^1.11.0",
36-
"rollup": "^3.20.2",
3737
"svelte": "^3.44.0",
3838
"typescript": "^4.9.3",
3939
"vite": "4.0.0"
4040
},
4141
"scripts": {
4242
"build": "run-p build:transpile build:types",
4343
"build:dev": "yarn build",
44-
"build:transpile": "rollup -c rollup.npm.config.js --bundleConfigAsCjs",
44+
"build:transpile": "ts-node scripts/buildRollup.ts",
4545
"build:types": "tsc -p tsconfig.types.json",
4646
"build:watch": "run-p build:transpile:watch build:types:watch",
4747
"build:dev:watch": "yarn build:watch",
48-
"build:transpile:watch": "rollup -c rollup.npm.config.js --watch",
48+
"build:transpile:watch": "nodemon --ext ts --watch src scripts/buildRollup.ts",
4949
"build:types:watch": "tsc -p tsconfig.types.json --watch",
5050
"build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build",
5151
"circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts",
Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,44 @@
11
import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js';
22

3-
export default makeNPMConfigVariants(
4-
makeBaseNPMConfig({
5-
entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'],
6-
packageSpecificConfig: {
7-
external: ['$app/stores'],
8-
output: {
9-
dynamicImportInCjs: true,
10-
}
11-
},
12-
}),
13-
);
3+
export default [
4+
5+
...makeNPMConfigVariants(
6+
makeBaseNPMConfig({
7+
entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'],
8+
packageSpecificConfig: {
9+
external: ['$app/stores'],
10+
output: {
11+
dynamicImportInCjs: true,
12+
}
13+
},
14+
}),
15+
),
16+
// Templates for load function wrappers
17+
...makeNPMConfigVariants(
18+
makeBaseNPMConfig({
19+
entrypoints: [
20+
'src/vite/templates/universalLoadTemplate.ts',
21+
'src/vite/templates/serverLoadTemplate.ts',
22+
],
23+
24+
packageSpecificConfig: {
25+
output: {
26+
// Preserve the original file structure (i.e., so that everything is still relative to `src`)
27+
entryFileNames: 'vite/templates/[name].js',
28+
29+
// this is going to be add-on code, so it doesn't need the trappings of a full module (and in fact actively
30+
// shouldn't have them, lest they muck with the module to which we're adding it)
31+
sourcemap: false,
32+
esModule: false,
33+
34+
// make it so Rollup calms down about the fact that we're combining default and named exports
35+
exports: 'named',
36+
},
37+
external: [
38+
'@sentry/sveltekit',
39+
'__SENTRY_WRAPPING_TARGET_FILE__',
40+
],
41+
},
42+
}),
43+
),
44+
];
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as childProcess from 'child_process';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
5+
/**
6+
* Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current
7+
* process. Returns contents of `stdout`.
8+
*/
9+
function run(cmd: string, options?: childProcess.ExecSyncOptions): string | Buffer {
10+
return childProcess.execSync(cmd, { stdio: 'inherit', ...options });
11+
}
12+
13+
run('./node_modules/.bin/rollup -c rollup.npm.config.js --bundleConfigAsCjs');
14+
15+
// Regardless of whether SvelteKit is using the CJS or ESM version of our SDK, we want the code from our templates to be in
16+
// ESM (since we'll be adding it onto page files which are themselves written in ESM), so copy the ESM versions of the
17+
// templates over into the CJS build directory. (Building only the ESM version and sticking it in both locations is
18+
// something which in theory Rollup could do, but it would mean refactoring our Rollup helper functions, which isn't
19+
// worth it just for this.)
20+
const cjsTemplateDir = 'build/cjs/vite/templates/';
21+
const esmTemplateDir = 'build/esm/vite/templates/';
22+
fs.readdirSync(esmTemplateDir).forEach(templateFile =>
23+
fs.copyFileSync(path.join(esmTemplateDir, templateFile), path.join(cjsTemplateDir, templateFile)),
24+
);
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
import type { SourceMap } from 'rollup';
5+
import { rollup } from 'rollup';
6+
import type { Plugin } from 'vite';
7+
8+
// Just a simple placeholder to make referencing module consistent
9+
const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module';
10+
11+
// Needs to end in .cjs in order for the `commonjs` plugin to pick it up
12+
const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET_FILE__.js';
13+
14+
export type AutoInstrumentSelection = {
15+
/**
16+
* If this flag is `true`, the Sentry plugins will automatically instrument the `load` function of
17+
* your universal `load` functions declared in your `+page.(js|ts)` and `+layout.(js|ts)` files.
18+
*
19+
* @default true
20+
*/
21+
load?: boolean;
22+
23+
/**
24+
* If this flag is `true`, the Sentry plugins will automatically instrument the `load` function of
25+
* your server-only `load` functions declared in your `+page.server.(js|ts)`
26+
* and `+layout.server.(js|ts)` files.
27+
*
28+
* @default true
29+
*/
30+
serverLoad?: boolean;
31+
};
32+
33+
type AutoInstrumentPluginOptions = AutoInstrumentSelection & {
34+
debug: boolean;
35+
};
36+
37+
/**
38+
* Creates a Vite plugin that automatically instruments the parts of the app
39+
* specified in @param options
40+
*
41+
* @returns the plugin
42+
*/
43+
export async function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptions): Promise<Plugin> {
44+
const universalLoadTemplatePath = path.resolve(__dirname, 'templates', 'universalLoadTemplate.js');
45+
const universalLoadTemplate = (await fs.promises.readFile(universalLoadTemplatePath, 'utf-8')).toString();
46+
47+
const serverLoadTemplatePath = path.resolve(__dirname, 'templates', 'serverLoadTemplate.js');
48+
const serverLoadTemplate = (await fs.promises.readFile(serverLoadTemplatePath, 'utf-8')).toString();
49+
50+
const universalLoadWrappingCode = universalLoadTemplate.replace(
51+
/__SENTRY_WRAPPING_TARGET_FILE__/g,
52+
WRAPPING_TARGET_MODULE_NAME,
53+
);
54+
const serverLoadWrappingCode = serverLoadTemplate.replace(
55+
/__SENTRY_WRAPPING_TARGET_FILE__/g,
56+
WRAPPING_TARGET_MODULE_NAME,
57+
);
58+
59+
const { load: shouldWrapLoad, serverLoad: shouldWrapServerLoad, debug } = options;
60+
61+
return {
62+
name: 'sentry-auto-instrumentation',
63+
enforce: 'post',
64+
async transform(userCode, id) {
65+
const shouldApplyUniversalLoadWrapper =
66+
shouldWrapLoad &&
67+
/\+(page|layout)\.(js|ts|mjs|mts)$/.test(id) &&
68+
// Simple check to see if users already instrumented the file manually
69+
!userCode.includes('@sentry/sveltekit');
70+
71+
if (shouldApplyUniversalLoadWrapper) {
72+
// eslint-disable-next-line no-console
73+
debug && console.log('[Sentry] Applying universal load wrapper to', id);
74+
return await wrapUserCode(universalLoadWrappingCode, userCode);
75+
}
76+
77+
const shouldApplyServerLoadWrapper =
78+
shouldWrapServerLoad &&
79+
/\+(page|layout)\.server\.(js|ts|mjs|mts)$/.test(id) &&
80+
!userCode.includes('@sentry/sveltekit');
81+
82+
if (shouldApplyServerLoadWrapper) {
83+
// eslint-disable-next-line no-console
84+
debug && console.log('[Sentry] Applying server load wrapper to', id);
85+
return await wrapUserCode(serverLoadWrappingCode, userCode);
86+
}
87+
88+
return null;
89+
},
90+
};
91+
}
92+
93+
/**
94+
* Uses rollup to bundle the wrapper code and the user code together, so that we can use rollup's source map support.
95+
* This works analogously to our NextJS wrapping solution.
96+
* The one exception is that we don't pass in any source map. This is because generating the userCode's
97+
* source map generally works but it breaks SvelteKit's source map generation for some reason.
98+
* Not passing a map actually works and things are still mapped correctly in the end.
99+
* No Sentry code is visible in the final source map.
100+
* @see {@link file:///./../../../nextjs/src/config/loaders/wrappingLoader.ts} for more details.
101+
*/
102+
async function wrapUserCode(
103+
wrapperCode: string,
104+
userModuleCode: string,
105+
): Promise<{ code: string; map?: SourceMap | null }> {
106+
const rollupBuild = await rollup({
107+
input: SENTRY_WRAPPER_MODULE_NAME,
108+
109+
plugins: [
110+
{
111+
name: 'virtualize-sentry-wrapper-modules',
112+
resolveId: id => {
113+
if (id === SENTRY_WRAPPER_MODULE_NAME || id === WRAPPING_TARGET_MODULE_NAME) {
114+
return id;
115+
} else {
116+
return null;
117+
}
118+
},
119+
load(id) {
120+
if (id === SENTRY_WRAPPER_MODULE_NAME) {
121+
return wrapperCode;
122+
} else if (id === WRAPPING_TARGET_MODULE_NAME) {
123+
return {
124+
code: userModuleCode,
125+
// map: userModuleSourceMap,
126+
};
127+
} else {
128+
return null;
129+
}
130+
},
131+
},
132+
],
133+
134+
external: sourceId => sourceId !== SENTRY_WRAPPER_MODULE_NAME && sourceId !== WRAPPING_TARGET_MODULE_NAME,
135+
136+
context: 'this',
137+
138+
makeAbsoluteExternalsRelative: false,
139+
140+
onwarn: (_warning, _warn) => {
141+
// Suppress all warnings - we don't want to bother people with this output
142+
// _warn(_warning); // uncomment to debug
143+
},
144+
});
145+
146+
const finalBundle = await rollupBuild.generate({
147+
format: 'esm',
148+
sourcemap: 'hidden',
149+
});
150+
151+
return finalBundle.output[0];
152+
}

packages/sveltekit/src/vite/sentryVitePlugins.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { SentryVitePluginOptions } from '@sentry/vite-plugin';
22
import type { Plugin } from 'vite';
33

4+
import type { AutoInstrumentSelection } from './autoInstrument';
5+
import { makeAutoInstrumentationPlugin } from './autoInstrument';
46
import { makeCustomSentryVitePlugin } from './sourceMaps';
57

68
type SourceMapsUploadOptions = {
79
/**
810
* If this flag is `true`, the Sentry plugins will automatically upload source maps to Sentry.
9-
* Defaults to `true`.
11+
* @default true`.
1012
*/
1113
autoUploadSourceMaps?: boolean;
1214

@@ -17,16 +19,32 @@ type SourceMapsUploadOptions = {
1719
sourceMapsUploadOptions?: Partial<SentryVitePluginOptions>;
1820
};
1921

22+
type AutoInstrumentOptions = {
23+
/**
24+
* The Sentry plugin will automatically instrument certain parts of your SvelteKit application at build time.
25+
* Set this option to `false` to disable this behavior or what is instrumentated by passing an object.
26+
*
27+
* Auto instrumentation includes:
28+
* - Universal `load` functions in `+page.(js|ts)` files
29+
* - Server-only `load` functions in `+page.server.(js|ts)` files
30+
*
31+
* @default true (meaning, the plugin will instrument all of the above)
32+
*/
33+
autoInstrument?: boolean | AutoInstrumentSelection;
34+
};
35+
2036
export type SentrySvelteKitPluginOptions = {
2137
/**
2238
* If this flag is `true`, the Sentry plugins will log some useful debug information.
23-
* Defaults to `false`.
39+
* @default false.
2440
*/
2541
debug?: boolean;
26-
} & SourceMapsUploadOptions;
42+
} & SourceMapsUploadOptions &
43+
AutoInstrumentOptions;
2744

2845
const DEFAULT_PLUGIN_OPTIONS: SentrySvelteKitPluginOptions = {
2946
autoUploadSourceMaps: true,
47+
autoInstrument: true,
3048
debug: false,
3149
};
3250

@@ -43,7 +61,22 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}
4361
...options,
4462
};
4563

46-
const sentryPlugins = [];
64+
const sentryPlugins: Plugin[] = [];
65+
66+
if (mergedOptions.autoInstrument) {
67+
const pluginOptions: AutoInstrumentSelection = {
68+
load: true,
69+
serverLoad: true,
70+
...(typeof mergedOptions.autoInstrument === 'object' ? mergedOptions.autoInstrument : {}),
71+
};
72+
73+
sentryPlugins.push(
74+
await makeAutoInstrumentationPlugin({
75+
...pluginOptions,
76+
debug: options.debug || false,
77+
}),
78+
);
79+
}
4780

4881
if (mergedOptions.autoUploadSourceMaps) {
4982
const pluginOptions = {

packages/sveltekit/src/vite/sourceMaps.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ export async function makeCustomSentryVitePlugin(options?: SentryVitePluginOptio
7474
let isSSRBuild = true;
7575

7676
const customPlugin: Plugin = {
77-
name: 'sentry-vite-plugin-custom',
77+
name: 'sentry-upload-source-maps',
7878
apply: 'build', // only apply this plugin at build time
79-
enforce: 'post',
79+
enforce: 'post', // this needs to be set to post, otherwise we don't pick up the output from the SvelteKit adapter
8080

8181
// These hooks are copied from the original Sentry Vite plugin.
8282
// They're mostly responsible for options parsing and release injection.
@@ -85,7 +85,7 @@ export async function makeCustomSentryVitePlugin(options?: SentryVitePluginOptio
8585
renderChunk,
8686
transform,
8787

88-
// Modify the config to generate source maps
88+
// // Modify the config to generate source maps
8989
config: config => {
9090
// eslint-disable-next-line no-console
9191
debug && console.log('[Source Maps Plugin] Enabeling source map generation');
@@ -117,6 +117,8 @@ export async function makeCustomSentryVitePlugin(options?: SentryVitePluginOptio
117117
}
118118

119119
const outDir = path.resolve(process.cwd(), outputDir);
120+
// eslint-disable-next-line no-console
121+
debug && console.log('[Source Maps Plugin] Looking up source maps in', outDir);
120122

121123
const jsFiles = getFiles(outDir).filter(file => file.endsWith('.js'));
122124
// eslint-disable-next-line no-console
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// @ts-ignore - this import placeholed will be replaced!
2+
import * as userModule from '__SENTRY_WRAPPING_TARGET_FILE__';
3+
// eslint-disable-next-line import/no-extraneous-dependencies
4+
import { wrapServerLoadWithSentry } from '@sentry/sveltekit';
5+
6+
// @ts-ignore whatever
7+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
8+
export const load = userModule.load ? wrapServerLoadWithSentry(userModule.load) : undefined;
9+
10+
// Re-export anything exported by the page module we're wrapping.
11+
// @ts-ignore See import on top
12+
export * from '__SENTRY_WRAPPING_TARGET_FILE__';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// @ts-ignore - this import placeholed will be replaced!
2+
import * as userModule from '__SENTRY_WRAPPING_TARGET_FILE__';
3+
// eslint-disable-next-line import/no-extraneous-dependencies
4+
import { wrapLoadWithSentry } from '@sentry/sveltekit';
5+
6+
// @ts-ignore whatever
7+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
8+
export const load = userModule.load ? wrapLoadWithSentry(userModule.load) : undefined;
9+
10+
// Re-export anything exported by the page module we're wrapping.
11+
// @ts-ignore See import on top
12+
export * from '__SENTRY_WRAPPING_TARGET_FILE__';

0 commit comments

Comments
 (0)