@sentry/wizard 3.13.0 → 3.14.1

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/package.json +2 -2
  3. package/dist/src/android/android-wizard.js +2 -4
  4. package/dist/src/android/android-wizard.js.map +1 -1
  5. package/dist/src/apple/apple-wizard.js +1 -1
  6. package/dist/src/apple/apple-wizard.js.map +1 -1
  7. package/dist/src/nextjs/nextjs-wizard.d.ts +1 -0
  8. package/dist/src/nextjs/nextjs-wizard.js +252 -161
  9. package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
  10. package/dist/src/nextjs/utils.d.ts +1 -0
  11. package/dist/src/nextjs/utils.js +25 -0
  12. package/dist/src/nextjs/utils.js.map +1 -0
  13. package/dist/src/remix/remix-wizard.js +5 -7
  14. package/dist/src/remix/remix-wizard.js.map +1 -1
  15. package/dist/src/remix/sdk-setup.js +10 -4
  16. package/dist/src/remix/sdk-setup.js.map +1 -1
  17. package/dist/src/sourcemaps/sourcemaps-wizard.js +1 -1
  18. package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
  19. package/dist/src/sveltekit/sdk-setup.js +7 -3
  20. package/dist/src/sveltekit/sdk-setup.js.map +1 -1
  21. package/dist/src/sveltekit/sveltekit-wizard.js +6 -10
  22. package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
  23. package/dist/src/telemetry.d.ts +1 -0
  24. package/dist/src/telemetry.js +27 -12
  25. package/dist/src/telemetry.js.map +1 -1
  26. package/dist/src/utils/clack-utils.d.ts +11 -1
  27. package/dist/src/utils/clack-utils.js +190 -126
  28. package/dist/src/utils/clack-utils.js.map +1 -1
  29. package/dist/src/utils/package-manager.js +12 -7
  30. package/dist/src/utils/package-manager.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/android/android-wizard.ts +4 -5
  33. package/src/apple/apple-wizard.ts +2 -2
  34. package/src/nextjs/nextjs-wizard.ts +262 -195
  35. package/src/nextjs/utils.ts +21 -0
  36. package/src/remix/remix-wizard.ts +7 -9
  37. package/src/remix/sdk-setup.ts +12 -3
  38. package/src/sourcemaps/sourcemaps-wizard.ts +2 -2
  39. package/src/sveltekit/sdk-setup.ts +6 -3
  40. package/src/sveltekit/sveltekit-wizard.ts +9 -12
  41. package/src/telemetry.ts +22 -11
  42. package/src/utils/clack-utils.ts +177 -107
  43. package/src/utils/package-manager.ts +12 -6
