@sentry/wizard 3.14.1 → 3.16.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 (120) hide show
  1. package/CHANGELOG.md +19 -4
  2. package/dist/lib/Steps/ChooseIntegration.js +1 -1
  3. package/dist/lib/Steps/ChooseIntegration.js.map +1 -1
  4. package/dist/lib/Steps/Integrations/ReactNative.d.ts +7 -32
  5. package/dist/lib/Steps/Integrations/ReactNative.js +17 -485
  6. package/dist/lib/Steps/Integrations/ReactNative.js.map +1 -1
  7. package/dist/package.json +1 -1
  8. package/dist/src/android/android-wizard.js +13 -18
  9. package/dist/src/android/android-wizard.js.map +1 -1
  10. package/dist/src/apple/apple-wizard.js +11 -4
  11. package/dist/src/apple/apple-wizard.js.map +1 -1
  12. package/dist/src/apple/cocoapod.d.ts +1 -0
  13. package/dist/src/apple/cocoapod.js +36 -13
  14. package/dist/src/apple/cocoapod.js.map +1 -1
  15. package/dist/src/nextjs/nextjs-wizard.js +1 -1
  16. package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
  17. package/dist/src/react-native/glob.d.ts +3 -0
  18. package/dist/src/react-native/glob.js +18 -0
  19. package/dist/src/react-native/glob.js.map +1 -0
  20. package/dist/src/react-native/gradle.d.ts +4 -0
  21. package/dist/src/react-native/gradle.js +49 -0
  22. package/dist/src/react-native/gradle.js.map +1 -0
  23. package/dist/src/react-native/javascript.d.ts +8 -0
  24. package/dist/src/react-native/javascript.js +25 -0
  25. package/dist/src/react-native/javascript.js.map +1 -0
  26. package/dist/src/react-native/options.d.ts +4 -0
  27. package/dist/src/react-native/options.js +3 -0
  28. package/dist/src/react-native/options.js.map +1 -0
  29. package/dist/src/react-native/react-native-wizard.d.ts +9 -0
  30. package/dist/src/react-native/react-native-wizard.js +356 -0
  31. package/dist/src/react-native/react-native-wizard.js.map +1 -0
  32. package/dist/src/react-native/uninstall.d.ts +2 -0
  33. package/dist/src/react-native/uninstall.js +130 -0
  34. package/dist/src/react-native/uninstall.js.map +1 -0
  35. package/dist/src/react-native/xcode.d.ts +18 -0
  36. package/dist/src/react-native/xcode.js +170 -0
  37. package/dist/src/react-native/xcode.js.map +1 -0
  38. package/dist/src/remix/codemods/handle-error.js +28 -0
  39. package/dist/src/remix/codemods/handle-error.js.map +1 -1
  40. package/dist/src/remix/codemods/root-common.d.ts +2 -0
  41. package/dist/src/remix/codemods/root-common.js +70 -0
  42. package/dist/src/remix/codemods/root-common.js.map +1 -0
  43. package/dist/src/remix/codemods/root-v1.js +5 -36
  44. package/dist/src/remix/codemods/root-v1.js.map +1 -1
  45. package/dist/src/remix/codemods/root-v2.js +53 -4
  46. package/dist/src/remix/codemods/root-v2.js.map +1 -1
  47. package/dist/src/remix/remix-wizard.js +8 -5
  48. package/dist/src/remix/remix-wizard.js.map +1 -1
  49. package/dist/src/remix/sdk-setup.d.ts +1 -0
  50. package/dist/src/remix/sdk-setup.js +10 -6
  51. package/dist/src/remix/sdk-setup.js.map +1 -1
  52. package/dist/src/remix/templates.d.ts +1 -1
  53. package/dist/src/remix/templates.js +1 -1
  54. package/dist/src/remix/templates.js.map +1 -1
  55. package/dist/src/remix/utils.d.ts +2 -0
  56. package/dist/src/remix/utils.js +6 -1
  57. package/dist/src/remix/utils.js.map +1 -1
  58. package/dist/src/sourcemaps/tools/nextjs.js +3 -3
  59. package/dist/src/sourcemaps/tools/nextjs.js.map +1 -1
  60. package/dist/src/sourcemaps/tools/sentry-cli.js +1 -1
  61. package/dist/src/sourcemaps/tools/sentry-cli.js.map +1 -1
  62. package/dist/src/sveltekit/sveltekit-wizard.js +1 -1
  63. package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
  64. package/dist/src/utils/clack-utils.d.ts +19 -3
  65. package/dist/src/utils/clack-utils.js +141 -39
  66. package/dist/src/utils/clack-utils.js.map +1 -1
  67. package/dist/src/utils/semver.d.ts +5 -0
  68. package/dist/src/utils/semver.js +27 -0
  69. package/dist/src/utils/semver.js.map +1 -0
  70. package/dist/src/utils/sentrycli-utils.js +4 -1
  71. package/dist/src/utils/sentrycli-utils.js.map +1 -1
  72. package/dist/src/utils/types.d.ts +3 -0
  73. package/dist/src/utils/types.js.map +1 -1
  74. package/dist/test/react-native/gradle.test.js +57 -0
  75. package/dist/test/react-native/gradle.test.js.map +1 -0
  76. package/dist/test/react-native/javascript.test.js +47 -0
  77. package/dist/test/react-native/javascript.test.js.map +1 -0
  78. package/dist/test/react-native/xcode.test.d.ts +1 -0
  79. package/dist/test/react-native/xcode.test.js +144 -0
  80. package/dist/test/react-native/xcode.test.js.map +1 -0
  81. package/lib/Steps/ChooseIntegration.ts +1 -1
  82. package/lib/Steps/Integrations/ReactNative.ts +17 -573
  83. package/package.json +1 -1
  84. package/src/android/android-wizard.ts +3 -18
  85. package/src/apple/apple-wizard.ts +12 -3
  86. package/src/apple/cocoapod.ts +20 -9
  87. package/src/nextjs/nextjs-wizard.ts +1 -1
  88. package/src/react-native/glob.ts +13 -0
  89. package/src/react-native/gradle.ts +26 -0
  90. package/src/react-native/javascript.ts +33 -0
  91. package/src/react-native/options.ts +5 -0
  92. package/src/react-native/react-native-wizard.ts +369 -0
  93. package/src/react-native/uninstall.ts +107 -0
  94. package/src/react-native/xcode.ts +228 -0
  95. package/src/remix/codemods/handle-error.ts +30 -0
  96. package/src/remix/codemods/root-common.ts +63 -0
  97. package/src/remix/codemods/root-v1.ts +3 -53
  98. package/src/remix/codemods/root-v2.ts +71 -2
  99. package/src/remix/remix-wizard.ts +9 -6
  100. package/src/remix/sdk-setup.ts +14 -6
  101. package/src/remix/templates.ts +2 -6
  102. package/src/remix/utils.ts +5 -0
  103. package/src/sourcemaps/tools/nextjs.ts +6 -6
  104. package/src/sourcemaps/tools/sentry-cli.ts +1 -1
  105. package/src/sveltekit/sveltekit-wizard.ts +1 -1
  106. package/src/utils/clack-utils.ts +229 -74
  107. package/src/utils/semver.ts +33 -0
  108. package/src/utils/sentrycli-utils.ts +3 -1
  109. package/src/utils/types.ts +3 -0
  110. package/test/react-native/gradle.test.ts +310 -0
  111. package/test/react-native/javascript.test.ts +131 -0
  112. package/test/react-native/xcode.test.ts +238 -0
  113. package/dist/lib/Steps/Integrations/__tests__/ReactNative.js +0 -198
  114. package/dist/lib/Steps/Integrations/__tests__/ReactNative.js.map +0 -1
  115. package/dist/lib/__tests__/Setup.js +0 -57
  116. package/dist/lib/__tests__/Setup.js.map +0 -1
  117. package/lib/Steps/Integrations/__tests__/ReactNative.ts +0 -136
  118. package/lib/__tests__/Setup.ts +0 -42
  119. /package/dist/{lib/Steps/Integrations/__tests__/ReactNative.d.ts → test/react-native/gradle.test.d.ts} +0 -0
  120. /package/dist/{lib/__tests__/Setup.d.ts → test/react-native/javascript.test.d.ts} +0 -0
