@sentry/wizard
Version:
Sentry wizard helping you to configure your project
251 lines (234 loc) • 7.19 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import * as fs from 'fs';
import { abortIfCancelled, askForItemSelection } from '../utils/clack-utils';
import {
plugin,
pluginKts,
pluginsBlock,
pluginsBlockKts,
sourceContext,
sourceContextKts,
} from './templates';
import * as bash from '../utils/bash';
import * as Sentry from '@sentry/node';
// @ts-ignore - clack is ESM and TS complains about that. It works though
import * as clack from '@clack/prompts';
import chalk from 'chalk';
import { fetchSdkVersion } from '../utils/release-registry';
/**
* A Gradle project may contain multiple modules, some of them may be applications, some of them may be libraries.
* We are only interested in applications. For example:
*
* myproject/
* app/
* lib1/
* lib2/
* wearApp/
*
* In this case^ we are interested in app/ and wearApp/
*
* @param buildGradleFiles a list of build.gradle(.kts) paths that contain the com.android.application plugin
* @returns the selected project for setting up
*/
export async function selectAppFile(
buildGradleFiles: string[],
): Promise<string> {
const appFiles = [];
for (let index = 0; index < buildGradleFiles.length; index++) {
const file = buildGradleFiles[index];
const text = fs.readFileSync(file, 'utf8');
if (/\(?["']com\.android\.application["']\)?(?!.*\S)/.test(text)) {
appFiles.push(file);
}
}
if (appFiles.length === 0) {
Sentry.setTag('custom-build-logic', true);
const appFile = await abortIfCancelled(
clack.text({
message: `Unable to find your app's directory.
Please enter the relative path to your app's build.gradle file from the root project`,
placeholder: 'app/build.gradle.kts',
validate(value) {
if (!value.includes('.gradle') || !fs.existsSync(value))
return `Not a valid gradle file.`;
},
}),
);
return appFile;
}
let appFile;
if (appFiles.length === 1) {
Sentry.setTag('multiple-projects', false);
appFile = appFiles[0];
} else {
Sentry.setTag('multiple-projects', true);
appFile = (
await askForItemSelection(
appFiles,
'Which project do you want to add Sentry to?',
)
).value;
}
Sentry.setTag('custom-build-logic', false);
return appFile;
}
/**
* Patches a build.gradle(.kts) file that contains `com.android.application` plugin.
* There are multiple cases we have to handle here:
* - An existing `plugins {}` block:
* - We just have to add our plugin inside the block
* - No existing `plugins {}` block
* - We have to add the entire block in the beginning of the file, BUT *after imports*
*
* For example (2nd case):
*
* ```
* import net.ltgt.gradle.errorprone.errorprone
*
* // our plugins block goes here <--
* plugins {
* id("io.sentry.android.gradle") version "3.12.0"
* }
*
* apply(plugin = "com.android.application")
*
* android {
* ...
* }
* ```
*
* In the end we run `./gradlew` to verify the config is build-able and not broken.
*
* @param appFile the selected Gradle application project
* @returns true if successfully added Sentry Gradle config, false otherwise
*/
export async function addGradlePlugin(
appFile: string,
orgSlug: string,
projectSlug: string,
): Promise<boolean> {
const gradleScript = fs.readFileSync(appFile, 'utf8');
if (/\(?["']io\.sentry\.android\.gradle["']\)?/.test(gradleScript)) {
// sentry gradle plugin is already installed
clack.log.success(
chalk.greenBright(
`${chalk.bold(
'Sentry Gradle plugin',
)} is already added to the project.`,
),
);
maybeAddSourceContextConfig(appFile, gradleScript, orgSlug, projectSlug);
return true;
}
const pluginVersion = await fetchSdkVersion(
'sentry.java.android.gradle-plugin',
);
const pluginsBlockMatch = /plugins\s*{[^{}]*}/.exec(gradleScript);
let newGradleScript;
if (!pluginsBlockMatch) {
// no "plugins {}" block, we can just add our own after imports
const regex = /import\s+[\w.]+/gm;
let importsMatch = regex.exec(gradleScript);
let insertIndex = 0;
while (importsMatch) {
insertIndex = importsMatch.index + importsMatch[0].length + 1;
importsMatch = regex.exec(gradleScript);
}
if (appFile.endsWith('.kts')) {
newGradleScript =
gradleScript.slice(0, insertIndex) +
pluginsBlockKts(pluginVersion) +
gradleScript.slice(insertIndex);
} else {
newGradleScript =
gradleScript.slice(0, insertIndex) +
pluginsBlock(pluginVersion) +
gradleScript.slice(insertIndex);
}
} else {
const insertIndex =
pluginsBlockMatch.index + pluginsBlockMatch[0].length - 1;
if (appFile.endsWith('.kts')) {
newGradleScript =
gradleScript.slice(0, insertIndex) +
pluginKts(pluginVersion) +
gradleScript.slice(insertIndex);
} else {
newGradleScript =
gradleScript.slice(0, insertIndex) +
plugin(pluginVersion) +
gradleScript.slice(insertIndex);
}
}
fs.writeFileSync(appFile, newGradleScript, 'utf8');
maybeAddSourceContextConfig(appFile, newGradleScript, orgSlug, projectSlug);
const buildSpinner = clack.spinner();
buildSpinner.start(
'Running ./gradlew to verify changes (this may take a few minutes)...',
);
try {
await bash.execute('./gradlew');
buildSpinner.stop(
chalk.greenBright(
`${chalk.bold('Sentry Gradle plugin')} added to the project.`,
),
);
} catch (e) {
buildSpinner.stop();
Sentry.captureException('Gradle Sync failed');
return false;
}
return true;
}
/**
* Looks for the applications packageName (namespace) in the specified build.gradle(.kts) file.
*
* ```
* android {
* namespace 'my.package.name' <-- this is what we extract
*
* compileSdkVersion = 31
* ...
* }
* ```
* @param appFile
* @returns the packageName(namespace) of the app if available
*/
export function getNamespace(appFile: string): string | undefined {
const gradleScript = fs.readFileSync(appFile, 'utf8');
const namespaceMatch = /namespace\s*=?\s*['"]([^'"]+)['"]/i.exec(
gradleScript,
);
if (!namespaceMatch || namespaceMatch.length <= 1) {
clack.log.warn('Unable to determine application package name.');
Sentry.captureException('No package name');
return undefined;
}
const namespace = namespaceMatch[1];
return namespace;
}
/**
* Adds source context configuration to the gradleScript if `sentry {}` block is not yet configured,
*
* @param appFile
* @param gradleScript
*/
function maybeAddSourceContextConfig(
appFile: string,
gradleScript: string,
orgSlug: string,
projectSlug: string,
) {
if (!/sentry\s*\{[^}]*\}/i.test(gradleScript)) {
// if no sentry {} block is configured, we add our own with source context enabled
if (appFile.endsWith('.kts')) {
fs.appendFileSync(
appFile,
sourceContextKts(orgSlug, projectSlug),
'utf8',
);
} else {
fs.appendFileSync(appFile, sourceContext(orgSlug, projectSlug), 'utf8');
}
}
}