@sentry/wizard 3.22.3 → 3.23.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.
@@ -25,11 +25,100 @@ const b = recast.types.builders;
25
25
 
26
26
  const metroConfigPath = 'metro.config.js';
27
27
 
28
- export async function patchMetroConfig() {
28
+ export async function patchMetroWithSentryConfig() {
29
29
  const mod = await parseMetroConfig();
30
30
 
31
31
  const showInstructions = () =>
32
- showCopyPasteInstructions(metroConfigPath, getMetroConfigSnippet(true));
32
+ showCopyPasteInstructions(
33
+ metroConfigPath,
34
+ getMetroWithSentryConfigSnippet(true),
35
+ );
36
+
37
+ const success = await patchMetroWithSentryConfigInMemory(
38
+ mod,
39
+ showInstructions,
40
+ );
41
+ if (!success) {
42
+ return;
43
+ }
44
+
45
+ const saved = await writeMetroConfig(mod);
46
+ if (saved) {
47
+ clack.log.success(
48
+ chalk.green(`${chalk.cyan(metroConfigPath)} changes saved.`),
49
+ );
50
+ } else {
51
+ clack.log.warn(
52
+ `Could not save changes to ${chalk.cyan(
53
+ metroConfigPath,
54
+ )}, please follow the manual steps.`,
55
+ );
56
+ return await showInstructions();
57
+ }
58
+ }
59
+
60
+ export async function patchMetroWithSentryConfigInMemory(
61
+ mod: ProxifiedModule,
62
+ showInstructions: () => Promise<void>,
63
+ ): Promise<boolean> {
64
+ if (hasSentryContent(mod.$ast as t.Program)) {
65
+ const shouldContinue = await confirmPathMetroConfig();
66
+ if (!shouldContinue) {
67
+ await showInstructions();
68
+ return false;
69
+ }
70
+ }
71
+
72
+ const configExpression = getModuleExportsAssignmentRight(
73
+ mod.$ast as t.Program,
74
+ );
75
+ if (!configExpression) {
76
+ clack.log.warn(
77
+ 'Could not find Metro config, please follow the manual steps.',
78
+ );
79
+ await showInstructions();
80
+ return false;
81
+ }
82
+
83
+ const wrappedConfig = wrapWithSentryConfig(configExpression);
84
+
85
+ const replacedModuleExportsRight = replaceModuleExportsRight(
86
+ mod.$ast as t.Program,
87
+ wrappedConfig,
88
+ );
89
+ if (!replacedModuleExportsRight) {
90
+ clack.log.warn(
91
+ 'Could not automatically wrap the config export, please follow the manual steps.',
92
+ );
93
+ await showInstructions();
94
+ return false;
95
+ }
96
+
97
+ const addedSentryMetroImport = addSentryMetroRequireToMetroConfig(
98
+ mod.$ast as t.Program,
99
+ );
100
+ if (!addedSentryMetroImport) {
101
+ clack.log.warn(
102
+ 'Could not add `@sentry/react-native/metro` import to Metro config, please follow the manual steps.',
103
+ );
104
+ await showInstructions();
105
+ return false;
106
+ }
107
+
108
+ clack.log.success(
109
+ `Added Sentry Metro plugin to ${chalk.cyan(metroConfigPath)}.`,
110
+ );
111
+ return true;
112
+ }
113
+
114
+ export async function patchMetroConfigWithSentrySerializer() {
115
+ const mod = await parseMetroConfig();
116
+
117
+ const showInstructions = () =>
118
+ showCopyPasteInstructions(
119
+ metroConfigPath,
120
+ getMetroSentrySerializerSnippet(true),
121
+ );
33
122
 
34
123
  if (hasSentryContent(mod.$ast as t.Program)) {
35
124
  const shouldContinue = await confirmPathMetroConfig();
@@ -282,12 +371,51 @@ export function addSentrySerializerRequireToMetroConfig(
282
371
  // insert after last require
283
372
  program.body.splice(lastRequireIndex + 1, 0, sentrySerializerRequire);
284
373
  } else {
285
- // insert at the end
286
- program.body.push(sentrySerializerRequire);
374
+ // insert at the beginning
375
+ program.body.unshift(sentrySerializerRequire);
376
+ }
377
+ return true;
378
+ }
379
+
380
+ export function addSentryMetroRequireToMetroConfig(
381
+ program: t.Program,
382
+ ): boolean {
383
+ const lastRequireIndex = getLastRequireIndex(program);
384
+ const sentryMetroRequire = createSentryMetroRequire();
385
+ const sentryImportIndex = lastRequireIndex + 1;
386
+ if (sentryImportIndex < program.body.length) {
387
+ // insert after last require
388
+ program.body.splice(lastRequireIndex + 1, 0, sentryMetroRequire);
389
+ } else {
390
+ // insert at the beginning
391
+ program.body.unshift(sentryMetroRequire);
287
392
  }
288
393
  return true;
289
394
  }
290
395
 
396
+ function wrapWithSentryConfig(
397
+ configObj: t.Identifier | t.CallExpression | t.ObjectExpression,
398
+ ): t.CallExpression {
399
+ return b.callExpression(b.identifier('withSentryConfig'), [configObj]);
400
+ }
401
+
402
+ function replaceModuleExportsRight(
403
+ program: t.Program,
404
+ wrappedConfig: t.CallExpression,
405
+ ): boolean {
406
+ const moduleExports = getModuleExports(program);
407
+ if (!moduleExports) {
408
+ return false;
409
+ }
410
+
411
+ if (moduleExports.expression.type === 'AssignmentExpression') {
412
+ moduleExports.expression.right = wrappedConfig;
413
+ return true;
414
+ }
415
+
416
+ return false;
417
+ }
418
+
291
419
  /**
292
420
  * Creates const {createSentryMetroSerializer} = require('@sentry/react-native/dist/js/tools/sentryMetroSerializer');
293
421
  */
@@ -308,6 +436,26 @@ function createSentrySerializerRequire() {
308
436
  ]);
309
437
  }