@@ -440,14 +440,14 @@ async function modifyViteConfig(
440
440
 
441
441
  const { org, project, url, selfHosted } = projectInfo;
442
442
 
443
+ const prettyViteConfigFilename = chalk.cyan(path.basename(viteConfigPath));
444
+
443
445
  try {
444
446
  const viteModule = parseModule(viteConfigContent);
445
447
 
446
448
  if (hasSentryContent(viteModule.$ast as t.Program)) {
447
449
  clack.log.warn(
448
- `File ${chalk.cyan(
449
- path.basename(viteConfigPath),
450
- )} already contains Sentry code.
450
+ `File ${prettyViteConfigFilename} already contains Sentry code.
451
451
  Skipping adding Sentry functionality to.`,
452
452
  );
453
453
  Sentry.setTag(`modified-vite-cfg`, 'fail');
@@ -490,6 +490,9 @@ Skipping adding Sentry functionality to.`,
490
490
  );
491
491
  Sentry.captureException('Sveltekit Vite Config Modification Fail');
492
492
  }
493
+
494
+ clack.log.success(`Added Sentry code to ${prettyViteConfigFilename}`);
495
+ Sentry.setTag(`modified-vite-cfg`, 'success');
493
496
  }
494
497
 
495
498
  async function showFallbackViteCopyPasteSnippet(
@@ -8,7 +8,7 @@ import {
8
8
  abort,
9
9
  abortIfCancelled,
10
10
  addSentryCliConfig,
11
- confirmContinueEvenThoughNoGitRepo,
11
+ confirmContinueIfNoOrDirtyGitRepo,
12
12
  ensurePackageIsInstalled,
13
13
  getOrAskForProjectData,
14
14
  getPackageDotJson,
@@ -43,12 +43,11 @@ export async function runSvelteKitWizardWithTelemetry(
43
43
  telemetryEnabled: options.telemetryEnabled,
44
44
  });
45
45
 
46
- await traceStep('detect-git', confirmContinueEvenThoughNoGitRepo);
46
+ await confirmContinueIfNoOrDirtyGitRepo();
47
47
 
48
48
  const packageJson = await getPackageDotJson();
49
- await traceStep('detect-framework-version', () =>
50
- ensurePackageIsInstalled(packageJson, '@sveltejs/kit', 'Sveltekit'),
51
- );
49
+
50
+ await ensurePackageIsInstalled(packageJson, '@sveltejs/kit', 'Sveltekit');
52
51
 
53
52
  const kitVersion = getPackageVersion('@sveltejs/kit', packageJson);
54
53
  const kitVersionBucket = getKitVersionBucket(kitVersion);
@@ -91,14 +90,12 @@ export async function runSvelteKitWizardWithTelemetry(
91
90
  );
92
91
  Sentry.setTag('sdk-already-installed', sdkAlreadyInstalled);
93
92
 
94
- await traceStep('install-sdk', () =>
95
- installPackage({
96
- packageName: '@sentry/sveltekit',
97
- alreadyInstalled: sdkAlreadyInstalled,
98
- }),
99
- );
93
+ await installPackage({
94
+ packageName: '@sentry/sveltekit',
95
+ alreadyInstalled: sdkAlreadyInstalled,
96
+ });
100
97
 
101
- await traceStep('add-cli-config', () => addSentryCliConfig(authToken));
98
+ await addSentryCliConfig(authToken);
102
99
 
103
100
  const svelteConfig = await traceStep('load-svelte-config', loadSvelteConfig);
104
101
 
package/src/telemetry.ts CHANGED
@@ -6,7 +6,8 @@ import {
6
6
  makeNodeTransport,
7
7
  NodeClient,
8
8
  runWithAsyncContext,
9
- trace,
9
+ setTag,
10
+ startSpan,
10
11
  } from '@sentry/node';
11
12
  import packageJson from '../package.json';
12
13
 
@@ -24,24 +25,29 @@ export async function withTelemetry<F>(
24
25
 
25
26
  makeMain(sentryHub);
26
27
 
27
- const transaction = sentryHub.startTransaction({
28
- name: 'sentry-wizard-execution',
29
- status: 'ok',
30
- op: 'wizard.flow',
31
- });
32
- sentryHub.getScope().setSpan(transaction);
33
28
  const sentrySession = sentryHub.startSession();
34
29
  sentryHub.captureSession();
35
30
 
36
31
  try {
37
- return await runWithAsyncContext(() => callback());
32
+ return await startSpan(
33
+ {
34
+ name: 'sentry-wizard-execution',
35
+ status: 'ok',
36
+ op: 'wizard.flow',
37
+ },
38
+ async () => {
39
+ updateProgress('start');
40
+ const res = await runWithAsyncContext(callback);
41
+ updateProgress('finished');
42
+
43
+ return res;
44
+ },
45
+ );
38
46
  } catch (e) {
39
47
  sentryHub.captureException('Error during wizard execution.');
40
- transaction.setStatus('internal_error');
41
48
  sentrySession.status = 'crashed';
42
49
  throw e;
43
50
  } finally {
44
- transaction.finish();
45
51
  sentryHub.endSession();
46
52
  await sentryClient.flush(3000);
47
53
  }
@@ -92,5 +98,10 @@ function createSentryInstance(enabled: boolean, integration: string) {
92
98
  }
93
99
 
94
100
  export function traceStep<T>(step: string, callback: () => T): T {
95
- return trace({ name: step, op: 'wizard.step' }, () => callback());
101
+ updateProgress(step);
102
+ return startSpan({ name: step, op: 'wizard.step' }, () => callback());
103
+ }
104
+
105
+ export function updateProgress(step: string) {
106
+ setTag('progress', step);
96
107
  }
@@ -5,6 +5,7 @@ import chalk from 'chalk';
5
5
  import * as childProcess from 'child_process';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
+ import * as os from 'os';
8
9
  import { setInterval } from 'timers';
9
10
  import { URL } from 'url';
10
11
  import * as Sentry from '@sentry/node';
@@ -147,24 +148,73 @@ You can turn this off at any time by running ${chalk.cyanBright(
147
148
  clack.note(welcomeText);
148
149
  }
149
150
 
150
- export async function confirmContinueEvenThoughNoGitRepo(): Promise<void> {
151
+ export async function confirmContinueIfNoOrDirtyGitRepo(): Promise<void> {
152
+ return traceStep('check-git-status', async () => {
153
+ if (!isInGitRepo()) {
154
+ const continueWithoutGit = await abortIfCancelled(
155
+ clack.confirm({
156
+ message:
157
+ 'You are not inside a git repository. The wizard will create and update files. Do you want to continue anyway?',
158
+ }),
159
+ );
160
+
161
+ Sentry.setTag('continue-without-git', continueWithoutGit);
162
+
163
+ if (!continueWithoutGit) {
164
+ await abort(undefined, 0);
165
+ }
166
+ }
167
+
168
+ const uncommittedOrUntrackedFiles = getUncommittedOrUntrackedFiles();
169
+ if (uncommittedOrUntrackedFiles.length) {
170
+ clack.log.warn(
171
+ `You have uncommitted or untracked files in your repo:
172
+
173
+ ${uncommittedOrUntrackedFiles.join('\n')}
174
+
175
+ The wizard will create and update files.`,
176
+ );
177
+ const continueWithDirtyRepo = await abortIfCancelled(
178
+ clack.confirm({
179
+ message: 'Do you want to continue anyway?',
180
+ }),
181
+ );
182
+
183
+ Sentry.setTag('continue-with-dirty-repo', continueWithDirtyRepo);
184
+
185
+ if (!continueWithDirtyRepo) {
186
+ await abort(undefined, 0);
187
+ }
188
+ }
189
+ });
190
+ }
191
+
192
+ function isInGitRepo() {
151
193
  try {
152
194
  childProcess.execSync('git rev-parse --is-inside-work-tree', {
153
195
  stdio: 'ignore',
154
196
  });
197
+ return true;
155
198
  } catch {
156
- const continueWithoutGit = await abortIfCancelled(
157
- clack.confirm({
158
- message:
159
- 'You are not inside a git repository. The wizard will create and update files. Do you still want to continue?',
160
- }),
161
- );
199
+ return false;
200
+ }
201
+ }
162
202
 
163
- Sentry.setTag('continue-without-git', continueWithoutGit);
203
+ function getUncommittedOrUntrackedFiles(): string[] {
204
+ try {
205
+ const gitStatus = childProcess
206
+ .execSync('git status --porcelain=v1')
207
+ .toString();
164
208
 
165
- if (!continueWithoutGit) {
166
- await abort(undefined, 0);
167
- }
209
+ const files = gitStatus
210
+ .split(os.EOL)
211
+ .map((line) => line.trim())
212
+ .filter(Boolean)
213
+ .map((f) => `- ${f.split(/\s+/)[1]}`);
214
+
215
+ return files;
216
+ } catch {
217
+ return [];
168
218
  }
169
219
  }
170
220
 
@@ -207,123 +257,127 @@ export async function installPackage({
207
257
  alreadyInstalled: boolean;
208
258
  askBeforeUpdating?: boolean;
209
259
  }): Promise<void> {
210
- if (alreadyInstalled && askBeforeUpdating) {
211
- const shouldUpdatePackage = await abortIfCancelled(
212
- clack.confirm({
213
- message: `The ${chalk.bold.cyan(
214
- packageName,
215
- )} package is already installed. Do you want to update it to the latest version?`,
216
- }),
217
- );
260
+ return traceStep('install-package', async () => {
261
+ if (alreadyInstalled && askBeforeUpdating) {
262
+ const shouldUpdatePackage = await abortIfCancelled(
263
+ clack.confirm({
264
+ message: `The ${chalk.bold.cyan(
265
+ packageName,
266
+ )} package is already installed. Do you want to update it to the latest version?`,
267
+ }),
268
+ );
218
269
 
219
- if (!shouldUpdatePackage) {
220
- return;
270
+ if (!shouldUpdatePackage) {
271
+ return;
272
+ }
221
273
  }
222
- }
223
-
224
- const sdkInstallSpinner = clack.spinner();
225
274
 
226
- const packageManager = await getPackageManager();
275
+ const sdkInstallSpinner = clack.spinner();
227
276
 
228
- sdkInstallSpinner.start(
229
- `${alreadyInstalled ? 'Updating' : 'Installing'} ${chalk.bold.cyan(
230
- packageName,
231
- )} with ${chalk.bold(packageManager.label)}.`,
232
- );
277
+ const packageManager = await getPackageManager();
233
278
 
234
- try {
235
- await installPackageWithPackageManager(packageManager, packageName);
236
- } catch (e) {
237
- sdkInstallSpinner.stop('Installation failed.');
238
- clack.log.error(
239
- `${chalk.red(
240
- 'Encountered the following error during installation:',
241
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
242
- )}\n\n${e}\n\n${chalk.dim(
243
- 'If you think this issue is caused by the Sentry wizard, let us know here:\nhttps://github.com/getsentry/sentry-wizard/issues',
244
- )}`,
279
+ sdkInstallSpinner.start(
280
+ `${alreadyInstalled ? 'Updating' : 'Installing'} ${chalk.bold.cyan(
281
+ packageName,
282
+ )} with ${chalk.bold(packageManager.label)}.`,
245
283
  );
246
- await abort();
247
- }
248
284
 
249
- sdkInstallSpinner.stop(
250
- `${alreadyInstalled ? 'Updated' : 'Installed'} ${chalk.bold.cyan(
251
- packageName,
252
- )} with ${chalk.bold(packageManager.label)}.`,
253
- );
285
+ try {
286
+ await installPackageWithPackageManager(packageManager, packageName);
287
+ } catch (e) {
288
+ sdkInstallSpinner.stop('Installation failed.');
289
+ clack.log.error(
290
+ `${chalk.red(
291
+ 'Encountered the following error during installation:',
292
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
293
+ )}\n\n${e}\n\n${chalk.dim(
294
+ 'If you think this issue is caused by the Sentry wizard, let us know here:\nhttps://github.com/getsentry/sentry-wizard/issues',
295
+ )}`,
296
+ );
297
+ await abort();
298
+ }
299
+
300
+ sdkInstallSpinner.stop(
301
+ `${alreadyInstalled ? 'Updated' : 'Installed'} ${chalk.bold.cyan(
302
+ packageName,
303
+ )} with ${chalk.bold(packageManager.label)}.`,
304
+ );
305
+ });
254
306
  }
255
307
 
256
308
  export async function addSentryCliConfig(
257
309
  authToken: string,
258
310
  setupConfig: CliSetupConfig = sourceMapsCliSetupConfig,
259
311
  ): Promise<void> {
260
- const configExists = fs.existsSync(
261
- path.join(process.cwd(), setupConfig.filename),
262
- );
263
- if (configExists) {
264
- const configContents = fs.readFileSync(
312
+ return traceStep('add-sentry-cli-config', async () => {
313
+ const configExists = fs.existsSync(
265
314
  path.join(process.cwd(), setupConfig.filename),
266
- 'utf8',
267
315
  );
268
-
269
- if (setupConfig.likelyAlreadyHasAuthToken(configContents)) {
270
- clack.log.warn(
271
- `${chalk.bold(
272
- setupConfig.filename,
273
- )} already has auth token. Will not add one.`,
316
+ if (configExists) {
317
+ const configContents = fs.readFileSync(
318
+ path.join(process.cwd(), setupConfig.filename),
319
+ 'utf8',
274
320
  );
321
+
322
+ if (setupConfig.likelyAlreadyHasAuthToken(configContents)) {
323
+ clack.log.warn(
324
+ `${chalk.bold(
325
+ setupConfig.filename,
326
+ )} already has auth token. Will not add one.`,
327
+ );
328
+ } else {
329
+ try {
330
+ await fs.promises.writeFile(
331
+ path.join(process.cwd(), setupConfig.filename),
332
+ `${configContents}\n${setupConfig.tokenContent(authToken)}\n`,
333
+ { encoding: 'utf8', flag: 'w' },
334
+ );
335
+ clack.log.success(
336
+ chalk.greenBright(
337
+ `Added auth token to ${chalk.bold(
338
+ setupConfig.filename,
339
+ )} for you to test uploading ${setupConfig.name} locally.`,
340
+ ),
341
+ );
342
+ } catch {
343
+ clack.log.warning(
344
+ `Failed to add auth token to ${chalk.bold(
345
+ setupConfig.filename,
346
+ )}. Uploading ${
347
+ setupConfig.name
348
+ } during build will likely not work locally.`,
349
+ );
350
+ }
351
+ }
275
352
  } else {
276
353
  try {
277
354
  await fs.promises.writeFile(
278
355
  path.join(process.cwd(), setupConfig.filename),
279
- `${configContents}\n${setupConfig.tokenContent(authToken)}\n`,
356
+ `${setupConfig.tokenContent(authToken)}\n`,
280
357
  { encoding: 'utf8', flag: 'w' },
281
358
  );
282
359
  clack.log.success(
283
360
  chalk.greenBright(
284
- `Added auth token to ${chalk.bold(
361
+ `Created ${chalk.bold(
285
362
  setupConfig.filename,
286
- )} for you to test uploading ${setupConfig.name} locally.`,
363
+ )} with auth token for you to test uploading ${
364
+ setupConfig.name
365
+ } locally.`,
287
366
  ),
288
367
  );
289
368
  } catch {
290
369
  clack.log.warning(
291
- `Failed to add auth token to ${chalk.bold(
370
+ `Failed to create ${chalk.bold(
292
371
  setupConfig.filename,
293
- )}. Uploading ${
372
+ )} with auth token. Uploading ${
294
373
  setupConfig.name
295
374
  } during build will likely not work locally.`,
296
375
  );
297
376
  }
298
377
  }
299
- } else {
300
- try {
301
- await fs.promises.writeFile(
302
- path.join(process.cwd(), setupConfig.filename),
303
- `${setupConfig.tokenContent(authToken)}\n`,
304
- { encoding: 'utf8', flag: 'w' },
305
- );
306
- clack.log.success(
307
- chalk.greenBright(
308
- `Created ${chalk.bold(
309
- setupConfig.filename,
310
- )} with auth token for you to test uploading ${
311
- setupConfig.name
312
- } locally.`,
313
- ),
314
- );
315
- } catch {
316
- clack.log.warning(
317
- `Failed to create ${chalk.bold(
318
- setupConfig.filename,
319
- )} with auth token. Uploading ${
320
- setupConfig.name
321
- } during build will likely not work locally.`,
322
- );
323
- }
324
- }
325
378
 
326
- await addAuthTokenFileToGitIgnore(setupConfig.filename);
379
+ await addAuthTokenFileToGitIgnore(setupConfig.filename);
380
+ });
327
381
  }
328
382
 
329
383
  export async function addDotEnvSentryBuildPluginFile(
@@ -418,26 +472,40 @@ async function addAuthTokenFileToGitIgnore(filename: string): Promise<void> {
418
472
  }
419
473
  }
420
474
 
475
+ /**
476
+ * Checks if @param packageId is listed as a dependency in @param packageJson.
477
+ * If not, it will ask users if they want to continue without the package.
478
+ *
479
+ * Use this function to check if e.g. a the framework of the SDK is installed
480
+ *
481
+ * @param packageJson the package.json object
482
+ * @param packageId the npm name of the package
483
+ * @param packageName a human readable name of the package
484
+ */
421
485
  export async function ensurePackageIsInstalled(
422
486
  packageJson: PackageDotJson,
423
487
  packageId: string,
424
488
  packageName: string,
425
- ) {
426
- if (!hasPackageInstalled(packageId, packageJson)) {
427
- Sentry.setTag('package-installed', false);
428
- const continueWithoutPackage = await abortIfCancelled(
429
- clack.confirm({
430
- message: `${packageName} does not seem to be installed. Do you still want to continue?`,
431
- initialValue: false,
432
- }),
433
- );
489
+ ): Promise<void> {
490
+ return traceStep('ensure-package-installed', async () => {
491
+ const installed = hasPackageInstalled(packageId, packageJson);
492
+
493
+ Sentry.setTag(`${packageName.toLowerCase()}-installed`, installed);
494
+
495
+ if (!installed) {
496
+ Sentry.setTag(`${packageName.toLowerCase()}-installed`, false);
497
+ const continueWithoutPackage = await abortIfCancelled(
498
+ clack.confirm({
499
+ message: `${packageName} does not seem to be installed. Do you still want to continue?`,
500
+ initialValue: false,
501
+ }),
502
+ );
434
503
 
435
- if (!continueWithoutPackage) {
436
- await abort(undefined, 0);
504
+ if (!continueWithoutPackage) {
505
+ await abort(undefined, 0);
506
+ }
437
507
  }
438
- } else {
439
- Sentry.setTag('package-installed', true);
440
- }
508
+ });
441
509
  }
442
510
 
443
511
  export async function getPackageDotJson(): Promise<PackageDotJson> {
@@ -457,7 +525,9 @@ export async function getPackageDotJson(): Promise<PackageDotJson> {
457
525
  packageJson = JSON.parse(packageJsonFileContents);
458
526
  } catch {
459
527
  clack.log.error(
460
- 'Unable to parse your package.json. Make sure it has a valid format!',
528
+ `Unable to parse your ${chalk.cyan(
529
+ 'package.json',
530
+ )}. Make sure it has a valid format!`,
461
531
  );
462
532
 
463
533
  await abort();
@@ -4,6 +4,9 @@ import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import { promisify } from 'util';
6
6
 
7
+ import * as Sentry from '@sentry/node';
8
+ import { traceStep } from '../telemetry';
9
+
7
10
  export interface PackageManager {
8
11
  name: string;
9
12
  label: string;
@@ -50,13 +53,16 @@ export const NPM: PackageManager = {
50
53
  export const packageManagers = [BUN, YARN, PNPM, NPM];
51
54
 
52
55
  export function detectPackageManger(): PackageManager | null {
53
- for (const packageManager of packageManagers) {
54
- if (fs.existsSync(path.join(process.cwd(), packageManager.lockFile))) {
55
- return packageManager;
56
+ return traceStep('detect-package-manager', () => {
57
+ for (const packageManager of packageManagers) {
58
+ if (fs.existsSync(path.join(process.cwd(), packageManager.lockFile))) {
59
+ Sentry.setTag('package-manager', packageManager.name);
60
+ return packageManager;
61
+ }
56
62
  }
57
- }
58
- // We make the default NPM - it's weird if we don't find any lock file
59
- return null;
63
+ Sentry.setTag('package-manager', 'not-detected');
64
+ return null;
65
+ });
60
66
  }
61
67
 
62
68
  export async function installPackageWithPackageManager(