@sentry/wizard 3.9.2 → 3.11.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 (168) hide show
  1. package/CHANGELOG.md +58 -6
  2. package/dist/lib/Constants.d.ts +2 -0
  3. package/dist/lib/Constants.js +10 -0
  4. package/dist/lib/Constants.js.map +1 -1
  5. package/dist/lib/Steps/ChooseIntegration.js +15 -4
  6. package/dist/lib/Steps/ChooseIntegration.js.map +1 -1
  7. package/dist/lib/Steps/Integrations/Android.d.ts +9 -0
  8. package/dist/lib/Steps/Integrations/Android.js +86 -0
  9. package/dist/lib/Steps/Integrations/Android.js.map +1 -0
  10. package/dist/lib/Steps/Integrations/Cordova.js +5 -1
  11. package/dist/lib/Steps/Integrations/Cordova.js.map +1 -1
  12. package/dist/lib/Steps/Integrations/ReactNative.js +3 -3
  13. package/dist/lib/Steps/Integrations/ReactNative.js.map +1 -1
  14. package/dist/lib/Steps/Integrations/Remix.d.ts +12 -0
  15. package/dist/lib/Steps/Integrations/Remix.js +98 -0
  16. package/dist/lib/Steps/Integrations/Remix.js.map +1 -0
  17. package/dist/lib/Steps/PromptForParameters.js +36 -3
  18. package/dist/lib/Steps/PromptForParameters.js.map +1 -1
  19. package/dist/lib/Steps/SentryProjectSelector.js +1 -1
  20. package/dist/lib/Steps/SentryProjectSelector.js.map +1 -1
  21. package/dist/package.json +4 -3
  22. package/dist/src/android/android-wizard.d.ts +2 -0
  23. package/dist/src/android/android-wizard.js +217 -0
  24. package/dist/src/android/android-wizard.js.map +1 -0
  25. package/dist/src/android/code-tools.d.ts +39 -0
  26. package/dist/src/android/code-tools.js +161 -0
  27. package/dist/src/android/code-tools.js.map +1 -0
  28. package/dist/src/android/gradle.d.ts +62 -0
  29. package/dist/src/android/gradle.js +281 -0
  30. package/dist/src/android/gradle.js.map +1 -0
  31. package/dist/src/android/manifest.d.ts +57 -0
  32. package/dist/src/android/manifest.js +183 -0
  33. package/dist/src/android/manifest.js.map +1 -0
  34. package/dist/src/android/templates.d.ts +11 -0
  35. package/dist/src/android/templates.js +34 -0
  36. package/dist/src/android/templates.js.map +1 -0
  37. package/dist/src/apple/apple-wizard.js +123 -64
  38. package/dist/src/apple/apple-wizard.js.map +1 -1
  39. package/dist/src/apple/cocoapod.js +4 -3
  40. package/dist/src/apple/cocoapod.js.map +1 -1
  41. package/dist/src/apple/code-tools.d.ts +1 -1
  42. package/dist/src/apple/code-tools.js +43 -19
  43. package/dist/src/apple/code-tools.js.map +1 -1
  44. package/dist/src/apple/fastlane.d.ts +1 -1
  45. package/dist/src/apple/fastlane.js +12 -6
  46. package/dist/src/apple/fastlane.js.map +1 -1
  47. package/dist/src/apple/templates.d.ts +2 -2
  48. package/dist/src/apple/templates.js +4 -4
  49. package/dist/src/apple/templates.js.map +1 -1
  50. package/dist/src/apple/xcode-manager.d.ts +19 -3
  51. package/dist/src/apple/xcode-manager.js +126 -24
  52. package/dist/src/apple/xcode-manager.js.map +1 -1
  53. package/dist/src/nextjs/nextjs-wizard.js +49 -11
  54. package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
  55. package/dist/src/nextjs/templates.d.ts +2 -0
  56. package/dist/src/nextjs/templates.js +6 -2
  57. package/dist/src/nextjs/templates.js.map +1 -1
  58. package/dist/src/remix/codemods/handle-error.d.ts +2 -0
  59. package/dist/src/remix/codemods/handle-error.js +70 -0
  60. package/dist/src/remix/codemods/handle-error.js.map +1 -0
  61. package/dist/src/remix/codemods/root-v1.d.ts +1 -0
  62. package/dist/src/remix/codemods/root-v1.js +133 -0
  63. package/dist/src/remix/codemods/root-v1.js.map +1 -0
  64. package/dist/src/remix/codemods/root-v2.d.ts +1 -0
  65. package/dist/src/remix/codemods/root-v2.js +134 -0
  66. package/dist/src/remix/codemods/root-v2.js.map +1 -0
  67. package/dist/src/remix/remix-wizard.d.ts +2 -0
  68. package/dist/src/remix/remix-wizard.js +196 -0
  69. package/dist/src/remix/remix-wizard.js.map +1 -0
  70. package/dist/src/remix/sdk-setup.d.ts +18 -0
  71. package/dist/src/remix/sdk-setup.js +293 -0
  72. package/dist/src/remix/sdk-setup.js.map +1 -0
  73. package/dist/src/remix/templates.d.ts +2 -0
  74. package/dist/src/remix/templates.js +6 -0
  75. package/dist/src/remix/templates.js.map +1 -0
  76. package/dist/src/remix/utils.d.ts +6 -0
  77. package/dist/src/remix/utils.js +55 -0
  78. package/dist/src/remix/utils.js.map +1 -0
  79. package/dist/src/sourcemaps/sourcemaps-wizard.js +49 -25
  80. package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
  81. package/dist/src/sourcemaps/tools/nextjs.js +1 -1
  82. package/dist/src/sourcemaps/tools/nextjs.js.map +1 -1
  83. package/dist/src/sourcemaps/tools/remix.d.ts +3 -0
  84. package/dist/src/sourcemaps/tools/remix.js +125 -0
  85. package/dist/src/sourcemaps/tools/remix.js.map +1 -0
  86. package/dist/src/sourcemaps/tools/sentry-cli.js +19 -16
  87. package/dist/src/sourcemaps/tools/sentry-cli.js.map +1 -1
  88. package/dist/src/sourcemaps/tools/vite.d.ts +2 -1
  89. package/dist/src/sourcemaps/tools/vite.js +99 -12
  90. package/dist/src/sourcemaps/tools/vite.js.map +1 -1
  91. package/dist/src/sourcemaps/utils/detect-tool.d.ts +1 -1
  92. package/dist/src/sourcemaps/utils/detect-tool.js +1 -0
  93. package/dist/src/sourcemaps/utils/detect-tool.js.map +1 -1
  94. package/dist/src/sveltekit/sdk-setup.js +3 -3
  95. package/dist/src/sveltekit/sdk-setup.js.map +1 -1
  96. package/dist/src/sveltekit/sveltekit-wizard.js +34 -44
  97. package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
  98. package/dist/src/telemetry.js +1 -0
  99. package/dist/src/telemetry.js.map +1 -1
  100. package/dist/src/utils/ast-utils.d.ts +2 -2
  101. package/dist/src/utils/ast-utils.js +7 -7
  102. package/dist/src/utils/ast-utils.js.map +1 -1
  103. package/dist/src/utils/clack-utils.d.ts +23 -28
  104. package/dist/src/utils/clack-utils.js +287 -244
  105. package/dist/src/utils/clack-utils.js.map +1 -1
  106. package/dist/src/utils/package-manager.d.ts +10 -0
  107. package/dist/{lib/Helper/PackageManager.js → src/utils/package-manager.js} +42 -74
  108. package/dist/src/utils/package-manager.js.map +1 -0
  109. package/dist/src/utils/release-registry.d.ts +1 -0
  110. package/dist/src/utils/release-registry.js +68 -0
  111. package/dist/src/utils/release-registry.js.map +1 -0
  112. package/dist/src/utils/sentrycli-utils.d.ts +4 -0
  113. package/dist/src/utils/sentrycli-utils.js +41 -0
  114. package/dist/src/utils/sentrycli-utils.js.map +1 -0
  115. package/dist/test/sourcemaps/tools/vite.test.d.ts +1 -0
  116. package/dist/test/sourcemaps/tools/vite.test.js +132 -0
  117. package/dist/test/sourcemaps/tools/vite.test.js.map +1 -0
  118. package/lib/Constants.ts +10 -0
  119. package/lib/Steps/ChooseIntegration.ts +14 -3
  120. package/lib/Steps/Integrations/Android.ts +23 -0
  121. package/lib/Steps/Integrations/Cordova.ts +5 -1
  122. package/lib/Steps/Integrations/ReactNative.ts +9 -3
  123. package/lib/Steps/Integrations/Remix.ts +32 -0
  124. package/lib/Steps/PromptForParameters.ts +48 -3
  125. package/lib/Steps/SentryProjectSelector.ts +3 -1
  126. package/package.json +4 -3
  127. package/src/android/android-wizard.ts +196 -0
  128. package/src/android/code-tools.ts +156 -0
  129. package/src/android/gradle.ts +245 -0
  130. package/src/android/manifest.ts +180 -0
  131. package/src/android/templates.ts +88 -0
  132. package/src/apple/apple-wizard.ts +113 -35
  133. package/src/apple/cocoapod.ts +6 -3
  134. package/src/apple/code-tools.ts +46 -18
  135. package/src/apple/fastlane.ts +6 -12
  136. package/src/apple/templates.ts +2 -8
  137. package/src/apple/xcode-manager.ts +167 -25
  138. package/src/nextjs/nextjs-wizard.ts +72 -8
  139. package/src/nextjs/templates.ts +16 -2
  140. package/src/remix/codemods/handle-error.ts +67 -0
  141. package/src/remix/codemods/root-v1.ts +91 -0
  142. package/src/remix/codemods/root-v2.ts +84 -0
  143. package/src/remix/remix-wizard.ts +132 -0
  144. package/src/remix/sdk-setup.ts +300 -0
  145. package/src/remix/templates.ts +15 -0
  146. package/src/remix/utils.ts +41 -0
  147. package/src/sourcemaps/sourcemaps-wizard.ts +28 -5
  148. package/src/sourcemaps/tools/nextjs.ts +2 -2
  149. package/src/sourcemaps/tools/remix.ts +90 -0
  150. package/src/sourcemaps/tools/sentry-cli.ts +8 -7
  151. package/src/sourcemaps/tools/vite.ts +136 -6
  152. package/src/sourcemaps/utils/detect-tool.ts +4 -1
  153. package/src/sveltekit/sdk-setup.ts +4 -4
  154. package/src/sveltekit/sveltekit-wizard.ts +5 -14
  155. package/src/telemetry.ts +2 -0
  156. package/src/utils/ast-utils.ts +7 -5
  157. package/src/utils/clack-utils.ts +366 -258
  158. package/src/utils/package-manager.ts +61 -0
  159. package/src/utils/release-registry.ts +19 -0
  160. package/src/utils/sentrycli-utils.ts +22 -0
  161. package/test/sourcemaps/tools/vite.test.ts +149 -0
  162. package/dist/lib/Helper/PackageManager.d.ts +0 -22
  163. package/dist/lib/Helper/PackageManager.js.map +0 -1
  164. package/dist/src/utils/vendor/clack-custom-select.d.ts +0 -21
  165. package/dist/src/utils/vendor/clack-custom-select.js +0 -137
  166. package/dist/src/utils/vendor/clack-custom-select.js.map +0 -1
  167. package/lib/Helper/PackageManager.ts +0 -59
  168. package/src/utils/vendor/clack-custom-select.ts +0 -160