@@ -0,0 +1,228 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
4
+ import * as fs from 'fs';
5
+ // @ts-ignore - clack is ESM and TS complains about that. It works though
6
+ import clack from '@clack/prompts';
7
+ import chalk from 'chalk';
8
+
9
+ type BuildPhase = { shellScript: string };
10
+ type BuildPhaseMap = Record<string, BuildPhase>;
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ export function getValidExistingBuildPhases(xcodeProject: any): BuildPhaseMap {
14
+ const map: BuildPhaseMap = {};
15
+ const raw = xcodeProject.hash.project.objects.PBXShellScriptBuildPhase || {};
16
+ for (const key in raw) {
17
+ const val = raw[key];
18
+ val.isa && (map[key] = val);
19
+ }
20
+
21
+ return map;
22
+ }
23
+
24
+ export function patchBundlePhase(bundlePhase: BuildPhase | undefined) {
25
+ if (!bundlePhase) {
26
+ clack.log.warn(
27
+ `Could not find ${chalk.cyan(
28
+ 'Bundle React Native code and images',
29
+ )} build phase.`,
30
+ );
31
+ return;
32
+ }
33
+
34
+ const bundlePhaseIncludesSentry = doesBundlePhaseIncludeSentry(bundlePhase);
35
+ if (bundlePhaseIncludesSentry) {
36
+ clack.log.warn(
37
+ `Build phase ${chalk.cyan(
38
+ 'Bundle React Native code and images',
39
+ )} already includes Sentry.`,
40
+ );
41
+ return;
42
+ }
43
+
44
+ const script: string = JSON.parse(bundlePhase.shellScript);
45
+ bundlePhase.shellScript = JSON.stringify(
46
+ addSentryToBundleShellScript(script),
47
+ );
48
+ clack.log.success(
49
+ `Patched Build phase ${chalk.cyan('Bundle React Native code and images')}.`,
50
+ );
51
+ }
52
+
53
+ export function unPatchBundlePhase(bundlePhase: BuildPhase | undefined) {
54
+ if (!bundlePhase) {
55
+ clack.log.warn(
56
+ `Could not find ${chalk.cyan(
57
+ 'Bundle React Native code and images',
58
+ )} build phase.`,
59
+ );
60
+ return;
61
+ }
62
+
63
+ if (!bundlePhase.shellScript.match(/sentry-cli\s+react-native\s+xcode/i)) {
64
+ clack.log.success(
65
+ `Build phase ${chalk.cyan(
66
+ 'Bundle React Native code and images',
67
+ )} does not include Sentry.`,
68
+ );
69
+ return;
70
+ }
71
+
72
+ bundlePhase.shellScript = JSON.stringify(
73
+ removeSentryFromBundleShellScript(
74
+ <string>JSON.parse(bundlePhase.shellScript),
75
+ ),
76
+ );
77
+ clack.log.success(
78
+ `Build phase ${chalk.cyan(
79
+ 'Bundle React Native code and images',
80
+ )} unpatched successfully.`,
81
+ );
82
+ }
83
+
84
+ export function removeSentryFromBundleShellScript(script: string): string {
85
+ return (
86
+ script
87
+ // remove sentry properties export
88
+ .replace(/^export SENTRY_PROPERTIES=sentry.properties\r?\n/m, '')
89
+ .replace(
90
+ /^\/bin\/sh .*?..\/node_modules\/@sentry\/react-native\/scripts\/collect-modules.sh"?\r?\n/m,
91
+ '',
92
+ )
93
+ // unwrap react-native-xcode.sh command. In case someone replaced it
94
+ // entirely with the sentry-cli command we need to put the original
95
+ // version back in.
96
+ .replace(
97
+ /\.\.\/node_modules\/@sentry\/cli\/bin\/sentry-cli\s+react-native\s+xcode\s+\$REACT_NATIVE_XCODE/i,
98
+ '$REACT_NATIVE_XCODE',
99
+ )
100
+ );
101
+ }
102
+
103
+ export function findBundlePhase(buildPhases: BuildPhaseMap) {
104
+ return Object.values(buildPhases).find((buildPhase) =>
105
+ buildPhase.shellScript.match(/\/scripts\/react-native-xcode\.sh/i),
106
+ );
107
+ }
108
+
109
+ export function doesBundlePhaseIncludeSentry(buildPhase: BuildPhase) {
110
+ return !!buildPhase.shellScript.match(/sentry-cli\s+react-native\s+xcode/i);
111
+ }
112
+
113
+ export function addSentryToBundleShellScript(script: string): string {
114
+ return (
115
+ 'export SENTRY_PROPERTIES=sentry.properties\n' +
116
+ 'export EXTRA_PACKAGER_ARGS="--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map"\n' +
117
+ script.replace(
118
+ '$REACT_NATIVE_XCODE',
119
+ () =>
120
+ // eslint-disable-next-line no-useless-escape
121
+ '\\"../node_modules/@sentry/cli/bin/sentry-cli react-native xcode $REACT_NATIVE_XCODE\\"',
122
+ ) +
123
+ '\n/bin/sh -c "$WITH_ENVIRONMENT ../node_modules/@sentry/react-native/scripts/collect-modules.sh"\n'
124
+ );
125
+ }
126
+
127
+ export function addDebugFilesUploadPhase(
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
+ xcodeProject: any,
130
+ { debugFilesUploadPhaseExists }: { debugFilesUploadPhaseExists: boolean },
131
+ ) {
132
+ if (debugFilesUploadPhaseExists) {
133
+ clack.log.warn(
134
+ `Build phase ${chalk.cyan(
135
+ 'Upload Debug Symbols to Sentry',
136
+ )} already exists.`,
137
+ );
138
+ return;
139
+ }
140
+
141
+ xcodeProject.addBuildPhase(
142
+ [],
143
+ 'PBXShellScriptBuildPhase',
144
+ 'Upload Debug Symbols to Sentry',
145
+ null,
146
+ {
147
+ shellPath: '/bin/sh',
148
+ shellScript: `
149
+ WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh"
150
+ if [ -f "$WITH_ENVIRONMENT" ]; then
151
+ . "$WITH_ENVIRONMENT"
152
+ fi
153
+ export SENTRY_PROPERTIES=sentry.properties
154
+ [ "$SENTRY_INCLUDE_NATIVE_SOURCES" = "true" ] && INCLUDE_SOURCES_FLAG="--include-sources" || INCLUDE_SOURCES_FLAG=""
155
+ ../node_modules/@sentry/cli/bin/sentry-cli debug-files upload "$INCLUDE_SOURCES_FLAG" "$DWARF_DSYM_FOLDER_PATH"
156
+ `,
157
+ },
158
+ );
159
+ clack.log.success(
160
+ `Added Build phase ${chalk.cyan('Upload Debug Symbols to Sentry')}.`,
161
+ );
162
+ }
163
+
164
+ export function unPatchDebugFilesUploadPhase(
165
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
+ xcodeProject: any,
167
+ ) {
168
+ const buildPhasesMap =
169
+ xcodeProject.hash.project.objects.PBXShellScriptBuildPhase || {};
170
+
171
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
172
+ const debugFilesUploadPhaseResult = findDebugFilesUploadPhase(buildPhasesMap);
173
+ if (!debugFilesUploadPhaseResult) {
174
+ clack.log.success(
175
+ `Build phase ${chalk.cyan('Upload Debug Symbols to Sentry')} not found.`,
176
+ );
177
+ return;
178
+ }
179
+
180
+ const [debugFilesUploadPhaseKey] = debugFilesUploadPhaseResult;
181
+ const firstTarget: string = xcodeProject.getFirstTarget().uuid;
182
+ const nativeTargets = xcodeProject.hash.project.objects.PBXNativeTarget;
183
+
184
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
185
+ delete buildPhasesMap[debugFilesUploadPhaseKey];
186
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
187
+ delete buildPhasesMap[`${debugFilesUploadPhaseKey}_comment`];
188
+ const phases = nativeTargets[firstTarget].buildPhases;
189
+ if (phases) {
190
+ for (let i = 0; i < phases.length; i++) {
191
+ if (phases[i].value === debugFilesUploadPhaseKey) {
192
+ phases.splice(i, 1);
193
+ break;
194
+ }
195
+ }
196
+ }
197
+ clack.log.success(
198
+ `Build phase ${chalk.cyan(
199
+ 'Upload Debug Symbols to Sentry',
200
+ )} removed successfully.`,
201
+ );
202
+ }
203
+
204
+ export function findDebugFilesUploadPhase(
205
+ buildPhasesMap: Record<string, BuildPhase>,
206
+ ): [key: string, buildPhase: BuildPhase] | undefined {
207
+ return Object.entries(buildPhasesMap).find(
208
+ ([_, buildPhase]) =>
209
+ typeof buildPhase !== 'string' &&
210
+ !!buildPhase.shellScript.match(
211
+ /sentry-cli\s+(upload-dsym|debug-files upload)\b/,
212
+ ),
213
+ );
214
+ }
215
+
216
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
+ export function writeXcodeProject(xcodeProjectPath: string, xcodeProject: any) {
218
+ const newContent = xcodeProject.writeSync();
219
+ const currentContent = fs.readFileSync(xcodeProjectPath, 'utf-8');
220
+ if (newContent === currentContent) {
221
+ return;
222
+ }
223
+
224
+ fs.writeFileSync(xcodeProjectPath, newContent, 'utf-8');
225
+ clack.log.success(
226
+ chalk.green(`Xcode project ${chalk.cyan(xcodeProjectPath)} changes saved.`),
227
+ );
228
+ }
@@ -55,12 +55,42 @@ export function instrumentHandleError(
55
55
  ) {
56
56
  return false;
57
57
  } else {
58
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
59
+ const implementation = recast.parse(HANDLE_ERROR_TEMPLATE_V2).program
60
+ .body[0];
61
+
58
62
  // @ts-expect-error - string works here because the AST is proxified by magicast
59
63
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
60
64
  handleErrorFunction.declaration.body.body.unshift(
61
65
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
62
66
  recast.parse(HANDLE_ERROR_TEMPLATE_V2).program.body[0].body.body[0],
63
67
  );
68
+
69
+ // First parameter is the error
70
+ //
71
+ // @ts-expect-error - string works here because the AST is proxified by magicast
72
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
73
+ handleErrorFunction.declaration.params[0] = implementation.params[0];
74
+
75
+ // Second parameter is the request inside an object
76
+ // Merging the object properties to make sure it includes request
77
+ //
78
+ // @ts-expect-error - string works here because the AST is proxified by magicast
79
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
80
+ if (handleErrorFunction.declaration.params?.[1]?.properties) {
81
+ // @ts-expect-error - string works here because the AST is proxified by magicast
82
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
83
+ handleErrorFunction.declaration.params[1].properties.push(
84
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
85
+ implementation.params[1].properties[0],
86
+ );
87
+ } else {
88
+ // Create second parameter if it doesn't exist
89
+ //
90
+ // @ts-expect-error - string works here because the AST is proxified by magicast
91
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
92
+ handleErrorFunction.declaration.params[1] = implementation.params[1];
93
+ }
64
94
  }
