@sentry/wizard
Version:
Sentry wizard helping you to configure your project
181 lines (163 loc) • 5.77 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* 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 Sentry from '@sentry/node';
import { manifest } from './templates';
import xml, { Attributes, ElementCompact } from 'xml-js';
import chalk from 'chalk';
/**
* Looks for the closing </application> tag in the manifest and adds the Sentry config after it.
*
* For example:
* ```xml
* <manifest xmlns:android="http://schemas.android.com/apk/res/android"
* xmlns:tools="http://schemas.android.com/tools">
*
* <application>
* ...
* // this is what we add and more
* <meta-data android:name="io.sentry.dsn" android:value="__dsn__" />
* </application> <!-- we are looking for this one
* </manifest>
* ```
*
* @param manifestFile the path to the main AndroidManifest.xml file
* @param dsn
* @returns true if successfully patched the manifest, false otherwise
*/
export function addManifestSnippet(manifestFile: string, dsn: string): boolean {
if (!fs.existsSync(manifestFile)) {
clack.log.warn('AndroidManifest.xml not found.');
Sentry.captureException('No AndroidManifest file');
return false;
}
const manifestContent = fs.readFileSync(manifestFile, 'utf8');
if (/android:name="io\.sentry[^"]*"/i.test(manifestContent)) {
// sentry is already configured
clack.log.success(chalk.greenBright('Sentry SDK is already configured.'));
return true;
}
const applicationMatch = /<\/application>/i.exec(manifestContent);
if (!applicationMatch) {
clack.log.warn('<application> tag not found within the manifest.');
Sentry.captureException('No <application> tag');
return false;
}
const insertionIndex = applicationMatch.index;
const newContent =
manifestContent.slice(0, insertionIndex) +
manifest(dsn) +
manifestContent.slice(insertionIndex);
fs.writeFileSync(manifestFile, newContent, 'utf8');
clack.log.success(
chalk.greenBright(
`Updated ${chalk.bold(
'AndroidManifest.xml',
)} with the Sentry SDK configuration.`,
),
);
return true;
}
/**
* There might be multiple <activity> in the manifest, as well as multiple <activity-alias> with category LAUNCHER,
* but only one main activity with action MAIN. We are looking for this one by parsing xml and walking it.
*
* In addition, older Android versions required to specify the packag name in the manifest,
* while the new ones - in the Gradle config. So we are just sanity checking if the package name
* is in the manifest and returning it as well.
*
* For example:
*
* ```xml
* <manifest xmlns:android="http://schemas.android.com/apk/res/android"
* xmlns:tools="http://schemas.android.com/tools"
* package="com.example.sample">
*
* <application>
* <activity
* android:name="ui.MainActivity"
* ...other props>
* <intent-filter>
* <action android:name="android.intent.action.MAIN" /> <!-- we are looking for this one
*
* <category android:name="android.intent.category.LAUNCHER" />
* </intent-filter>
* </activity>
* </application>
* </manifest>
* ```
*
* @param manifestFile path to the AndroidManifest.xml file
* @returns package name (if available in the manifest) + the main activity name
*/
export function getMainActivity(manifestFile: string): {
packageName?: string;
activityName?: string;
} {
if (!fs.existsSync(manifestFile)) {
clack.log.warn('AndroidManifest.xml not found.');
Sentry.captureException('No AndroidManifest file');
return {};
}
const manifestContent = fs.readFileSync(manifestFile, 'utf8');
const converted: ElementCompact = xml.xml2js(manifestContent, {
compact: true,
});
const activities: ElementCompact[] | ElementCompact | undefined =
converted.manifest?.application?.activity;
const packageName: string | undefined =
converted.manifest?._attributes?.['package'];
if (!activities) {
clack.log.warn('No activity found in AndroidManifest.');
Sentry.captureException('No Activity');
return {};
}
let mainActivity;
if (Array.isArray(activities)) {
const withIntentFilter = activities.filter((a) => !!a['intent-filter']);
mainActivity = withIntentFilter.find((a) => isMainActivity(a));
} else if (isMainActivity(activities)) {
mainActivity = activities;
}
if (!mainActivity) {
clack.log.warn('No main activity found in AndroidManifest.');
Sentry.captureException('No Main Activity');
return {};
}
const attrs = mainActivity._attributes;
const activityName = attrs?.['android:name'] as string | undefined;
return { packageName: packageName, activityName: activityName };
}
function isMainActivity(activity: ElementCompact): boolean {
const intentFilters: ElementCompact[] | ElementCompact =
activity['intent-filter'];
if (Array.isArray(intentFilters)) {
return intentFilters.some((i) => {
const action: ElementCompact[] | ElementCompact | undefined = i.action;
return hasMainAction(action);
});
} else {
const action: ElementCompact[] | ElementCompact | undefined =
intentFilters.action;
return hasMainAction(action);
}
}
function hasMainAction(
action: ElementCompact[] | ElementCompact | undefined,
): boolean {
if (!action) {
return false;
}
function isMain(attrs?: Attributes): boolean {
return attrs?.['android:name'] === 'android.intent.action.MAIN';
}
if (Array.isArray(action)) {
return action.some((c) => {
return isMain(c._attributes);
});
} else {
return isMain(action._attributes);
}
}