@sentry/wizard 3.9.1 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/lib/Constants.d.ts +1 -0
  3. package/dist/lib/Constants.js +5 -0
  4. package/dist/lib/Constants.js.map +1 -1
  5. package/dist/lib/Steps/ChooseIntegration.js +7 -0
  6. package/dist/lib/Steps/ChooseIntegration.js.map +1 -1
  7. package/dist/lib/Steps/Integrations/Cordova.js +5 -1
  8. package/dist/lib/Steps/Integrations/Cordova.js.map +1 -1
  9. package/dist/lib/Steps/Integrations/Remix.d.ts +12 -0
  10. package/dist/lib/Steps/Integrations/Remix.js +98 -0
  11. package/dist/lib/Steps/Integrations/Remix.js.map +1 -0
  12. package/dist/package.json +1 -1
  13. package/dist/src/remix/codemods/handle-error.d.ts +2 -0
  14. package/dist/src/remix/codemods/handle-error.js +70 -0
  15. package/dist/src/remix/codemods/handle-error.js.map +1 -0
  16. package/dist/src/remix/codemods/root-v1.d.ts +1 -0
  17. package/dist/src/remix/codemods/root-v1.js +133 -0
  18. package/dist/src/remix/codemods/root-v1.js.map +1 -0
  19. package/dist/src/remix/codemods/root-v2.d.ts +1 -0
  20. package/dist/src/remix/codemods/root-v2.js +134 -0
  21. package/dist/src/remix/codemods/root-v2.js.map +1 -0
  22. package/dist/src/remix/remix-wizard.d.ts +2 -0
  23. package/dist/src/remix/remix-wizard.js +206 -0
  24. package/dist/src/remix/remix-wizard.js.map +1 -0
  25. package/dist/src/remix/sdk-setup.d.ts +18 -0
  26. package/dist/src/remix/sdk-setup.js +293 -0
  27. package/dist/src/remix/sdk-setup.js.map +1 -0
  28. package/dist/src/remix/templates.d.ts +2 -0
  29. package/dist/src/remix/templates.js +6 -0
  30. package/dist/src/remix/templates.js.map +1 -0
  31. package/dist/src/remix/utils.d.ts +6 -0
  32. package/dist/src/remix/utils.js +55 -0
  33. package/dist/src/remix/utils.js.map +1 -0
  34. package/dist/src/sourcemaps/sourcemaps-wizard.js +23 -12
  35. package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
  36. package/dist/src/sourcemaps/tools/remix.d.ts +3 -0
  37. package/dist/src/sourcemaps/tools/remix.js +125 -0
  38. package/dist/src/sourcemaps/tools/remix.js.map +1 -0
  39. package/dist/src/sourcemaps/tools/sentry-cli.js +6 -3
  40. package/dist/src/sourcemaps/tools/sentry-cli.js.map +1 -1
  41. package/dist/src/sourcemaps/utils/detect-tool.d.ts +1 -1
  42. package/dist/src/sourcemaps/utils/detect-tool.js +1 -0
  43. package/dist/src/sourcemaps/utils/detect-tool.js.map +1 -1
  44. package/dist/src/utils/clack-utils.d.ts +3 -2
  45. package/dist/src/utils/clack-utils.js +39 -2
  46. package/dist/src/utils/clack-utils.js.map +1 -1
  47. package/lib/Constants.ts +5 -0
  48. package/lib/Steps/ChooseIntegration.ts +7 -0
  49. package/lib/Steps/Integrations/Cordova.ts +5 -1
  50. package/lib/Steps/Integrations/Remix.ts +32 -0
  51. package/package.json +1 -1
  52. package/src/remix/codemods/handle-error.ts +67 -0
  53. package/src/remix/codemods/root-v1.ts +91 -0
  54. package/src/remix/codemods/root-v2.ts +84 -0
  55. package/src/remix/remix-wizard.ts +137 -0
  56. package/src/remix/sdk-setup.ts +300 -0
  57. package/src/remix/templates.ts +15 -0
  58. package/src/remix/utils.ts +41 -0
  59. package/src/sourcemaps/sourcemaps-wizard.ts +9 -0
  60. package/src/sourcemaps/tools/remix.ts +90 -0
  61. package/src/sourcemaps/tools/sentry-cli.ts +5 -3
  62. package/src/sourcemaps/utils/detect-tool.ts +3 -1
  63. package/src/utils/clack-utils.ts +56 -2
