@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.
- package/CHANGELOG.md +19 -4
- package/dist/lib/Steps/ChooseIntegration.js +1 -1
- package/dist/lib/Steps/ChooseIntegration.js.map +1 -1
- package/dist/lib/Steps/Integrations/ReactNative.d.ts +7 -32
- package/dist/lib/Steps/Integrations/ReactNative.js +17 -485
- package/dist/lib/Steps/Integrations/ReactNative.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/src/android/android-wizard.js +13 -18
- package/dist/src/android/android-wizard.js.map +1 -1
- package/dist/src/apple/apple-wizard.js +11 -4
- package/dist/src/apple/apple-wizard.js.map +1 -1
- package/dist/src/apple/cocoapod.d.ts +1 -0
- package/dist/src/apple/cocoapod.js +36 -13
- package/dist/src/apple/cocoapod.js.map +1 -1
- package/dist/src/nextjs/nextjs-wizard.js +1 -1
- package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
- package/dist/src/react-native/glob.d.ts +3 -0
- package/dist/src/react-native/glob.js +18 -0
- package/dist/src/react-native/glob.js.map +1 -0
- package/dist/src/react-native/gradle.d.ts +4 -0
- package/dist/src/react-native/gradle.js +49 -0
- package/dist/src/react-native/gradle.js.map +1 -0
- package/dist/src/react-native/javascript.d.ts +8 -0
- package/dist/src/react-native/javascript.js +25 -0
- package/dist/src/react-native/javascript.js.map +1 -0
- package/dist/src/react-native/options.d.ts +4 -0
- package/dist/src/react-native/options.js +3 -0
- package/dist/src/react-native/options.js.map +1 -0
- package/dist/src/react-native/react-native-wizard.d.ts +9 -0
- package/dist/src/react-native/react-native-wizard.js +356 -0
- package/dist/src/react-native/react-native-wizard.js.map +1 -0
- package/dist/src/react-native/uninstall.d.ts +2 -0
- package/dist/src/react-native/uninstall.js +130 -0
- package/dist/src/react-native/uninstall.js.map +1 -0
- package/dist/src/react-native/xcode.d.ts +18 -0
- package/dist/src/react-native/xcode.js +170 -0
- package/dist/src/react-native/xcode.js.map +1 -0
- package/dist/src/remix/codemods/handle-error.js +28 -0
- package/dist/src/remix/codemods/handle-error.js.map +1 -1
- package/dist/src/remix/codemods/root-common.d.ts +2 -0
- package/dist/src/remix/codemods/root-common.js +70 -0
- package/dist/src/remix/codemods/root-common.js.map +1 -0
- package/dist/src/remix/codemods/root-v1.js +5 -36
- package/dist/src/remix/codemods/root-v1.js.map +1 -1
- package/dist/src/remix/codemods/root-v2.js +53 -4
- package/dist/src/remix/codemods/root-v2.js.map +1 -1
- package/dist/src/remix/remix-wizard.js +8 -5
- package/dist/src/remix/remix-wizard.js.map +1 -1
- package/dist/src/remix/sdk-setup.d.ts +1 -0
- package/dist/src/remix/sdk-setup.js +10 -6
- package/dist/src/remix/sdk-setup.js.map +1 -1
- package/dist/src/remix/templates.d.ts +1 -1
- package/dist/src/remix/templates.js +1 -1
- package/dist/src/remix/templates.js.map +1 -1
- package/dist/src/remix/utils.d.ts +2 -0
- package/dist/src/remix/utils.js +6 -1
- package/dist/src/remix/utils.js.map +1 -1
- package/dist/src/sourcemaps/tools/nextjs.js +3 -3
- package/dist/src/sourcemaps/tools/nextjs.js.map +1 -1
- package/dist/src/sourcemaps/tools/sentry-cli.js +1 -1
- package/dist/src/sourcemaps/tools/sentry-cli.js.map +1 -1
- package/dist/src/sveltekit/sveltekit-wizard.js +1 -1
- package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
- package/dist/src/utils/clack-utils.d.ts +19 -3
- package/dist/src/utils/clack-utils.js +141 -39
- package/dist/src/utils/clack-utils.js.map +1 -1
- package/dist/src/utils/semver.d.ts +5 -0
- package/dist/src/utils/semver.js +27 -0
- package/dist/src/utils/semver.js.map +1 -0
- package/dist/src/utils/sentrycli-utils.js +4 -1
- package/dist/src/utils/sentrycli-utils.js.map +1 -1
- package/dist/src/utils/types.d.ts +3 -0
- package/dist/src/utils/types.js.map +1 -1
- package/dist/test/react-native/gradle.test.js +57 -0
- package/dist/test/react-native/gradle.test.js.map +1 -0
- package/dist/test/react-native/javascript.test.js +47 -0
- package/dist/test/react-native/javascript.test.js.map +1 -0
- package/dist/test/react-native/xcode.test.d.ts +1 -0
- package/dist/test/react-native/xcode.test.js +144 -0
- package/dist/test/react-native/xcode.test.js.map +1 -0
- package/lib/Steps/ChooseIntegration.ts +1 -1
- package/lib/Steps/Integrations/ReactNative.ts +17 -573
- package/package.json +1 -1
- package/src/android/android-wizard.ts +3 -18
- package/src/apple/apple-wizard.ts +12 -3
- package/src/apple/cocoapod.ts +20 -9
- package/src/nextjs/nextjs-wizard.ts +1 -1
- package/src/react-native/glob.ts +13 -0
- package/src/react-native/gradle.ts +26 -0
- package/src/react-native/javascript.ts +33 -0
- package/src/react-native/options.ts +5 -0
- package/src/react-native/react-native-wizard.ts +369 -0
- package/src/react-native/uninstall.ts +107 -0
- package/src/react-native/xcode.ts +228 -0
- package/src/remix/codemods/handle-error.ts +30 -0
- package/src/remix/codemods/root-common.ts +63 -0
- package/src/remix/codemods/root-v1.ts +3 -53
- package/src/remix/codemods/root-v2.ts +71 -2
- package/src/remix/remix-wizard.ts +9 -6
- package/src/remix/sdk-setup.ts +14 -6
- package/src/remix/templates.ts +2 -6
- package/src/remix/utils.ts +5 -0
- package/src/sourcemaps/tools/nextjs.ts +6 -6
- package/src/sourcemaps/tools/sentry-cli.ts +1 -1
- package/src/sveltekit/sveltekit-wizard.ts +1 -1
- package/src/utils/clack-utils.ts +229 -74
- package/src/utils/semver.ts +33 -0
- package/src/utils/sentrycli-utils.ts +3 -1
- package/src/utils/types.ts +3 -0
- package/test/react-native/gradle.test.ts +310 -0
- package/test/react-native/javascript.test.ts +131 -0
- package/test/react-native/xcode.test.ts +238 -0
- package/dist/lib/Steps/Integrations/__tests__/ReactNative.js +0 -198
- package/dist/lib/Steps/Integrations/__tests__/ReactNative.js.map +0 -1
- package/dist/lib/__tests__/Setup.js +0 -57
- package/dist/lib/__tests__/Setup.js.map +0 -1
- package/lib/Steps/Integrations/__tests__/ReactNative.ts +0 -136
- package/lib/__tests__/Setup.ts +0 -42
- /package/dist/{lib/Steps/Integrations/__tests__/ReactNative.d.ts → test/react-native/gradle.test.d.ts} +0 -0
- /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 {
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
});
|
package/src/remix/sdk-setup.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
190
|
-
|
|
197
|
+
buildCommand,
|
|
198
|
+
instrumentedBuildCommand,
|
|
191
199
|
);
|
|
192
200
|
}
|
|
193
201
|
|
package/src/remix/templates.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
`;
|
package/src/remix/utils.ts
CHANGED
|
@@ -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
|
+
}
|