310
438
 
439
+ /**
440
+ * Creates const {withSentryConfig} = require('@sentry/react-native/metro');
441
+ */
442
+ function createSentryMetroRequire() {
443
+ return b.variableDeclaration('const', [
444
+ b.variableDeclarator(
445
+ b.objectPattern([
446
+ b.objectProperty.from({
447
+ key: b.identifier('withSentryConfig'),
448
+ value: b.identifier('withSentryConfig'),
449
+ shorthand: true,
450
+ }),
451
+ ]),
452
+ b.callExpression(b.identifier('require'), [
453
+ b.literal('@sentry/react-native/metro'),
454
+ ]),
455
+ ),
456
+ ]);
457
+ }
458
+
311
459
  async function confirmPathMetroConfig() {
312
460
  const shouldContinue = await abortIfCancelled(
313
461
  clack.select({
@@ -361,8 +509,48 @@ export function getMetroConfigObject(
361
509
  return configVariable.declarations[0].init;
362
510
  }
363
511
 
512
+ return getModuleExportsObject(program);
513
+ }
514
+
515
+ function getModuleExportsObject(
516
+ program: t.Program,
517
+ ): t.ObjectExpression | undefined {
518
+ // check module.exports
519
+ const moduleExports = getModuleExportsAssignmentRight(program);
520
+
521
+ if (moduleExports?.type === 'ObjectExpression') {
522
+ return moduleExports;
523
+ }
524
+
525
+ Sentry.setTag('metro-config', 'not-found');
526
+ return undefined;
527
+ }
528
+
529
+ export function getModuleExportsAssignmentRight(
530
+ program: t.Program,
531
+ ): t.Identifier | t.CallExpression | t.ObjectExpression | undefined {
364
532
  // check module.exports
365
- const moduleExports = program.body.find((s) => {
533
+ const moduleExports = getModuleExports(program);
534
+
535
+ if (
536
+ moduleExports?.expression.type === 'AssignmentExpression' &&
537
+ (moduleExports.expression.right.type === 'ObjectExpression' ||
538
+ moduleExports.expression.right.type === 'CallExpression' ||
539
+ moduleExports.expression.right.type === 'Identifier')
540
+ ) {
541
+ Sentry.setTag('metro-config', 'module-exports');
542
+ return moduleExports?.expression.right;
543
+ }
544
+
545
+ Sentry.setTag('metro-config', 'not-found');
546
+ return undefined;
547
+ }
548
+
549
+ function getModuleExports(
550
+ program: t.Program,
551
+ ): t.ExpressionStatement | undefined {
552
+ // find module.exports
553
+ return program.body.find((s) => {
366
554
  if (
367
555
  s.type === 'ExpressionStatement' &&
368
556
  s.expression.type === 'AssignmentExpression' &&
@@ -376,21 +564,9 @@ export function getMetroConfigObject(
376
564
  }
377
565
  return false;
378
566
  }) 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
567
  }
392
568
 
393
- function getMetroConfigSnippet(colors: boolean) {
569
+ function getMetroSentrySerializerSnippet(colors: boolean) {
394
570
  return makeCodeSnippet(colors, (unchanged, plus, _) =>
395
571
  unchanged(`const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');";
396
572
  ${plus(
@@ -407,3 +583,17 @@ module.exports = mergeConfig(getDefaultConfig(__dirname), config);
407
583
  `),
408
584
  );
409
585
  }
586
+
587
+ function getMetroWithSentryConfigSnippet(colors: boolean) {
588
+ return makeCodeSnippet(colors, (unchanged, plus, _) =>
589
+ unchanged(`const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');";
590
+ ${plus("const {withSentryConfig} = require('@sentry/react-native/metro');")}
591
+
592
+ const config = {};
593
+
594
+ module.exports = ${plus(
595
+ 'withSentryConfig(',
596
+ )}mergeConfig(getDefaultConfig(__dirname), config)${plus(')')};
597
+ `),
598
+ );
599
+ }
@@ -50,7 +50,10 @@ import { traceStep, withTelemetry } from '../telemetry';
50
50
  import * as Sentry from '@sentry/node';
51
51
  import { fulfillsVersionRange } from '../utils/semver';
52
52
  import { getIssueStreamUrl } from '../utils/url';
53
- import { patchMetroConfig } from './metro';
53
+ import {
54
+ patchMetroConfigWithSentrySerializer,
55
+ patchMetroWithSentryConfig,
56
+ } from './metro';
54
57
 
55
58
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
56
59
  const xcode = require('xcode');
@@ -69,6 +72,10 @@ export const SDK_XCODE_SCRIPTS_SUPPORTED_SDK_RANGE = '>=5.11.0';
69
72
  // The following SDK version ship with Sentry Metro plugin
70
73
  export const SDK_SENTRY_METRO_PLUGIN_SUPPORTED_SDK_RANGE = '>=5.11.0';
71
74
 
75
+ // The following SDK version shipped `withSentryConfig`
76
+ export const SDK_SENTRY_METRO_WITH_SENTRY_CONFIG_SUPPORTED_SDK_RANGE =
77
+ '>=5.17.0';
78
+
72
79
  export type RNCliSetupConfigContent = Pick<
73
80
  Required<CliSetupConfigContent>,
74
81
  'authToken' | 'org' | 'project' | 'url'
@@ -181,23 +188,33 @@ export async function runReactNativeWizardWithTelemetry(
181
188
  }
182
189
  }
183
190
 
184
- async function addSentryToMetroConfig({
191
+ function addSentryToMetroConfig({
185
192
  sdkVersion,
186
193
  }: {
187
194
  sdkVersion: string | undefined;
188
195
  }) {
189
196
  if (
190
- !sdkVersion ||
191
- !fulfillsVersionRange({
197
+ sdkVersion &&
198
+ fulfillsVersionRange({
192
199
  version: sdkVersion,
193
- acceptableVersions: SDK_SENTRY_METRO_PLUGIN_SUPPORTED_SDK_RANGE,
200
+ acceptableVersions:
201
+ SDK_SENTRY_METRO_WITH_SENTRY_CONFIG_SUPPORTED_SDK_RANGE,
194
202
  canBeLatest: true,
195
203
  })
196
204
  ) {
197
- return;
205
+ return patchMetroWithSentryConfig();
198
206
  }
199
207
 
200
- await patchMetroConfig();
208
+ if (
209
+ sdkVersion &&
210
+ fulfillsVersionRange({
211
+ version: sdkVersion,
212
+ acceptableVersions: SDK_SENTRY_METRO_PLUGIN_SUPPORTED_SDK_RANGE,
213
+ canBeLatest: true,
214
+ })
215
+ ) {
216
+ return patchMetroConfigWithSentrySerializer();
217
+ }
201
218
  }
202
219
 
203
220
  async function addSentryInit({ dsn }: { dsn: string }) {
@@ -221,10 +221,10 @@ export function printJsonC(ast: t.Program): string {
221
221
  * Walks the program body and returns index of the last variable assignment initialized by require statement.
222
222
  * Only counts top level require statements.
223
223
  *
224
- * @returns index of the last `const foo = require('bar');` statement
224
+ * @returns index of the last `const foo = require('bar');` statement or -1 if none was found.
225
225
  */
226
226
  export function getLastRequireIndex(program: t.Program): number {
227
- let lastRequireIdex = 0;
227
+ let lastRequireIdex = -1;
228
228
  program.body.forEach((s, i) => {
229
229
  if (
230
230
  s.type === 'VariableDeclaration' &&
@@ -31,6 +31,9 @@ import * as Sentry from '@sentry/react-native';
31
31
 
32
32
  Sentry.init({
33
33
  dsn: 'dsn',
34
+
35
+ // uncomment the line below to enable Spotlight (https://spotlightjs.com)
36
+ // enableSpotlight: __DEV__,
34
37
  });
35
38
 
36
39
  const App = () => {
@@ -9,11 +9,124 @@ import {
9
9
  addSentrySerializerRequireToMetroConfig,
10
10
  addSentrySerializerToMetroConfig,
11
11
  getMetroConfigObject,
12
+ patchMetroWithSentryConfigInMemory,
12
13
  removeSentryRequire,
13
14
  removeSentrySerializerFromMetroConfig,
14
15
  } from '../../src/react-native/metro';
15
16
 
16
17
  describe('patch metro config - sentry serializer', () => {
18
+ describe('patchMetroWithSentryConfigInMemory', () => {
19
+ it('patches react native 0.72 default metro config', async () => {
20
+ const mod =
21
+ parseModule(`const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
22
+
23
+ /**
24
+ * Metro configuration
25
+ * https://reactnative.dev/docs/metro
26
+ *
27
+ * @type {import('metro-config').MetroConfig}
28
+ */
29
+ const config = {};
30
+
31
+ module.exports = mergeConfig(getDefaultConfig(__dirname), config);`);
32
+
33
+ const result = await patchMetroWithSentryConfigInMemory(mod, async () => {
34
+ /* noop */
35
+ });
36
+ expect(result).toBe(true);
37
+ expect(generateCode(mod.$ast).code)
38
+ .toBe(`const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
39
+
40
+ const {
41
+ withSentryConfig
42
+ } = require("@sentry/react-native/metro");
43
+
44
+ /**
45
+ * Metro configuration
46
+ * https://reactnative.dev/docs/metro
47
+ *
48
+ * @type {import('metro-config').MetroConfig}
49
+ */
50
+ const config = {};
51
+
52
+ module.exports = withSentryConfig(mergeConfig(getDefaultConfig(__dirname), config));`);
53
+ });
54
+
55
+ it('patches react native 0.65 default metro config', async () => {
56
+ const mod = parseModule(`/**
57
+ * Metro configuration for React Native
58
+ * https://github.com/facebook/react-native
59
+ *
60
+ * @format
61
+ */
62
+
63
+ module.exports = {
64
+ transformer: {
65
+ getTransformOptions: async () => ({
66
+ transform: {
67
+ experimentalImportSupport: false,
68
+ inlineRequires: true,
69
+ },
70
+ }),
71
+ },
72
+ };`);
73
+
74
+ const result = await patchMetroWithSentryConfigInMemory(mod, async () => {
75
+ /* noop */
76
+ });
77
+ expect(result).toBe(true);
78
+ expect(generateCode(mod.$ast).code).toBe(`const {
79
+ withSentryConfig
80
+ } = require("@sentry/react-native/metro");
81
+
82
+ /**
83
+ * Metro configuration for React Native
84
+ * https://github.com/facebook/react-native
85
+ *
86
+ * @format
87
+ */
88
+
89
+ module.exports = withSentryConfig({
90
+ transformer: {
91
+ getTransformOptions: async () => ({
92
+ transform: {
93
+ experimentalImportSupport: false,
94
+ inlineRequires: true,
95
+ },
96
+ }),
97
+ },
98
+ });`);
99
+ });
100
+
101
+ it('patches react native metro config exported variable', async () => {
102
+ const mod = parseModule(`const testConfig = {};
103
+
104
+ module.exports = testConfig;`);
105
+
106
+ const result = await patchMetroWithSentryConfigInMemory(mod, async () => {
107
+ /* noop */
108
+ });
109
+ expect(result).toBe(true);
110
+ expect(generateCode(mod.$ast).code).toBe(`const {
111
+ withSentryConfig
112
+ } = require("@sentry/react-native/metro");
113
+
114
+ const testConfig = {};
115
+
116
+ module.exports = withSentryConfig(testConfig);`);
117
+ });
118
+
119
+ it('does not patch react native metro config exported as factory function', async () => {
120
+ const mod = parseModule(`module.exports = () => ({});`);
121
+
122
+ const result = await patchMetroWithSentryConfigInMemory(mod, async () => {
123
+ /* noop */
124
+ });
125
+ expect(result).toBe(false);
126
+ expect(generateCode(mod.$ast).code).toBe(`module.exports = () => ({});`);
127
+ });
128
+ });
129
+
17
130
  describe('addSentrySerializerToMetroConfig', () => {
18
131
  it('add to empty config', () => {
19
132
  const mod = parseModule(`module.exports = {