@@ -0,0 +1,300 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+
3
+ import type { Program } from '@babel/types';
4
+
5
+ // @ts-expect-error - magicast is ESM and TS complains about that. It works though
6
+ import type { ProxifiedModule } from 'magicast';
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import * as url from 'url';
11
+
12
+ // @ts-expect-error - clack is ESM and TS complains about that. It works though
13
+ import clack from '@clack/prompts';
14
+ import chalk from 'chalk';
15
+ import { parse } from 'semver';
16
+
17
+ // @ts-expect-error - magicast is ESM and TS complains about that. It works though
18
+ import { builders, generateCode, loadFile, writeFile } from 'magicast';
19
+ import { PackageDotJson, getPackageVersion } from '../utils/package-json';
20
+ import { getInitCallInsertionIndex, hasSentryContent } from './utils';
21
+ import { instrumentRootRouteV1 } from './codemods/root-v1';
22
+ import { instrumentRootRouteV2 } from './codemods/root-v2';
23
+ import { instrumentHandleError } from './codemods/handle-error';
24
+
25
+ export type PartialRemixConfig = {
26
+ unstable_dev?: boolean;
27
+ future?: {
28
+ v2_dev?: boolean;
29
+ v2_errorBoundary?: boolean;
30
+ v2_headers?: boolean;
31
+ v2_meta?: boolean;
32
+ v2_normalizeFormMethod?: boolean;
33
+ v2_routeConvention?: boolean;
34
+ };
35
+ };
36
+
37
+ const REMIX_CONFIG_FILE = 'remix.config.js';
38
+
39
+ function insertClientInitCall(
40
+ dsn: string,
41
+ originalHooksMod: ProxifiedModule<any>,
42
+ ): void {
43
+ const initCall = builders.functionCall('Sentry.init', {
44
+ dsn,
45
+ tracesSampleRate: 1.0,
46
+ replaysSessionSampleRate: 0.1,
47
+ replaysOnErrorSampleRate: 1.0,
48
+ integrations: [
49
+ builders.newExpression('Sentry.BrowserTracing', {
50
+ routingInstrumentation: builders.functionCall(
51
+ 'Sentry.remixRouterInstrumentation',
52
+ builders.raw('useEffect'),
53
+ builders.raw('useLocation'),
54
+ builders.raw('useMatches'),
55
+ ),
56
+ }),
57
+ builders.newExpression('Sentry.Replay'),
58
+ ],
59
+ });
60
+
61
+ const originalHooksModAST = originalHooksMod.$ast as Program;
62
+ const initCallInsertionIndex = getInitCallInsertionIndex(originalHooksModAST);
63
+
64
+ originalHooksModAST.body.splice(
65
+ initCallInsertionIndex,
66
+ 0,
67
+ // @ts-expect-error - string works here because the AST is proxified by magicast
68
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
69
+ generateCode(initCall).code,
70
+ );
71
+ }
72
+
73
+ function insertServerInitCall(
74
+ dsn: string,
75
+ originalHooksMod: ProxifiedModule<any>,
76
+ ) {
77
+ const initCall = builders.functionCall('Sentry.init', {
78
+ dsn,
79
+ tracesSampleRate: 1.0,
80
+ });
81
+
82
+ const originalHooksModAST = originalHooksMod.$ast as Program;
83
+
84
+ const initCallInsertionIndex = getInitCallInsertionIndex(originalHooksModAST);
85
+
86
+ originalHooksModAST.body.splice(
87
+ initCallInsertionIndex,
88
+ 0,
89
+ // @ts-expect-error - string works here because the AST is proxified by magicast
90
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
91
+ generateCode(initCall).code,
92
+ );
93
+ }
94
+
95
+ export function isRemixV2(
96
+ remixConfig: PartialRemixConfig,
97
+ packageJson: PackageDotJson,
98
+ ): boolean {
99
+ const remixVersion = getPackageVersion('@remix-run/react', packageJson);
100
+ const remixVersionMajor = remixVersion && parse(remixVersion)?.major;
101
+ const isV2Remix = remixVersionMajor && remixVersionMajor >= 2;
102
+
103
+ return isV2Remix || remixConfig?.future?.v2_errorBoundary || false;
104
+ }
105
+
106
+ export async function loadRemixConfig(): Promise<PartialRemixConfig> {
107
+ const configFilePath = path.join(process.cwd(), REMIX_CONFIG_FILE);
108
+
109
+ try {
110
+ if (!fs.existsSync(configFilePath)) {
111
+ return {};
112
+ }
113
+
114
+ const configUrl = url.pathToFileURL(configFilePath).href;
115
+ const remixConfigModule = (await import(configUrl)) as {
116
+ default: PartialRemixConfig;
117
+ };
118
+
119
+ return remixConfigModule?.default || {};
120
+ } catch (e: unknown) {
121
+ clack.log.error(`Couldn't load ${REMIX_CONFIG_FILE}.`);
122
+ clack.log.info(
123
+ chalk.dim(
124
+ typeof e === 'object' && e != null && 'toString' in e
125
+ ? e.toString()
126
+ : typeof e === 'string'
127
+ ? e
128
+ : 'Unknown error',
129
+ ),
130
+ );
131
+
132
+ return {};
133
+ }
134
+ }
135
+
136
+ export async function instrumentRootRoute(
137
+ isV2?: boolean,
138
+ isTS?: boolean,
139
+ ): Promise<void> {
140
+ const rootFilename = `root.${isTS ? 'tsx' : 'jsx'}`;
141
+
142
+ if (isV2) {
143
+ await instrumentRootRouteV2(rootFilename);
144
+ } else {
145
+ await instrumentRootRouteV1(rootFilename);
146
+ }
147
+
148
+ clack.log.success(
149
+ `Successfully instrumented root route ${chalk.cyan(rootFilename)}.`,
150
+ );
151
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access */
152
+ }
153
+
154
+ export async function updateBuildScript(): Promise<void> {
155
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
156
+ // Add sourcemaps option to build script
157
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
158
+ const packageJsonString = (
159
+ await fs.promises.readFile(packageJsonPath)
160
+ ).toString();
161
+ const packageJson = JSON.parse(packageJsonString);
162
+
163
+ if (!packageJson.scripts) {
164
+ packageJson.scripts = {};
165
+ }
166
+
167
+ if (!packageJson.scripts.build) {
168
+ packageJson.scripts.build =
169
+ 'remix build --sourcemap && sentry-upload-sourcemaps';
170
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
171
+ } else if (packageJson.scripts.build.includes('remix build')) {
172
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
173
+ packageJson.scripts.build = packageJson.scripts.build.replace(
174
+ 'remix build',
175
+ 'remix build --sourcemap && sentry-upload-sourcemaps',
176
+ );
177
+ }
178
+
179
+ await fs.promises.writeFile(
180
+ packageJsonPath,
181
+ JSON.stringify(packageJson, null, 2),
182
+ );
183
+
184
+ clack.log.success(
185
+ `Successfully updated ${chalk.cyan('build')} script in ${chalk.cyan(
186
+ 'package.json',
187
+ )} to generate and upload sourcemaps.`,
188
+ );
189
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access */
190
+ }
191
+
192
+ export async function initializeSentryOnEntryClient(
193
+ dsn: string,
194
+ isTS: boolean,
195
+ ): Promise<void> {
196
+ const clientEntryFilename = `entry.client.${isTS ? 'tsx' : 'jsx'}`;
197
+
198
+ const originalEntryClient = path.join(
199
+ process.cwd(),
200
+ 'app',
201
+ clientEntryFilename,
202
+ );
203
+
204
+ const originalEntryClientMod = await loadFile(originalEntryClient);
205
+
206
+ if (hasSentryContent(originalEntryClient, originalEntryClientMod.$code)) {
207
+ return;
208
+ }
209
+
210
+ originalEntryClientMod.imports.$add({
211
+ from: '@sentry/remix',
212
+ imported: '*',
213
+ local: 'Sentry',
214
+ });
215
+
216
+ originalEntryClientMod.imports.$add({
217
+ from: 'react',
218
+ imported: 'useEffect',
219
+ local: 'useEffect',
220
+ });
221
+
222
+ originalEntryClientMod.imports.$add({
223
+ from: '@remix-run/react',
224
+ imported: 'useLocation',
225
+ local: 'useLocation',
226
+ });
227
+
228
+ originalEntryClientMod.imports.$add({
229
+ from: '@remix-run/react',
230
+ imported: 'useMatches',
231
+ local: 'useMatches',
232
+ });
233
+
234
+ insertClientInitCall(dsn, originalEntryClientMod);
235
+
236
+ await writeFile(
237
+ originalEntryClientMod.$ast,
238
+ path.join(process.cwd(), 'app', clientEntryFilename),
239
+ );
240
+
241
+ clack.log.success(
242
+ `Successfully initialized Sentry on client entry point ${chalk.cyan(
243
+ clientEntryFilename,
244
+ )}`,
245
+ );
246
+ }
247
+
248
+ export async function initializeSentryOnEntryServer(
249
+ dsn: string,
250
+ isV2: boolean,
251
+ isTS: boolean,
252
+ ): Promise<void> {
253
+ const serverEntryFilename = `entry.server.${isTS ? 'tsx' : 'jsx'}`;
254
+
255
+ const originalEntryServer = path.join(
256
+ process.cwd(),
257
+ 'app',
258
+ serverEntryFilename,
259
+ );
260
+
261
+ const originalEntryServerMod = await loadFile(originalEntryServer);
262
+
263
+ if (hasSentryContent(originalEntryServer, originalEntryServerMod.$code)) {
264
+ return;
265
+ }
266
+
267
+ originalEntryServerMod.imports.$add({
268
+ from: '@sentry/remix',
269
+ imported: '*',
270
+ local: 'Sentry',
271
+ });
272
+
273
+ insertServerInitCall(dsn, originalEntryServerMod);
274
+
275
+ if (isV2) {
276
+ const handleErrorInstrumented = instrumentHandleError(
277
+ originalEntryServerMod,
278
+ serverEntryFilename,
279
+ );
280
+
281
+ if (handleErrorInstrumented) {
282
+ clack.log.success(
283
+ `Instrumented ${chalk.cyan('handleError')} in ${chalk.cyan(
284
+ `${serverEntryFilename}`,
285
+ )}`,
286
+ );
287
+ }
288
+ }
289
+
290
+ await writeFile(
291
+ originalEntryServerMod.$ast,
292
+ path.join(process.cwd(), 'app', serverEntryFilename),
293
+ );
294
+
295
+ clack.log.success(
296
+ `Successfully initialized Sentry on server entry point ${chalk.cyan(
297
+ serverEntryFilename,
298
+ )}.`,
299
+ );
300
+ }
@@ -0,0 +1,15 @@
1
+ export const ERROR_BOUNDARY_TEMPLATE_V2 = `const ErrorBoundary = () => {
2
+ const error = useRouteError();
3
+ captureRemixErrorBoundaryError(error);
4
+ return <div>Something went wrong</div>;
5
+ };
6
+ `;
7
+
8
+ export const HANDLE_ERROR_TEMPLATE_V2 = `function handleError(error) {
9
+ if (error instanceof Error) {
10
+ Sentry.captureRemixErrorBoundaryError(error);
11
+ } else {
12
+ Sentry.captureException(error);
13
+ }
14
+ }
15
+ `;
@@ -0,0 +1,41 @@
1
+ import type { Program } from '@babel/types';
2
+
3
+ import * as path from 'path';
4
+
5
+ // @ts-expect-error - clack is ESM and TS complains about that. It works though
6
+ import clack from '@clack/prompts';
7
+ import chalk from 'chalk';
8
+
9
+ // Copied from sveltekit wizard
10
+ export function hasSentryContent(
11
+ fileName: string,
12
+ fileContent: string,
13
+ ): boolean {
14
+ const includesContent = fileContent.includes('@sentry/remix');
15
+
16
+ if (includesContent) {
17
+ clack.log.warn(
18
+ `File ${chalk.cyan(path.basename(fileName))} already contains Sentry code.
19
+ Skipping adding Sentry functionality to ${chalk.cyan(
20
+ path.basename(fileName),
21
+ )}.`,
22
+ );
23
+ }
24
+
25
+ return includesContent;
26
+ }
27
+
28
+ /**
29
+ * We want to insert the init call on top of the file but after all import statements
30
+ */
31
+ export function getInitCallInsertionIndex(
32
+ originalHooksModAST: Program,
33
+ ): number {
34
+ for (let x = originalHooksModAST.body.length - 1; x >= 0; x--) {
35
+ if (originalHooksModAST.body[x].type === 'ImportDeclaration') {
36
+ return x + 1;
37
+ }
38
+ }
39
+
40
+ return 0;
41
+ }
@@ -29,6 +29,7 @@ import { checkIfMoreSuitableWizardExistsAndAskForRedirect } from './utils/other-
29
29
  import { configureAngularSourcemapGenerationFlow } from './tools/angular';