65
95
 
66
96
  return true;
@@ -0,0 +1,63 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
4
+
5
+ import * as recast from 'recast';
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, ProxifiedModule, generateCode } from 'magicast';
12
+
13
+ export function wrapAppWithSentry(
14
+ rootRouteAst: ProxifiedModule,
15
+ rootFileName: string,
16
+ ) {
17
+ rootRouteAst.imports.$add({
18
+ from: '@sentry/remix',
19
+ imported: 'withSentry',
20
+ local: 'withSentry',
21
+ });
22
+
23
+ recast.visit(rootRouteAst.$ast, {
24
+ visitExportDefaultDeclaration(path) {
25
+ if (path.value.declaration.type === 'FunctionDeclaration') {
26
+ // Move the function declaration just before the default export
27
+ path.insertBefore(path.value.declaration);
28
+
29
+ // Get the name of the function to be wrapped
30
+ const functionName: string = path.value.declaration.id.name as string;
31
+
32
+ // Create the wrapped function call
33
+ const functionCall = recast.types.builders.callExpression(
34
+ recast.types.builders.identifier('withSentry'),
35
+ [recast.types.builders.identifier(functionName)],
36
+ );
37
+
38
+ // Replace the default export with the wrapped function call
39
+ path.value.declaration = functionCall;
40
+ } else if (path.value.declaration.type === 'Identifier') {
41
+ const rootRouteExport = rootRouteAst.exports.default;
42
+
43
+ const expressionToWrap = generateCode(rootRouteExport.$ast).code;
44
+
45
+ rootRouteAst.exports.default = builders.raw(
46
+ `withSentry(${expressionToWrap})`,
47
+ );
48
+ } else {
49
+ clack.log.warn(
50
+ chalk.yellow(
51
+ `Couldn't instrument ${chalk.bold(
52
+ rootFileName,
53
+ )} automatically. Wrap your default export with: ${chalk.dim(
54
+ 'withSentry()',
55
+ )}\n`,
56
+ ),
57
+ );
58
+ }
59
+
60
+ this.traverse(path);
61
+ },
62
+ });
63
+ }
@@ -1,6 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
2
 