@@ -2,17 +2,30 @@
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-member-access */
3
3
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
4
  /* eslint-disable @typescript-eslint/no-unsafe-call */
5
- /* eslint-disable @typescript-eslint/no-unused-vars */
5
+ // @ts-ignore - clack is ESM and TS complains about that. It works though
6
+ import clack from '@clack/prompts';
6
7
  import * as fs from 'fs';
7
8
  import { SentryProjectData } from '../utils/types';
8
9
  import * as templates from './templates';
10
+ import * as path from 'path';
9
11
  const xcode = require('xcode');
10
12
 
11
- /* eslint-enable @typescript-eslint/no-unused-vars */
13
+ interface ProjetFile {
14
+ key: string;
15
+ path: string;
16
+ }
12
17
 
13
- function setDebugInformationFormat(proj: any): void {
18
+ function setDebugInformationFormat(proj: any, targetName: string): void {
14
19
  const xcObjects = proj.hash.project.objects;
15
- const target = proj.getFirstTarget().firstTarget;
20
+ const targetKey: string = Object.keys(xcObjects.PBXNativeTarget || {}).filter(
21
+ (key) => {
22
+ return (
23
+ !key.endsWith('_comment') &&
24
+ xcObjects.PBXNativeTarget[key].name === targetName
25
+ );
26
+ },
27
+ )[0];
28
+ const target = xcObjects.PBXNativeTarget[targetKey];
16
29
 
17
30
  xcObjects.XCConfigurationList[
18
31
  target.buildConfigurationList
@@ -23,7 +36,7 @@ function setDebugInformationFormat(proj: any): void {
23
36
  });
24
37
  }
25
38
 
26
- function addSentrySPM(proj: any): void {
39
+ function addSentrySPM(proj: any, targetName: string): void {
27
40
  const xcObjects = proj.hash.project.objects;
28
41
 
29
42
  const sentryFrameworkUUID = proj.generateUuid() as string;
@@ -61,7 +74,16 @@ function addSentrySPM(proj: any): void {
61
74
  }
62
75
  }
63
76
 
64
- const target = proj.getFirstTarget().firstTarget;
77
+ const targetKey: string = Object.keys(xcObjects.PBXNativeTarget || {}).filter(
78
+ (key) => {
79
+ return (
80
+ !key.endsWith('_comment') &&
81
+ xcObjects.PBXNativeTarget[key].name === targetName
82
+ );
83
+ },
84
+ )[0];
85
+ const target = xcObjects.PBXNativeTarget[targetKey];
86
+
65
87
  if (!target.packageProductDependencies) {
66
88
  target.packageProductDependencies = [];
67
89
  }
@@ -106,15 +128,25 @@ function addSentrySPM(proj: any): void {
106
128
  };
107
129
  xcObjects.XCSwiftPackageProductDependency[sentrySPMUUID + '_comment'] =
108
130
  'Sentry';
131
+
132
+ clack.log.step('Added Sentry SPM dependency to your project');
109
133
  }
110
134
 
111
135
  function addUploadSymbolsScript(
112
136
  xcodeProject: any,
113
137
  sentryProject: SentryProjectData,
114
- apiKeys: { token: string },
138
+ targetName: string,
115
139
  uploadSource = true,
116
140
  ): void {
117
141
  const xcObjects = xcodeProject.hash.project.objects;
142
+ const targetKey: string = Object.keys(xcObjects.PBXNativeTarget || {}).filter(
143
+ (key) => {
144
+ return (
145
+ !key.endsWith('_comment') &&
146
+ xcObjects.PBXNativeTarget[key].name === targetName
147
+ );
148
+ },
149
+ )[0];
118
150
 
119
151
  for (const scriptKey in xcObjects.PBXShellScriptBuildPhase || {}) {
120
152
  if (!scriptKey.endsWith('_comment')) {
@@ -132,7 +164,7 @@ function addUploadSymbolsScript(
132
164
  [],
133
165
  'PBXShellScriptBuildPhase',
134
166
  'Upload Debug Symbols to Sentry',
135
- null,
167
+ targetKey,
136
168
  {
137
169
  inputFileListPaths: [],
138
170
  outputFileListPaths: [],
@@ -141,29 +173,139 @@ function addUploadSymbolsScript(
141
173
  shellScript: templates.getRunScriptTemplate(
142
174
  sentryProject.organization.slug,
143
175
  sentryProject.slug,
144
- apiKeys.token,
145
176
  uploadSource,
146
177
  ),
147
178
  },
148
179
  );
180
+ clack.log.step(`Added Sentry upload script to "${targetName}" build phase`);
149
181
  }
150
182
 
151
- export function updateXcodeProject(
152
- projectPath: string,
153
- sentryProject: SentryProjectData,
154
- apiKeys: { token: string },
155
- addSPMReference: boolean,
156
- uploadSource = true,
157
- ): void {
158
- const proj = xcode.project(projectPath);
159
- proj.parseSync();
160
- addUploadSymbolsScript(proj, sentryProject, apiKeys, uploadSource);
161
- if (uploadSource) {
162
- setDebugInformationFormat(proj);
183
+ export class XcodeProject {
184
+ projectPath: string;
185
+ project: any;
186
+ objects: any;
187
+ files: ProjetFile[] | undefined;
188
+
189
+ public constructor(projectPath: string) {
190
+ this.projectPath = projectPath;
191
+ this.project = xcode.project(projectPath);
192
+ this.project.parseSync();
193
+ this.objects = this.project.hash.project.objects;
194
+ }
195
+
196
+ public getAllTargets(): string[] {
197
+ return Object.keys(this.objects.PBXNativeTarget || {})
198
+ .filter((key) => {
199
+ return (
200
+ !key.endsWith('_comment') &&
201
+ this.objects.PBXNativeTarget[key].productType.startsWith(
202
+ '"com.apple.product-type.application',
203
+ )
204
+ );
205
+ })
206
+ .map((key) => {
207
+ return this.objects.PBXNativeTarget[key].name as string;
208
+ });
209
+ }
210
+
211
+ public updateXcodeProject(
212
+ sentryProject: SentryProjectData,
213
+ target: string,
214
+ apiKeys: { token: string },
215
+ addSPMReference: boolean,
216
+ uploadSource = true,
217
+ ): void {
218
+ addUploadSymbolsScript(this.project, sentryProject, target, uploadSource);
219
+ if (uploadSource) {
220
+ setDebugInformationFormat(this.project, target);
221
+ }
222
+ if (addSPMReference) {
223
+ addSentrySPM(this.project, target);
224
+ }
225
+ const newContent = this.project.writeSync();
226
+ fs.writeFileSync(this.projectPath, newContent);
227
+ }
228
+
229
+ public filesForTarget(target: string): string[] | undefined {
230
+ const files = this.projectFiles();
231
+ const fileDictionary: any = {};
232
+ files.forEach((file) => {
233
+ fileDictionary[file.key] = file.path;
234
+ });
235
+
236
+ const nativeTarget = Object.keys(this.objects.PBXNativeTarget || {}).filter(
237
+ (key) => {
238
+ return (
239
+ !key.endsWith('_comment') &&
240
+ this.objects.PBXNativeTarget[key].name === target
241
+ );
242
+ },
243
+ )[0];
244
+
245
+ if (nativeTarget === undefined) {
246
+ return undefined;
247
+ }
248
+
249
+ const buildPhaseKey = this.objects.PBXNativeTarget[
250
+ nativeTarget
251
+ ].buildPhases.filter((phase: any) => {
252
+ return this.objects.PBXSourcesBuildPhase[phase.value] !== undefined;
253
+ })[0];
254
+
255
+ if (buildPhaseKey === undefined) {
256
+ return undefined;
257
+ }
258
+
259
+ const buildPhases = this.objects.PBXSourcesBuildPhase[buildPhaseKey.value];
260
+ if (buildPhases === undefined) {
261
+ return undefined;
262
+ }
263
+
264
+ const baseDir = path.dirname(path.dirname(this.projectPath));
265
+
266
+ return buildPhases.files
267
+ .map((file: any) => {
268
+ const buildFile = fileDictionary[
269
+ this.objects.PBXBuildFile[file.value].fileRef
270
+ ] as string;
271
+ if (!buildFile) {
272
+ return '';
273
+ }
274
+ return path.join(baseDir, buildFile);
275
+ })
276
+ .filter((f: string) => f.length > 0) as string[];
163
277
  }
164
- if (addSPMReference) {
165
- addSentrySPM(proj);
278
+
279
+ projectFiles(): ProjetFile[] {
280
+ if (this.files === undefined) {
281
+ const proj = this.project.getFirstProject();
282
+ const mainGroupKey = proj.firstProject.mainGroup;
283
+ const mainGroup = this.objects.PBXGroup[mainGroupKey];
284
+ this.files = this.buildGroup(mainGroup);
285
+ }
286
+ return this.files;
287
+ }
288
+
289
+ buildGroup(group: any, path = ''): ProjetFile[] {
290
+ const result: ProjetFile[] = [];
291
+ for (const child of group.children) {
292
+ if (this.objects.PBXFileReference[child.value]) {
293
+ const fileReference = this.objects.PBXFileReference[child.value];
294
+ result.push({
295
+ key: child.value,
296
+ path: `${path}${fileReference.path.replace(/"/g, '')}`,
297
+ });
298
+ } else if (this.objects.PBXGroup[child.value]) {
299
+ const groupReference = this.objects.PBXGroup[child.value];
300
+ const groupChildren = this.buildGroup(
301
+ groupReference,
302
+ groupReference.path
303
+ ? `${path}${groupReference.path.replace(/"/g, '')}/`
304
+ : path,
305
+ );
306
+ result.push(...groupChildren);
307
+ }
308
+ }
309
+ return result;
166
310
  }
167
- const newContent = proj.writeSync();
168
- fs.writeFileSync(projectPath, newContent);
169
311
  }
@@ -10,7 +10,7 @@ import * as path from 'path';
10
10
  import {
11
11
  abort,
12
12
  abortIfCancelled,
13
- addSentryCliRc,
13
+ addSentryCliConfig,
14
14
  confirmContinueEvenThoughNoGitRepo,
15
15
  ensurePackageIsInstalled,
16
16
  getOrAskForProjectData,
@@ -28,6 +28,7 @@ import {
28
28
  getNextjsWebpackPluginOptionsTemplate,
29
29
  getSentryConfigContents,
30
30
  getSentryExampleApiRoute,
31
+ getSentryExampleAppDirApiRoute,
31
32
  getSentryExamplePageContents,
32
33
  } from './templates';
33
34
 
@@ -44,7 +45,7 @@ export async function runNextjsWizard(options: WizardOptions): Promise<void> {
44
45
  await ensurePackageIsInstalled(packageJson, 'next', 'Next.js');
45
46
 
46
47
  const { selectedProject, authToken, selfHosted, sentryUrl } =
47
- await getOrAskForProjectData(options);
48
+ await getOrAskForProjectData(options, 'javascript-nextjs');
48
49
 
49
50
  await installPackage({
50
51
  packageName: '@sentry/nextjs',
@@ -279,8 +280,11 @@ export async function runNextjsWizard(options: WizardOptions): Promise<void> {
279
280
  }
280
281
  }
281
282
 
283
+ const srcDir = path.join(process.cwd(), 'src');
282
284
  const maybePagesDirPath = path.join(process.cwd(), 'pages');
283
- const maybeSrcPagesDirPath = path.join(process.cwd(), 'src', 'pages');
285
+ const maybeSrcPagesDirPath = path.join(srcDir, 'pages');
286
+ const maybeAppDirPath = path.join(process.cwd(), 'app');
287
+ const maybeSrcAppDirPath = path.join(srcDir, 'app');
284
288
 
285
289
  let pagesLocation =
286
290
  fs.existsSync(maybePagesDirPath) &&
@@ -291,23 +295,83 @@ export async function runNextjsWizard(options: WizardOptions): Promise<void> {
291
295
  ? ['src', 'pages']
292
296
  : undefined;
293
297
 
294
- if (!pagesLocation) {
295
- pagesLocation = ['pages'];
298
+ const appLocation =
299
+ fs.existsSync(maybeAppDirPath) &&
300
+ fs.lstatSync(maybeAppDirPath).isDirectory()
301
+ ? ['app']
302
+ : fs.existsSync(maybeSrcAppDirPath) &&
303
+ fs.lstatSync(maybeSrcAppDirPath).isDirectory()
304
+ ? ['src', 'app']
305
+ : undefined;
306
+
307
+ if (!pagesLocation && !appLocation) {
308
+ pagesLocation =
309
+ fs.existsSync(srcDir) && fs.lstatSync(srcDir).isDirectory()
310
+ ? ['src', 'pages']
311
+ : ['pages'];
296
312
  fs.mkdirSync(path.join(process.cwd(), ...pagesLocation), {
297
313
  recursive: true,
298
314
  });
299
315
  }
300
316
 
301
- if (pagesLocation) {
317
+ if (appLocation) {
318
+ const examplePageContents = getSentryExamplePageContents({
319
+ selfHosted,
320
+ orgSlug: selectedProject.organization.slug,
321
+ projectId: selectedProject.id,
322
+ url: sentryUrl,
323
+ useClient: true,
324
+ });
325
+
326
+ await fs.promises.writeFile(
327
+ path.join(
328
+ process.cwd(),
329
+ ...appLocation,
330
+ 'sentry-example-page',
331
+ 'page.jsx',
332
+ ),
333
+ examplePageContents,
334
+ { encoding: 'utf8', flag: 'w' },
335
+ );
336
+
337
+ clack.log.success(
338
+ `Created ${chalk.bold(
339
+ path.join(...appLocation, 'sentry-example-page', 'page.jsx'),
340
+ )}.`,
341
+ );
342
+
343
+ fs.mkdirSync(path.join(process.cwd(), ...appLocation, 'api'), {
344
+ recursive: true,
345
+ });
346
+
347
+ await fs.promises.writeFile(
348
+ path.join(
349
+ process.cwd(),
350
+ ...appLocation,
351
+ 'api',
352
+ 'sentry-example-api',
353
+ 'route.js',
354
+ ),
355
+ getSentryExampleAppDirApiRoute(),
356
+ { encoding: 'utf8', flag: 'w' },
357
+ );
358
+
359
+ clack.log.success(
360
+ `Created ${chalk.bold(
361
+ path.join(...appLocation, 'api', 'sentry-example-api', 'route.js'),
362
+ )}.`,
363
+ );
364
+ } else if (pagesLocation) {
302
365
  const examplePageContents = getSentryExamplePageContents({
303
366
  selfHosted,
304
367
  orgSlug: selectedProject.organization.slug,
305
368
  projectId: selectedProject.id,
306
369
  url: sentryUrl,
370
+ useClient: false,
307
371
  });
308
372
 
309
373
  await fs.promises.writeFile(
310
- path.join(process.cwd(), ...pagesLocation, 'sentry-example-page.js'),
374
+ path.join(process.cwd(), ...pagesLocation, 'sentry-example-page.jsx'),
311
375
  examplePageContents,
312
376
  { encoding: 'utf8', flag: 'w' },
313
377
  );
@@ -340,7 +404,7 @@ export async function runNextjsWizard(options: WizardOptions): Promise<void> {
340
404
  );
341
405
  }
342
406
 
343
- await addSentryCliRc(authToken);
407
+ await addSentryCliConfig(authToken);
344
408
 
345
409
  const mightBeUsingVercel = fs.existsSync(
346
410
  path.join(process.cwd(), 'vercel.json'),
@@ -150,15 +150,18 @@ export function getSentryExamplePageContents(options: {
150
150
  url: string;
151
151
  orgSlug: string;
152
152
  projectId: string;
153
+ useClient: boolean;
153
154
  }): string {
154
155
  const issuesPageLink = options.selfHosted
155
156
  ? `${options.url}organizations/${options.orgSlug}/issues/?project=${options.projectId}`
156
157
  : `https://${options.orgSlug}.sentry.io/issues/?project=${options.projectId}`;
157
158
 
158
- return `import Head from "next/head";
159
+ return `${
160
+ options.useClient ? '"use client";\n\n' : ''
161
+ }import Head from "next/head";
159
162
  import * as Sentry from "@sentry/nextjs";
160
163
 
161
- export default function Home() {
164
+ export default function Page() {
162
165
  return (
163
166
  <div>
164
167
  <Head>
@@ -250,3 +253,14 @@ export default function handler(_req, res) {
250
253
  }
251
254
  `;
252
255
  }
256
+
257
+ export function getSentryExampleAppDirApiRoute() {
258
+ return `import { NextResponse } from "next/server";
259
+
260
+ // A faulty API route to test Sentry's error monitoring
261
+ export function GET() {
262
+ throw new Error("Sentry Example API Route Error");
263
+ return NextResponse.json({ data: "Testing Sentry Error..." });
264
+ }
265
+ `;
266
+ }
@@ -0,0 +1,67 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+
3
+ // @ts-expect-error - magicast is ESM and TS complains about that. It works though
4
+ import type { ProxifiedModule } from 'magicast';
5
+ import type { Program } from '@babel/types';
6
+
7
+ import * as recast from 'recast';
8
+
9
+ import { HANDLE_ERROR_TEMPLATE_V2 } from '../templates';
10
+ import { getInitCallInsertionIndex, hasSentryContent } from '../utils';
11
+
12
+ // @ts-expect-error - clack is ESM and TS complains about that. It works though
13
+ import clack from '@clack/prompts';
14
+ import chalk from 'chalk';
15
+
16
+ // @ts-expect-error - magicast is ESM and TS complains about that. It works though
17
+ import { generateCode } from 'magicast';
18
+
19
+ export function instrumentHandleError(
20
+ originalEntryServerMod: ProxifiedModule<any>,
21
+ serverEntryFilename: string,
22
+ ): boolean {
23
+ const originalEntryServerModAST = originalEntryServerMod.$ast as Program;
24
+
25
+ const handleErrorFunction = originalEntryServerModAST.body.find(
26
+ (node) =>
27
+ node.type === 'ExportNamedDeclaration' &&
28
+ node.declaration?.type === 'FunctionDeclaration' &&
29
+ node.declaration.id?.name === 'handleError',
30
+ );
31
+
32
+ if (!handleErrorFunction) {
33
+ clack.log.warn(
34
+ `Could not find function ${chalk.cyan('handleError')} in ${chalk.cyan(
35
+ serverEntryFilename,
36
+ )}. Creating one for you.`,
37
+ );
38
+
39
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
40
+ const implementation = recast.parse(HANDLE_ERROR_TEMPLATE_V2).program
41
+ .body[0];
42
+
43
+ originalEntryServerModAST.body.splice(
44
+ getInitCallInsertionIndex(originalEntryServerModAST),
45
+ 0,
46
+ // @ts-expect-error - string works here because the AST is proxified by magicast
47
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
48
+ recast.types.builders.exportNamedDeclaration(implementation),
49
+ );
50
+ } else if (
51
+ hasSentryContent(
52
+ generateCode(handleErrorFunction).code,
53
+ originalEntryServerMod.$code,
54
+ )
55
+ ) {
56
+ return false;
57
+ } else {
58
+ // @ts-expect-error - string works here because the AST is proxified by magicast
59
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
60
+ handleErrorFunction.declaration.body.body.unshift(
61
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
62
+ recast.parse(HANDLE_ERROR_TEMPLATE_V2).program.body[0].body.body[0],
63
+ );
64
+ }
65
+
66
+ return true;
67
+ }
@@ -0,0 +1,91 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+
3
+ import * as recast from 'recast';
4
+ import * as path from 'path';
5
+
6
+ // @ts-expect-error - clack is ESM and TS complains about that. It works though
7
+ import clack from '@clack/prompts';
8
+ import chalk from 'chalk';
9
+
10
+ // @ts-expect-error - magicast is ESM and TS complains about that. It works though
11
+ import { builders, generateCode, loadFile, writeFile } from 'magicast';
12
+
13
+ export async function instrumentRootRouteV1(
14
+ rootFileName: string,
15
+ ): Promise<void> {
16
+ try {
17
+ const rootRouteAst = await loadFile(
18
+ path.join(process.cwd(), 'app', rootFileName),
19
+ );
20
+
21
+ rootRouteAst.imports.$add({
22
+ from: '@sentry/remix',
23
+ imported: 'withSentry',
24
+ local: 'withSentry',
25
+ });
26
+
27
+ recast.visit(rootRouteAst.$ast, {
28
+ visitExportDefaultDeclaration(path) {
29
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
30
+ if (path.value.declaration.type === 'FunctionDeclaration') {
31
+ // Move the function declaration just before the default export
32
+ path.insertBefore(path.value.declaration);
33
+
34
+ // Get the name of the function to be wrapped
35
+ const functionName: string = path.value.declaration.id.name as string;
36
+
37
+ // Create the wrapped function call
38
+ const functionCall = recast.types.builders.callExpression(
39
+ recast.types.builders.identifier('withSentry'),
40
+ [recast.types.builders.identifier(functionName)],
41
+ );
42
+
43
+ // Replace the default export with the wrapped function call
44
+ path.value.declaration = functionCall;
45
+ } else if (path.value.declaration.type === 'Identifier') {
46
+ const rootRouteExport = rootRouteAst.exports.default;
47
+
48
+ const expressionToWrap = generateCode(
49
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
50
+ rootRouteExport.$ast,
51
+ ).code;
52
+
53
+ rootRouteAst.exports.default = builders.raw(
54
+ `withSentry(${expressionToWrap})`,
55
+ );
56
+ } else {
57
+ clack.log.warn(
58
+ chalk.yellow(
59
+ `Couldn't instrument ${chalk.bold(
60
+ rootFileName,
61
+ )} automatically. Wrap your default export with: ${chalk.dim(
62
+ 'withSentry()',
63
+ )}\n`,
64
+ ),
65
+ );
66
+ }
67
+
68
+ this.traverse(path);
69
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access */
70
+ },
71
+ });
72
+
73
+ await writeFile(
74
+ rootRouteAst.$ast,
75
+ path.join(process.cwd(), 'app', rootFileName),
76
+ );
77
+ } catch (e: unknown) {
78
+ // eslint-disable-next-line no-console
79
+ console.error(e);
80
+ clack.log.warn(
81
+ chalk.yellow(
82
+ `Something went wrong writing to ${chalk.bold(rootFileName)}`,
83
+ ),
84
+ );
85
+ clack.log.info(
86
+ `Please put the following code snippet into ${chalk.bold(
87
+ rootFileName,
88
+ )}: ${chalk.dim('You probably have to clean it up a bit.')}\n`,
89
+ );
90
+ }
91
+ }
@@ -0,0 +1,84 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+
3
+ import * as recast from 'recast';
4
+ import * as path from 'path';
5
+
6
+ import type { ExportNamedDeclaration, Program } from '@babel/types';
7
+
8
+ // @ts-expect-error - magicast is ESM and TS complains about that. It works though
9
+ import { loadFile, writeFile } from 'magicast';
10
+
11
+ import { ERROR_BOUNDARY_TEMPLATE_V2 } from '../templates';
12
+
13
+ export async function instrumentRootRouteV2(
14
+ rootFileName: string,
15
+ ): Promise<void> {
16
+ const rootRouteAst = await loadFile(
17
+ path.join(process.cwd(), 'app', rootFileName),
18
+ );
19
+
20
+ const exportsAst = rootRouteAst.exports.$ast as Program;
21
+
22
+ const namedExports = exportsAst.body.filter(
23
+ (node) => node.type === 'ExportNamedDeclaration',
24
+ ) as ExportNamedDeclaration[];
25
+
26
+ let foundErrorBoundary = false;
27
+
28
+ namedExports.forEach((namedExport) => {
29
+ const declaration = namedExport.declaration;
30
+
31
+ if (!declaration) {
32
+ return;
33
+ }
34
+
35
+ if (declaration.type === 'FunctionDeclaration') {
36
+ if (declaration.id?.name === 'ErrorBoundary') {
37
+ foundErrorBoundary = true;
38
+ }
39
+ } else if (declaration.type === 'VariableDeclaration') {
40
+ const declarations = declaration.declarations;
41
+
42
+ declarations.forEach((declaration) => {
43
+ // @ts-expect-error - id should always have a name in this case
44
+ if (declaration.id?.name === 'ErrorBoundary') {
45
+ foundErrorBoundary = true;
46
+ }
47
+ });
48
+ }
49
+ });
50
+
51
+ if (!foundErrorBoundary) {
52
+ rootRouteAst.imports.$add({
53
+ from: '@sentry/remix',
54
+ imported: 'captureRemixErrorBoundaryError',
55
+ local: 'captureRemixErrorBoundaryError',
56
+ });
57
+
58
+ rootRouteAst.imports.$add({
59
+ from: '@remix-run/react',
60
+ imported: 'useRouteError',
61
+ local: 'useRouteError',
62
+ });
63
+
64
+ recast.visit(rootRouteAst.$ast, {
65
+ visitExportDefaultDeclaration(path) {
66
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
67
+ const implementation = recast.parse(ERROR_BOUNDARY_TEMPLATE_V2).program
68
+ .body[0];
69
+
70
+ path.insertBefore(
71
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
72
+ recast.types.builders.exportDeclaration(false, implementation),
73
+ );
74
+
75
+ this.traverse(path);
76
+ },
77
+ });
78
+ }
79
+
80
+ await writeFile(
81
+ rootRouteAst.$ast,
82
+ path.join(process.cwd(), 'app', rootFileName),
83
+ );
84
+ }