Skip to content

Commit 434cbf0

Browse files
andreiborzas1gr1d
authored andcommitted
Rewrite plugin to add auto-instrument option
1 parent 70d8410 commit 434cbf0

File tree

16 files changed

+231
-201
lines changed

16 files changed

+231
-201
lines changed
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { sentrySolidStartVite } from '@sentry/solidstart';
1+
import { withSentry } from '@sentry/solidstart';
22
import { defineConfig } from '@solidjs/start/config';
33

4-
export default defineConfig({
5-
ssr: false,
6-
vite: {
7-
plugins: [sentrySolidStartVite()],
8-
},
9-
});
4+
export default defineConfig(
5+
withSentry({
6+
ssr: false,
7+
middleware: './src/middleware.ts',
8+
}),
9+
);

dev-packages/e2e-tests/test-applications/solidstart-spa/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
"version": "0.0.0",
44
"scripts": {
55
"clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output",
6-
"dev": "NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi dev",
7-
"build": "vinxi build && sh ./post_build.sh",
8-
"preview": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start",
6+
"build": "vinxi build && sh post_build.sh",
7+
"preview": "HOST=localhost PORT=3030 vinxi start",
98
"test:prod": "TEST_ENV=production playwright test",
109
"test:build": "pnpm install && pnpm build",
1110
"test:assert": "pnpm test:prod"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { sentryBeforeResponseMiddleware } from '@sentry/solidstart';
2+
import { createMiddleware } from '@solidjs/start/middleware';
3+
4+
export default createMiddleware({
5+
onBeforeResponse: [sentryBeforeResponseMiddleware()],
6+
});

dev-packages/e2e-tests/test-applications/solidstart/app.config.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,7 @@ import { withSentry } from '@sentry/solidstart';
22
import { defineConfig } from '@solidjs/start/config';
33

44
export default defineConfig(
5-
withSentry(
6-
{},
7-
{
8-
// Typically we want to default to ./src/instrument.sever.ts
9-
// `withSentry` would then build and copy the file over to
10-
// the .output folder, but since we can't use the production
11-
// server for our e2e tests, we have to delete the build folders
12-
// prior to using the dev server for our tests. Which also gets
13-
// rid of the instrument.server.mjs file that we need to --import.
14-
// Therefore, we specify the .mjs file here and to ensure
15-
// `withSentry` gets its file to build and we continue to reference
16-
// the file from the `src` folder for --import without needing to
17-
// transpile before.
18-
// This can be removed once we get the production server to work
19-
// with our e2e tests.
20-
instrumentation: './src/instrument.server.mjs',
21-
},
22-
),
5+
withSentry({
6+
middleware: './src/middleware.ts',
7+
}),
238
);

dev-packages/e2e-tests/test-applications/solidstart/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
"version": "0.0.0",
44
"scripts": {
55
"clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output",
6-
"dev": "NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi dev",
7-
"build": "vinxi build && sh ./post_build.sh",
8-
"preview": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start",
6+
"build": "vinxi build && sh post_build.sh",
7+
"preview": "HOST=localhost PORT=3030 vinxi start",
98
"test:prod": "TEST_ENV=production playwright test",
109
"test:build": "pnpm install && pnpm build",
1110
"test:assert": "pnpm test:prod"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { sentryBeforeResponseMiddleware } from '@sentry/solidstart';
2+
import { createMiddleware } from '@solidjs/start/middleware';
3+
4+
export default createMiddleware({
5+
onBeforeResponse: [sentryBeforeResponseMiddleware()],
6+
});
Lines changed: 79 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
3-
import { consoleSandbox } from '@sentry/utils';
4-
import type { Nitro } from './types';
3+
import { consoleSandbox, flatten } from '@sentry/utils';
4+
import type { Nitro } from 'nitropack';
5+
import type { InputPluginOption } from 'rollup';
6+
import type { RollupConfig } from './types';
7+
import {
8+
QUERY_END_INDICATOR,
9+
SENTRY_FUNCTIONS_REEXPORT,
10+
SENTRY_WRAPPED_ENTRY,
11+
constructFunctionReExport,
12+
removeSentryQueryFromPath,
13+
} from './utils';
514

615
// Nitro presets for hosts that only host static files
716
export const staticHostPresets = ['github_pages'];
@@ -42,54 +51,81 @@ export async function addInstrumentationFileToBuild(nitro: Nitro): Promise<void>
4251
}
4352

4453
/**
45-
* Adds an `instrument.server.mjs` import to the top of the server entry file.
4654
*
47-
* This is meant as an escape hatch and should only be used in environments where
48-
* it's not possible to `--import` the file instead as it comes with a limited
49-
* tracing experience, only collecting http traces.
5055
*/
51-
export async function experimental_addInstrumentationFileTopLevelImportToServerEntry(
52-
serverDir: string,
53-
preset: string,
54-
): Promise<void> {
56+
export async function addAutoInstrumentation(nitro: Nitro, config: RollupConfig): Promise<void> {
5557
// Static file hosts have no server component so there's nothing to do
56-
if (staticHostPresets.includes(preset)) {
58+
if (staticHostPresets.includes(nitro.options.preset)) {
5759
return;
5860
}
5961

60-
const instrumentationFile = path.resolve(serverDir, 'instrument.server.mjs');
61-
const serverEntryFileName = serverFilePresets.includes(preset) ? 'server.mjs' : 'index.mjs';
62-
const serverEntryFile = path.resolve(serverDir, serverEntryFileName);
62+
const buildDir = nitro.options.buildDir;
63+
const serverInstrumentationPath = path.resolve(buildDir, 'build', 'ssr', 'instrument.server.js');
6364

64-
try {
65-
await fs.promises.access(instrumentationFile, fs.constants.F_OK);
66-
} catch (error) {
67-
consoleSandbox(() => {
68-
// eslint-disable-next-line no-console
69-
console.warn(
70-
`[Sentry SolidStart withSentry] Failed to add \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`,
71-
error,
72-
);
73-
});
74-
return;
75-
}
65+
config.plugins.push({
66+
name: 'sentry-solidstart-auto-instrument',
67+
async resolveId(source, importer, options) {
68+
if (source.includes('instrument.server.js')) {
69+
return { id: source, moduleSideEffects: true };
70+
}
7671

77-
try {
78-
const content = await fs.promises.readFile(serverEntryFile, 'utf-8');
79-
const updatedContent = `import './instrument.server.mjs';\n${content}`;
80-
await fs.promises.writeFile(serverEntryFile, updatedContent);
72+
if (source === 'import-in-the-middle/hook.mjs') {
73+
// We are importing "import-in-the-middle" in the returned code of the `load()` function below
74+
// By setting `moduleSideEffects` to `true`, the import is added to the bundle, although nothing is imported from it
75+
// By importing "import-in-the-middle/hook.mjs", we can make sure this file is included, as not all node builders are including files imported with `module.register()`.
76+
// Prevents the error "Failed to register ESM hook Error: Cannot find module 'import-in-the-middle/hook.mjs'"
77+
return { id: source, moduleSideEffects: true, external: true };
78+
}
8179

82-
consoleSandbox(() => {
83-
// eslint-disable-next-line no-console
84-
console.log(
85-
`[Sentry SolidStart withSentry] Added \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`,
86-
);
87-
});
88-
} catch (error) {
89-
// eslint-disable-next-line no-console
90-
console.warn(
91-
`[Sentry SolidStart withSentry] An error occurred when trying to add \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`,
92-
error,
93-
);
94-
}
80+
if (options.isEntry && source.includes('.mjs') && !source.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) {
81+
const resolution = await this.resolve(source, importer, options);
82+
83+
// If it cannot be resolved or is external, just return it so that Rollup can display an error
84+
if (!resolution || resolution?.external) return resolution;
85+
86+
const moduleInfo = await this.load(resolution);
87+
88+
moduleInfo.moduleSideEffects = true;
89+
90+
// The key `.` in `exportedBindings` refer to the exports within the file
91+
const functionsToExport = flatten(Object.values(moduleInfo.exportedBindings || {})).filter(functionName =>
92+
['default', 'handler', 'server'].includes(functionName),
93+
);
94+
95+
// The enclosing `if` already checks for the suffix in `source`, but a check in `resolution.id` is needed as well to prevent multiple attachment of the suffix
96+
return resolution.id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)
97+
? resolution.id
98+
: resolution.id
99+
// Concatenates the query params to mark the file (also attaches names of re-exports - this is needed for serverless functions to re-export the handler)
100+
.concat(SENTRY_WRAPPED_ENTRY)
101+
.concat(functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : '')
102+
.concat(QUERY_END_INDICATOR);
103+
}
104+
105+
return null;
106+
},
107+
load(id: string) {
108+
if (id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) {
109+
const entryId = removeSentryQueryFromPath(id);
110+
111+
// Mostly useful for serverless `handler` functions
112+
const reExportedFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT)
113+
? constructFunctionReExport(id, entryId)
114+
: '';
115+
116+
return [
117+
// Regular `import` of the Sentry config
118+
`import ${JSON.stringify(serverInstrumentationPath)};`,
119+
// Dynamic `import()` for the previous, actual entry point.
120+
// `import()` can be used for any code that should be run after the hooks are registered (https://nodejs.org/api/module.html#enabling)
121+
`import(${JSON.stringify(entryId)});`,
122+
// By importing "import-in-the-middle/hook.mjs", we can make sure this file wil be included, as not all node builders are including files imported with `module.register()`.
123+
"import 'import-in-the-middle/hook.mjs';",
124+
`${reExportedFunctions}`,
125+
].join('\n');
126+
}
127+
128+
return null;
129+
},
130+
} satisfies InputPluginOption);
95131
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export * from './withSentry';
2-
export type { Nitro, SentrySolidStartConfigOptions } from './types';
Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import type { defineConfig } from '@solidjs/start/config';
2-
// Types to avoid pulling in extra dependencies
3-
// These are non-exhaustive
4-
export type Nitro = {
5-
options: {
6-
buildDir: string;
7-
output: {
8-
serverDir: string;
9-
};
10-
preset: string;
11-
};
2+
import type { Nitro } from 'nitropack';
3+
4+
// Nitro does not export this type
5+
export type RollupConfig = {
6+
plugins: unknown[];
127
};
138

149
export type SolidStartInlineConfig = Parameters<typeof defineConfig>[0];
@@ -19,17 +14,3 @@ export type SolidStartInlineServerConfig = {
1914
'rollup:before'?: (nitro: Nitro) => unknown;
2015
};
2116
};
22-
23-
export type SentrySolidStartConfigOptions = {
24-
/**
25-
* Enabling basic server tracing can be used for environments where modifying the node option `--import` is not possible.
26-
* However, enabling this option only supports limited tracing instrumentation. Only http traces will be collected (but no database-specific traces etc.).
27-
*
28-
* If this option is `true`, the Sentry SDK will import the instrumentation.server.ts|js file at the top of the server entry file to load the SDK on the server.
29-
*
30-
* **DO NOT** enable this option if you've already added the node option `--import` in your node start script. This would initialize Sentry twice on the server-side and leads to unexpected issues.
31-
*
32-
* @default false
33-
*/
34-
experimental_basicServerTracing?: boolean;
35-
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry';
2+
export const SENTRY_FUNCTIONS_REEXPORT = '?sentry-query-functions-reexport=';
3+
export const QUERY_END_INDICATOR = 'SENTRY-QUERY-END';
4+
5+
/**
6+
* Strips the Sentry query part from a path.
7+
* Example: example/path?sentry-query-wrapped-entry?sentry-query-functions-reexport=foo,SENTRY-QUERY-END -> /example/path
8+
*
9+
* Only exported for testing.
10+
*/
11+
export function removeSentryQueryFromPath(url: string): string {
12+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
13+
const regex = new RegExp(`\\${SENTRY_WRAPPED_ENTRY}.*?\\${QUERY_END_INDICATOR}`);
14+
return url.replace(regex, '');
15+
}
16+
17+
/**
18+
* Extracts and sanitizes function re-export query parameters from a query string.
19+
* If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting
20+
* serverless `handler` functions.
21+
*
22+
* Only exported for testing.
23+
*/
24+
export function extractFunctionReexportQueryParameters(query: string): string[] {
25+
// Regex matches the comma-separated params between the functions query
26+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
27+
const regex = new RegExp(`\\${SENTRY_FUNCTIONS_REEXPORT}(.*?)\\${QUERY_END_INDICATOR}`);
28+
const match = query.match(regex);
29+
30+
return match && match[1]
31+
? match[1]
32+
.split(',')
33+
.filter(param => param !== '')
34+
// Sanitize, as code could be injected with another rollup plugin
35+
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
36+
: [];
37+
}
38+
39+
/**
40+
* Constructs a code snippet with function reexports (can be used in Rollup plugins)
41+
*/
42+
export function constructFunctionReExport(pathWithQuery: string, entryId: string): string {
43+
const functionNames = extractFunctionReexportQueryParameters(pathWithQuery);
44+
45+
return functionNames.reduce(
46+
(functionsCode, currFunctionName) =>
47+
functionsCode.concat(
48+
'async function reExport(...args) {\n' +
49+
` const res = await import(${JSON.stringify(entryId)});\n` +
50+
` return res.${currFunctionName}.call(this, ...args);\n` +
51+
'}\n' +
52+
`export { reExport as ${currFunctionName} };\n`,
53+
),
54+
'',
55+
);
56+
}

packages/solidstart/src/config/withSentry.ts

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import type { Nitro } from 'nitropack';
12
import { addSentryPluginToVite } from '../vite';
23
import type { SentrySolidStartPluginOptions } from '../vite/types';
3-
import {
4-
addInstrumentationFileToBuild,
5-
experimental_addInstrumentationFileTopLevelImportToServerEntry,
6-
} from './addInstrumentation';
7-
import type { Nitro, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types';
4+
import { addAutoInstrumentation, addInstrumentationFileToBuild } from './addInstrumentation';
5+
import type { RollupConfig, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types';
6+
7+
const defaultSentrySolidStartPluginOptions: SentrySolidStartPluginOptions = {
8+
autoInstrument: true,
9+
};
810

911
/**
1012
* Modifies the passed in Solid Start configuration with build-time enhancements such as
@@ -17,7 +19,7 @@ import type { Nitro, SolidStartInlineConfig, SolidStartInlineServerConfig } from
1719
*/
1820
export const withSentry = (
1921
solidStartConfig: SolidStartInlineConfig = {},
20-
sentrySolidStartPluginOptions: SentrySolidStartPluginOptions = {},
22+
sentrySolidStartPluginOptions: SentrySolidStartPluginOptions = defaultSentrySolidStartPluginOptions,
2123
): SolidStartInlineConfig => {
2224
const server = (solidStartConfig.server || {}) as SolidStartInlineServerConfig;
2325
const hooks = server.hooks || {};
@@ -26,31 +28,19 @@ export const withSentry = (
2628
? (...args: unknown[]) => addSentryPluginToVite(solidStartConfig.vite(...args), sentrySolidStartPluginOptions)
2729
: addSentryPluginToVite(solidStartConfig.vite, sentrySolidStartPluginOptions);
2830

29-
let serverDir: string;
30-
let buildPreset: string;
31-
3231
return {
3332
...solidStartConfig,
3433
vite,
3534
server: {
3635
...server,
3736
hooks: {
3837
...hooks,
39-
async close() {
40-
if (sentrySolidStartPluginOptions.experimental_basicServerTracing) {
41-
await experimental_addInstrumentationFileTopLevelImportToServerEntry(serverDir, buildPreset);
42-
}
43-
44-
// Run user provided hook
45-
if (hooks.close) {
46-
hooks.close();
38+
async 'rollup:before'(nitro: Nitro, config: RollupConfig) {
39+
if (sentrySolidStartPluginOptions.autoInstrument) {
40+
await addAutoInstrumentation(nitro, config);
41+
} else {
42+
await addInstrumentationFileToBuild(nitro);
4743
}
48-
},
49-
async 'rollup:before'(nitro: Nitro) {
50-
serverDir = nitro.options.output.serverDir;
51-
buildPreset = nitro.options.preset;
52-
53-
await addInstrumentationFileToBuild(nitro);
5444

5545
// Run user provided hook
5646
if (hooks['rollup:before']) {

0 commit comments

Comments
 (0)