@sentry/wizard 3.16.4 → 3.17.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 +11 -0
- package/dist/package.json +2 -2
- package/dist/src/apple/cocoapod.js +2 -0
- package/dist/src/apple/cocoapod.js.map +1 -1
- package/dist/src/react-native/metro.d.ts +13 -0
- package/dist/src/react-native/metro.js +398 -0
- package/dist/src/react-native/metro.js.map +1 -0
- package/dist/src/react-native/react-native-wizard.d.ts +2 -0
- package/dist/src/react-native/react-native-wizard.js +145 -42
- package/dist/src/react-native/react-native-wizard.js.map +1 -1
- package/dist/src/react-native/uninstall.js +4 -0
- package/dist/src/react-native/uninstall.js.map +1 -1
- package/dist/src/react-native/xcode.d.ts +7 -3
- package/dist/src/react-native/xcode.js +43 -11
- package/dist/src/react-native/xcode.js.map +1 -1
- package/dist/src/remix/remix-wizard.js +80 -37
- 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 +21 -1
- package/dist/src/remix/sdk-setup.js.map +1 -1
- package/dist/src/sourcemaps/sourcemaps-wizard.js +2 -6
- package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
- package/dist/src/utils/ast-utils.d.ts +14 -0
- package/dist/src/utils/ast-utils.js +49 -1
- package/dist/src/utils/ast-utils.js.map +1 -1
- package/dist/src/utils/clack-utils.js +0 -1
- package/dist/src/utils/clack-utils.js.map +1 -1
- package/dist/src/utils/types.d.ts +8 -4
- package/dist/src/utils/types.js.map +1 -1
- package/dist/src/utils/url.d.ts +10 -0
- package/dist/src/utils/url.js +19 -0
- package/dist/src/utils/url.js.map +1 -0
- package/dist/test/react-native/metro.test.d.ts +1 -0
- package/dist/test/react-native/metro.test.js +125 -0
- package/dist/test/react-native/metro.test.js.map +1 -0
- package/dist/test/react-native/xcode.test.js +40 -2
- package/dist/test/react-native/xcode.test.js.map +1 -1
- package/package.json +2 -2
- package/src/apple/cocoapod.ts +2 -0
- package/src/react-native/metro.ts +409 -0
- package/src/react-native/react-native-wizard.ts +115 -12
- package/src/react-native/uninstall.ts +3 -0
- package/src/react-native/xcode.ts +70 -12
- package/src/remix/remix-wizard.ts +51 -15
- package/src/remix/sdk-setup.ts +31 -0
- package/src/sourcemaps/sourcemaps-wizard.ts +2 -7
- package/src/utils/ast-utils.ts +52 -0
- package/src/utils/clack-utils.ts +0 -1
- package/src/utils/types.ts +8 -5
- package/src/utils/url.ts +23 -0
- package/test/react-native/metro.test.ts +283 -0
- package/test/react-native/xcode.test.ts +76 -3
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// @ts-ignore - clack is ESM and TS complains about that. It works though
|
|
2
|
+
import * as clack from '@clack/prompts';
|
|
3
|
+
// @ts-ignore - magicast is ESM and TS complains about that. It works though
|
|
4
|
+
import { ProxifiedModule, parseModule, writeFile } from 'magicast';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as Sentry from '@sentry/node';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getLastRequireIndex,
|
|
10
|
+
hasSentryContent,
|
|
11
|
+
removeRequire,
|
|
12
|
+
} from '../utils/ast-utils';
|
|
13
|
+
import {
|
|
14
|
+
abortIfCancelled,
|
|
15
|
+
makeCodeSnippet,
|
|
16
|
+
showCopyPasteInstructions,
|
|
17
|
+
} from '../utils/clack-utils';
|
|
18
|
+
|
|
19
|
+
import * as recast from 'recast';
|
|
20
|
+
import x = recast.types;
|
|
21
|
+
import t = x.namedTypes;
|
|
22
|
+
import chalk from 'chalk';
|
|
23
|
+
|
|
24
|
+
const b = recast.types.builders;
|
|
25
|
+
|
|
26
|
+
const metroConfigPath = 'metro.config.js';
|
|
27
|
+
|
|
28
|
+
export async function patchMetroConfig() {
|
|
29
|
+
const mod = await parseMetroConfig();
|
|
30
|
+
|
|
31
|
+
const showInstructions = () =>
|
|
32
|
+
showCopyPasteInstructions(metroConfigPath, getMetroConfigSnippet(true));
|
|
33
|
+
|
|
34
|
+
if (hasSentryContent(mod.$ast as t.Program)) {
|
|
35
|
+
const shouldContinue = await confirmPathMetroConfig();
|
|
36
|
+
if (!shouldContinue) {
|
|
37
|
+
return await showInstructions();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const configObj = getMetroConfigObject(mod.$ast as t.Program);
|
|
42
|
+
if (!configObj) {
|
|
43
|
+
clack.log.warn(
|
|
44
|
+
'Could not find Metro config object, please follow the manual steps.',
|
|
45
|
+
);
|
|
46
|
+
return showInstructions();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const addedSentrySerializer = addSentrySerializerToMetroConfig(configObj);
|
|
50
|
+
if (!addedSentrySerializer) {
|
|
51
|
+
clack.log.warn(
|
|
52
|
+
'Could not add Sentry serializer to Metro config, please follow the manual steps.',
|
|
53
|
+
);
|
|
54
|
+
return await showInstructions();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const addedSentrySerializerImport = addSentrySerializerRequireToMetroConfig(
|
|
58
|
+
mod.$ast as t.Program,
|
|
59
|
+
);
|
|
60
|
+
if (!addedSentrySerializerImport) {
|
|
61
|
+
clack.log.warn(
|
|
62
|
+
'Could not add Sentry serializer import to Metro config, please follow the manual steps.',
|
|
63
|
+
);
|
|
64
|
+
return await showInstructions();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
clack.log.success(
|
|
68
|
+
`Added Sentry Metro plugin to ${chalk.cyan(metroConfigPath)}.`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const saved = await writeMetroConfig(mod);
|
|
72
|
+
if (saved) {
|
|
73
|
+
clack.log.success(
|
|
74
|
+
chalk.green(`${chalk.cyan(metroConfigPath)} changes saved.`),
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
clack.log.warn(
|
|
78
|
+
`Could not save changes to ${chalk.cyan(
|
|
79
|
+
metroConfigPath,
|
|
80
|
+
)}, please follow the manual steps.`,
|
|
81
|
+
);
|
|
82
|
+
return await showInstructions();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function unPatchMetroConfig() {
|
|
87
|
+
const mod = await parseMetroConfig();
|
|
88
|
+
|
|
89
|
+
const removedAtLeastOneRequire = removeSentryRequire(mod.$ast as t.Program);
|
|
90
|
+
const removedSerializerConfig = removeSentrySerializerFromMetroConfig(
|
|
91
|
+
mod.$ast as t.Program,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (removedAtLeastOneRequire || removedSerializerConfig) {
|
|
95
|
+
const saved = await writeMetroConfig(mod);
|
|
96
|
+
if (saved) {
|
|
97
|
+
clack.log.success(
|
|
98
|
+
`Removed Sentry Metro plugin from ${chalk.cyan(metroConfigPath)}.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
clack.log.warn(
|
|
103
|
+
`No Sentry Metro plugin found in ${chalk.cyan(metroConfigPath)}.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function removeSentrySerializerFromMetroConfig(
|
|
109
|
+
program: t.Program,
|
|
110
|
+
): boolean {
|
|
111
|
+
const configObject = getMetroConfigObject(program);
|
|
112
|
+
if (!configObject) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const serializerProp = getSerializerProp(configObject);
|
|
117
|
+
if ('invalid' === serializerProp || 'undefined' === serializerProp) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const customSerializerProp = getCustomSerializerProp(serializerProp);
|
|
122
|
+
if (
|
|
123
|
+
'invalid' === customSerializerProp ||
|
|
124
|
+
'undefined' === customSerializerProp
|
|
125
|
+
) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
serializerProp.value.type === 'ObjectExpression' &&
|
|
131
|
+
customSerializerProp.value.type === 'CallExpression' &&
|
|
132
|
+
customSerializerProp.value.callee.type === 'Identifier' &&
|
|
133
|
+
customSerializerProp.value.callee.name === 'createSentryMetroSerializer'
|
|
134
|
+
) {
|
|
135
|
+
if (customSerializerProp.value.arguments.length === 0) {
|
|
136
|
+
// FROM serializer: { customSerializer: createSentryMetroSerializer() }
|
|
137
|
+
// TO serializer: {}
|
|
138
|
+
let removed = false;
|
|
139
|
+
serializerProp.value.properties = serializerProp.value.properties.filter(
|
|
140
|
+
(p) => {
|
|
141
|
+
if (
|
|
142
|
+
p.type === 'ObjectProperty' &&
|
|
143
|
+
p.key.type === 'Identifier' &&
|
|
144
|
+
p.key.name === 'customSerializer'
|
|
145
|
+
) {
|
|
146
|
+
removed = true;
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (removed) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
if (customSerializerProp.value.arguments[0].type !== 'SpreadElement') {
|
|
158
|
+
// FROM serializer: { customSerializer: createSentryMetroSerializer(wrapperSerializer) }
|
|
159
|
+
// TO serializer: { customSerializer: wrapperSerializer }
|
|
160
|
+
customSerializerProp.value = customSerializerProp.value.arguments[0];
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function removeSentryRequire(program: t.Program): boolean {
|
|
170
|
+
return removeRequire(program, '@sentry');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function parseMetroConfig(): Promise<ProxifiedModule> {
|
|
174
|
+
const metroConfigContent = (
|
|
175
|
+
await fs.promises.readFile(metroConfigPath)
|
|
176
|
+
).toString();
|
|
177
|
+
|
|
178
|
+
return parseModule(metroConfigContent);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function writeMetroConfig(mod: ProxifiedModule): Promise<boolean> {
|
|
182
|
+
try {
|
|
183
|
+
await writeFile(mod.$ast, metroConfigPath);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
clack.log.error(
|
|
186
|
+
`Failed to write to ${chalk.cyan(metroConfigPath)}: ${JSON.stringify(e)}`,
|
|
187
|
+
);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function addSentrySerializerToMetroConfig(
|
|
194
|
+
configObj: t.ObjectExpression,
|
|
195
|
+
): boolean {
|
|
196
|
+
const serializerProp = getSerializerProp(configObj);
|
|
197
|
+
if ('invalid' === serializerProp) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// case 1: serializer property doesn't exist yet, so we can just add it
|
|
202
|
+
if ('undefined' === serializerProp) {
|
|
203
|
+
configObj.properties.push(
|
|
204
|
+
b.objectProperty(
|
|
205
|
+
b.identifier('serializer'),
|
|
206
|
+
b.objectExpression([
|
|
207
|
+
b.objectProperty(
|
|
208
|
+
b.identifier('customSerializer'),
|
|
209
|
+
b.callExpression(b.identifier('createSentryMetroSerializer'), []),
|
|
210
|
+
),
|
|
211
|
+
]),
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const customSerializerProp = getCustomSerializerProp(serializerProp);
|
|
218
|
+
// case 2: serializer.customSerializer property doesn't exist yet, so we just add it
|
|
219
|
+
if (
|
|
220
|
+
'undefined' === customSerializerProp &&
|
|
221
|
+
serializerProp.value.type === 'ObjectExpression'
|
|
222
|
+
) {
|
|
223
|
+
serializerProp.value.properties.push(
|
|
224
|
+
b.objectProperty(
|
|
225
|
+
b.identifier('customSerializer'),
|
|
226
|
+
b.callExpression(b.identifier('createSentryMetroSerializer'), []),
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getCustomSerializerProp(
|
|
236
|
+
prop: t.ObjectProperty,
|
|
237
|
+
): t.ObjectProperty | 'undefined' | 'invalid' {
|
|
238
|
+
const customSerializerProp =
|
|
239
|
+
prop.value.type === 'ObjectExpression' &&
|
|
240
|
+
prop.value.properties.find(
|
|
241
|
+
(p: t.ObjectProperty) =>
|
|
242
|
+
p.key.type === 'Identifier' && p.key.name === 'customSerializer',
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (!customSerializerProp) {
|
|
246
|
+
return 'undefined';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (customSerializerProp.type === 'ObjectProperty') {
|
|
250
|
+
return customSerializerProp;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return 'invalid';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getSerializerProp(
|
|
257
|
+
obj: t.ObjectExpression,
|
|
258
|
+
): t.ObjectProperty | 'undefined' | 'invalid' {
|
|
259
|
+
const serializerProp = obj.properties.find(
|
|
260
|
+
(p: t.ObjectProperty) =>
|
|
261
|
+
p.key.type === 'Identifier' && p.key.name === 'serializer',
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (!serializerProp) {
|
|
265
|
+
return 'undefined';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (serializerProp.type === 'ObjectProperty') {
|
|
269
|
+
return serializerProp;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return 'invalid';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function addSentrySerializerRequireToMetroConfig(
|
|
276
|
+
program: t.Program,
|
|
277
|
+
): boolean {
|
|
278
|
+
const lastRequireIndex = getLastRequireIndex(program);
|
|
279
|
+
const sentrySerializerRequire = createSentrySerializerRequire();
|
|
280
|
+
const sentryImportIndex = lastRequireIndex + 1;
|
|
281
|
+
if (sentryImportIndex < program.body.length) {
|
|
282
|
+
// insert after last require
|
|
283
|
+
program.body.splice(lastRequireIndex + 1, 0, sentrySerializerRequire);
|
|
284
|
+
} else {
|
|
285
|
+
// insert at the end
|
|
286
|
+
program.body.push(sentrySerializerRequire);
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Creates const {createSentryMetroSerializer} = require('@sentry/react-native/dist/js/tools/sentryMetroSerializer');
|
|
293
|
+
*/
|
|
294
|
+
function createSentrySerializerRequire() {
|
|
295
|
+
return b.variableDeclaration('const', [
|
|
296
|
+
b.variableDeclarator(
|
|
297
|
+
b.objectPattern([
|
|
298
|
+
b.objectProperty.from({
|
|
299
|
+
key: b.identifier('createSentryMetroSerializer'),
|
|
300
|
+
value: b.identifier('createSentryMetroSerializer'),
|
|
301
|
+
shorthand: true,
|
|
302
|
+
}),
|
|
303
|
+
]),
|
|
304
|
+
b.callExpression(b.identifier('require'), [
|
|
305
|
+
b.literal('@sentry/react-native/dist/js/tools/sentryMetroSerializer'),
|
|
306
|
+
]),
|
|
307
|
+
),
|
|
308
|
+
]);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function confirmPathMetroConfig() {
|
|
312
|
+
const shouldContinue = await abortIfCancelled(
|
|
313
|
+
clack.select({
|
|
314
|
+
message: `Metro Config already contains Sentry-related code. Should the wizard modify it anyway?`,
|
|
315
|
+
options: [
|
|
316
|
+
{
|
|
317
|
+
label: 'Yes, add the Sentry Metro plugin',
|
|
318
|
+
value: true,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
label: 'No, show me instructions to manually add the plugin',
|
|
322
|
+
value: false,
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
initialValue: true,
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
if (!shouldContinue) {
|
|
330
|
+
Sentry.setTag('ast-mod-fail-reason', 'has-sentry-content');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return shouldContinue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Returns value from `module.exports = value` or `const config = value`
|
|
338
|
+
*/
|
|
339
|
+
export function getMetroConfigObject(
|
|
340
|
+
program: t.Program,
|
|
341
|
+
): t.ObjectExpression | undefined {
|
|
342
|
+
// check config variable
|
|
343
|
+
const configVariable = program.body.find((s) => {
|
|
344
|
+
if (
|
|
345
|
+
s.type === 'VariableDeclaration' &&
|
|
346
|
+
s.declarations.length === 1 &&
|
|
347
|
+
s.declarations[0].type === 'VariableDeclarator' &&
|
|
348
|
+
s.declarations[0].id.type === 'Identifier' &&
|
|
349
|
+
s.declarations[0].id.name === 'config'
|
|
350
|
+
) {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}) as t.VariableDeclaration | undefined;
|
|
355
|
+
|
|
356
|
+
if (
|
|
357
|
+
configVariable?.declarations[0].type === 'VariableDeclarator' &&
|
|
358
|
+
configVariable?.declarations[0].init?.type === 'ObjectExpression'
|
|
359
|
+
) {
|
|
360
|
+
Sentry.setTag('metro-config', 'config-variable');
|
|
361
|
+
return configVariable.declarations[0].init;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// check module.exports
|
|
365
|
+
const moduleExports = program.body.find((s) => {
|
|
366
|
+
if (
|
|
367
|
+
s.type === 'ExpressionStatement' &&
|
|
368
|
+
s.expression.type === 'AssignmentExpression' &&
|
|
369
|
+
s.expression.left.type === 'MemberExpression' &&
|
|
370
|
+
s.expression.left.object.type === 'Identifier' &&
|
|
371
|
+
s.expression.left.object.name === 'module' &&
|
|
372
|
+
s.expression.left.property.type === 'Identifier' &&
|
|
373
|
+
s.expression.left.property.name === 'exports'
|
|
374
|
+
) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
}) as t.ExpressionStatement | undefined;
|
|
379
|
+
|
|
380
|
+
if (
|
|
381
|
+
(moduleExports?.expression as t.AssignmentExpression).right.type ===
|
|
382
|
+
'ObjectExpression'
|
|
383
|
+
) {
|
|
384
|
+
Sentry.setTag('metro-config', 'module-exports');
|
|
385
|
+
return (moduleExports?.expression as t.AssignmentExpression)
|
|
386
|
+
.right as t.ObjectExpression;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
Sentry.setTag('metro-config', 'not-found');
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function getMetroConfigSnippet(colors: boolean) {
|
|
394
|
+
return makeCodeSnippet(colors, (unchanged, plus, _) =>
|
|
395
|
+
unchanged(`const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');";
|
|
396
|
+
${plus(
|
|
397
|
+
"const {createSentryMetroSerializer} = require('@sentry/react-native/dist/js/tools/sentryMetroSerializer');",
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
const config = {
|
|
401
|
+
${plus(`serializer: {
|
|
402
|
+
customSerializer: createSentryMetroSerializer(),
|
|
403
|
+
},`)}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
|
407
|
+
`),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
@@ -7,6 +7,7 @@ import * as path from 'path';
|
|
|
7
7
|
import * as process from 'process';
|
|
8
8
|
import {
|
|
9
9
|
CliSetupConfigContent,
|
|
10
|
+
abortIfCancelled,
|
|
10
11
|
addSentryCliConfig,
|
|
11
12
|
confirmContinueIfNoOrDirtyGitRepo,
|
|
12
13
|
confirmContinueIfPackageVersionNotSupported,
|
|
@@ -26,8 +27,11 @@ import {
|
|
|
26
27
|
findBundlePhase,
|
|
27
28
|
patchBundlePhase,
|
|
28
29
|
findDebugFilesUploadPhase,
|
|
29
|
-
|
|
30
|
+
addDebugFilesUploadPhaseWithCli,
|
|
30
31
|
writeXcodeProject,
|
|
32
|
+
addSentryWithCliToBundleShellScript,
|
|
33
|
+
addSentryWithBundledScriptsToBundleShellScript,
|
|
34
|
+
addDebugFilesUploadPhaseWithBundledScripts,
|
|
31
35
|
} from './xcode';
|
|
32
36
|
import {
|
|
33
37
|
doesAppBuildGradleIncludeRNSentryGradlePlugin,
|
|
@@ -37,7 +41,6 @@ import {
|
|
|
37
41
|
import { runReactNativeUninstall } from './uninstall';
|
|
38
42
|
import { APP_BUILD_GRADLE, XCODE_PROJECT, getFirstMatchedPath } from './glob';
|
|
39
43
|
import { ReactNativeWizardOptions } from './options';
|
|
40
|
-
import { SentryProjectData } from '../utils/types';
|
|
41
44
|
import {
|
|
42
45
|
addSentryInitWithSdkImport,
|
|
43
46
|
doesJsCodeIncludeSdkSentryImport,
|
|
@@ -45,6 +48,9 @@ import {
|
|
|
45
48
|
} from './javascript';
|
|
46
49
|
import { traceStep, withTelemetry } from '../telemetry';
|
|
47
50
|
import * as Sentry from '@sentry/node';
|
|
51
|
+
import { fulfillsVersionRange } from '../utils/semver';
|
|
52
|
+
import { getIssueStreamUrl } from '../utils/url';
|
|
53
|
+
import { patchMetroConfig } from './metro';
|
|
48
54
|
|
|
49
55
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
50
56
|
const xcode = require('xcode');
|
|
@@ -56,6 +62,13 @@ export const RN_HUMAN_NAME = 'React Native';
|
|
|
56
62
|
|
|
57
63
|
export const SUPPORTED_RN_RANGE = '>=0.69.0';
|
|
58
64
|
|
|
65
|
+
// The following SDK version ship with bundled Xcode scripts
|
|
66
|
+
// which simplifies the Xcode Build Phases setup.
|
|
67
|
+
export const SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE = '>=5.11.0';
|
|
68
|
+
|
|
69
|
+
// The following SDK version ship with Sentry Metro plugin
|
|
70
|
+
export const SDK_SENTRY_METRO_PLUGIN_SUPPORTED_SDK_RANGE = '>=5.11.0';
|
|
71
|
+
|
|
59
72
|
export type RNCliSetupConfigContent = Pick<
|
|
60
73
|
Required<CliSetupConfigContent>,
|
|
61
74
|
'authToken' | 'org' | 'project' | 'url'
|
|
@@ -107,6 +120,7 @@ export async function runReactNativeWizardWithTelemetry(
|
|
|
107
120
|
await getOrAskForProjectData(options, 'react-native');
|
|
108
121
|
const orgSlug = selectedProject.organization.slug;
|
|
109
122
|
const projectSlug = selectedProject.slug;
|
|
123
|
+
const projectId = selectedProject.id;
|
|
110
124
|
const cliConfig: RNCliSetupConfigContent = {
|
|
111
125
|
authToken,
|
|
112
126
|
org: orgSlug,
|
|
@@ -118,14 +132,24 @@ export async function runReactNativeWizardWithTelemetry(
|
|
|
118
132
|
packageName: RN_SDK_PACKAGE,
|
|
119
133
|
alreadyInstalled: hasPackageInstalled(RN_SDK_PACKAGE, packageJson),
|
|
120
134
|
});
|
|
135
|
+
const sdkVersion = getPackageVersion(
|
|
136
|
+
RN_SDK_PACKAGE,
|
|
137
|
+
await getPackageDotJson(),
|
|
138
|
+
);
|
|
121
139
|
|
|
122
140
|
await traceStep('patch-js', () =>
|
|
123
141
|
addSentryInit({ dsn: selectedProject.keys[0].dsn.public }),
|
|
124
142
|
);
|
|
125
143
|
|
|
144
|
+
await traceStep('patch-metro-config', () =>
|
|
145
|
+
addSentryToMetroConfig({ sdkVersion }),
|
|
146
|
+
);
|
|
147
|
+
|
|
126
148
|
if (fs.existsSync('ios')) {
|
|
127
149
|
Sentry.setTag('patch-ios', true);
|
|
128
|
-
await traceStep('patch-xcode-files', () =>
|
|
150
|
+
await traceStep('patch-xcode-files', () =>
|
|
151
|
+
patchXcodeFiles(cliConfig, { sdkVersion }),
|
|
152
|
+
);
|
|
129
153
|
}
|
|
130
154
|
|
|
131
155
|
if (fs.existsSync('android')) {
|
|
@@ -134,7 +158,9 @@ export async function runReactNativeWizardWithTelemetry(
|
|
|
134
158
|
}
|
|
135
159
|
|
|
136
160
|
const confirmedFirstException = await confirmFirstSentryException(
|
|
137
|
-
|
|
161
|
+
sentryUrl,
|
|
162
|
+
orgSlug,
|
|
163
|
+
projectId,
|
|
138
164
|
);
|
|
139
165
|
Sentry.setTag('user-confirmed-first-error', confirmedFirstException);
|
|
140
166
|
|
|
@@ -155,6 +181,25 @@ export async function runReactNativeWizardWithTelemetry(
|
|
|
155
181
|
}
|
|
156
182
|
}
|
|
157
183
|
|
|
184
|
+
async function addSentryToMetroConfig({
|
|
185
|
+
sdkVersion,
|
|
186
|
+
}: {
|
|
187
|
+
sdkVersion: string | undefined;
|
|
188
|
+
}) {
|
|
189
|
+
if (
|
|
190
|
+
!sdkVersion ||
|
|
191
|
+
!fulfillsVersionRange({
|
|
192
|
+
version: sdkVersion,
|
|
193
|
+
acceptableVersions: SDK_SENTRY_METRO_PLUGIN_SUPPORTED_SDK_RANGE,
|
|
194
|
+
canBeLatest: true,
|
|
195
|
+
})
|
|
196
|
+
) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await patchMetroConfig();
|
|
201
|
+
}
|
|
202
|
+
|
|
158
203
|
async function addSentryInit({ dsn }: { dsn: string }) {
|
|
159
204
|
const prefixGlob = '{.,./src}';
|
|
160
205
|
const suffixGlob = '@(j|t|cj|mj)s?(x)';
|
|
@@ -207,8 +252,12 @@ async function addSentryInit({ dsn }: { dsn: string }) {
|
|
|
207
252
|
);
|
|
208
253
|
}
|
|
209
254
|
|
|
210
|
-
async function confirmFirstSentryException(
|
|
211
|
-
|
|
255
|
+
async function confirmFirstSentryException(
|
|
256
|
+
url: string,
|
|
257
|
+
orgSlug: string,
|
|
258
|
+
projectId: string,
|
|
259
|
+
) {
|
|
260
|
+
const issuesStreamUrl = getIssueStreamUrl({ url, orgSlug, projectId });
|
|
212
261
|
|
|
213
262
|
clack.log
|
|
214
263
|
.step(`To make sure everything is set up correctly, put the following code snippet into your application.
|
|
@@ -216,7 +265,7 @@ The snippet will create a button that, when tapped, sends a test event to Sentry
|
|
|
216
265
|
|
|
217
266
|
After that check your project issues:
|
|
218
267
|
|
|
219
|
-
${chalk.cyan(
|
|
268
|
+
${chalk.cyan(issuesStreamUrl)}`);
|
|
220
269
|
|
|
221
270
|
// We want the code snippet to be easily copy-pasteable, without any clack artifacts
|
|
222
271
|
// eslint-disable-next-line no-console
|
|
@@ -233,7 +282,12 @@ ${chalk.cyan(projectsIssuesUrl)}`);
|
|
|
233
282
|
return firstErrorConfirmed;
|
|
234
283
|
}
|
|
235
284
|
|
|
236
|
-
async function patchXcodeFiles(
|
|
285
|
+
async function patchXcodeFiles(
|
|
286
|
+
config: RNCliSetupConfigContent,
|
|
287
|
+
context: {
|
|
288
|
+
sdkVersion: string | undefined;
|
|
289
|
+
},
|
|
290
|
+
) {
|
|
237
291
|
await addSentryCliConfig(config, {
|
|
238
292
|
...propertiesCliSetupConfig,
|
|
239
293
|
name: 'source maps and iOS debug files',
|
|
@@ -241,9 +295,8 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) {
|
|
|
241
295
|
gitignore: false,
|
|
242
296
|
});
|
|
243
297
|
|
|
244
|
-
if (platform() === 'darwin') {
|
|
298
|
+
if (platform() === 'darwin' && (await confirmPodInstall())) {
|
|
245
299
|
await traceStep('pod-install', () => podInstall('ios'));
|
|
246
|
-
Sentry.setTag('pods-installed', true);
|
|
247
300
|
}
|
|
248
301
|
|
|
249
302
|
const xcodeProjectPath = traceStep('find-xcode-project', () =>
|
|
@@ -281,7 +334,21 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) {
|
|
|
281
334
|
'xcode-bundle-phase-status',
|
|
282
335
|
bundlePhase ? 'found' : 'not-found',
|
|
283
336
|
);
|
|
284
|
-
|
|
337
|
+
if (
|
|
338
|
+
context.sdkVersion &&
|
|
339
|
+
fulfillsVersionRange({
|
|
340
|
+
version: context.sdkVersion,
|
|
341
|
+
acceptableVersions: SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE,
|
|
342
|
+
canBeLatest: true,
|
|
343
|
+
})
|
|
344
|
+
) {
|
|
345
|
+
patchBundlePhase(
|
|
346
|
+
bundlePhase,
|
|
347
|
+
addSentryWithBundledScriptsToBundleShellScript,
|
|
348
|
+
);
|
|
349
|
+
} else {
|
|
350
|
+
patchBundlePhase(bundlePhase, addSentryWithCliToBundleShellScript);
|
|
351
|
+
}
|
|
285
352
|
Sentry.setTag('xcode-bundle-phase-status', 'patched');
|
|
286
353
|
});
|
|
287
354
|
|
|
@@ -292,7 +359,22 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) {
|
|
|
292
359
|
'xcode-debug-files-upload-phase-status',
|
|
293
360
|
debugFilesUploadPhaseExists ? 'already-exists' : undefined,
|
|
294
361
|
);
|
|
295
|
-
|
|
362
|
+
if (
|
|
363
|
+
context.sdkVersion &&
|
|
364
|
+
fulfillsVersionRange({
|
|
365
|
+
version: context.sdkVersion,
|
|
366
|
+
acceptableVersions: SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE,
|
|
367
|
+
canBeLatest: true,
|
|
368
|
+
})
|
|
369
|
+
) {
|
|
370
|
+
addDebugFilesUploadPhaseWithBundledScripts(xcodeProject, {
|
|
371
|
+
debugFilesUploadPhaseExists,
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
addDebugFilesUploadPhaseWithCli(xcodeProject, {
|
|
375
|
+
debugFilesUploadPhaseExists,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
296
378
|
Sentry.setTag('xcode-debug-files-upload-phase-status', 'added');
|
|
297
379
|
});
|
|
298
380
|
|
|
@@ -367,3 +449,24 @@ async function patchAndroidFiles(config: RNCliSetupConfigContent) {
|
|
|
367
449
|
chalk.green(`Android ${chalk.cyan('app/build.gradle')} saved.`),
|
|
368
450
|
);
|
|
369
451
|
}
|
|
452
|
+
|
|
453
|
+
async function confirmPodInstall(): Promise<boolean> {
|
|
454
|
+
return traceStep('confirm-pod-install', async () => {
|
|
455
|
+
const continueWithPodInstall = await abortIfCancelled(
|
|
456
|
+
clack.select({
|
|
457
|
+
message: 'Do you want to run `pod install` now?',
|
|
458
|
+
options: [
|
|
459
|
+
{
|
|
460
|
+
value: true,
|
|
461
|
+
label: 'Yes',
|
|
462
|
+
hint: 'Recommended for smaller projects, this might take several minutes',
|
|
463
|
+
},
|
|
464
|
+
{ value: false, label: `No, I'll do it later` },
|
|
465
|
+
],
|
|
466
|
+
initialValue: true,
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
Sentry.setTag('continue-with-pod-install', continueWithPodInstall);
|
|
470
|
+
return continueWithPodInstall;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
writeAppBuildGradle,
|
|
22
22
|
} from './gradle';
|
|
23
23
|
import { ReactNativeWizardOptions } from './options';
|
|
24
|
+
import { unPatchMetroConfig } from './metro';
|
|
24
25
|
|
|
25
26
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
26
27
|
const xcode = require('xcode');
|
|
@@ -36,6 +37,8 @@ export async function runReactNativeUninstall(
|
|
|
36
37
|
|
|
37
38
|
await confirmContinueIfNoOrDirtyGitRepo();
|
|
38
39
|
|
|
40
|
+
await unPatchMetroConfig();
|
|
41
|
+
|
|
39
42
|
unPatchXcodeFiles();
|
|
40
43
|
|
|
41
44
|
unPatchAndroidFiles();
|