30
30
  import { detectUsedTool, SupportedTools } from './utils/detect-tool';
31
31
  import { configureNextJsSourceMapsUpload } from './tools/nextjs';
32
+ import { configureRemixSourceMapsUpload } from './tools/remix';
32
33
 
33
34
  export async function runSourcemapsWizard(
34
35
  options: WizardOptions,
@@ -133,6 +134,11 @@ async function askForUsedBundlerTool(): Promise<SupportedTools> {
133
134
  value: 'nextjs',
134
135
  hint: 'Select this option if you want to set up source maps in a NextJS project.',
135
136
  },
137
+ {
138
+ label: 'Remix',
139
+ value: 'remix',
140
+ hint: 'Select this option if you want to set up source maps in a Remix project.',
141
+ },
136
142
  {
137
143
  label: 'Webpack',
138
144
  value: 'webpack',
@@ -204,6 +210,9 @@ async function startToolSetupFlow(
204
210
  case 'nextjs':
205
211
  await configureNextJsSourceMapsUpload(options, wizardOptions);
206
212
  break;
213
+ case 'remix':
214
+ await configureRemixSourceMapsUpload(options, wizardOptions);
215
+ break;
207
216
  default:
208
217
  await configureSentryCLI(options);
209
218
  break;
@@ -0,0 +1,90 @@
1
+ // @ts-ignore - clack is ESM and TS complains about that. It works though
2
+ import * as clack from '@clack/prompts';
3
+ import chalk from 'chalk';
4
+ import { runRemixWizard } from '../../remix/remix-wizard';
5
+ import { traceStep } from '../../telemetry';
6
+ import { abortIfCancelled } from '../../utils/clack-utils';
7
+ import { WizardOptions } from '../../utils/types';
8
+ import { SourceMapUploadToolConfigurationOptions } from './types';
9
+
10
+ import * as Sentry from '@sentry/node';
11
+
12
+ export const configureRemixSourceMapsUpload = async (
13
+ options: SourceMapUploadToolConfigurationOptions,
14
+ wizardOptions: WizardOptions,
15
+ ) => {
16
+ clack.log
17
+ .info(`Source Maps upload for Remix is configured automatically by default if you run the Sentry Wizard for Remix.
18
+ But don't worry, we can redirect you to the wizard now!
19
+ In case you already tried the wizard, we can also show you how to configure your ${chalk.cyan(
20
+ 'remix.config.js',
21
+ )} file manually instead.`);
22
+
23
+ const shouldRedirect: boolean = await abortIfCancelled(
24
+ clack.select({
25
+ message: 'Do you want to run the Sentry Wizard for Remix now?',
26
+ options: [
27
+ {
28
+ label: 'Yes, run the wizard!',
29
+ value: true,
30
+ hint: 'The wizard can also configure your SDK setup',
31
+ },
32
+ {
33
+ label: 'No, show me how to configure it manually',
34
+ value: false,
35
+ },
36
+ ],
37
+ }),
38
+ );
39
+
40
+ Sentry.setTag('redirect-remix-wizard', shouldRedirect);
41
+
42
+ if (shouldRedirect) {
43
+ await traceStep('run-remix-wizard', () => runRemixWizard(wizardOptions));
44
+ clack.intro('Sentry Source Maps Upload Configuration Wizard');
45
+ clack.log.info(
46
+ "Welcome back to the Source Maps wizard - we're almost done ;)",
47
+ );
48
+ } else {
49
+ clack.log.step(
50
+ `Build your app with ${chalk.cyan(
51
+ 'remix build --sourcemap',
52
+ )}, then upload your source maps using ${chalk.cyan(
53
+ 'sentry-upload-sourcemaps',
54
+ )} cli tool.`,
55
+ );
56
+
57
+ clack.log.step(
58
+ `You can add ${chalk.cyan(
59
+ 'sentry-upload-sourcemaps',
60
+ )} to your build script in ${chalk.cyan('package.json')} like this:`,
61
+ );
62
+
63
+ // Intentially logging directly to console here so that the code can be copied/pasted directly
64
+ // eslint-disable-next-line no-console
65
+ console.log(codeSnippet);
66
+
67
+ clack.log.step(`or run it manually after building your app.
68
+
69
+ To see all available options for ${chalk.cyan(
70
+ 'sentry-upload-sourcemaps',
71
+ )}, run ${chalk.cyan('sentry-upload-sourcemaps --help')}
72
+ `);
73
+
74
+ await abortIfCancelled(
75
+ clack.select({
76
+ message: 'Did you finish configuring your build and prod scripts?',
77
+ options: [{ label: 'Yes, continue!', value: true }],
78
+ initialValue: true,
79
+ }),
80
+ );
81
+ }
82
+ };
83
+
84
+ const codeSnippet = chalk.gray(`
85
+ "scripts": {
86
+ ${chalk.greenBright(
87
+ '"build": "remix build --sourcemap && sentry-upload-sourcemaps"',
88
+ )};
89
+ }
90
+ `);
@@ -211,8 +211,9 @@ async function addSentryCommandToBuildCommand(
211
211
  // Often, 'build' is the prod build command, so we favour it.
212
212
  // If it's not there, commands that include 'build' might be the prod build command.
213
213
  let buildCommand =
214
- packageDotJson.scripts.build ||
215
- allNpmScripts.find((s) => s.toLocaleLowerCase().includes('build'));
214
+ typeof packageDotJson.scripts.build === 'string'
215
+ ? 'build'
216
+ : allNpmScripts.find((s) => s.toLocaleLowerCase().includes('build'));
216
217
 
217
218
  const isProdBuildCommand =
218
219
  !!buildCommand &&
@@ -252,7 +253,8 @@ Please add it manually to your prod build command.`,
252
253
 
253
254
  packageDotJson.scripts[
254
255
  buildCommand
255
- ] = `${buildCommand} && ${pacMan} run ${SENTRY_NPM_SCRIPT_NAME}`;
256
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
257
+ ] = `${packageDotJson.scripts[buildCommand]} && ${pacMan} run ${SENTRY_NPM_SCRIPT_NAME}`;
256
258
 
257
259
  await fs.promises.writeFile(
258
260
  path.join(process.cwd(), 'package.json'),
@@ -10,7 +10,8 @@ export type SupportedTools =
10
10
  | 'sentry-cli'
11
11
  | 'create-react-app'
12
12
  | 'angular'
13
- | 'nextjs';
13
+ | 'nextjs'
14
+ | 'remix';
14
15
 
15
16
  // A map of package names pointing to the tool slug.
16
17
  // The order is important, because we want to detect the most specific tool first.
@@ -25,6 +26,7 @@ export const TOOL_PACKAGE_MAP: Record<string, SupportedTools> = {
25
26
  esbuild: 'esbuild',
26
27
  rollup: 'rollup',
27
28
  typescript: 'tsc',
29
+ remix: 'remix',
28
30
  };
29
31
 
30
32
  export async function detectUsedTool(): Promise<SupportedTools> {
@@ -66,6 +66,7 @@ export function printWelcome(options: {
66
66
  wizardName: string;
67
67
  promoCode?: string;
68
68
  message?: string;
69
+ telemetryEnabled?: boolean;
69
70
  }): void {
70
71
  let wizardPackage: { version?: string } = {};
71
72
 
@@ -96,6 +97,10 @@ export function printWelcome(options: {
96
97
  welcomeText += `\n\nVersion: ${wizardPackage.version}`;
97
98
  }
98
99
 
100
+ if (options.telemetryEnabled) {
101
+ welcomeText += `\n\nYou are using the Sentry Wizard with telemetry enabled. This helps us improve the Wizard.\nYou can disable it at any time by running \`sentry-wizard --disable-telemetry\`.`;
102
+ }
103
+
99
104
  clack.note(welcomeText);
100
105
  }
101
106
 
@@ -132,7 +137,11 @@ export async function askToInstallSentryCLI(): Promise<boolean> {
132
137
  export async function askForWizardLogin(options: {
133
138
  url: string;
134
139
  promoCode?: string;
135
- platform?: 'javascript-nextjs' | 'javascript-sveltekit' | 'apple-ios';
140
+ platform?:
141
+ | 'javascript-nextjs'
142
+ | 'javascript-remix'
143
+ | 'javascript-sveltekit'
144
+ | 'apple-ios';
136
145
  }): Promise<WizardProjectData> {
137
146
  Sentry.setTag('has-promo-code', !!options.promoCode);
138
147
 
@@ -408,7 +417,48 @@ export async function askForSelfHosted(urlFromArgs?: string): Promise<{
408
417
  return { url: validUrl, selfHosted: true };
409
418
  }
410
419
 
411
- export async function addSentryCliRc(authToken: string): Promise<void> {
420
+ async function addOrgAndProjectToSentryCliRc(
421
+ org: string,
422
+ project: string,
423
+ ): Promise<void> {
424
+ const clircContents = fs.readFileSync(
425
+ path.join(process.cwd(), SENTRY_CLI_RC_FILE),
426
+ 'utf8',
427
+ );
428
+
429
+ const likelyAlreadyHasOrgAndProject = !!(
430
+ clircContents.includes('[defaults]') &&
431
+ clircContents.match(/org=./g) &&
432
+ clircContents.match(/project=./g)
433
+ );
434
+
435
+ if (likelyAlreadyHasOrgAndProject) {
436
+ clack.log.warn(
437
+ `${chalk.bold(
438
+ SENTRY_CLI_RC_FILE,
439
+ )} already has org and project. Will not add them.`,
440
+ );
441
+ } else {
442
+ try {
443
+ await fs.promises.appendFile(
444
+ path.join(process.cwd(), SENTRY_CLI_RC_FILE),
445
+ `\n[defaults]\norg=${org}\nproject=${project}\n`,
446
+ );
447
+ } catch (e) {
448
+ clack.log.warn(
449
+ `${chalk.bold(
450
+ SENTRY_CLI_RC_FILE,
451
+ )} could not be updated with org and project.`,
452
+ );
453
+ }
454
+ }
455
+ }
456
+
457
+ export async function addSentryCliRc(
458
+ authToken: string,
459
+ orgSlug?: string,
460
+ projectSlug?: string,
461
+ ): Promise<void> {
412
462
  const clircExists = fs.existsSync(
413
463
  path.join(process.cwd(), SENTRY_CLI_RC_FILE),
414
464
  );
@@ -469,6 +519,10 @@ export async function addSentryCliRc(authToken: string): Promise<void> {
469
519
  }
470
520
  }
471
521
 
522
+ if (orgSlug && projectSlug) {
523
+ await addOrgAndProjectToSentryCliRc(orgSlug, projectSlug);
524
+ }
525
+
472
526
  await addAuthTokenFileToGitIgnore(SENTRY_CLI_RC_FILE);
473
527
  }
474
528