@sentry/wizard 3.24.1 → 3.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/package.json +1 -1
- package/dist/src/react-native/expo-env-file.d.ts +2 -0
- package/dist/src/react-native/expo-env-file.js +127 -0
- package/dist/src/react-native/expo-env-file.js.map +1 -0
- package/dist/src/react-native/expo-metro.d.ts +7 -0
- package/dist/src/react-native/expo-metro.js +236 -0
- package/dist/src/react-native/expo-metro.js.map +1 -0
- package/dist/src/react-native/expo.d.ts +16 -0
- package/dist/src/react-native/expo.js +195 -0
- package/dist/src/react-native/expo.js.map +1 -0
- package/dist/src/react-native/git.d.ts +1 -0
- package/dist/src/react-native/git.js +85 -0
- package/dist/src/react-native/git.js.map +1 -0
- package/dist/src/react-native/javascript.d.ts +3 -0
- package/dist/src/react-native/javascript.js +119 -1
- package/dist/src/react-native/javascript.js.map +1 -1
- package/dist/src/react-native/metro.d.ts +3 -0
- package/dist/src/react-native/metro.js +17 -15
- package/dist/src/react-native/metro.js.map +1 -1
- package/dist/src/react-native/react-native-wizard.d.ts +12 -0
- package/dist/src/react-native/react-native-wizard.js +91 -78
- package/dist/src/react-native/react-native-wizard.js.map +1 -1
- package/dist/src/react-native/xcode.js +14 -3
- package/dist/src/react-native/xcode.js.map +1 -1
- package/dist/src/remix/codemods/handle-error.js +35 -33
- package/dist/src/remix/codemods/handle-error.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/utils/clack-utils.d.ts +2 -1
- package/dist/src/utils/clack-utils.js +2 -2
- package/dist/src/utils/clack-utils.js.map +1 -1
- package/dist/test/react-native/expo-metro.test.d.ts +1 -0
- package/dist/test/react-native/expo-metro.test.js +26 -0
- package/dist/test/react-native/expo-metro.test.js.map +1 -0
- package/dist/test/react-native/expo.test.d.ts +1 -0
- package/dist/test/react-native/expo.test.js +57 -0
- package/dist/test/react-native/expo.test.js.map +1 -0
- package/dist/test/react-native/xcode.test.js +5 -0
- package/dist/test/react-native/xcode.test.js.map +1 -1
- package/package.json +1 -1
- package/src/react-native/expo-env-file.ts +55 -0
- package/src/react-native/expo-metro.ts +212 -0
- package/src/react-native/expo.ts +175 -0
- package/src/react-native/git.ts +25 -0
- package/src/react-native/javascript.ts +68 -1
- package/src/react-native/metro.ts +3 -3
- package/src/react-native/react-native-wizard.ts +72 -76
- package/src/react-native/xcode.ts +21 -5
- package/src/remix/codemods/handle-error.ts +57 -37
- package/src/remix/templates.ts +3 -3
- package/src/utils/clack-utils.ts +4 -4
- package/test/react-native/expo-metro.test.ts +81 -0
- package/test/react-native/expo.test.ts +86 -0
- package/test/react-native/xcode.test.ts +90 -0
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
import clack from '@clack/prompts';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
|
-
|
|
7
|
-
import * as process from 'process';
|
|
6
|
+
|
|
8
7
|
import {
|
|
9
8
|
CliSetupConfigContent,
|
|
10
9
|
abortIfCancelled,
|
|
@@ -17,7 +16,6 @@ import {
|
|
|
17
16
|
installPackage,
|
|
18
17
|
printWelcome,
|
|
19
18
|
propertiesCliSetupConfig,
|
|
20
|
-
showCopyPasteInstructions,
|
|
21
19
|
} from '../utils/clack-utils';
|
|
22
20
|
import { getPackageVersion, hasPackageInstalled } from '../utils/package-json';
|
|
23
21
|
import { podInstall } from '../apple/cocoapod';
|
|
@@ -41,11 +39,7 @@ import {
|
|
|
41
39
|
import { runReactNativeUninstall } from './uninstall';
|
|
42
40
|
import { APP_BUILD_GRADLE, XCODE_PROJECT, getFirstMatchedPath } from './glob';
|
|
43
41
|
import { ReactNativeWizardOptions } from './options';
|
|
44
|
-
import {
|
|
45
|
-
addSentryInitWithSdkImport,
|
|
46
|
-
doesJsCodeIncludeSdkSentryImport,
|
|
47
|
-
getSentryInitColoredCodeSnippet,
|
|
48
|
-
} from './javascript';
|
|
42
|
+
import { addSentryInit } from './javascript';
|
|
49
43
|
import { traceStep, withTelemetry } from '../telemetry';
|
|
50
44
|
import * as Sentry from '@sentry/node';
|
|
51
45
|
import { fulfillsVersionRange } from '../utils/semver';
|
|
@@ -54,6 +48,9 @@ import {
|
|
|
54
48
|
patchMetroConfigWithSentrySerializer,
|
|
55
49
|
patchMetroWithSentryConfig,
|
|
56
50
|
} from './metro';
|
|
51
|
+
import { patchExpoAppConfig, printSentryExpoMigrationOutro } from './expo';
|
|
52
|
+
import { addSentryToExpoMetroConfig } from './expo-metro';
|
|
53
|
+
import { addExpoEnvLocal } from './expo-env-file';
|
|
57
54
|
|
|
58
55
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
59
56
|
const xcode = require('xcode');
|
|
@@ -64,14 +61,24 @@ export const RN_PACKAGE = 'react-native';
|
|
|
64
61
|
export const RN_HUMAN_NAME = 'React Native';
|
|
65
62
|
|
|
66
63
|
export const SUPPORTED_RN_RANGE = '>=0.69.0';
|
|
64
|
+
export const SUPPORTED_EXPO_RANGE = '>=50.0.0';
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The following SDK version ship with bundled Xcode scripts
|
|
68
|
+
* which simplifies the Xcode Build Phases setup.
|
|
69
|
+
*/
|
|
70
70
|
export const SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE = '>=5.11.0';
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* The following SDK version ship with Sentry Metro plugin
|
|
74
|
+
*/
|
|
73
75
|
export const SDK_SENTRY_METRO_PLUGIN_SUPPORTED_SDK_RANGE = '>=5.11.0';
|
|
74
76
|
|
|
77
|
+
/**
|
|
78
|
+
* The following SDK version ship with bundled Expo plugin
|
|
79
|
+
*/
|
|
80
|
+
export const SDK_EXPO_SUPPORTED_SDK_RANGE = `>=5.16.0`;
|
|
81
|
+
|
|
75
82
|
// The following SDK version shipped `withSentryConfig`
|
|
76
83
|
export const SDK_SENTRY_METRO_WITH_SENTRY_CONFIG_SUPPORTED_SDK_RANGE =
|
|
77
84
|
'>=5.17.0';
|
|
@@ -110,6 +117,13 @@ export async function runReactNativeWizardWithTelemetry(
|
|
|
110
117
|
await confirmContinueIfNoOrDirtyGitRepo();
|
|
111
118
|
|
|
112
119
|
const packageJson = await getPackageDotJson();
|
|
120
|
+
const hasInstalled = (dep: string) => hasPackageInstalled(dep, packageJson);
|
|
121
|
+
|
|
122
|
+
if (hasInstalled('sentry-expo')) {
|
|
123
|
+
Sentry.setTag('has-sentry-expo-installed', true);
|
|
124
|
+
printSentryExpoMigrationOutro();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
113
127
|
|
|
114
128
|
await ensurePackageIsInstalled(packageJson, RN_PACKAGE, RN_HUMAN_NAME);
|
|
115
129
|
|
|
@@ -120,6 +134,38 @@ export async function runReactNativeWizardWithTelemetry(
|
|
|
120
134
|
packageVersion: rnVersion,
|
|
121
135
|
packageId: RN_PACKAGE,
|
|
122
136
|
acceptableVersions: SUPPORTED_RN_RANGE,
|
|
137
|
+
note: `Please upgrade to ${SUPPORTED_RN_RANGE} if you wish to use the Sentry Wizard.
|
|
138
|
+
Or setup using ${chalk.cyan(
|
|
139
|
+
'https://docs.sentry.io/platforms/react-native/manual-setup/manual-setup/',
|
|
140
|
+
)}`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await installPackage({
|
|
145
|
+
packageName: RN_SDK_PACKAGE,
|
|
146
|
+
alreadyInstalled: hasPackageInstalled(RN_SDK_PACKAGE, packageJson),
|
|
147
|
+
});
|
|
148
|
+
const sdkVersion = getPackageVersion(
|
|
149
|
+
RN_SDK_PACKAGE,
|
|
150
|
+
await getPackageDotJson(),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const expoVersion = getPackageVersion('expo', packageJson);
|
|
154
|
+
const isExpo = !!expoVersion;
|
|
155
|
+
if (expoVersion && sdkVersion) {
|
|
156
|
+
await confirmContinueIfPackageVersionNotSupported({
|
|
157
|
+
packageName: 'Sentry React Native SDK',
|
|
158
|
+
packageVersion: sdkVersion,
|
|
159
|
+
packageId: RN_SDK_PACKAGE,
|
|
160
|
+
acceptableVersions: SDK_EXPO_SUPPORTED_SDK_RANGE,
|
|
161
|
+
note: `Please upgrade to ${SDK_EXPO_SUPPORTED_SDK_RANGE} to continue with the wizard in this Expo project.`,
|
|
162
|
+
});
|
|
163
|
+
await confirmContinueIfPackageVersionNotSupported({
|
|
164
|
+
packageName: 'Expo SDK',
|
|
165
|
+
packageVersion: expoVersion,
|
|
166
|
+
packageId: 'expo',
|
|
167
|
+
acceptableVersions: SUPPORTED_EXPO_RANGE,
|
|
168
|
+
note: `Please upgrade to ${SUPPORTED_EXPO_RANGE} to continue with the wizard in this Expo project.`,
|
|
123
169
|
});
|
|
124
170
|
}
|
|
125
171
|
|
|
@@ -135,22 +181,24 @@ export async function runReactNativeWizardWithTelemetry(
|
|
|
135
181
|
url: sentryUrl,
|
|
136
182
|
};
|
|
137
183
|
|
|
138
|
-
await
|
|
139
|
-
packageName: RN_SDK_PACKAGE,
|
|
140
|
-
alreadyInstalled: hasPackageInstalled(RN_SDK_PACKAGE, packageJson),
|
|
141
|
-
});
|
|
142
|
-
const sdkVersion = getPackageVersion(
|
|
143
|
-
RN_SDK_PACKAGE,
|
|
144
|
-
await getPackageDotJson(),
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
await traceStep('patch-js', () =>
|
|
184
|
+
await traceStep('patch-app-js', () =>
|
|
148
185
|
addSentryInit({ dsn: selectedProject.keys[0].dsn.public }),
|
|
149
186
|
);
|
|
150
187
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
188
|
+
if (isExpo) {
|
|
189
|
+
await traceStep('patch-expo-app-config', () =>
|
|
190
|
+
patchExpoAppConfig(cliConfig),
|
|
191
|
+
);
|
|
192
|
+
await traceStep('add-expo-env-local', () => addExpoEnvLocal(cliConfig));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (isExpo) {
|
|
196
|
+
await traceStep('patch-metro-config', addSentryToExpoMetroConfig);
|
|
197
|
+
} else {
|
|
198
|
+
await traceStep('patch-metro-config', () =>
|
|
199
|
+
addSentryToMetroConfig({ sdkVersion }),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
154
202
|
|
|
155
203
|
if (fs.existsSync('ios')) {
|
|
156
204
|
Sentry.setTag('patch-ios', true);
|
|
@@ -217,58 +265,6 @@ function addSentryToMetroConfig({
|
|
|
217
265
|
}
|
|
218
266
|
}
|
|
219
267
|
|
|
220
|
-
async function addSentryInit({ dsn }: { dsn: string }) {
|
|
221
|
-
const prefixGlob = '{.,./src}';
|
|
222
|
-
const suffixGlob = '@(j|t|cj|mj)s?(x)';
|
|
223
|
-
const universalGlob = `App.${suffixGlob}`;
|
|
224
|
-
const jsFileGlob = `${prefixGlob}/+(${universalGlob})`;
|
|
225
|
-
const jsPath = traceStep('find-app-js-file', () =>
|
|
226
|
-
getFirstMatchedPath(jsFileGlob),
|
|
227
|
-
);
|
|
228
|
-
Sentry.setTag('app-js-file-status', jsPath ? 'found' : 'not-found');
|
|
229
|
-
if (!jsPath) {
|
|
230
|
-
clack.log.warn(
|
|
231
|
-
`Could not find main App file using ${chalk.cyan(jsFileGlob)}.`,
|
|
232
|
-
);
|
|
233
|
-
await showCopyPasteInstructions(
|
|
234
|
-
'App.js',
|
|
235
|
-
getSentryInitColoredCodeSnippet(dsn),
|
|
236
|
-
'This ensures the Sentry SDK is ready to capture errors.',
|
|
237
|
-
);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
const jsRelativePath = path.relative(process.cwd(), jsPath);
|
|
241
|
-
|
|
242
|
-
const js = fs.readFileSync(jsPath, 'utf-8');
|
|
243
|
-
const includesSentry = doesJsCodeIncludeSdkSentryImport(js, {
|
|
244
|
-
sdkPackageName: RN_SDK_PACKAGE,
|
|
245
|
-
});
|
|
246
|
-
if (includesSentry) {
|
|
247
|
-
Sentry.setTag('app-js-file-status', 'already-includes-sentry');
|
|
248
|
-
clack.log.warn(
|
|
249
|
-
`${chalk.cyan(
|
|
250
|
-
jsRelativePath,
|
|
251
|
-
)} already includes Sentry. We wont't add it again.`,
|
|
252
|
-
);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
traceStep('add-sentry-init', () => {
|
|
257
|
-
const newContent = addSentryInitWithSdkImport(js, { dsn });
|
|
258
|
-
|
|
259
|
-
clack.log.success(
|
|
260
|
-
`Added ${chalk.cyan('Sentry.init')} to ${chalk.cyan(jsRelativePath)}.`,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
fs.writeFileSync(jsPath, newContent, 'utf-8');
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
Sentry.setTag('app-js-file-status', 'added-sentry-init');
|
|
267
|
-
clack.log.success(
|
|
268
|
-
chalk.green(`${chalk.cyan(jsRelativePath)} changes saved.`),
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
268
|
async function confirmFirstSentryException(
|
|
273
269
|
url: string,
|
|
274
270
|
orgSlug: string,
|
|
@@ -128,11 +128,27 @@ export function doesBundlePhaseIncludeSentry(buildPhase: BuildPhase) {
|
|
|
128
128
|
export function addSentryWithBundledScriptsToBundleShellScript(
|
|
129
129
|
script: string,
|
|
130
130
|
): string {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
const isLikelyPlainReactNativeScript = script.includes('$REACT_NATIVE_XCODE');
|
|
132
|
+
if (isLikelyPlainReactNativeScript) {
|
|
133
|
+
return script.replace(
|
|
134
|
+
'$REACT_NATIVE_XCODE',
|
|
135
|
+
// eslint-disable-next-line no-useless-escape
|
|
136
|
+
'\\"/bin/sh ../node_modules/@sentry/react-native/scripts/sentry-xcode.sh $REACT_NATIVE_XCODE\\"',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const isLikelyExpoScript = script.includes('expo');
|
|
141
|
+
if (isLikelyExpoScript) {
|
|
142
|
+
const SENTRY_REACT_NATIVE_XCODE_PATH =
|
|
143
|
+
"`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"`";
|
|
144
|
+
return script.replace(
|
|
145
|
+
/^.*?(packager|scripts)\/react-native-xcode\.sh\s*(\\'\\\\")?/m,
|
|
146
|
+
// eslint-disable-next-line no-useless-escape
|
|
147
|
+
(match: string) => `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_PATH} ${match}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return script;
|
|
136
152
|
}
|
|
137
153
|
|
|
138
154
|
export function addSentryWithCliToBundleShellScript(script: string): string {
|
|
@@ -22,14 +22,29 @@ export function instrumentHandleError(
|
|
|
22
22
|
): boolean {
|
|
23
23
|
const originalEntryServerModAST = originalEntryServerMod.$ast as Program;
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
(node) =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const handleErrorFunctionExport = originalEntryServerModAST.body.find(
|
|
26
|
+
(node) => {
|
|
27
|
+
return (
|
|
28
|
+
node.type === 'ExportNamedDeclaration' &&
|
|
29
|
+
node.declaration?.type === 'FunctionDeclaration' &&
|
|
30
|
+
node.declaration.id?.name === 'handleError'
|
|
31
|
+
);
|
|
32
|
+
},
|
|
30
33
|
);
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
const handleErrorFunctionVariableDeclarationExport =
|
|
36
|
+
originalEntryServerModAST.body.find(
|
|
37
|
+
(node) =>
|
|
38
|
+
node.type === 'ExportNamedDeclaration' &&
|
|
39
|
+
node.declaration?.type === 'VariableDeclaration' &&
|
|
40
|
+
// @ts-expect-error - id should always have a name in this case
|
|
41
|
+
node.declaration.declarations[0].id.name === 'handleError',
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
!handleErrorFunctionExport &&
|
|
46
|
+
!handleErrorFunctionVariableDeclarationExport
|
|
47
|
+
) {
|
|
33
48
|
clack.log.warn(
|
|
34
49
|
`Could not find function ${chalk.cyan('handleError')} in ${chalk.cyan(
|
|
35
50
|
serverEntryFilename,
|
|
@@ -48,50 +63,55 @@ export function instrumentHandleError(
|
|
|
48
63
|
recast.types.builders.exportNamedDeclaration(implementation),
|
|
49
64
|
);
|
|
50
65
|
} else if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
(handleErrorFunctionExport &&
|
|
67
|
+
['wrapHandleErrorWithSentry', 'sentryHandleError'].some((util) =>
|
|
68
|
+
hasSentryContent(
|
|
69
|
+
generateCode(handleErrorFunctionExport).code,
|
|
70
|
+
originalEntryServerMod.$code,
|
|
71
|
+
util,
|
|
72
|
+
),
|
|
73
|
+
)) ||
|
|
74
|
+
(handleErrorFunctionVariableDeclarationExport &&
|
|
75
|
+
['wrapHandleErrorWithSentry', 'sentryHandleError'].some((util) =>
|
|
76
|
+
hasSentryContent(
|
|
77
|
+
generateCode(handleErrorFunctionVariableDeclarationExport).code,
|
|
78
|
+
originalEntryServerMod.$code,
|
|
79
|
+
util,
|
|
80
|
+
),
|
|
81
|
+
))
|
|
56
82
|
) {
|
|
57
83
|
return false;
|
|
58
|
-
} else {
|
|
84
|
+
} else if (handleErrorFunctionExport) {
|
|
59
85
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
60
86
|
const implementation = recast.parse(HANDLE_ERROR_TEMPLATE_V2).program
|
|
61
87
|
.body[0];
|
|
62
88
|
|
|
63
|
-
//
|
|
89
|
+
// If the current handleError function has a body, we need to merge the new implementation with the existing one
|
|
64
90
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
65
|
-
|
|
91
|
+
implementation.declarations[0].init.arguments[0].body.body.unshift(
|
|
92
|
+
// @ts-expect-error - declaration works here because the AST is proxified by magicast
|
|
66
93
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
67
|
-
|
|
94
|
+
...handleErrorFunctionExport.declaration.body.body,
|
|
68
95
|
);
|
|
69
96
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
// @ts-expect-error - declaration works here because the AST is proxified by magicast
|
|
98
|
+
handleErrorFunctionExport.declaration = implementation;
|
|
99
|
+
} else if (handleErrorFunctionVariableDeclarationExport) {
|
|
73
100
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
74
|
-
|
|
101
|
+
const implementation = recast.parse(HANDLE_ERROR_TEMPLATE_V2).program
|
|
102
|
+
.body[0];
|
|
75
103
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
81
|
-
if (handleErrorFunction.declaration.params?.[1]?.properties) {
|
|
82
|
-
// @ts-expect-error - string works here because the AST is proxified by magicast
|
|
83
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
84
|
-
handleErrorFunction.declaration.params[1].properties.push(
|
|
85
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
86
|
-
implementation.params[1].properties[0],
|
|
87
|
-
);
|
|
88
|
-
} else {
|
|
89
|
-
// Create second parameter if it doesn't exist
|
|
90
|
-
//
|
|
91
|
-
// @ts-expect-error - string works here because the AST is proxified by magicast
|
|
104
|
+
// If the current handleError function has a body, we need to merge the new implementation with the existing one
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
106
|
+
implementation.declarations[0].init.arguments[0].body.body.unshift(
|
|
107
|
+
// @ts-expect-error - declaration works here because the AST is proxified by magicast
|
|
92
108
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
...handleErrorFunctionVariableDeclarationExport.declaration
|
|
110
|
+
.declarations[0].init.body.body,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// @ts-expect-error - declaration works here because the AST is proxified by magicast
|
|
114
|
+
handleErrorFunctionVariableDeclarationExport.declaration = implementation;
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
return true;
|
package/src/remix/templates.ts
CHANGED
|
@@ -5,7 +5,7 @@ export const ERROR_BOUNDARY_TEMPLATE_V2 = `const ErrorBoundary = () => {
|
|
|
5
5
|
};
|
|
6
6
|
`;
|
|
7
7
|
|
|
8
|
-
export const HANDLE_ERROR_TEMPLATE_V2 = `
|
|
9
|
-
|
|
10
|
-
}
|
|
8
|
+
export const HANDLE_ERROR_TEMPLATE_V2 = `const handleError = Sentry.wrapHandleErrorWithSentry((error, { request }) => {
|
|
9
|
+
// Custom handleError implementation
|
|
10
|
+
});
|
|
11
11
|
`;
|
package/src/utils/clack-utils.ts
CHANGED
|
@@ -295,11 +295,13 @@ export async function confirmContinueIfPackageVersionNotSupported({
|
|
|
295
295
|
packageName,
|
|
296
296
|
packageVersion,
|
|
297
297
|
acceptableVersions,
|
|
298
|
+
note,
|
|
298
299
|
}: {
|
|
299
300
|
packageId: string;
|
|
300
301
|
packageName: string;
|
|
301
302
|
packageVersion: string;
|
|
302
303
|
acceptableVersions: string;
|
|
304
|
+
note?: string;
|
|
303
305
|
}): Promise<void> {
|
|
304
306
|
return traceStep(`check-package-version`, async () => {
|
|
305
307
|
Sentry.setTag(`${packageName.toLowerCase()}-version`, packageVersion);
|
|
@@ -321,10 +323,8 @@ export async function confirmContinueIfPackageVersionNotSupported({
|
|
|
321
323
|
);
|
|
322
324
|
|
|
323
325
|
clack.note(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
'https://docs.sentry.io/platforms/react-native/manual-setup/manual-setup/',
|
|
327
|
-
)}`,
|
|
326
|
+
note ??
|
|
327
|
+
`Please upgrade to ${acceptableVersions} if you wish to use the Sentry Wizard.`,
|
|
328
328
|
);
|
|
329
329
|
const continueWithUnsupportedVersion = await abortIfCancelled(
|
|
330
330
|
clack.confirm({
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @ts-ignore - magicast is ESM and TS complains about that. It works though
|
|
2
|
+
import { generateCode, parseModule } from 'magicast';
|
|
3
|
+
import { patchMetroInMemory } from '../../src/react-native/expo-metro';
|
|
4
|
+
|
|
5
|
+
describe('expo-metro config', () => {
|
|
6
|
+
test('patches minimal expo config', () => {
|
|
7
|
+
const mod = parseModule(`
|
|
8
|
+
const { getDefaultConfig } = require("expo/metro-config");
|
|
9
|
+
|
|
10
|
+
/** @type {import('expo/metro-config').MetroConfig} */
|
|
11
|
+
const config = getDefaultConfig(__dirname);
|
|
12
|
+
|
|
13
|
+
config.resolver.assetExts.push(
|
|
14
|
+
// Adds support for .db files for SQLite databases
|
|
15
|
+
'db'
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
module.exports = config;
|
|
19
|
+
`);
|
|
20
|
+
|
|
21
|
+
const result = patchMetroInMemory(mod);
|
|
22
|
+
expect(result).toBe(true);
|
|
23
|
+
expect(generateCode(mod.$ast).code).toBe(
|
|
24
|
+
`
|
|
25
|
+
const {
|
|
26
|
+
getSentryExpoConfig
|
|
27
|
+
} = require("@sentry/react-native/metro");
|
|
28
|
+
|
|
29
|
+
/** @type {import('expo/metro-config').MetroConfig} */
|
|
30
|
+
const config = getSentryExpoConfig(__dirname);
|
|
31
|
+
|
|
32
|
+
config.resolver.assetExts.push(
|
|
33
|
+
// Adds support for .db files for SQLite databases
|
|
34
|
+
'db'
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
module.exports = config;
|
|
38
|
+
`.trim(),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('keeps expo metro config if other imports are present', () => {
|
|
43
|
+
const mod = parseModule(`
|
|
44
|
+
const { getDefaultConfig, otherExport } = require("expo/metro-config");
|
|
45
|
+
|
|
46
|
+
const config = getDefaultConfig(__dirname);
|
|
47
|
+
|
|
48
|
+
module.exports = config;
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
const result = patchMetroInMemory(mod);
|
|
52
|
+
expect(result).toBe(true);
|
|
53
|
+
expect(generateCode(mod.$ast).code).toBe(
|
|
54
|
+
`
|
|
55
|
+
const { getDefaultConfig, otherExport } = require("expo/metro-config");
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
getSentryExpoConfig
|
|
59
|
+
} = require("@sentry/react-native/metro");
|
|
60
|
+
|
|
61
|
+
const config = getSentryExpoConfig(__dirname);
|
|
62
|
+
|
|
63
|
+
module.exports = config;
|
|
64
|
+
`.trim(),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('does not modify when sentry already present', () => {
|
|
69
|
+
const mod = parseModule(`
|
|
70
|
+
const { getSentryExpoConfig } = require("@sentry/react-native/metro");
|
|
71
|
+
`);
|
|
72
|
+
|
|
73
|
+
const result = patchMetroInMemory(mod);
|
|
74
|
+
expect(result).toBe(false);
|
|
75
|
+
expect(generateCode(mod.$ast).code).toBe(
|
|
76
|
+
`
|
|
77
|
+
const { getSentryExpoConfig } = require("@sentry/react-native/metro");
|
|
78
|
+
`.trim(),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { addWithSentryToAppConfigJson } from '../../src/react-native/expo';
|
|
2
|
+
import { RNCliSetupConfigContent } from '../../src/react-native/react-native-wizard';
|
|
3
|
+
|
|
4
|
+
describe('expo', () => {
|
|
5
|
+
const MOCK_CONFIG: RNCliSetupConfigContent = {
|
|
6
|
+
url: 'https://sentry.mock/',
|
|
7
|
+
org: 'sentry-mock',
|
|
8
|
+
project: 'project-mock',
|
|
9
|
+
authToken: 'authToken-mock',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('addWithSentryToAppConfigJson', () => {
|
|
13
|
+
test('do not add if sentry-expo present', () => {
|
|
14
|
+
const appConfigJson = `{
|
|
15
|
+
"expo": {
|
|
16
|
+
"plugins": ["sentry-expo"]
|
|
17
|
+
}
|
|
18
|
+
}`;
|
|
19
|
+
expect(
|
|
20
|
+
addWithSentryToAppConfigJson(appConfigJson, MOCK_CONFIG),
|
|
21
|
+
).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('do not add if sentry-react-native/expo present', () => {
|
|
25
|
+
const appConfigJson = `{
|
|
26
|
+
"expo": {
|
|
27
|
+
"plugins": ["@sentry/react-native/expo"]
|
|
28
|
+
}
|
|
29
|
+
}`;
|
|
30
|
+
expect(
|
|
31
|
+
addWithSentryToAppConfigJson(appConfigJson, MOCK_CONFIG),
|
|
32
|
+
).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test.each([
|
|
36
|
+
[
|
|
37
|
+
`{
|
|
38
|
+
"expo": {
|
|
39
|
+
"plugins": "should be an array, but it is not"
|
|
40
|
+
}
|
|
41
|
+
}`,
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
`{
|
|
45
|
+
"expo": ["should be an object, but it is not"]
|
|
46
|
+
}`,
|
|
47
|
+
],
|
|
48
|
+
])('do not add if plugins is not an array', (appConfigJson) => {
|
|
49
|
+
expect(
|
|
50
|
+
addWithSentryToAppConfigJson(appConfigJson, MOCK_CONFIG),
|
|
51
|
+
).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test.each([
|
|
55
|
+
[
|
|
56
|
+
`{
|
|
57
|
+
"expo": {
|
|
58
|
+
"plugins": []
|
|
59
|
+
}
|
|
60
|
+
}`,
|
|
61
|
+
],
|
|
62
|
+
[`{}`],
|
|
63
|
+
[
|
|
64
|
+
`{
|
|
65
|
+
"expo": {}
|
|
66
|
+
}`,
|
|
67
|
+
],
|
|
68
|
+
])('add sentry react native expo plugin configuration', (appConfigJson) => {
|
|
69
|
+
const result = addWithSentryToAppConfigJson(appConfigJson, MOCK_CONFIG);
|
|
70
|
+
expect(JSON.parse(result ?? '{}')).toStrictEqual({
|
|
71
|
+
expo: {
|
|
72
|
+
plugins: [
|
|
73
|
+
[
|
|
74
|
+
'@sentry/react-native/expo',
|
|
75
|
+
{
|
|
76
|
+
url: 'https://sentry.mock/',
|
|
77
|
+
organization: 'sentry-mock',
|
|
78
|
+
project: 'project-mock',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -57,6 +57,96 @@ REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh"
|
|
|
57
57
|
expectedOutput,
|
|
58
58
|
);
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
it('adds sentry cli to expo bundle build phase', () => {
|
|
62
|
+
const input = `
|
|
63
|
+
if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then
|
|
64
|
+
source "$PODS_ROOT/../.xcode.env"
|
|
65
|
+
fi
|
|
66
|
+
if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then
|
|
67
|
+
source "$PODS_ROOT/../.xcode.env.local"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# The project root by default is one level up from the ios directory
|
|
71
|
+
export PROJECT_ROOT="$PROJECT_DIR"/..
|
|
72
|
+
|
|
73
|
+
if [[ "$CONFIGURATION" = *Debug* ]]; then
|
|
74
|
+
export SKIP_BUNDLING=1
|
|
75
|
+
fi
|
|
76
|
+
if [[ -z "$ENTRY_FILE" ]]; then
|
|
77
|
+
# Set the entry JS file using the bundler's entry resolution.
|
|
78
|
+
export ENTRY_FILE="$("$NODE_BINARY" -e "require('expo/scripts/resolveAppEntry')" "$PROJECT_ROOT" ios absolute | tail -n 1)"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
if [[ -z "$CLI_PATH" ]]; then
|
|
82
|
+
# Use Expo CLI
|
|
83
|
+
export CLI_PATH="$("$NODE_BINARY" --print "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })")"
|
|
84
|
+
fi
|
|
85
|
+
if [[ -z "$BUNDLE_COMMAND" ]]; then
|
|
86
|
+
# Default Expo CLI command for bundling
|
|
87
|
+
export BUNDLE_COMMAND="export:embed"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Source .xcode.env.updates if it exists to allow
|
|
91
|
+
# SKIP_BUNDLING to be unset if needed
|
|
92
|
+
if [[ -f "$PODS_ROOT/../.xcode.env.updates" ]]; then
|
|
93
|
+
source "$PODS_ROOT/../.xcode.env.updates"
|
|
94
|
+
fi
|
|
95
|
+
# Source local changes to allow overrides
|
|
96
|
+
# if needed
|
|
97
|
+
if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then
|
|
98
|
+
source "$PODS_ROOT/../.xcode.env.local"
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
\`"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"\`
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
const expectedOutput = `
|
|
105
|
+
if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then
|
|
106
|
+
source "$PODS_ROOT/../.xcode.env"
|
|
107
|
+
fi
|
|
108
|
+
if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then
|
|
109
|
+
source "$PODS_ROOT/../.xcode.env.local"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# The project root by default is one level up from the ios directory
|
|
113
|
+
export PROJECT_ROOT="$PROJECT_DIR"/..
|
|
114
|
+
|
|
115
|
+
if [[ "$CONFIGURATION" = *Debug* ]]; then
|
|
116
|
+
export SKIP_BUNDLING=1
|
|
117
|
+
fi
|
|
118
|
+
if [[ -z "$ENTRY_FILE" ]]; then
|
|
119
|
+
# Set the entry JS file using the bundler's entry resolution.
|
|
120
|
+
export ENTRY_FILE="$("$NODE_BINARY" -e "require('expo/scripts/resolveAppEntry')" "$PROJECT_ROOT" ios absolute | tail -n 1)"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
if [[ -z "$CLI_PATH" ]]; then
|
|
124
|
+
# Use Expo CLI
|
|
125
|
+
export CLI_PATH="$("$NODE_BINARY" --print "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })")"
|
|
126
|
+
fi
|
|
127
|
+
if [[ -z "$BUNDLE_COMMAND" ]]; then
|
|
128
|
+
# Default Expo CLI command for bundling
|
|
129
|
+
export BUNDLE_COMMAND="export:embed"
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Source .xcode.env.updates if it exists to allow
|
|
133
|
+
# SKIP_BUNDLING to be unset if needed
|
|
134
|
+
if [[ -f "$PODS_ROOT/../.xcode.env.updates" ]]; then
|
|
135
|
+
source "$PODS_ROOT/../.xcode.env.updates"
|
|
136
|
+
fi
|
|
137
|
+
# Source local changes to allow overrides
|
|
138
|
+
# if needed
|
|
139
|
+
if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then
|
|
140
|
+
source "$PODS_ROOT/../.xcode.env.local"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
/bin/sh \`"$NODE_BINARY" --print "require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'"\` \`"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"\`
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
expect(addSentryWithBundledScriptsToBundleShellScript(input)).toBe(
|
|
147
|
+
expectedOutput,
|
|
148
|
+
);
|
|
149
|
+
});
|
|
60
150
|
});
|
|
61
151
|
|
|
62
152
|
describe('removeSentryFromBundleShellScript', () => {
|