3
- import * as recast from 'recast';
4
3
  import * as path from 'path';
5
4
 
6
5
  // @ts-expect-error - clack is ESM and TS complains about that. It works though
@@ -8,7 +7,8 @@ import clack from '@clack/prompts';
8
7
  import chalk from 'chalk';
9
8
 
10
9
  // @ts-expect-error - magicast is ESM and TS complains about that. It works though
11
- import { builders, generateCode, loadFile, writeFile } from 'magicast';
10
+ import { loadFile, writeFile } from 'magicast';
11
+ import { wrapAppWithSentry } from './root-common';
12
12
 
13
13
  export async function instrumentRootRouteV1(
14
14
  rootFileName: string,
@@ -18,57 +18,7 @@ export async function instrumentRootRouteV1(
18
18
  path.join(process.cwd(), 'app', rootFileName),
19
19
  );
20
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
- });
21
+ wrapAppWithSentry(rootRouteAst, rootFileName);
72
22
 
73
23
  await writeFile(
74
24
  rootRouteAst.$ast,
@@ -1,4 +1,7 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
1
2
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
5
 
3
6
  import * as recast from 'recast';
4
7
  import * as path from 'path';
@@ -9,6 +12,8 @@ import type { ExportNamedDeclaration, Program } from '@babel/types';
9
12
  import { loadFile, writeFile } from 'magicast';
10
13
 
11
14
  import { ERROR_BOUNDARY_TEMPLATE_V2 } from '../templates';
15
+ import { hasSentryContent } from '../utils';
16
+ import { wrapAppWithSentry } from './root-common';
12
17
 
13
18
  export async function instrumentRootRouteV2(
14
19
  rootFileName: string,
@@ -63,18 +68,82 @@ export async function instrumentRootRouteV2(
63
68
 
64
69
  recast.visit(rootRouteAst.$ast, {
65
70
  visitExportDefaultDeclaration(path) {
66
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
67
71
  const implementation = recast.parse(ERROR_BOUNDARY_TEMPLATE_V2).program
68
72
  .body[0];
69
73
 
70
74
  path.insertBefore(
71
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
72
75
  recast.types.builders.exportDeclaration(false, implementation),
73
76
  );
74
77
 
75
78
  this.traverse(path);
76
79
  },
77
80
  });
81
+ // If there is already a ErrorBoundary export, and it doesn't have Sentry content
82
+ } else if (!hasSentryContent(rootFileName, rootRouteAst.$code)) {
83
+ rootRouteAst.imports.$add({
84
+ from: '@sentry/remix',
85
+ imported: 'captureRemixErrorBoundaryError',
86
+ local: 'captureRemixErrorBoundaryError',
87
+ });
88
+
89
+ wrapAppWithSentry(rootRouteAst, rootFileName);
90
+
91
+ recast.visit(rootRouteAst.$ast, {
92
+ visitExportNamedDeclaration(path) {
93
+ // Find ErrorBoundary export
94
+ if (path.value.declaration?.id?.name === 'ErrorBoundary') {
95
+ const errorBoundaryExport = path.value.declaration;
96
+
97
+ let errorIdentifier;
98
+
99
+ // check if useRouteError is called
100
+ recast.visit(errorBoundaryExport, {
101
+ visitVariableDeclaration(path) {
102
+ const variableDeclaration = path.value.declarations[0];
103
+ const initializer = variableDeclaration.init;
104
+
105
+ if (
106
+ initializer.type === 'CallExpression' &&
107
+ initializer.callee.name === 'useRouteError'
108
+ ) {
109
+ errorIdentifier = variableDeclaration.id.name;
110
+ }
111
+
112
+ this.traverse(path);
113
+ },
114
+ });
115
+
116
+ // We don't have an errorIdentifier, which means useRouteError is not called / imported
117
+ // We need to add it and capture the error
118
+ if (!errorIdentifier) {
119
+ rootRouteAst.imports.$add({
120
+ from: '@remix-run/react',
121
+ imported: 'useRouteError',
122
+ local: 'useRouteError',
123
+ });
124
+
125
+ const useRouteErrorCall = recast.parse(
126
+ `const error = useRouteError();`,
127
+ ).program.body[0];
128
+
129
+ // Insert at the top of ErrorBoundary body
130
+ errorBoundaryExport.body.body.splice(0, 0, useRouteErrorCall);
131
+ }
132
+
133
+ const captureErrorCall = recast.parse(
134
+ `captureRemixErrorBoundaryError(error);`,
135
+ ).program.body[0];
136
+
137
+ // Insert just before the the fallback page is returned
138
+ errorBoundaryExport.body.body.splice(
139
+ errorBoundaryExport.body.body.length - 1,
140
+ 0,
141
+ captureErrorCall,
142
+ );
143
+ }
144
+ this.traverse(path);
145
+ },
146
+ });
78
147
  }
79
148
 
80
149
  await writeFile(
@@ -11,7 +11,7 @@ import {
11
11
  installPackage,
12
12
  isUsingTypeScript,
13
13
  printWelcome,
14
- sourceMapsCliSetupConfig,
14
+ rcCliSetupConfig,
15
15
  } from '../utils/clack-utils';
16
16
  import { hasPackageInstalled } from '../utils/package-json';
17
17
  import { WizardOptions } from '../utils/types';
@@ -25,6 +25,8 @@ import {
25
25
  } from './sdk-setup';
26
26
  import { debug } from '../utils/debug';
27
27
  import { traceStep, withTelemetry } from '../telemetry';
28
+ import { isHydrogenApp } from './utils';
29
+ import { DEFAULT_URL } from '../../lib/Constants';
28
30
 
29
31
  export async function runRemixWizard(options: WizardOptions): Promise<void> {
30
32
  return withTelemetry(
@@ -66,14 +68,15 @@ async function runRemixWizardWithTelemetry(
66
68
  const isTS = isUsingTypeScript();
67
69
  const isV2 = isRemixV2(remixConfig, packageJson);
68
70
 
69
- await addSentryCliConfig(authToken, sourceMapsCliSetupConfig);
71
+ await addSentryCliConfig({ authToken }, rcCliSetupConfig);
70
72
 
71
73
  await traceStep('Update build script for sourcemap uploads', async () => {
72
74
  try {
73
75
  await updateBuildScript({
74
76
  org: selectedProject.organization.slug,
75
77
  project: selectedProject.name,
76
- url: sentryUrl,
78
+ url: sentryUrl === DEFAULT_URL ? undefined : sentryUrl,
79
+ isHydrogen: isHydrogenApp(packageJson),
77
80
  });
78
81
  } catch (e) {
79
82
  clack.log
@@ -88,7 +91,7 @@ async function runRemixWizardWithTelemetry(
88
91
  await instrumentRootRoute(isV2, isTS);
89
92
  } catch (e) {
90
93
  clack.log.warn(`Could not instrument root route.
91
- Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/`);
94
+ Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/manual-setup/`);
92
95
  debug(e);
93
96
  }
94
97
  });
@@ -98,7 +101,7 @@ async function runRemixWizardWithTelemetry(
98
101
  await initializeSentryOnEntryClient(dsn, isTS);
99
102
  } catch (e) {
100
103
  clack.log.warn(`Could not initialize Sentry on client entry.
101
- Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/`);
104
+ Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/manual-setup/`);
102
105
  debug(e);
103
106
  }
104
107
  });
@@ -108,7 +111,7 @@ async function runRemixWizardWithTelemetry(
108
111
  await initializeSentryOnEntryServer(dsn, isV2, isTS);
109
112
  } catch (e) {
110
113
  clack.log.warn(`Could not initialize Sentry on server entry.
111
- Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/`);
114
+ Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/manual-setup/`);
112
115
  debug(e);
113
116
  }
114
117
  });
@@ -164,6 +164,7 @@ export async function updateBuildScript(args: {
164
164
  org: string;
165
165
  project: string;
166
166
  url?: string;
167
+ isHydrogen: boolean;
167
168
  }): Promise<void> {
168
169
  /* eslint-disable @typescript-eslint/no-unsafe-member-access */
169
170
  // Add sourcemaps option to build script
@@ -177,17 +178,24 @@ export async function updateBuildScript(args: {
177
178
  packageJson.scripts = {};
178
179
  }
179
180
 
181
+ const buildCommand = args.isHydrogen
182
+ ? 'shopify hydrogen build'
183
+ : 'remix build';
184
+
185
+ const instrumentedBuildCommand =
186
+ `${buildCommand} --sourcemap && sentry-upload-sourcemaps --org ${args.org} --project ${args.project}` +
187
+ (args.url ? ` --url ${args.url}` : '') +
188
+ (args.isHydrogen ? ' --buildPath ./dist' : '');
189
+
180
190
  if (!packageJson.scripts.build) {
181
- packageJson.scripts.build =
182
- `remix build --sourcemap && sentry-upload-sourcemaps --org ${args.org} --project ${args.project}` +
183
- (args.url ? ` --url ${args.url}` : '');
191
+ packageJson.scripts.build = instrumentedBuildCommand;
184
192
 
185
193
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
186
- } else if (packageJson.scripts.build.includes('remix build')) {
194
+ } else if (packageJson.scripts.build.includes(buildCommand)) {
187
195
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
188
196
  packageJson.scripts.build = packageJson.scripts.build.replace(
189
- 'remix build',
190
- 'remix build --sourcemap && sentry-upload-sourcemaps',
197
+ buildCommand,
198
+ instrumentedBuildCommand,
191
199
  );
192
200
  }
193
201
 
@@ -5,11 +5,7 @@ export const ERROR_BOUNDARY_TEMPLATE_V2 = `const ErrorBoundary = () => {
5
5
  };
6
6
  `;
7
7
 
8
- export const HANDLE_ERROR_TEMPLATE_V2 = `function handleError(error) {
9
- if (error instanceof Error) {
10
- Sentry.captureRemixErrorBoundaryError(error);
11
- } else {
12
- Sentry.captureException(error);
13
- }
8
+ export const HANDLE_ERROR_TEMPLATE_V2 = `function handleError(error, { request }) {
9
+ Sentry.captureRemixServerException(error, 'remix.server', request);
14
10
  }
15
11
  `;
@@ -5,6 +5,7 @@ import * as path from 'path';
5
5
  // @ts-expect-error - clack is ESM and TS complains about that. It works though
6
6
  import clack from '@clack/prompts';
7
7
  import chalk from 'chalk';
8
+ import { PackageDotJson, hasPackageInstalled } from '../utils/package-json';
8
9
 
9
10
  // Copied from sveltekit wizard
10
11
  export function hasSentryContent(
@@ -39,3 +40,7 @@ export function getInitCallInsertionIndex(
39
40
 
40
41
  return 0;
41
42
  }
43
+
44
+ export function isHydrogenApp(packageJson: PackageDotJson): boolean {
45
+ return hasPackageInstalled('@shopify/hydrogen', packageJson);
46
+ }