@sentry/wizard 3.0.0 → 3.1.0-beta.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/bin.ts +9 -2
  3. package/dist/bin.js +10 -2
  4. package/dist/bin.js.map +1 -1
  5. package/dist/lib/Helper/MergeConfig.js +1 -0
  6. package/dist/lib/Helper/MergeConfig.js.map +1 -1
  7. package/dist/lib/Steps/BaseStep.js.map +1 -1
  8. package/dist/lib/Steps/Initial.js +1 -0
  9. package/dist/lib/Steps/Initial.js.map +1 -1
  10. package/dist/lib/Steps/Integrations/BaseIntegration.js +2 -0
  11. package/dist/lib/Steps/Integrations/BaseIntegration.js.map +1 -1
  12. package/dist/lib/Steps/Integrations/Cordova.js +1 -0
  13. package/dist/lib/Steps/Integrations/Cordova.js.map +1 -1
  14. package/dist/lib/Steps/Integrations/Electron.js +2 -0
  15. package/dist/lib/Steps/Integrations/Electron.js.map +1 -1
  16. package/dist/lib/Steps/Integrations/MobileProject.js +1 -0
  17. package/dist/lib/Steps/Integrations/MobileProject.js.map +1 -1
  18. package/dist/lib/Steps/Integrations/NextJs.js +1 -0
  19. package/dist/lib/Steps/Integrations/NextJs.js.map +1 -1
  20. package/dist/lib/Steps/Integrations/ReactNative.js +2 -1
  21. package/dist/lib/Steps/Integrations/ReactNative.js.map +1 -1
  22. package/dist/lib/Steps/Result.js +1 -0
  23. package/dist/lib/Steps/Result.js.map +1 -1
  24. package/dist/lib/Steps/WaitForSentry.js +42 -47
  25. package/dist/lib/Steps/WaitForSentry.js.map +1 -1
  26. package/dist/lib/Steps/Welcome.js +1 -0
  27. package/dist/lib/Steps/Welcome.js.map +1 -1
  28. package/dist/src/clack-utils.d.ts +40 -0
  29. package/dist/src/clack-utils.js +329 -0
  30. package/dist/src/clack-utils.js.map +1 -0
  31. package/dist/src/nextjs-wizard.d.ts +5 -0
  32. package/dist/src/nextjs-wizard.js +346 -0
  33. package/dist/src/nextjs-wizard.js.map +1 -0
  34. package/lib/Helper/MergeConfig.ts +1 -0
  35. package/lib/Steps/BaseStep.ts +1 -1
  36. package/lib/Steps/Initial.ts +1 -0
  37. package/lib/Steps/Integrations/BaseIntegration.ts +2 -0
  38. package/lib/Steps/Integrations/Cordova.ts +2 -1
  39. package/lib/Steps/Integrations/Electron.ts +3 -1
  40. package/lib/Steps/Integrations/MobileProject.ts +1 -0
  41. package/lib/Steps/Integrations/NextJs.ts +1 -0
  42. package/lib/Steps/Integrations/ReactNative.ts +3 -2
  43. package/lib/Steps/Result.ts +1 -0
  44. package/lib/Steps/WaitForSentry.ts +1 -1
  45. package/lib/Steps/Welcome.ts +2 -1
  46. package/package.json +10 -3
  47. package/src/clack-utils.ts +346 -0
  48. package/src/nextjs-wizard.ts +619 -0
