Skip to content

Commit

Permalink
feat(android): Support for Android wizard (#389)
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn authored Aug 30, 2023
1 parent 5b76c51 commit 0c2a920
Show file tree
Hide file tree
Showing 17 changed files with 823 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ build
node_modules
npm-debug.log
ios
android
./android
yarn-error.log

scratch/
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Unreleased

- feat(android): Add wizard support for Android (#389)

Set up the Sentry Android SDK in your app with one command:

```sh
npx @sentry/wizard -i android
# or via brew
brew install getsentry/tools/sentry-wizard && sentry-wizard -i android
```

- feat(craft): Add `brew` target for automatically publishing `sentry-wizard` to Sentry's custom Homebrew tap (#406)

You can now install `sentry-wizard` via Homebrew:
Expand Down
5 changes: 5 additions & 0 deletions lib/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export enum Integration {
reactNative = 'reactNative',
ios = 'ios',
android = 'android',
cordova = 'cordova',
electron = 'electron',
nextjs = 'nextjs',
Expand Down Expand Up @@ -35,6 +36,8 @@ export function getPlatformDescription(type: string): string {

export function getIntegrationDescription(type: string): string {
switch (type) {
case Integration.android:
return 'Android';
case Integration.reactNative:
return 'React Native';
case Integration.cordova:
Expand All @@ -58,6 +61,8 @@ export function getIntegrationDescription(type: string): string {

export function mapIntegrationToPlatform(type: string): string | undefined {
switch (type) {
case Integration.android:
return 'android';
case Integration.reactNative:
return 'react-native';
case Integration.cordova:
Expand Down
8 changes: 6 additions & 2 deletions lib/Steps/ChooseIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Apple } from './Integrations/Apple';
import { SvelteKitShim } from './Integrations/SvelteKitShim';
import { hasPackageInstalled } from '../../src/utils/package-json';
import { Remix } from './Integrations/Remix';
import { Android } from './Integrations/Android';

let projectPackage: any = {};

Expand All @@ -38,6 +39,9 @@ export class ChooseIntegration extends BaseStep {

let integration = null;
switch (integrationPrompt.integration) {
case Integration.android:
integration = new Android(this._argv);
break;
case Integration.cordova:
integration = new Cordova(sanitizeUrl(this._argv));
break;
Expand Down Expand Up @@ -97,7 +101,7 @@ export class ChooseIntegration extends BaseStep {
return { integration: this._argv.integration };
} else {
if (this._argv.quiet) {
throw new Error('You need to choose a integration');
throw new Error('You need to choose a platform');
}

const detectedDefaultSelection = this.tryDetectingIntegration();
Expand All @@ -106,7 +110,7 @@ export class ChooseIntegration extends BaseStep {
{
choices: getIntegrationChoices(),
default: detectedDefaultSelection,
message: 'What integration do you want to set up?',
message: 'What platform do you want to set up?',
name: 'integration',
type: 'list',
},
Expand Down
23 changes: 23 additions & 0 deletions lib/Steps/Integrations/Android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Answers } from 'inquirer';
import { BaseIntegration } from './BaseIntegration';
import { Args } from '../../Constants';
import { runAndroidWizard } from '../../../src/android/android-wizard';

export class Android extends BaseIntegration {
public constructor(protected _argv: Args) {
super(_argv);
}

public async emit(_answers: Answers): Promise<Answers> {
await runAndroidWizard({
promoCode: this._argv.promoCode,
url: this._argv.url,
telemetryEnabled: !this._argv.disableTelemetry,
});
return {};
}

public shouldConfigure(_answers: Answers): Promise<Answers> {
return this._shouldConfigure;
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"read-env": "^1.3.0",
"semver": "^7.5.3",
"xcode": "3.0.1",
"xml-js": "^1.6.11",
"yargs": "^16.2.0"
},
"devDependencies": {
Expand Down
156 changes: 156 additions & 0 deletions src/android/android-wizard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import * as fs from 'fs';
// @ts-ignore - clack is ESM and TS complains about that. It works though
import * as clack from '@clack/prompts';
import * as path from 'path';
import * as gradle from './gradle';
import * as manifest from './manifest';
import * as codetools from './code-tools';
import {
abort,
confirmContinueEvenThoughNoGitRepo,
getOrAskForProjectData,
printWelcome,
} from '../utils/clack-utils';
import { WizardOptions } from '../utils/types';
import { traceStep, withTelemetry } from '../telemetry';
import chalk from 'chalk';

export async function runAndroidWizard(options: WizardOptions): Promise<void> {
return withTelemetry(
{
enabled: options.telemetryEnabled,
integration: 'android',
},
() => runAndroidWizardWithTelemetry(options),
);
}

async function runAndroidWizardWithTelemetry(
options: WizardOptions,
): Promise<void> {
printWelcome({
wizardName: 'Sentry Android Wizard',
promoCode: options.promoCode,
});

await confirmContinueEvenThoughNoGitRepo();

const projectDir = process.cwd();
const buildGradleFiles = findFilesWithExtensions(projectDir, [
'.gradle',
'gradle.kts',
]);

if (!buildGradleFiles || buildGradleFiles.length === 0) {
clack.log.error(
'No Gradle project found. Please run this command from the root of your project.',
);
await abort();
return;
}

const appFile = await traceStep('Select App File', () =>
gradle.selectAppFile(buildGradleFiles),
);

// ======== STEP 1. Add Sentry Gradle Plugin to build.gradle(.kts) ============
clack.log.step(
`Adding ${chalk.bold('Sentry Gradle plugin')} to your app's ${chalk.cyan(
'build.gradle',
)} file.`,
);
const pluginAdded = await traceStep('Add Gradle Plugin', () =>
gradle.addGradlePlugin(appFile),
);
if (!pluginAdded) {
clack.log.warn(
"Could not add Sentry Gradle plugin to your app's build.gradle file. You'll have to add it manually.\nPlease follow the instructions at https://docs.sentry.io/platforms/android/#install",
);
}

const { selectedProject } = await getOrAskForProjectData(options, 'android');

// ======== STEP 2. Configure Sentry SDK via AndroidManifest ============
clack.log.step(
`Configuring Sentry SDK via ${chalk.cyan('AndroidManifest.xml')}`,
);
const appDir = path.dirname(appFile);
const manifestFile = path.join(appDir, 'src', 'main', 'AndroidManifest.xml');

const manifestUpdated = traceStep('Update Android Manifest', () =>
manifest.addManifestSnippet(
manifestFile,
selectedProject.keys[0].dsn.public,
),
);
if (!manifestUpdated) {
clack.log.warn(
"Could not configure the Sentry SDK. You'll have to do it manually.\nPlease follow the instructions at https://docs.sentry.io/platforms/android/#configure",
);
}

// ======== STEP 3. Patch Main Activity with a test error snippet ============
clack.log.step(
`Patching ${chalk.bold('Main Activity')} with a test error snippet.`,
);
const mainActivity = traceStep('Find Main Activity', () =>
manifest.getMainActivity(manifestFile),
);
let packageName = mainActivity.packageName;
if (!packageName) {
// if no package name in AndroidManifest, look into gradle script
packageName = gradle.getNamespace(appFile);
}
const activityName = mainActivity.activityName;
if (!activityName || !packageName) {
clack.log.warn(
"Could not find Activity with intent action MAIN. You'll have to manually verify the setup.\nPlease follow the instructions at https://docs.sentry.io/platforms/android/#verify",
);
} else {
const packageNameStable = packageName;
const activityFile = traceStep('Find Main Activity Source File', () =>
codetools.findActivitySourceFile(appDir, packageNameStable, activityName),
);

const activityPatched = traceStep('Patch Main Activity', () =>
codetools.patchMainActivity(activityFile),
);
if (!activityPatched) {
clack.log.warn(
"Could not patch main activity. You'll have to manually verify the setup.\nPlease follow the instructions at https://docs.sentry.io/platforms/android/#verify",
);
}
}

// ======== OUTRO ========
clack.outro(`
${chalk.green('Successfully installed the Sentry Android SDK!')}
${chalk.cyan(
'You can validate your setup by launching your application and checking Sentry issues page afterwards',
)}
Check out the SDK documentation for further configuration:
https://docs.sentry.io/platforms/android/
`);
}

//find files with the given extension
function findFilesWithExtensions(
dir: string,
extensions: string[],
filesWithExtensions: string[] = [],
): string[] {
const cwd = process.cwd();
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
const childDir = path.join(dir, file.name);
findFilesWithExtensions(childDir, extensions, filesWithExtensions);
} else if (extensions.some((ext) => file.name.endsWith(ext))) {
filesWithExtensions.push(path.relative(cwd, path.join(dir, file.name)));
}
}
return filesWithExtensions;
}
Loading

0 comments on commit 0c2a920

Please sign in to comment.