@sentry/wizard 3.10.0 → 3.12.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 +54 -7
- package/dist/lib/Constants.d.ts +1 -0
- package/dist/lib/Constants.js +5 -0
- package/dist/lib/Constants.js.map +1 -1
- package/dist/lib/Steps/ChooseIntegration.js +8 -4
- package/dist/lib/Steps/ChooseIntegration.js.map +1 -1
- package/dist/lib/Steps/Integrations/Android.d.ts +9 -0
- package/dist/lib/Steps/Integrations/Android.js +86 -0
- package/dist/lib/Steps/Integrations/Android.js.map +1 -0
- package/dist/lib/Steps/Integrations/ReactNative.js +3 -3
- package/dist/lib/Steps/Integrations/ReactNative.js.map +1 -1
- package/dist/lib/Steps/PromptForParameters.js +36 -3
- package/dist/lib/Steps/PromptForParameters.js.map +1 -1
- package/dist/lib/Steps/SentryProjectSelector.js +1 -1
- package/dist/lib/Steps/SentryProjectSelector.js.map +1 -1
- package/dist/package.json +4 -3
- package/dist/src/android/android-wizard.d.ts +2 -0
- package/dist/src/android/android-wizard.js +225 -0
- package/dist/src/android/android-wizard.js.map +1 -0
- package/dist/src/android/code-tools.d.ts +47 -0
- package/dist/src/android/code-tools.js +173 -0
- package/dist/src/android/code-tools.js.map +1 -0
- package/dist/src/android/gradle.d.ts +62 -0
- package/dist/src/android/gradle.js +286 -0
- package/dist/src/android/gradle.js.map +1 -0
- package/dist/src/android/manifest.d.ts +57 -0
- package/dist/src/android/manifest.js +183 -0
- package/dist/src/android/manifest.js.map +1 -0
- package/dist/src/android/templates.d.ts +11 -0
- package/dist/src/android/templates.js +34 -0
- package/dist/src/android/templates.js.map +1 -0
- package/dist/src/apple/apple-wizard.js +123 -64
- package/dist/src/apple/apple-wizard.js.map +1 -1
- package/dist/src/apple/cocoapod.js +4 -3
- package/dist/src/apple/cocoapod.js.map +1 -1
- package/dist/src/apple/code-tools.d.ts +1 -1
- package/dist/src/apple/code-tools.js +43 -19
- package/dist/src/apple/code-tools.js.map +1 -1
- package/dist/src/apple/fastlane.d.ts +1 -1
- package/dist/src/apple/fastlane.js +12 -6
- package/dist/src/apple/fastlane.js.map +1 -1
- package/dist/src/apple/templates.d.ts +2 -2
- package/dist/src/apple/templates.js +4 -4
- package/dist/src/apple/templates.js.map +1 -1
- package/dist/src/apple/xcode-manager.d.ts +19 -3
- package/dist/src/apple/xcode-manager.js +126 -24
- package/dist/src/apple/xcode-manager.js.map +1 -1
- package/dist/src/nextjs/nextjs-wizard.js +49 -11
- package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
- package/dist/src/nextjs/templates.d.ts +2 -0
- package/dist/src/nextjs/templates.js +6 -2
- package/dist/src/nextjs/templates.js.map +1 -1
- package/dist/src/remix/remix-wizard.js +10 -20
- package/dist/src/remix/remix-wizard.js.map +1 -1
- package/dist/src/sourcemaps/sourcemaps-wizard.js +26 -13
- package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
- package/dist/src/sourcemaps/tools/nextjs.js +1 -1
- package/dist/src/sourcemaps/tools/nextjs.js.map +1 -1
- package/dist/src/sourcemaps/tools/sentry-cli.js +19 -16
- package/dist/src/sourcemaps/tools/sentry-cli.js.map +1 -1
- package/dist/src/sourcemaps/tools/vite.d.ts +2 -1
- package/dist/src/sourcemaps/tools/vite.js +123 -111
- package/dist/src/sourcemaps/tools/vite.js.map +1 -1
- package/dist/src/sourcemaps/tools/webpack.d.ts +6 -1
- package/dist/src/sourcemaps/tools/webpack.js +290 -25
- package/dist/src/sourcemaps/tools/webpack.js.map +1 -1
- package/dist/src/sourcemaps/utils/detect-tool.d.ts +1 -1
- package/dist/src/sourcemaps/utils/detect-tool.js.map +1 -1
- package/dist/src/sveltekit/sdk-setup.js +5 -5
- package/dist/src/sveltekit/sdk-setup.js.map +1 -1
- package/dist/src/sveltekit/sveltekit-wizard.js +34 -44
- package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
- package/dist/src/telemetry.js +1 -0
- package/dist/src/telemetry.js.map +1 -1
- package/dist/src/utils/ast-utils.d.ts +9 -5
- package/dist/src/utils/ast-utils.js +26 -11
- package/dist/src/utils/ast-utils.js.map +1 -1
- package/dist/src/utils/clack-utils.d.ts +74 -28
- package/dist/src/utils/clack-utils.js +427 -264
- package/dist/src/utils/clack-utils.js.map +1 -1
- package/dist/src/utils/package-manager.d.ts +10 -0
- package/dist/{lib/Helper/PackageManager.js → src/utils/package-manager.js} +42 -74
- package/dist/src/utils/package-manager.js.map +1 -0
- package/dist/src/utils/release-registry.d.ts +1 -0
- package/dist/src/utils/release-registry.js +68 -0
- package/dist/src/utils/release-registry.js.map +1 -0
- package/dist/src/utils/sentrycli-utils.d.ts +4 -0
- package/dist/src/utils/sentrycli-utils.js +41 -0
- package/dist/src/utils/sentrycli-utils.js.map +1 -0
- package/dist/test/android/code-tools.test.d.ts +1 -0
- package/dist/test/android/code-tools.test.js +34 -0
- package/dist/test/android/code-tools.test.js.map +1 -0
- package/dist/test/sourcemaps/tools/vite.test.d.ts +1 -0
- package/dist/test/sourcemaps/tools/vite.test.js +132 -0
- package/dist/test/sourcemaps/tools/vite.test.js.map +1 -0
- package/dist/test/sourcemaps/tools/webpack.test.d.ts +1 -0
- package/dist/test/sourcemaps/tools/webpack.test.js +179 -0
- package/dist/test/sourcemaps/tools/webpack.test.js.map +1 -0
- package/dist/test/utils/ast-utils.test.js +42 -7
- package/dist/test/utils/ast-utils.test.js.map +1 -1
- package/dist/test/utils/clack-utils.test.d.ts +1 -0
- package/dist/test/utils/clack-utils.test.js +200 -0
- package/dist/test/utils/clack-utils.test.js.map +1 -0
- package/lib/Constants.ts +5 -0
- package/lib/Steps/ChooseIntegration.ts +7 -3
- package/lib/Steps/Integrations/Android.ts +23 -0
- package/lib/Steps/Integrations/ReactNative.ts +9 -3
- package/lib/Steps/PromptForParameters.ts +48 -3
- package/lib/Steps/SentryProjectSelector.ts +3 -1
- package/package.json +4 -3
- package/src/android/android-wizard.ts +204 -0
- package/src/android/code-tools.ts +170 -0
- package/src/android/gradle.ts +250 -0
- package/src/android/manifest.ts +180 -0
- package/src/android/templates.ts +88 -0
- package/src/apple/apple-wizard.ts +113 -35
- package/src/apple/cocoapod.ts +6 -3
- package/src/apple/code-tools.ts +46 -18
- package/src/apple/fastlane.ts +6 -12
- package/src/apple/templates.ts +2 -8
- package/src/apple/xcode-manager.ts +167 -25
- package/src/nextjs/nextjs-wizard.ts +72 -8
- package/src/nextjs/templates.ts +16 -2
- package/src/remix/remix-wizard.ts +10 -15
- package/src/sourcemaps/sourcemaps-wizard.ts +19 -5
- package/src/sourcemaps/tools/nextjs.ts +2 -2
- package/src/sourcemaps/tools/sentry-cli.ts +8 -7
- package/src/sourcemaps/tools/vite.ts +143 -79
- package/src/sourcemaps/tools/webpack.ts +369 -30
- package/src/sourcemaps/utils/detect-tool.ts +2 -1
- package/src/sveltekit/sdk-setup.ts +10 -6
- package/src/sveltekit/sveltekit-wizard.ts +5 -14
- package/src/telemetry.ts +2 -0
- package/src/utils/ast-utils.ts +29 -11
- package/src/utils/clack-utils.ts +485 -283
- package/src/utils/package-manager.ts +61 -0
- package/src/utils/release-registry.ts +19 -0
- package/src/utils/sentrycli-utils.ts +22 -0
- package/test/android/code-tools.test.ts +49 -0
- package/test/sourcemaps/tools/vite.test.ts +149 -0
- package/test/sourcemaps/tools/webpack.test.ts +303 -0
- package/test/utils/ast-utils.test.ts +28 -9
- package/test/utils/clack-utils.test.ts +142 -0
- package/dist/lib/Helper/PackageManager.d.ts +0 -22
- package/dist/lib/Helper/PackageManager.js.map +0 -1
- package/dist/src/utils/vendor/clack-custom-select.d.ts +0 -21
- package/dist/src/utils/vendor/clack-custom-select.js +0 -137
- package/dist/src/utils/vendor/clack-custom-select.js.map +0 -1
- package/lib/Helper/PackageManager.ts +0 -59
- package/src/utils/vendor/clack-custom-select.ts +0 -160
package/src/utils/clack-utils.ts
CHANGED
|
@@ -7,12 +7,17 @@ import * as fs from 'fs';
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { setInterval } from 'timers';
|
|
9
9
|
import { URL } from 'url';
|
|
10
|
-
import { promisify } from 'util';
|
|
11
10
|
import * as Sentry from '@sentry/node';
|
|
12
|
-
import { windowedSelect } from './vendor/clack-custom-select';
|
|
13
11
|
import { hasPackageInstalled, PackageDotJson } from './package-json';
|
|
14
12
|
import { SentryProjectData, WizardOptions } from './types';
|
|
15
13
|
import { traceStep } from '../telemetry';
|
|
14
|
+
import {
|
|
15
|
+
detectPackageManger,
|
|
16
|
+
PackageManager,
|
|
17
|
+
installPackageWithPackageManager,
|
|
18
|
+
packageManagers,
|
|
19
|
+
} from './package-manager';
|
|
20
|
+
import { debug } from './debug';
|
|
16
21
|
|
|
17
22
|
const opn = require('opn') as (
|
|
18
23
|
url: string,
|
|
@@ -20,6 +25,7 @@ const opn = require('opn') as (
|
|
|
20
25
|
|
|
21
26
|
export const SENTRY_DOT_ENV_FILE = '.env.sentry-build-plugin';
|
|
22
27
|
export const SENTRY_CLI_RC_FILE = '.sentryclirc';
|
|
28
|
+
export const SENTRY_PROPERTIES_FILE = 'sentry.properties';
|
|
23
29
|
|
|
24
30
|
const SAAS_URL = 'https://sentry.io/';
|
|
25
31
|
|
|
@@ -30,6 +36,38 @@ interface WizardProjectData {
|
|
|
30
36
|
projects: SentryProjectData[];
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
export interface CliSetupConfig {
|
|
40
|
+
filename: string;
|
|
41
|
+
name: string;
|
|
42
|
+
|
|
43
|
+
likelyAlreadyHasAuthToken(contents: string): boolean;
|
|
44
|
+
tokenContent(authToken: string): string;
|
|
45
|
+
|
|
46
|
+
likelyAlreadyHasOrgAndProject(contents: string): boolean;
|
|
47
|
+
orgAndProjContent(org: string, project: string): string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const sourceMapsCliSetupConfig: CliSetupConfig = {
|
|
51
|
+
filename: SENTRY_CLI_RC_FILE,
|
|
52
|
+
name: 'source maps',
|
|
53
|
+
likelyAlreadyHasAuthToken: function (contents: string): boolean {
|
|
54
|
+
return !!(contents.includes('[auth]') && contents.match(/token=./g));
|
|
55
|
+
},
|
|
56
|
+
tokenContent: function (authToken: string): string {
|
|
57
|
+
return `[auth]\ntoken=${authToken}`;
|
|
58
|
+
},
|
|
59
|
+
likelyAlreadyHasOrgAndProject: function (contents: string): boolean {
|
|
60
|
+
return !!(
|
|
61
|
+
contents.includes('[defaults]') &&
|
|
62
|
+
contents.match(/org=./g) &&
|
|
63
|
+
contents.match(/project=./g)
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
orgAndProjContent: function (org: string, project: string): string {
|
|
67
|
+
return `[defaults]\norg=${org}\nproject=${project}`;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
33
71
|
export async function abort(message?: string, status?: number): Promise<never> {
|
|
34
72
|
clack.outro(message ?? 'Wizard setup cancelled.');
|
|
35
73
|
const sentryHub = Sentry.getCurrentHub();
|
|
@@ -134,119 +172,13 @@ export async function askToInstallSentryCLI(): Promise<boolean> {
|
|
|
134
172
|
);
|
|
135
173
|
}
|
|
136
174
|
|
|
137
|
-
export async function askForWizardLogin(options: {
|
|
138
|
-
url: string;
|
|
139
|
-
promoCode?: string;
|
|
140
|
-
platform?:
|
|
141
|
-
| 'javascript-nextjs'
|
|
142
|
-
| 'javascript-remix'
|
|
143
|
-
| 'javascript-sveltekit'
|
|
144
|
-
| 'apple-ios';
|
|
145
|
-
}): Promise<WizardProjectData> {
|
|
146
|
-
Sentry.setTag('has-promo-code', !!options.promoCode);
|
|
147
|
-
|
|
148
|
-
let hasSentryAccount = await clack.confirm({
|
|
149
|
-
message: 'Do you already have a Sentry account?',
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
hasSentryAccount = await abortIfCancelled(hasSentryAccount);
|
|
153
|
-
|
|
154
|
-
Sentry.setTag('already-has-sentry-account', hasSentryAccount);
|
|
155
|
-
|
|
156
|
-
let wizardHash: string;
|
|
157
|
-
try {
|
|
158
|
-
wizardHash = (
|
|
159
|
-
await axios.get<{ hash: string }>(`${options.url}api/0/wizard/`)
|
|
160
|
-
).data.hash;
|
|
161
|
-
} catch {
|
|
162
|
-
if (options.url !== SAAS_URL) {
|
|
163
|
-
clack.log.error('Loading Wizard failed. Did you provide the right URL?');
|
|
164
|
-
await abort(
|
|
165
|
-
chalk.red(
|
|
166
|
-
'Please check your configuration and try again.\n\n Let us know if you think this is an issue with the wizard or Sentry: https://github.com/getsentry/sentry-wizard/issues',
|
|
167
|
-
),
|
|
168
|
-
);
|
|
169
|
-
} else {
|
|
170
|
-
clack.log.error('Loading Wizard failed.');
|
|
171
|
-
await abort(
|
|
172
|
-
chalk.red(
|
|
173
|
-
'Please try again in a few minutes and let us know if this issue persists: https://github.com/getsentry/sentry-wizard/issues',
|
|
174
|
-
),
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const loginUrl = new URL(
|
|
180
|
-
`${options.url}account/settings/wizard/${wizardHash!}/`,
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
if (!hasSentryAccount) {
|
|
184
|
-
loginUrl.searchParams.set('signup', '1');
|
|
185
|
-
if (options.platform) {
|
|
186
|
-
loginUrl.searchParams.set('project_platform', options.platform);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (options.promoCode) {
|
|
191
|
-
loginUrl.searchParams.set('code', options.promoCode);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const urlToOpen = loginUrl.toString();
|
|
195
|
-
clack.log.info(
|
|
196
|
-
`${chalk.bold(
|
|
197
|
-
`If the browser window didn't open automatically, please open the following link to ${
|
|
198
|
-
hasSentryAccount ? 'log' : 'sign'
|
|
199
|
-
} into Sentry:`,
|
|
200
|
-
)}\n\n${chalk.cyan(urlToOpen)}`,
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
opn(urlToOpen).catch(() => {
|
|
204
|
-
// opn throws in environments that don't have a browser (e.g. remote shells) so we just noop here
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const loginSpinner = clack.spinner();
|
|
208
|
-
|
|
209
|
-
loginSpinner.start('Waiting for you to log in using the link above');
|
|
210
|
-
|
|
211
|
-
const data = await new Promise<WizardProjectData>((resolve) => {
|
|
212
|
-
const pollingInterval = setInterval(() => {
|
|
213
|
-
axios
|
|
214
|
-
.get<WizardProjectData>(`${options.url}api/0/wizard/${wizardHash}/`)
|
|
215
|
-
.then((result) => {
|
|
216
|
-
resolve(result.data);
|
|
217
|
-
clearTimeout(timeout);
|
|
218
|
-
clearInterval(pollingInterval);
|
|
219
|
-
void axios.delete(`${options.url}api/0/wizard/${wizardHash}/`);
|
|
220
|
-
})
|
|
221
|
-
.catch(() => {
|
|
222
|
-
// noop - just try again
|
|
223
|
-
});
|
|
224
|
-
}, 500);
|
|
225
|
-
|
|
226
|
-
const timeout = setTimeout(() => {
|
|
227
|
-
clearInterval(pollingInterval);
|
|
228
|
-
loginSpinner.stop(
|
|
229
|
-
'Login timed out. No worries - it happens to the best of us.',
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
Sentry.setTag('opened-wizard-link', false);
|
|
233
|
-
void abort('Please restart the Wizard and log in to complete the setup.');
|
|
234
|
-
}, 180_000);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
loginSpinner.stop('Login complete.');
|
|
238
|
-
Sentry.setTag('opened-wizard-link', true);
|
|
239
|
-
|
|
240
|
-
return data;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
175
|
export async function askForItemSelection(
|
|
244
176
|
items: string[],
|
|
245
177
|
message: string,
|
|
246
178
|
): Promise<{ value: string; index: number }> {
|
|
247
179
|
const selection: { value: string; index: number } | symbol =
|
|
248
180
|
await abortIfCancelled(
|
|
249
|
-
|
|
181
|
+
clack.select({
|
|
250
182
|
maxItems: 12,
|
|
251
183
|
message: message,
|
|
252
184
|
options: items.map((item, index) => {
|
|
@@ -261,29 +193,6 @@ export async function askForItemSelection(
|
|
|
261
193
|
return selection;
|
|
262
194
|
}
|
|
263
195
|
|
|
264
|
-
export async function askForProjectSelection(
|
|
265
|
-
projects: SentryProjectData[],
|
|
266
|
-
): Promise<SentryProjectData> {
|
|
267
|
-
const selection: SentryProjectData | symbol = await abortIfCancelled(
|
|
268
|
-
windowedSelect({
|
|
269
|
-
maxItems: 12,
|
|
270
|
-
message: 'Select your Sentry project.',
|
|
271
|
-
options: projects.map((project) => {
|
|
272
|
-
return {
|
|
273
|
-
value: project,
|
|
274
|
-
label: `${project.organization.slug}/${project.slug}`,
|
|
275
|
-
};
|
|
276
|
-
}),
|
|
277
|
-
}),
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
Sentry.setTag('project', selection.slug);
|
|
281
|
-
Sentry.setTag('project-platform', selection.platform);
|
|
282
|
-
Sentry.setUser({ id: selection.organization.slug });
|
|
283
|
-
|
|
284
|
-
return selection;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
196
|
export async function installPackage({
|
|
288
197
|
packageName,
|
|
289
198
|
alreadyInstalled,
|
|
@@ -314,17 +223,11 @@ export async function installPackage({
|
|
|
314
223
|
sdkInstallSpinner.start(
|
|
315
224
|
`${alreadyInstalled ? 'Updating' : 'Installing'} ${chalk.bold.cyan(
|
|
316
225
|
packageName,
|
|
317
|
-
)} with ${chalk.bold(packageManager)}.`,
|
|
226
|
+
)} with ${chalk.bold(packageManager.label)}.`,
|
|
318
227
|
);
|
|
319
228
|
|
|
320
229
|
try {
|
|
321
|
-
|
|
322
|
-
await promisify(childProcess.exec)(`yarn add ${packageName}@latest`);
|
|
323
|
-
} else if (packageManager === 'pnpm') {
|
|
324
|
-
await promisify(childProcess.exec)(`pnpm add ${packageName}@latest`);
|
|
325
|
-
} else if (packageManager === 'npm') {
|
|
326
|
-
await promisify(childProcess.exec)(`npm install ${packageName}@latest`);
|
|
327
|
-
}
|
|
230
|
+
await installPackageWithPackageManager(packageManager, packageName);
|
|
328
231
|
} catch (e) {
|
|
329
232
|
sdkInstallSpinner.stop('Installation failed.');
|
|
330
233
|
clack.log.error(
|
|
@@ -341,189 +244,119 @@ export async function installPackage({
|
|
|
341
244
|
sdkInstallSpinner.stop(
|
|
342
245
|
`${alreadyInstalled ? 'Updated' : 'Installed'} ${chalk.bold.cyan(
|
|
343
246
|
packageName,
|
|
344
|
-
)} with ${chalk.bold(packageManager)}.`,
|
|
247
|
+
)} with ${chalk.bold(packageManager.label)}.`,
|
|
345
248
|
);
|
|
346
249
|
}
|
|
347
250
|
|
|
348
|
-
/**
|
|
349
|
-
* Asks users if they are using SaaS or self-hosted Sentry and returns the validated URL.
|
|
350
|
-
*
|
|
351
|
-
* If users started the wizard with a --url arg, that URL is used as the default and we skip
|
|
352
|
-
* the self-hosted question. However, the passed url is still validated and in case it's
|
|
353
|
-
* invalid, users are asked to enter a new one until it is valid.
|
|
354
|
-
*
|
|
355
|
-
* @param urlFromArgs the url passed via the --url arg
|
|
356
|
-
*/
|
|
357
|
-
export async function askForSelfHosted(urlFromArgs?: string): Promise<{
|
|
358
|
-
url: string;
|
|
359
|
-
selfHosted: boolean;
|
|
360
|
-
}> {
|
|
361
|
-
if (!urlFromArgs) {
|
|
362
|
-
const choice: 'saas' | 'self-hosted' | symbol = await abortIfCancelled(
|
|
363
|
-
clack.select({
|
|
364
|
-
message: 'Are you using Sentry SaaS or self-hosted Sentry?',
|
|
365
|
-
options: [
|
|
366
|
-
{ value: 'saas', label: 'Sentry SaaS (sentry.io)' },
|
|
367
|
-
{
|
|
368
|
-
value: 'self-hosted',
|
|
369
|
-
label: 'Self-hosted/on-premise/single-tenant',
|
|
370
|
-
},
|
|
371
|
-
],
|
|
372
|
-
}),
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
if (choice === 'saas') {
|
|
376
|
-
Sentry.setTag('url', SAAS_URL);
|
|
377
|
-
Sentry.setTag('self-hosted', false);
|
|
378
|
-
return { url: SAAS_URL, selfHosted: false };
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
let validUrl: string | undefined;
|
|
383
|
-
let tmpUrlFromArgs = urlFromArgs;
|
|
384
|
-
|
|
385
|
-
while (validUrl === undefined) {
|
|
386
|
-
const url =
|
|
387
|
-
tmpUrlFromArgs ||
|
|
388
|
-
(await abortIfCancelled(
|
|
389
|
-
clack.text({
|
|
390
|
-
message: `Please enter the URL of your ${
|
|
391
|
-
urlFromArgs ? '' : 'self-hosted '
|
|
392
|
-
}Sentry instance.`,
|
|
393
|
-
placeholder: 'https://sentry.io/',
|
|
394
|
-
}),
|
|
395
|
-
));
|
|
396
|
-
tmpUrlFromArgs = undefined;
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
validUrl = new URL(url).toString();
|
|
400
|
-
|
|
401
|
-
// We assume everywhere else that the URL ends in a slash
|
|
402
|
-
if (!validUrl.endsWith('/')) {
|
|
403
|
-
validUrl += '/';
|
|
404
|
-
}
|
|
405
|
-
} catch {
|
|
406
|
-
clack.log.error(
|
|
407
|
-
'Please enter a valid URL. (It should look something like "https://sentry.mydomain.com/")',
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const isSelfHostedUrl = new URL(validUrl).host !== new URL(SAAS_URL).host;
|
|
413
|
-
|
|
414
|
-
Sentry.setTag('url', validUrl);
|
|
415
|
-
Sentry.setTag('self-hosted', isSelfHostedUrl);
|
|
416
|
-
|
|
417
|
-
return { url: validUrl, selfHosted: true };
|
|
418
|
-
}
|
|
419
|
-
|
|
420
251
|
async function addOrgAndProjectToSentryCliRc(
|
|
421
252
|
org: string,
|
|
422
253
|
project: string,
|
|
254
|
+
setupConfig: CliSetupConfig,
|
|
423
255
|
): Promise<void> {
|
|
424
|
-
const
|
|
425
|
-
path.join(process.cwd(),
|
|
256
|
+
const configContents = fs.readFileSync(
|
|
257
|
+
path.join(process.cwd(), setupConfig.filename),
|
|
426
258
|
'utf8',
|
|
427
259
|
);
|
|
428
260
|
|
|
429
|
-
|
|
430
|
-
clircContents.includes('[defaults]') &&
|
|
431
|
-
clircContents.match(/org=./g) &&
|
|
432
|
-
clircContents.match(/project=./g)
|
|
433
|
-
);
|
|
434
|
-
|
|
435
|
-
if (likelyAlreadyHasOrgAndProject) {
|
|
261
|
+
if (setupConfig.likelyAlreadyHasOrgAndProject(configContents)) {
|
|
436
262
|
clack.log.warn(
|
|
437
263
|
`${chalk.bold(
|
|
438
|
-
|
|
264
|
+
setupConfig.filename,
|
|
439
265
|
)} already has org and project. Will not add them.`,
|
|
440
266
|
);
|
|
441
267
|
} else {
|
|
442
268
|
try {
|
|
443
269
|
await fs.promises.appendFile(
|
|
444
|
-
path.join(process.cwd(),
|
|
445
|
-
`\n
|
|
270
|
+
path.join(process.cwd(), setupConfig.filename),
|
|
271
|
+
`\n${setupConfig.orgAndProjContent(org, project)}\n`,
|
|
446
272
|
);
|
|
447
273
|
} catch (e) {
|
|
448
274
|
clack.log.warn(
|
|
449
275
|
`${chalk.bold(
|
|
450
|
-
|
|
276
|
+
setupConfig.filename,
|
|
451
277
|
)} could not be updated with org and project.`,
|
|
452
278
|
);
|
|
453
279
|
}
|
|
454
280
|
}
|
|
455
281
|
}
|
|
456
282
|
|
|
457
|
-
export async function
|
|
283
|
+
export async function addSentryCliConfig(
|
|
458
284
|
authToken: string,
|
|
285
|
+
setupConfig: CliSetupConfig = sourceMapsCliSetupConfig,
|
|
459
286
|
orgSlug?: string,
|
|
460
287
|
projectSlug?: string,
|
|
461
288
|
): Promise<void> {
|
|
462
|
-
const
|
|
463
|
-
path.join(process.cwd(),
|
|
289
|
+
const configExists = fs.existsSync(
|
|
290
|
+
path.join(process.cwd(), setupConfig.filename),
|
|
464
291
|
);
|
|
465
|
-
if (
|
|
466
|
-
const
|
|
467
|
-
path.join(process.cwd(),
|
|
292
|
+
if (configExists) {
|
|
293
|
+
const configContents = fs.readFileSync(
|
|
294
|
+
path.join(process.cwd(), setupConfig.filename),
|
|
468
295
|
'utf8',
|
|
469
296
|
);
|
|
470
297
|
|
|
471
|
-
|
|
472
|
-
clircContents.includes('[auth]') && clircContents.match(/token=./g)
|
|
473
|
-
);
|
|
474
|
-
|
|
475
|
-
if (likelyAlreadyHasAuthToken) {
|
|
298
|
+
if (setupConfig.likelyAlreadyHasAuthToken(configContents)) {
|
|
476
299
|
clack.log.warn(
|
|
477
300
|
`${chalk.bold(
|
|
478
|
-
|
|
301
|
+
setupConfig.filename,
|
|
479
302
|
)} already has auth token. Will not add one.`,
|
|
480
303
|
);
|
|
481
304
|
} else {
|
|
482
305
|
try {
|
|
483
306
|
await fs.promises.writeFile(
|
|
484
|
-
path.join(process.cwd(),
|
|
485
|
-
`${
|
|
307
|
+
path.join(process.cwd(), setupConfig.filename),
|
|
308
|
+
`${configContents}\n${setupConfig.tokenContent(authToken)}\n`,
|
|
486
309
|
{ encoding: 'utf8', flag: 'w' },
|
|
487
310
|
);
|
|
488
311
|
clack.log.success(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
312
|
+
chalk.greenBright(
|
|
313
|
+
`Added auth token to ${chalk.bold(
|
|
314
|
+
setupConfig.filename,
|
|
315
|
+
)} for you to test uploading ${setupConfig.name} locally.`,
|
|
316
|
+
),
|
|
492
317
|
);
|
|
493
318
|
} catch {
|
|
494
319
|
clack.log.warning(
|
|
495
320
|
`Failed to add auth token to ${chalk.bold(
|
|
496
|
-
|
|
497
|
-
)}. Uploading
|
|
321
|
+
setupConfig.filename,
|
|
322
|
+
)}. Uploading ${
|
|
323
|
+
setupConfig.name
|
|
324
|
+
} during build will likely not work locally.`,
|
|
498
325
|
);
|
|
499
326
|
}
|
|
500
327
|
}
|
|
501
328
|
} else {
|
|
502
329
|
try {
|
|
503
330
|
await fs.promises.writeFile(
|
|
504
|
-
path.join(process.cwd(),
|
|
505
|
-
|
|
331
|
+
path.join(process.cwd(), setupConfig.filename),
|
|
332
|
+
`${setupConfig.tokenContent(authToken)}\n`,
|
|
506
333
|
{ encoding: 'utf8', flag: 'w' },
|
|
507
334
|
);
|
|
508
335
|
clack.log.success(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
336
|
+
chalk.greenBright(
|
|
337
|
+
`Created ${chalk.bold(
|
|
338
|
+
setupConfig.filename,
|
|
339
|
+
)} with auth token for you to test uploading ${
|
|
340
|
+
setupConfig.name
|
|
341
|
+
} locally.`,
|
|
342
|
+
),
|
|
512
343
|
);
|
|
513
344
|
} catch {
|
|
514
345
|
clack.log.warning(
|
|
515
346
|
`Failed to create ${chalk.bold(
|
|
516
|
-
|
|
517
|
-
)} with auth token. Uploading
|
|
347
|
+
setupConfig.filename,
|
|
348
|
+
)} with auth token. Uploading ${
|
|
349
|
+
setupConfig.name
|
|
350
|
+
} during build will likely not work locally.`,
|
|
518
351
|
);
|
|
519
352
|
}
|
|
520
353
|
}
|
|
521
354
|
|
|
522
355
|
if (orgSlug && projectSlug) {
|
|
523
|
-
await addOrgAndProjectToSentryCliRc(orgSlug, projectSlug);
|
|
356
|
+
await addOrgAndProjectToSentryCliRc(orgSlug, projectSlug, setupConfig);
|
|
524
357
|
}
|
|
525
358
|
|
|
526
|
-
await addAuthTokenFileToGitIgnore(
|
|
359
|
+
await addAuthTokenFileToGitIgnore(setupConfig.filename);
|
|
527
360
|
}
|
|
528
361
|
|
|
529
362
|
export async function addDotEnvSentryBuildPluginFile(
|
|
@@ -605,7 +438,9 @@ async function addAuthTokenFileToGitIgnore(filename: string): Promise<void> {
|
|
|
605
438
|
{ encoding: 'utf8' },
|
|
606
439
|
);
|
|
607
440
|
clack.log.success(
|
|
608
|
-
|
|
441
|
+
chalk.greenBright(
|
|
442
|
+
`Added ${chalk.bold(filename)} to ${chalk.bold('.gitignore')}.`,
|
|
443
|
+
),
|
|
609
444
|
);
|
|
610
445
|
} catch {
|
|
611
446
|
clack.log.error(
|
|
@@ -661,42 +496,29 @@ export async function getPackageDotJson(): Promise<PackageDotJson> {
|
|
|
661
496
|
return packageJson || {};
|
|
662
497
|
}
|
|
663
498
|
|
|
664
|
-
async function getPackageManager(): Promise<
|
|
665
|
-
const detectedPackageManager =
|
|
499
|
+
async function getPackageManager(): Promise<PackageManager> {
|
|
500
|
+
const detectedPackageManager = detectPackageManger();
|
|
666
501
|
|
|
667
502
|
if (detectedPackageManager) {
|
|
668
503
|
return detectedPackageManager;
|
|
669
504
|
}
|
|
670
505
|
|
|
671
|
-
const selectedPackageManager:
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
506
|
+
const selectedPackageManager: PackageManager | symbol =
|
|
507
|
+
await abortIfCancelled(
|
|
508
|
+
clack.select({
|
|
509
|
+
message: 'Please select your package manager.',
|
|
510
|
+
options: packageManagers.map((packageManager) => ({
|
|
511
|
+
value: packageManager,
|
|
512
|
+
label: packageManager.label,
|
|
513
|
+
})),
|
|
514
|
+
}),
|
|
515
|
+
);
|
|
681
516
|
|
|
682
|
-
Sentry.setTag('package-manager', selectedPackageManager);
|
|
517
|
+
Sentry.setTag('package-manager', selectedPackageManager.name);
|
|
683
518
|
|
|
684
519
|
return selectedPackageManager;
|
|
685
520
|
}
|
|
686
521
|
|
|
687
|
-
export function detectPackageManager(): 'yarn' | 'npm' | 'pnpm' | undefined {
|
|
688
|
-
if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) {
|
|
689
|
-
return 'yarn';
|
|
690
|
-
}
|
|
691
|
-
if (fs.existsSync(path.join(process.cwd(), 'package-lock.json'))) {
|
|
692
|
-
return 'npm';
|
|
693
|
-
}
|
|
694
|
-
if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) {
|
|
695
|
-
return 'pnpm';
|
|
696
|
-
}
|
|
697
|
-
return undefined;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
522
|
export function isUsingTypeScript() {
|
|
701
523
|
try {
|
|
702
524
|
return fs.existsSync(path.join(process.cwd(), 'tsconfig.json'));
|
|
@@ -705,7 +527,26 @@ export function isUsingTypeScript() {
|
|
|
705
527
|
}
|
|
706
528
|
}
|
|
707
529
|
|
|
708
|
-
|
|
530
|
+
/**
|
|
531
|
+
* Checks if we already got project data from a previous wizard invocation.
|
|
532
|
+
* If yes, this data is returned.
|
|
533
|
+
* Otherwise, we start the login flow and ask the user to select a project.
|
|
534
|
+
*
|
|
535
|
+
* Use this function to get project data for the wizard.
|
|
536
|
+
*
|
|
537
|
+
* @param options wizard options
|
|
538
|
+
* @param platform the platform of the wizard
|
|
539
|
+
* @returns project data (org, project, token, url)
|
|
540
|
+
*/
|
|
541
|
+
export async function getOrAskForProjectData(
|
|
542
|
+
options: WizardOptions,
|
|
543
|
+
platform?:
|
|
544
|
+
| 'javascript-nextjs'
|
|
545
|
+
| 'javascript-remix'
|
|
546
|
+
| 'javascript-sveltekit'
|
|
547
|
+
| 'apple-ios'
|
|
548
|
+
| 'android',
|
|
549
|
+
): Promise<{
|
|
709
550
|
sentryUrl: string;
|
|
710
551
|
selfHosted: boolean;
|
|
711
552
|
selectedProject: SentryProjectData;
|
|
@@ -728,10 +569,18 @@ export async function getOrAskForProjectData(options: WizardOptions): Promise<{
|
|
|
728
569
|
askForWizardLogin({
|
|
729
570
|
promoCode: options.promoCode,
|
|
730
571
|
url: sentryUrl,
|
|
731
|
-
platform:
|
|
572
|
+
platform: platform,
|
|
732
573
|
}),
|
|
733
574
|
);
|
|
734
575
|
|
|
576
|
+
if (!projects || !projects.length) {
|
|
577
|
+
clack.log.error(
|
|
578
|
+
'No projects found. Please create a project in Sentry and try again.',
|
|
579
|
+
);
|
|
580
|
+
Sentry.setTag('no-projects-found', true);
|
|
581
|
+
await abort();
|
|
582
|
+
}
|
|
583
|
+
|
|
735
584
|
const selectedProject = await traceStep('select-project', () =>
|
|
736
585
|
askForProjectSelection(projects),
|
|
737
586
|
);
|
|
@@ -743,3 +592,356 @@ export async function getOrAskForProjectData(options: WizardOptions): Promise<{
|
|
|
743
592
|
selectedProject,
|
|
744
593
|
};
|
|
745
594
|
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Asks users if they are using SaaS or self-hosted Sentry and returns the validated URL.
|
|
598
|
+
*
|
|
599
|
+
* If users started the wizard with a --url arg, that URL is used as the default and we skip
|
|
600
|
+
* the self-hosted question. However, the passed url is still validated and in case it's
|
|
601
|
+
* invalid, users are asked to enter a new one until it is valid.
|
|
602
|
+
*
|
|
603
|
+
* @param urlFromArgs the url passed via the --url arg
|
|
604
|
+
*/
|
|
605
|
+
async function askForSelfHosted(urlFromArgs?: string): Promise<{
|
|
606
|
+
url: string;
|
|
607
|
+
selfHosted: boolean;
|
|
608
|
+
}> {
|
|
609
|
+
if (!urlFromArgs) {
|
|
610
|
+
const choice: 'saas' | 'self-hosted' | symbol = await abortIfCancelled(
|
|
611
|
+
clack.select({
|
|
612
|
+
message: 'Are you using Sentry SaaS or self-hosted Sentry?',
|
|
613
|
+
options: [
|
|
614
|
+
{ value: 'saas', label: 'Sentry SaaS (sentry.io)' },
|
|
615
|
+
{
|
|
616
|
+
value: 'self-hosted',
|
|
617
|
+
label: 'Self-hosted/on-premise/single-tenant',
|
|
618
|
+
},
|
|
619
|
+
],
|
|
620
|
+
}),
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
if (choice === 'saas') {
|
|
624
|
+
Sentry.setTag('url', SAAS_URL);
|
|
625
|
+
Sentry.setTag('self-hosted', false);
|
|
626
|
+
return { url: SAAS_URL, selfHosted: false };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
let validUrl: string | undefined;
|
|
631
|
+
let tmpUrlFromArgs = urlFromArgs;
|
|
632
|
+
|
|
633
|
+
while (validUrl === undefined) {
|
|
634
|
+
const url =
|
|
635
|
+
tmpUrlFromArgs ||
|
|
636
|
+
(await abortIfCancelled(
|
|
637
|
+
clack.text({
|
|
638
|
+
message: `Please enter the URL of your ${
|
|
639
|
+
urlFromArgs ? '' : 'self-hosted '
|
|
640
|
+
}Sentry instance.`,
|
|
641
|
+
placeholder: 'https://sentry.io/',
|
|
642
|
+
}),
|
|
643
|
+
));
|
|
644
|
+
tmpUrlFromArgs = undefined;
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
validUrl = new URL(url).toString();
|
|
648
|
+
|
|
649
|
+
// We assume everywhere else that the URL ends in a slash
|
|
650
|
+
if (!validUrl.endsWith('/')) {
|
|
651
|
+
validUrl += '/';
|
|
652
|
+
}
|
|
653
|
+
} catch {
|
|
654
|
+
clack.log.error(
|
|
655
|
+
'Please enter a valid URL. (It should look something like "https://sentry.mydomain.com/")',
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const isSelfHostedUrl = new URL(validUrl).host !== new URL(SAAS_URL).host;
|
|
661
|
+
|
|
662
|
+
Sentry.setTag('url', validUrl);
|
|
663
|
+
Sentry.setTag('self-hosted', isSelfHostedUrl);
|
|
664
|
+
|
|
665
|
+
return { url: validUrl, selfHosted: true };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function askForWizardLogin(options: {
|
|
669
|
+
url: string;
|
|
670
|
+
promoCode?: string;
|
|
671
|
+
platform?:
|
|
672
|
+
| 'javascript-nextjs'
|
|
673
|
+
| 'javascript-remix'
|
|
674
|
+
| 'javascript-sveltekit'
|
|
675
|
+
| 'apple-ios'
|
|
676
|
+
| 'android';
|
|
677
|
+
}): Promise<WizardProjectData> {
|
|
678
|
+
Sentry.setTag('has-promo-code', !!options.promoCode);
|
|
679
|
+
|
|
680
|
+
let hasSentryAccount = await clack.confirm({
|
|
681
|
+
message: 'Do you already have a Sentry account?',
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
hasSentryAccount = await abortIfCancelled(hasSentryAccount);
|
|
685
|
+
|
|
686
|
+
Sentry.setTag('already-has-sentry-account', hasSentryAccount);
|
|
687
|
+
|
|
688
|
+
let wizardHash: string;
|
|
689
|
+
try {
|
|
690
|
+
wizardHash = (
|
|
691
|
+
await axios.get<{ hash: string }>(`${options.url}api/0/wizard/`)
|
|
692
|
+
).data.hash;
|
|
693
|
+
} catch {
|
|
694
|
+
if (options.url !== SAAS_URL) {
|
|
695
|
+
clack.log.error('Loading Wizard failed. Did you provide the right URL?');
|
|
696
|
+
await abort(
|
|
697
|
+
chalk.red(
|
|
698
|
+
'Please check your configuration and try again.\n\n Let us know if you think this is an issue with the wizard or Sentry: https://github.com/getsentry/sentry-wizard/issues',
|
|
699
|
+
),
|
|
700
|
+
);
|
|
701
|
+
} else {
|
|
702
|
+
clack.log.error('Loading Wizard failed.');
|
|
703
|
+
await abort(
|
|
704
|
+
chalk.red(
|
|
705
|
+
'Please try again in a few minutes and let us know if this issue persists: https://github.com/getsentry/sentry-wizard/issues',
|
|
706
|
+
),
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const loginUrl = new URL(
|
|
712
|
+
`${options.url}account/settings/wizard/${wizardHash!}/`,
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
if (!hasSentryAccount) {
|
|
716
|
+
loginUrl.searchParams.set('signup', '1');
|
|
717
|
+
if (options.platform) {
|
|
718
|
+
loginUrl.searchParams.set('project_platform', options.platform);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (options.promoCode) {
|
|
723
|
+
loginUrl.searchParams.set('code', options.promoCode);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const urlToOpen = loginUrl.toString();
|
|
727
|
+
clack.log.info(
|
|
728
|
+
`${chalk.bold(
|
|
729
|
+
`If the browser window didn't open automatically, please open the following link to ${
|
|
730
|
+
hasSentryAccount ? 'log' : 'sign'
|
|
731
|
+
} into Sentry:`,
|
|
732
|
+
)}\n\n${chalk.cyan(urlToOpen)}`,
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
opn(urlToOpen).catch(() => {
|
|
736
|
+
// opn throws in environments that don't have a browser (e.g. remote shells) so we just noop here
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const loginSpinner = clack.spinner();
|
|
740
|
+
|
|
741
|
+
loginSpinner.start('Waiting for you to log in using the link above');
|
|
742
|
+
|
|
743
|
+
const data = await new Promise<WizardProjectData>((resolve) => {
|
|
744
|
+
const pollingInterval = setInterval(() => {
|
|
745
|
+
axios
|
|
746
|
+
.get<WizardProjectData>(`${options.url}api/0/wizard/${wizardHash}/`, {
|
|
747
|
+
headers: {
|
|
748
|
+
'Accept-Encoding': 'deflate',
|
|
749
|
+
},
|
|
750
|
+
})
|
|
751
|
+
.then((result) => {
|
|
752
|
+
resolve(result.data);
|
|
753
|
+
clearTimeout(timeout);
|
|
754
|
+
clearInterval(pollingInterval);
|
|
755
|
+
void axios.delete(`${options.url}api/0/wizard/${wizardHash}/`);
|
|
756
|
+
})
|
|
757
|
+
.catch(() => {
|
|
758
|
+
// noop - just try again
|
|
759
|
+
});
|
|
760
|
+
}, 500);
|
|
761
|
+
|
|
762
|
+
const timeout = setTimeout(() => {
|
|
763
|
+
clearInterval(pollingInterval);
|
|
764
|
+
loginSpinner.stop(
|
|
765
|
+
'Login timed out. No worries - it happens to the best of us.',
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
Sentry.setTag('opened-wizard-link', false);
|
|
769
|
+
void abort('Please restart the Wizard and log in to complete the setup.');
|
|
770
|
+
}, 180_000);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
loginSpinner.stop('Login complete.');
|
|
774
|
+
Sentry.setTag('opened-wizard-link', true);
|
|
775
|
+
|
|
776
|
+
return data;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function askForProjectSelection(
|
|
780
|
+
projects: SentryProjectData[],
|
|
781
|
+
): Promise<SentryProjectData> {
|
|
782
|
+
const label = (project: SentryProjectData): string => {
|
|
783
|
+
return `${project.organization.slug}/${project.slug}`;
|
|
784
|
+
};
|
|
785
|
+
const sortedProjects = [...projects];
|
|
786
|
+
sortedProjects.sort((a: SentryProjectData, b: SentryProjectData) => {
|
|
787
|
+
return label(a).localeCompare(label(b));
|
|
788
|
+
});
|
|
789
|
+
const selection: SentryProjectData | symbol = await abortIfCancelled(
|
|
790
|
+
clack.select({
|
|
791
|
+
maxItems: 12,
|
|
792
|
+
message: 'Select your Sentry project.',
|
|
793
|
+
options: sortedProjects.map((project) => {
|
|
794
|
+
return {
|
|
795
|
+
value: project,
|
|
796
|
+
label: label(project),
|
|
797
|
+
};
|
|
798
|
+
}),
|
|
799
|
+
}),
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
Sentry.setTag('project', selection.slug);
|
|
803
|
+
Sentry.setTag('project-platform', selection.platform);
|
|
804
|
+
Sentry.setUser({ id: selection.organization.slug });
|
|
805
|
+
|
|
806
|
+
return selection;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Asks users if they have a config file for @param tool (e.g. Vite).
|
|
811
|
+
* If yes, asks users to specify the path to their config file.
|
|
812
|
+
*
|
|
813
|
+
* Use this helper function as a fallback mechanism if the lookup for
|
|
814
|
+
* a config file with its most usual location/name fails.
|
|
815
|
+
*
|
|
816
|
+
* @param toolName Name of the tool for which we're looking for the config file
|
|
817
|
+
* @param configFileName Name of the most common config file name (e.g. vite.config.js)
|
|
818
|
+
*
|
|
819
|
+
* @returns a user path to the config file or undefined if the user doesn't have a config file
|
|
820
|
+
*/
|
|
821
|
+
export async function askForToolConfigPath(
|
|
822
|
+
toolName: string,
|
|
823
|
+
configFileName: string,
|
|
824
|
+
): Promise<string | undefined> {
|
|
825
|
+
const hasConfig = await abortIfCancelled(
|
|
826
|
+
clack.confirm({
|
|
827
|
+
message: `Do you have a ${toolName} config file (e.g. ${chalk.cyan(
|
|
828
|
+
configFileName,
|
|
829
|
+
)}?`,
|
|
830
|
+
initialValue: true,
|
|
831
|
+
}),
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
if (!hasConfig) {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return await abortIfCancelled(
|
|
839
|
+
clack.text({
|
|
840
|
+
message: `Please enter the path to your ${toolName} config file:`,
|
|
841
|
+
placeholder: path.join('.', configFileName),
|
|
842
|
+
validate: (value) => {
|
|
843
|
+
if (!value) {
|
|
844
|
+
return 'Please enter a path.';
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
try {
|
|
848
|
+
fs.accessSync(value);
|
|
849
|
+
} catch {
|
|
850
|
+
return 'Could not access the file at this path.';
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
}),
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Prints copy/paste-able instructions to the console.
|
|
859
|
+
* Afterwards asks the user if they added the code snippet to their file.
|
|
860
|
+
*
|
|
861
|
+
* While there's no point in providing a "no" answer here, it gives users time to fulfill the
|
|
862
|
+
* task before the wizard continues with additional steps.
|
|
863
|
+
*
|
|
864
|
+
* Use this function if you want to show users instructions on how to add/modify
|
|
865
|
+
* code in their file. This is helpful if automatic insertion failed or is not possible/feasible.
|
|
866
|
+
*
|
|
867
|
+
* @param filename the name of the file to which the code snippet should be applied.
|
|
868
|
+
* If a path is provided, only the filename will be used.
|
|
869
|
+
* @param codeSnippet the snippet to be printed.
|
|
870
|
+
* Make sure to follow the diff-like format of highlighting lines that require changes
|
|
871
|
+
* and showing unchanged lines in gray.
|
|
872
|
+
*
|
|
873
|
+
* TODO: Link to wizard spec (develop) once it is live
|
|
874
|
+
* TODO: refactor copy paste instructions across different wizards to use this function.
|
|
875
|
+
* this might require adding a custom message parameter to the function
|
|
876
|
+
*/
|
|
877
|
+
export async function showCopyPasteInstructions(
|
|
878
|
+
filename: string,
|
|
879
|
+
codeSnippet: string,
|
|
880
|
+
): Promise<void> {
|
|
881
|
+
clack.log.step(
|
|
882
|
+
`Add the following code to your ${chalk.cyan(
|
|
883
|
+
path.basename(filename),
|
|
884
|
+
)} file:`,
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
// Intentionally logging directly to console here so that the code can be copied/pasted directly
|
|
888
|
+
// eslint-disable-next-line no-console
|
|
889
|
+
console.log(`\n${codeSnippet}`);
|
|
890
|
+
|
|
891
|
+
await abortIfCancelled(
|
|
892
|
+
clack.select({
|
|
893
|
+
message: 'Did you apply the snippet above?',
|
|
894
|
+
options: [{ label: 'Yes, continue!', value: true }],
|
|
895
|
+
initialValue: true,
|
|
896
|
+
}),
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Creates a new config file with the given @param filepath and @param codeSnippet.
|
|
902
|
+
*
|
|
903
|
+
* Use this function to create a new config file for users. This is useful
|
|
904
|
+
* when users answered that they don't yet have a config file for a tool.
|
|
905
|
+
*
|
|
906
|
+
* (This doesn't mean that they don't yet have some other way of configuring
|
|
907
|
+
* their tool but we can leave it up to them to figure out how to merge configs
|
|
908
|
+
* here.)
|
|
909
|
+
*
|
|
910
|
+
* @param filepath absolute path to the new config file
|
|
911
|
+
* @param codeSnippet the snippet to be inserted into the file
|
|
912
|
+
* @param moreInformation (optional) the message to be printed after the file was created
|
|
913
|
+
* For example, this can be a link to more information about configuring the tool.
|
|
914
|
+
*
|
|
915
|
+
* @returns true on sucess, false otherwise
|
|
916
|
+
*/
|
|
917
|
+
export async function createNewConfigFile(
|
|
918
|
+
filepath: string,
|
|
919
|
+
codeSnippet: string,
|
|
920
|
+
moreInformation?: string,
|
|
921
|
+
): Promise<boolean> {
|
|
922
|
+
if (!path.isAbsolute(filepath)) {
|
|
923
|
+
debug(`createNewConfigFile: filepath is not absolute: ${filepath}`);
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const prettyFilename = chalk.cyan(path.relative(process.cwd(), filepath));
|
|
928
|
+
|
|
929
|
+
try {
|
|
930
|
+
await fs.promises.writeFile(filepath, codeSnippet);
|
|
931
|
+
|
|
932
|
+
clack.log.success(`Added new ${prettyFilename} file.`);
|
|
933
|
+
|
|
934
|
+
if (moreInformation) {
|
|
935
|
+
clack.log.info(chalk.gray(moreInformation));
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return true;
|
|
939
|
+
} catch (e) {
|
|
940
|
+
debug(e);
|
|
941
|
+
clack.log.warn(
|
|
942
|
+
`Could not create a new ${prettyFilename} file. Please create one manually and follow the instructions below.`,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return false;
|
|
947
|
+
}
|