@@ -0,0 +1,619 @@
1
+ /* eslint-disable max-lines */
2
+ import * as clack from '@clack/prompts';
3
+ import chalk from 'chalk';
4
+ import * as fs from 'fs';
5
+ import { builders, generateCode, parseModule } from 'magicast';
6
+ import * as path from 'path';
7
+
8
+ import {
9
+ abort,
10
+ abortIfCancelled,
11
+ addSentryCliRc,
12
+ askForSelfHosted,
13
+ askForWizardLogin,
14
+ confirmContinueEvenThoughNoGitRepo,
15
+ installPackage,
16
+ printWelcome,
17
+ SentryProjectData,
18
+ } from './clack-utils';
19
+
20
+ interface NextjsWizardOptions {
21
+ promoCode?: string;
22
+ }
23
+
24
+ // eslint-disable-next-line complexity
25
+ export async function runNextjsWizard(
26
+ options: NextjsWizardOptions,
27
+ ): Promise<void> {
28
+ printWelcome({
29
+ wizardName: 'Sentry Next.js Wizard',
30
+ promoCode: options.promoCode,
31
+ });
32
+
33
+ await confirmContinueEvenThoughNoGitRepo();
34
+
35
+ const packageJsonFileContents = await fs.promises
36
+ .readFile(path.join(process.cwd(), 'package.json'), 'utf8')
37
+ .catch(() => {
38
+ clack.log.error(
39
+ 'Could not find package.json. Make sure to run the wizard in the root of your Next.js app!',
40
+ );
41
+ abort();
42
+ });
43
+
44
+ let packageJson:
45
+ | { dependencies?: { ['@sentry/nextjs']: string; ['next']: string } }
46
+ | undefined = undefined;
47
+
48
+ try {
49
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
50
+ packageJson = JSON.parse(packageJsonFileContents);
51
+ } catch (e) {
52
+ clack.log.error(
53
+ 'Unable to parse your package.json. Make sure it has a valid format!',
54
+ );
55
+
56
+ abort();
57
+ }
58
+
59
+ if (!packageJson?.dependencies?.['next']) {
60
+ const continueWithoutNext = await clack.confirm({
61
+ message:
62
+ 'Next.js does not seem to be installed. Do you still want to continue?',
63
+ initialValue: false,
64
+ });
65
+
66
+ abortIfCancelled(continueWithoutNext);
67
+
68
+ if (!continueWithoutNext) {
69
+ abort();
70
+ }
71
+ }
72
+
73
+ const { url: sentryUrl, selfHosted } = await askForSelfHosted();
74
+
75
+ const { projects, apiKeys } = await askForWizardLogin({
76
+ promoCode: options.promoCode,
77
+ url: sentryUrl,
78
+ });
79
+
80
+ const selectedProject: SentryProjectData | symbol = await clack.select({
81
+ message: 'Select your Sentry project.',
82
+ options: projects.map(project => {
83
+ return {
84
+ value: project,
85
+ label: `${project.organization.slug}/${project.slug}`,
86
+ };
87
+ }),
88
+ });
89
+
90
+ abortIfCancelled(selectedProject);
91
+
92
+ await installPackage({
93
+ packageName: '@sentry/nextjs',
94
+ alreadyInstalled: !!packageJson?.dependencies?.['@sentry/nextjs'],
95
+ });
96
+
97
+ let isUsingTypescript = false;
98
+ try {
99
+ isUsingTypescript = fs.existsSync(
100
+ path.join(process.cwd(), 'tsconfig.json'),
101
+ );
102
+ } catch (e) {
103
+ // noop - Default to assuming user is not using typescript
104
+ }
105
+
106
+ const configVariants = ['server', 'client', 'edge'] as const;
107
+
108
+ for (const configVariant of configVariants) {
109
+ const jsConfig = `sentry.${configVariant}.config.js`;
110
+ const tsConfig = `sentry.${configVariant}.config.ts`;
111
+
112
+ const jsConfigExists = fs.existsSync(path.join(process.cwd(), jsConfig));
113
+ const tsConfigExists = fs.existsSync(path.join(process.cwd(), tsConfig));
114
+
115
+ let shouldWriteFile = true;
116
+
117
+ if (jsConfigExists || tsConfigExists) {
118
+ const existingConfigs = [];
119
+
120
+ if (jsConfigExists) {
121
+ existingConfigs.push(jsConfig);
122
+ }
123
+
124
+ if (tsConfigExists) {
125
+ existingConfigs.push(tsConfig);
126
+ }
127
+
128
+ const overwriteExistingConfigs = await clack.confirm({
129
+ message: `Found existing Sentry ${configVariant} config (${existingConfigs.join(
130
+ ', ',
131
+ )}). Overwrite ${existingConfigs.length > 1 ? 'them' : 'it'}?`,
132
+ });
133
+
134
+ abortIfCancelled(overwriteExistingConfigs);
135
+
136
+ shouldWriteFile = overwriteExistingConfigs;
137
+
138
+ if (overwriteExistingConfigs) {
139
+ if (jsConfigExists) {
140
+ fs.unlinkSync(path.join(process.cwd(), jsConfig));
141
+ clack.log.warn(`Removed existing ${chalk.bold(jsConfig)}.`);
142
+ }
143
+ if (tsConfigExists) {
144
+ fs.unlinkSync(path.join(process.cwd(), tsConfig));
145
+ clack.log.warn(`Removed existing ${chalk.bold(tsConfig)}.`);
146
+ }
147
+ }
148
+ }
149
+
150
+ if (shouldWriteFile) {
151
+ await fs.promises.writeFile(
152
+ path.join(process.cwd(), isUsingTypescript ? tsConfig : jsConfig),
153
+ getSentryConfigContents(
154
+ selectedProject.keys[0].dsn.public,
155
+ configVariant,
156
+ ),
157
+ { encoding: 'utf8', flag: 'w' },
158
+ );
159
+ clack.log.success(
160
+ `Created fresh ${chalk.bold(isUsingTypescript ? tsConfig : jsConfig)}.`,
161
+ );
162
+ }
163
+ }
164
+
165
+ const webpackOptionsTemplate = `{
166
+ // For all available options, see:
167
+ // https://github.com/getsentry/sentry-webpack-plugin#options
168
+
169
+ // Suppresses source map uploading logs during build
170
+ silent: true,
171
+
172
+ org: "${selectedProject.organization.slug}",
173
+ project: "${selectedProject.slug}",
174
+ }`;
175
+
176
+ const sentryBuildOptionsTemplate = `{
177
+ // For all available options, see:
178
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
179
+
180
+ // Upload a larger set of source maps for prettier stack traces (increases build time)
181
+ widenClientFileUpload: true,
182
+
183
+ // Transpiles SDK to be compatible with IE11 (increases bundle size)
184
+ transpileClientSDK: true,
185
+
186
+ // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
187
+ tunnelRoute: "/monitoring",
188
+
189
+ // Hides source maps from generated client bundles
190
+ hideSourceMaps: true,
191
+
192
+ // Automatically tree-shake Sentry logger statements to reduce bundle size
193
+ disableLogger: true,
194
+ }`;
195
+
196
+ const newNextConfigTemplate = `const { withSentryConfig } = require("@sentry/nextjs");
197
+
198
+ /** @type {import('next').NextConfig} */
199
+ const nextConfig = {};
200
+
201
+ module.exports = withSentryConfig(
202
+ nextConfig,
203
+ ${webpackOptionsTemplate},
204
+ ${sentryBuildOptionsTemplate}
205
+ );
206
+ `;
207
+
208
+ const nextConfigJs = 'next.config.js';
209
+ const nextConfigMjs = 'next.config.mjs';
210
+
211
+ const nextConfigJsExists = fs.existsSync(
212
+ path.join(process.cwd(), nextConfigJs),
213
+ );
214
+ const nextConfigMjsExists = fs.existsSync(
215
+ path.join(process.cwd(), nextConfigMjs),
216
+ );
217
+
218
+ if (!nextConfigJsExists && !nextConfigMjsExists) {
219
+ await fs.promises.writeFile(
220
+ path.join(process.cwd(), nextConfigJs),
221
+ newNextConfigTemplate,
222
+ { encoding: 'utf8', flag: 'w' },
223
+ );
224
+
225
+ clack.log.success(
226
+ `Created ${chalk.bold('next.config.js')} with Sentry configuration.`,
227
+ );
228
+ }
229
+
230
+ if (nextConfigJsExists) {
231
+ const nextConfgiJsContent = fs.readFileSync(
232
+ path.join(process.cwd(), nextConfigJs),
233
+ 'utf8',
234
+ );
235
+
236
+ const probablyIncludesSdk =
237
+ nextConfgiJsContent.includes('@sentry/nextjs') &&
238
+ nextConfgiJsContent.includes('withSentryConfig');
239
+
240
+ let shouldInject = true;
241
+
242
+ if (probablyIncludesSdk) {
243
+ const injectAnyhow = await clack.confirm({
244
+ message: `${chalk.bold(
245
+ nextConfigMjs,
246
+ )} already contains Sentry SDK configuration. Should the wizard modify it anyways?`,
247
+ });
248
+
249
+ abortIfCancelled(injectAnyhow);
250
+
251
+ shouldInject = injectAnyhow;
252
+ }
253
+
254
+ if (shouldInject) {
255
+ const cjsAppendix = `
256
+
257
+ // Inected Content via Sentry Wizard Below
258
+
259
+ const { withSentryConfig } = require("@sentry/nextjs");
260
+
261
+ module.exports = withSentryConfig(
262
+ module.exports,
263
+ ${webpackOptionsTemplate},
264
+ ${sentryBuildOptionsTemplate}
265
+ );
266
+ `;
267
+ fs.appendFileSync(
268
+ path.join(process.cwd(), nextConfigJs),
269
+ cjsAppendix,
270
+ 'utf8',
271
+ );
272
+
273
+ clack.log.success(
274
+ `Added Sentry configuration to ${chalk.bold(nextConfigJs)}. ${chalk.dim(
275
+ '(you probably want to clean this up a bit!)',
276
+ )}`,
277
+ );
278
+ }
279
+ }
280
+
281
+ if (nextConfigMjsExists) {
282
+ const nextConfgiMjsContent = fs.readFileSync(
283
+ path.join(process.cwd(), nextConfigMjs),
284
+ 'utf8',
285
+ );
286
+
287
+ const probablyIncludesSdk =
288
+ nextConfgiMjsContent.includes('@sentry/nextjs') &&
289
+ nextConfgiMjsContent.includes('withSentryConfig');
290
+
291
+ let shouldInject = true;
292
+
293
+ if (probablyIncludesSdk) {
294
+ const injectAnyhow = await clack.confirm({
295
+ message: `${chalk.bold(
296
+ nextConfigMjs,
297
+ )} already contains Sentry SDK configuration. Should the wizard modify it anyways?`,
298
+ });
299
+
300
+ abortIfCancelled(injectAnyhow);
301
+ shouldInject = injectAnyhow;
302
+ }
303
+
304
+ try {
305
+ if (shouldInject) {
306
+ const mod = parseModule(nextConfgiMjsContent);
307
+ mod.imports.$add({
308
+ from: '@sentry/nextjs',
309
+ imported: 'withSentryConfig',
310
+ local: 'withSentryConfig',
311
+ });
312
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
313
+ const expressionToWrap = generateCode(mod.exports.default.$ast).code;
314
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
315
+ mod.exports.default = builders.raw(`withSentryConfig(
316
+ ${expressionToWrap},
317
+ ${webpackOptionsTemplate},
318
+ ${sentryBuildOptionsTemplate}
319
+ )`);
320
+ const newCode = mod.generate().code;
321
+
322
+ await fs.promises.writeFile(
323
+ path.join(process.cwd(), nextConfigMjs),
324
+ newCode,
325
+ {
326
+ encoding: 'utf8',
327
+ flag: 'w',
328
+ },
329
+ );
330
+ clack.log.success(
331
+ `Added Sentry configuration to ${chalk.bold(
332
+ nextConfigMjs,
333
+ )}. ${chalk.dim('(you probably want to clean this up a bit!)')}`,
334
+ );
335
+ }
336
+ } catch (e) {
337
+ clack.log.warn(
338
+ chalk.yellow(
339
+ `Something went wrong writing to ${chalk.bold(nextConfigMjs)}`,
340
+ ),
341
+ );
342
+ clack.log.info(
343
+ `Please put the following code snippet into ${chalk.bold(
344
+ nextConfigMjs,
345
+ )}: ${chalk.dim('You probably have to clean it up a bit.')}\n`,
346
+ );
347
+
348
+ // eslint-disable-next-line no-console
349
+ console.log(`\n
350
+ // next.config.mjs
351
+ import { withSentryConfig } from "@sentry/nextjs";
352
+
353
+ export default withSentryConfig(
354
+ yourNextConfig,
355
+ ${webpackOptionsTemplate},
356
+ ${sentryBuildOptionsTemplate}
357
+ );\n`);
358
+
359
+ const shouldContinue = await clack.confirm({
360
+ message: `Are you done putting the snippet above into ${chalk.bold(
361
+ nextConfigMjs,
362
+ )}?`,
363
+ active: 'Yes',
364
+ inactive: 'No, get me out of here',
365
+ });
366
+
367
+ abortIfCancelled(shouldContinue);
368
+ if (!shouldContinue) {
369
+ abort();
370
+ }
371
+ }
372
+ }
373
+
374
+ const maybePagesDirPath = path.join(process.cwd(), 'pages');
375
+ const maybeSrcPagesDirPath = path.join(process.cwd(), 'src', 'pages');
376
+
377
+ let pagesLocation =
378
+ fs.existsSync(maybePagesDirPath) &&
379
+ fs.lstatSync(maybePagesDirPath).isDirectory()
380
+ ? ['pages']
381
+ : fs.existsSync(maybeSrcPagesDirPath) &&
382
+ fs.lstatSync(maybeSrcPagesDirPath).isDirectory()
383
+ ? ['src', 'pages']
384
+ : undefined;
385
+
386
+ if (!pagesLocation) {
387
+ pagesLocation = ['pages'];
388
+ fs.mkdirSync(path.join(process.cwd(), ...pagesLocation), {
389
+ recursive: true,
390
+ });
391
+ }
392
+
393
+ if (pagesLocation) {
394
+ const examplePageContents = createExamplePage({
395
+ selfHosted,
396
+ orgSlug: selectedProject.organization.slug,
397
+ projectId: selectedProject.id,
398
+ url: sentryUrl,
399
+ });
400
+
401
+ await fs.promises.writeFile(
402
+ path.join(process.cwd(), ...pagesLocation, 'sentry-example-page.js'),
403
+ examplePageContents,
404
+ { encoding: 'utf8', flag: 'w' },
405
+ );
406
+
407
+ clack.log.success(
408
+ `Created ${chalk.bold(
409
+ path.join(...pagesLocation, 'sentry-example-page.js'),
410
+ )}.`,
411
+ );
412
+
413
+ fs.mkdirSync(path.join(process.cwd(), ...pagesLocation, 'api'), {
414
+ recursive: true,
415
+ });
416
+
417
+ await fs.promises.writeFile(
418
+ path.join(
419
+ process.cwd(),
420
+ ...pagesLocation,
421
+ 'api',
422
+ 'sentry-example-api.js',
423
+ ),
424
+ exampleApiRoute,
425
+ { encoding: 'utf8', flag: 'w' },
426
+ );
427
+
428
+ clack.log.success(
429
+ `Created ${chalk.bold(
430
+ path.join(...pagesLocation, 'api', 'sentry-example-api.js'),
431
+ )}.`,
432
+ );
433
+ }
434
+
435
+ await addSentryCliRc(apiKeys.token);
436
+
437
+ const mightBeUsingVercel = fs.existsSync(
438
+ path.join(process.cwd(), 'vercel.json'),
439
+ );
440
+
441
+ clack.outro(
442
+ `${chalk.green('Everything is set up!')}
443
+
444
+ ${chalk.cyan(
445
+ 'You can validate your setup by starting your dev environment (`next dev`) and visiting "/sentry-example-page".',
446
+ )}
447
+ ${
448
+ mightBeUsingVercel
449
+ ? `
450
+ ▲ It seems like you're using Vercel. We recommend using the Sentry Vercel integration: https://vercel.com/integrations/sentry
451
+ `
452
+ : ''
453
+ }
454
+ ${chalk.dim(
455
+ 'If you encounter any issues, let us know here: https://github.com/getsentry/sentry-javascript/issues',
456
+ )}`,
457
+ );
458
+ }
459
+
460
+ function getSentryConfigContents(
461
+ dsn: string,
462
+ config: 'server' | 'client' | 'edge',
463
+ ): string {
464
+ let primer;
465
+ if (config === 'server') {
466
+ primer = `// This file configures the initialization of Sentry on the server.
467
+ // The config you add here will be used whenever the server handles a request.
468
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/`;
469
+ } else if (config === 'client') {
470
+ primer = `// This file configures the initialization of Sentry on the client.
471
+ // The config you add here will be used whenever a users loads a page in their browser.
472
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/`;
473
+ } else if (config === 'edge') {
474
+ primer = `// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
475
+ // The config you add here will be used whenever one of the edge features is loaded.
476
+ // Note that this config is unrelated to the Verel Edge Runtime and is also required when running locally.
477
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/`;
478
+ }
479
+
480
+ let additionalOptions = '';
481
+ if (config === 'client') {
482
+ additionalOptions = `
483
+
484
+ replaysOnErrorSampleRate: 1.0,
485
+
486
+ // This sets the sample rate to be 10%. You may want this to be 100% while
487
+ // in development and sample at a lower rate in production
488
+ replaysSessionSampleRate: 0.1,
489
+
490
+ // You can remove this option if you're not planning to use the Sentry Session Replay feature:
491
+ integrations: [
492
+ new Sentry.Replay({
493
+ // Additional Replay configuration goes in here, for example:
494
+ maskAllText: true,
495
+ blockAllMedia: true,
496
+ }),
497
+ ],`;
498
+ }
499
+
500
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
501
+ return `${primer}
502
+
503
+ import * as Sentry from "@sentry/nextjs";
504
+
505
+ Sentry.init({
506
+ dsn: "${dsn}",
507
+
508
+ // Adjust this value in production, or use tracesSampler for greater control
509
+ tracesSampleRate: 1,
510
+
511
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
512
+ debug: false,${additionalOptions}
513
+ });
514
+ `;
515
+ }
516
+
517
+ function createExamplePage(options: {
518
+ selfHosted: boolean;
519
+ url: string;
520
+ orgSlug: string;
521
+ projectId: string;
522
+ }): string {
523
+ const issuesPageLink = options.selfHosted
524
+ ? `${options.url}organizations/${options.orgSlug}/issues/?project=${options.projectId}`
525
+ : `https://${options.orgSlug}.sentry.io/issues/?project=${options.projectId}`;
526
+
527
+ return `import Head from "next/head";
528
+ import * as Sentry from "@sentry/nextjs";
529
+
530
+ export default function Home() {
531
+ return (
532
+ <div>
533
+ <Head>
534
+ <title>Sentry Onboarding</title>
535
+ <meta name="description" content="Test Sentry for your Next.js app!" />
536
+ </Head>
537
+
538
+ <main
539
+ style={{
540
+ minHeight: "100vh",
541
+ display: "flex",
542
+ flexDirection: "column",
543
+ justifyContent: "center",
544
+ alignItems: "center",
545
+ }}
546
+ >
547
+ <h1 style={{ fontSize: "4rem", margin: "14px 0" }}>
548
+ <svg
549
+ style={{
550
+ height: "1em",
551
+ }}
552
+ xmlns="http://www.w3.org/2000/svg"
553
+ viewBox="0 0 200 44"
554
+ >
555
+ <path
556
+ fill="currentColor"
557
+ d="M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z"
558
+ ></path>
559
+ </svg>
560
+ </h1>
561
+
562
+ <p>Get started by sending us a sample error:</p>
563
+ <button
564
+ type="button"
565
+ style={{
566
+ padding: "12px",
567
+ cursor: "pointer",
568
+ backgroundColor: "#AD6CAA",
569
+ borderRadius: "4px",
570
+ border: "none",
571
+ color: "white",
572
+ fontSize: "14px",
573
+ margin: "18px",
574
+ }}
575
+ onClick={async () => {
576
+ const transaction = Sentry.startTransaction({
577
+ name: "Example Frontend Transaction",
578
+ });
579
+
580
+ Sentry.configureScope((scope) => {
581
+ scope.setSpan(transaction);
582
+ });
583
+
584
+ try {
585
+ const res = await fetch("/api/sentry-example-api");
586
+ if (!res.ok) {
587
+ throw new Error("Sentry Example Frontend Error");
588
+ }
589
+ } finally {
590
+ transaction.finish();
591
+ }
592
+ }}
593
+ >
594
+ Throw error!
595
+ </button>
596
+
597
+ <p>
598
+ Next, look for the error on the{" "}
599
+ <a href="${issuesPageLink}">Issues Page</a>.
600
+ </p>
601
+ <p style={{ marginTop: "24px" }}>
602
+ For more information, see{" "}
603
+ <a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/">
604
+ https://docs.sentry.io/platforms/javascript/guides/nextjs/
605
+ </a>
606
+ </p>
607
+ </main>
608
+ </div>
609
+ );
610
+ }
611
+ `;
612
+ }
613
+
614
+ const exampleApiRoute = `// A faulty API route to test Sentry's error monitoring
615
+ export default function handler(_req, res) {
616
+ throw new Error("Sentry Example API Route Error");
617
+ res.status(200).json({ name: "John Doe" });
618
+ }
619
+ `;