@sentry/wizard 3.23.3 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,13 +18,15 @@ import { hasPackageInstalled } from '../utils/package-json';
18
18
  import { WizardOptions } from '../utils/types';
19
19
  import {
20
20
  initializeSentryOnEntryClient,
21
- initializeSentryOnEntryServer,
21
+ instrumentSentryOnEntryServer,
22
22
  updateBuildScript,
23
23
  instrumentRootRoute,
24
24
  isRemixV2,
25
25
  loadRemixConfig,
26
26
  runRemixReveal,
27
- instrumentExpressServer,
27
+ insertServerInstrumentationFile,
28
+ createServerInstrumentationFile,
29
+ updateStartScript,
28
30
  } from './sdk-setup';
29
31
  import { debug } from '../utils/debug';
30
32
  import { traceStep, withTelemetry } from '../telemetry';
@@ -145,31 +147,57 @@ async function runRemixWizardWithTelemetry(
145
147
  }
146
148
  });
147
149
 
148
- await traceStep('Initialize Sentry on server entry', async () => {
150
+ let instrumentationFile = '';
151
+
152
+ await traceStep('Create server instrumentation file', async () => {
149
153
  try {
150
- await initializeSentryOnEntryServer(dsn, isV2, isTS);
154
+ instrumentationFile = await createServerInstrumentationFile(dsn);
151
155
  } catch (e) {
152
- clack.log.warn(`Could not initialize Sentry on server entry.
153
- Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/manual-setup/`);
156
+ clack.log.warn(
157
+ 'Could not create a server instrumentation file. Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/manual-setup/',
158
+ );
154
159
  debug(e);
155
160
  }
156
161
  });
157
162
 
158
- await traceStep('Instrument custom Express server', async () => {
159
- try {
160
- const hasExpressAdapter = hasPackageInstalled(
161
- '@remix-run/express',
162
- packageJson,
163
- );
163
+ let serverFileInstrumented = false;
164
164
 
165
- if (!hasExpressAdapter) {
166
- return;
165
+ await traceStep(
166
+ 'Create server instrumentation file and import it',
167
+ async () => {
168
+ try {
169
+ serverFileInstrumented = await insertServerInstrumentationFile(dsn);
170
+ } catch (e) {
171
+ clack.log.warn(
172
+ 'Could not create a server instrumentation file. Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/manual-setup/',
173
+ );
174
+ debug(e);
167
175
  }
176
+ },
177
+ );
168
178
 
169
- await instrumentExpressServer();
179
+ if (!serverFileInstrumented && instrumentationFile) {
180
+ await traceStep(
181
+ 'Update `start` script to import instrumentation file.',
182
+ async () => {
183
+ try {
184
+ await updateStartScript(instrumentationFile);
185
+ } catch (e) {
186
+ clack.log
187
+ .warn(`Could not automatically add Sentry initialization to server entry.
188
+ Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/manual-setup/`);
189
+ debug(e);
190
+ }
191
+ },
192
+ );
193
+ }
194
+
195
+ await traceStep('Instrument server `handleError`', async () => {
196
+ try {
197
+ await instrumentSentryOnEntryServer(isV2, isTS);
170
198
  } catch (e) {
171
- clack.log.warn(`Could not instrument custom Express server.
172
- Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/manual-setup/#custom-express-server`);
199
+ clack.log.warn(`Could not initialize Sentry on server entry.
200
+ Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/manual-setup/`);
173
201
  debug(e);
174
202
  }
175
203
  });
@@ -46,8 +46,6 @@ export function getSentryExamplePageContents(options: {
46
46
  : `https://${options.orgSlug}.sentry.io/issues/?project=${options.projectId}`;
47
47
 
48
48
  return `
49
- import * as Sentry from '@sentry/remix';
50
-
51
49
  export default function SentryExamplePage() {
52
50
  return (
53
51
  <div>
@@ -15,18 +15,26 @@ import clack from '@clack/prompts';
15
15
  import chalk from 'chalk';
16
16
  import { gte, minVersion } from 'semver';
17
17
 
18
- // @ts-expect-error - magicast is ESM and TS complains about that. It works though
19
- import { builders, generateCode, loadFile, writeFile } from 'magicast';
20
- import { PackageDotJson, getPackageVersion } from '../utils/package-json';
21
- import { getInitCallInsertionIndex, hasSentryContent } from './utils';
18
+ import {
19
+ builders,
20
+ generateCode,
21
+ loadFile,
22
+ parseModule,
23
+ writeFile,
24
+ // @ts-expect-error - magicast is ESM and TS complains about that. It works though
25
+ } from 'magicast';
26
+ import type { PackageDotJson } from '../utils/package-json';
27
+ import { getPackageVersion } from '../utils/package-json';
28
+ import {
29
+ getAfterImportsInsertionIndex,
30
+ hasSentryContent,
31
+ serverHasInstrumentationImport,
32
+ } from './utils';
22
33
  import { instrumentRootRouteV1 } from './codemods/root-v1';
23
34
  import { instrumentRootRouteV2 } from './codemods/root-v2';
24
35
  import { instrumentHandleError } from './codemods/handle-error';
25
- import {
26
- findCustomExpressServerImplementation,
27
- instrumentExpressCreateRequestHandler,
28
- } from './codemods/express-server';
29
36
  import { getPackageDotJson } from '../utils/clack-utils';
37
+ import { findCustomExpressServerImplementation } from './codemods/express-server';
30
38
 
31
39
  export type PartialRemixConfig = {
32
40
  unstable_dev?: boolean;
@@ -87,7 +95,8 @@ function insertClientInitCall(
87
95
  });
88
96
 
89
97
  const originalHooksModAST = originalHooksMod.$ast as Program;
90
- const initCallInsertionIndex = getInitCallInsertionIndex(originalHooksModAST);
98
+ const initCallInsertionIndex =
99
+ getAfterImportsInsertionIndex(originalHooksModAST);
91
100
 
92
101
  originalHooksModAST.body.splice(
93
102
  initCallInsertionIndex,
@@ -98,26 +107,76 @@ function insertClientInitCall(
98
107
  );
99
108
  }
100
109
 
101
- function insertServerInitCall(
102
- dsn: string,
103
- originalHooksMod: ProxifiedModule<any>,
104
- ) {
110
+ export async function createServerInstrumentationFile(dsn: string) {
111
+ // create an empty file named `instrument.server.mjs`
112
+ const instrumentationFile = 'instrumentation.server.mjs';
113
+ const instrumentationFileMod = parseModule('');
114
+
115
+ instrumentationFileMod.imports.$add({
116
+ from: '@sentry/remix',
117
+ imported: '*',
118
+ local: 'Sentry',
119
+ });
120
+
105
121
  const initCall = builders.functionCall('Sentry.init', {
106
122
  dsn,
107
123
  tracesSampleRate: 1.0,
124
+ autoInstrumentRemix: true,
108
125
  });
109
126
 
110
- const originalHooksModAST = originalHooksMod.$ast as Program;
127
+ const instrumentationFileModAST = instrumentationFileMod.$ast as Program;
111
128
 
112
- const initCallInsertionIndex = getInitCallInsertionIndex(originalHooksModAST);
129
+ const initCallInsertionIndex = getAfterImportsInsertionIndex(
130
+ instrumentationFileModAST,
131
+ );
113
132
 
114
- originalHooksModAST.body.splice(
133
+ instrumentationFileModAST.body.splice(
115
134
  initCallInsertionIndex,
116
135
  0,
117
136
  // @ts-expect-error - string works here because the AST is proxified by magicast
118
137
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
119
138
  generateCode(initCall).code,
120
139
  );
140
+
141
+ await writeFile(instrumentationFileModAST, instrumentationFile);
142
+
143
+ return instrumentationFile;
144
+ }
145
+
146
+ export async function insertServerInstrumentationFile(dsn: string) {
147
+ const instrumentationFile = await createServerInstrumentationFile(dsn);
148
+
149
+ const expressServerPath = await findCustomExpressServerImplementation();
150
+
151
+ if (!expressServerPath) {
152
+ return false;
153
+ }
154
+
155
+ const originalExpressServerMod = await loadFile(expressServerPath);
156
+
157
+ if (
158
+ serverHasInstrumentationImport(
159
+ expressServerPath,
160
+ originalExpressServerMod.$code,
161
+ )
162
+ ) {
163
+ clack.log.warn(
164
+ `File ${chalk.cyan(
165
+ path.basename(expressServerPath),
166
+ )} already contains instrumentation import.
167
+ Skipping adding instrumentation functionality to ${chalk.cyan(
168
+ path.basename(expressServerPath),
169
+ )}.`,
170
+ );
171
+
172
+ return true;
173
+ }
174
+
175
+ originalExpressServerMod.$code = `import './${instrumentationFile}';\n${originalExpressServerMod.$code}`;
176
+
177
+ fs.writeFileSync(expressServerPath, originalExpressServerMod.$code);
178
+
179
+ return true;
121
180
  }
122
181
 
123
182
  export function isRemixV2(
@@ -294,8 +353,56 @@ export async function initializeSentryOnEntryClient(
294
353
  );
295
354
  }
296
355
 
297
- export async function initializeSentryOnEntryServer(
298
- dsn: string,
356
+ export async function updateStartScript(instrumentationFile: string) {
357
+ const packageJson = await getPackageDotJson();
358
+
359
+ if (!packageJson.scripts || !packageJson.scripts.start) {
360
+ throw new Error(
361
+ "Couldn't find a `start` script in your package.json. Please add one manually.",
362
+ );
363
+ }
364
+
365
+ if (packageJson.scripts.start.includes('NODE_OPTIONS')) {
366
+ clack.log.warn(
367
+ `Found existing NODE_OPTIONS in ${chalk.cyan(
368
+ 'start',
369
+ )} script. Skipping adding Sentry initialization.`,
370
+ );
371
+
372
+ return;
373
+ }
374
+
375
+ if (
376
+ !packageJson.scripts.start.includes('remix-serve') &&
377
+ // Adding a following empty space not to match a path that includes `node`
378
+ !packageJson.scripts.start.includes('node ')
379
+ ) {
380
+ clack.log.warn(
381
+ `Found a ${chalk.cyan('start')} script that doesn't use ${chalk.cyan(
382
+ 'remix-serve',
383
+ )} or ${chalk.cyan('node')}. Skipping adding Sentry initialization.`,
384
+ );
385
+
386
+ return;
387
+ }
388
+
389
+ const startCommand = packageJson.scripts.start;
390
+
391
+ packageJson.scripts.start = `NODE_OPTIONS='--import ./${instrumentationFile}' ${startCommand}`;
392
+
393
+ await fs.promises.writeFile(
394
+ path.join(process.cwd(), 'package.json'),
395
+ JSON.stringify(packageJson, null, 2),
396
+ );
397
+
398
+ clack.log.success(
399
+ `Successfully updated ${chalk.cyan('start')} script in ${chalk.cyan(
400
+ 'package.json',
401
+ )} to include Sentry initialization on start.`,
402
+ );
403
+ }
404
+
405
+ export async function instrumentSentryOnEntryServer(
299
406
  isV2: boolean,
300
407
  isTS: boolean,
301
408
  ): Promise<void> {
@@ -319,8 +426,6 @@ export async function initializeSentryOnEntryServer(
319
426
  local: 'Sentry',
320
427
  });
321
428
 
322
- insertServerInitCall(dsn, originalEntryServerMod);
323
-
324
429
  if (isV2) {
325
430
  const handleErrorInstrumented = instrumentHandleError(
326
431
  originalEntryServerMod,
@@ -347,16 +452,3 @@ export async function initializeSentryOnEntryServer(
347
452
  )}.`,
348
453
  );
349
454
  }
350
-
351
- export async function instrumentExpressServer() {
352
- const expressServerPath = await findCustomExpressServerImplementation();
353
-
354
- if (!expressServerPath) {
355
- clack.log.warn(
356
- `Could not find custom Express server implementation. Please instrument it manually.`,
357
- );
358
- return;
359
- }
360
-
361
- await instrumentExpressCreateRequestHandler(expressServerPath);
362
- }
@@ -6,6 +6,6 @@ export const ERROR_BOUNDARY_TEMPLATE_V2 = `const ErrorBoundary = () => {
6
6
  `;
7
7
 
8
8
  export const HANDLE_ERROR_TEMPLATE_V2 = `function handleError(error, { request }) {
9
- Sentry.captureRemixServerException(error, 'remix.server', request);
9
+ Sentry.captureRemixServerException(error, 'remix.server', request, true);
10
10
  }
11
11
  `;
@@ -7,16 +7,23 @@ import clack from '@clack/prompts';
7
7
  import chalk from 'chalk';
8
8
  import { PackageDotJson, hasPackageInstalled } from '../utils/package-json';
9
9
 
10
- // Copied from sveltekit wizard
10
+ export const POSSIBLE_SERVER_INSTRUMENTATION_PATHS = [
11
+ './instrumentation',
12
+ './instrumentation.server',
13
+ ];
14
+
11
15
  export function hasSentryContent(
12
16
  fileName: string,
13
17
  fileContent: string,
18
+ expectedContent = '@sentry/remix',
14
19
  ): boolean {
15
- const includesContent = fileContent.includes('@sentry/remix');
20
+ const includesContent = fileContent.includes(expectedContent);
16
21
 
17
22
  if (includesContent) {
18
23
  clack.log.warn(
19
- `File ${chalk.cyan(path.basename(fileName))} already contains Sentry code.
24
+ `File ${chalk.cyan(
25
+ path.basename(fileName),
26
+ )} already contains ${expectedContent}.
20
27
  Skipping adding Sentry functionality to ${chalk.cyan(
21
28
  path.basename(fileName),
22
29
  )}.`,
@@ -26,14 +33,57 @@ Skipping adding Sentry functionality to ${chalk.cyan(
26
33
  return includesContent;
27
34
  }
28
35
 
36
+ export function serverHasInstrumentationImport(
37
+ serverFileName: string,
38
+ serverFileContent: string,
39
+ ): boolean {
40
+ const includesServerInstrumentationImport =
41
+ POSSIBLE_SERVER_INSTRUMENTATION_PATHS.some((path) =>
42
+ serverFileContent.includes(path),
43
+ );
44
+
45
+ if (includesServerInstrumentationImport) {
46
+ clack.log.warn(
47
+ `File ${chalk.cyan(
48
+ path.basename(serverFileName),
49
+ )} already contains instrumentation import.
50
+ Skipping adding instrumentation functionality to ${chalk.cyan(
51
+ path.basename(serverFileName),
52
+ )}.`,
53
+ );
54
+ }
55
+
56
+ return includesServerInstrumentationImport;
57
+ }
58
+
29
59
  /**
30
- * We want to insert the init call on top of the file but after all import statements
60
+ * We want to insert the init call on top of the file, before any other imports.
31
61
  */
32
- export function getInitCallInsertionIndex(
62
+ export function getBeforeImportsInsertionIndex(
33
63
  originalHooksModAST: Program,
34
64
  ): number {
35
- for (let x = originalHooksModAST.body.length - 1; x >= 0; x--) {
36
- if (originalHooksModAST.body[x].type === 'ImportDeclaration') {
65
+ for (let x = 0; x < originalHooksModAST.body.length - 1; x++) {
66
+ if (
67
+ originalHooksModAST.body[x].type === 'ImportDeclaration' &&
68
+ // @ts-expect-error - source is available in body
69
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
70
+ originalHooksModAST.body[x].source.value === '@sentry/remix'
71
+ ) {
72
+ return x + 1;
73
+ }
74
+ }
75
+
76
+ return 0;
77
+ }
78
+
79
+ /**
80
+ * We want to insert the handleError function just after all imports
81
+ */
82
+ export function getAfterImportsInsertionIndex(
83
+ originalEntryServerModAST: Program,
84
+ ): number {
85
+ for (let x = originalEntryServerModAST.body.length - 1; x >= 0; x--) {
86
+ if (originalEntryServerModAST.body[x].type === 'ImportDeclaration') {
37
87
  return x + 1;
38
88
  }
39
89
  }