Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(flutter): Add Flutter support #735

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- feat(nuxt): Add `import-in-the-middle` install step when using pnpm ([#727](https://github.com/getsentry/sentry-wizard/pull/727))
- fix(nuxt): Remove unused parameter in sentry-example-api template ([#734](https://github.com/getsentry/sentry-wizard/pull/734))
- feat(flutter): Add Flutter support ([#735](https://github.com/getsentry/sentry-wizard/pull/735))

## 3.36.0

Expand Down
5 changes: 5 additions & 0 deletions lib/Constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Key value should be the same here */
export enum Integration {
reactNative = 'reactNative',
flutter = 'flutter',
ios = 'ios',
android = 'android',
cordova = 'cordova',
Expand Down Expand Up @@ -41,6 +42,8 @@ export function getIntegrationDescription(type: string): string {
return 'Android';
case Integration.reactNative:
return 'React Native';
case Integration.flutter:
return 'Flutter';
case Integration.cordova:
return 'Cordova';
case Integration.electron:
Expand All @@ -66,6 +69,8 @@ export function mapIntegrationToPlatform(type: string): string | undefined {
return 'android';
case Integration.reactNative:
return 'react-native';
case Integration.flutter:
return 'flutter';
case Integration.cordova:
return 'cordova';
case Integration.electron:
Expand Down
235 changes: 235 additions & 0 deletions src/flutter/code-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import * as fs from 'fs';
import * as path from 'path';
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 {
sentryImport,
pubspecOptions,
sentryProperties,
initSnippet,
} from './templates';
import { fetchSdkVersion } from '../utils/release-registry';

/**
* Recursively finds a file per name in subfolders.
* @param dir - The directory to start searching.
* @param name - The name of the file including path extension.
* @returns The path to the main.dart file or null if not found.
*/
export function findFile(dir: string, name: string): string | null {
const files: string[] = fs.readdirSync(dir);

for (const file of files) {
const fullPath: string = path.join(dir, file);
const stats: fs.Stats = fs.statSync(fullPath);

if (stats.isDirectory()) {
const result: string | null = findFile(fullPath, name);
if (result) {
return result;
}
} else if (file === name) {
return fullPath;
}
}

return null;
}

export async function patchPubspec(pubspecFile: string | null, project: string, org: string): Promise<boolean> {
if (!pubspecFile || !fs.existsSync(pubspecFile)) {
clack.log.warn('No pubspec.yaml source file found in filesystem.');
Sentry.captureException('No pubspec.yaml source file');
return false;
}
let pubspecContent = fs.readFileSync(pubspecFile, 'utf8');

if (!pubspecContent.includes('sentry_flutter:')) {
const dependenciesIndex = getDependenciesLocation(pubspecContent);

const sentryDartFlutterVersion = await fetchSdkVersion("sentry.dart.flutter") ?? "any";
pubspecContent = pubspecContent.slice(0, dependenciesIndex) +
` sentry_flutter: ${sentryDartFlutterVersion ? `^${sentryDartFlutterVersion}` : "any"}\n` +
pubspecContent.slice(dependenciesIndex);

clack.log.success(
chalk.greenBright(
`${chalk.bold(
'sentry_flutter',
)} added to pubspec.yaml`,
),
);
} else {
clack.log.success(
chalk.greenBright(
`${chalk.bold(
'sentry_flutter',
)} is already included in pubspec.yaml`,
),
);
}

if (!pubspecContent.includes('sentry_dart_plugin:')) {
const devDependenciesIndex = getDevDependenciesLocation(pubspecContent);

const sentryDartPluginVersion = await fetchSdkVersion("sentry.dart.plugin") ?? "any";
pubspecContent = pubspecContent.slice(0, devDependenciesIndex) +
` sentry_dart_plugin: ${sentryDartPluginVersion ? `^${sentryDartPluginVersion}` : "any"}\n` +
pubspecContent.slice(devDependenciesIndex);

clack.log.success(
chalk.greenBright(
`${chalk.bold(
'sentry_dart_plugin',
)} added to pubspec.yaml`,
),
);
} else {
clack.log.success(
chalk.greenBright(
`${chalk.bold(
'sentry_dart_plugin',
)} is already included in pubspec.yaml`,
),
);
}

if (!pubspecContent.includes('sentry:')) {
pubspecContent += '\n'
pubspecContent += pubspecOptions(project, org);

clack.log.success(
chalk.greenBright(
`${chalk.bold(
'sentry plugin configuration',
)} added to pubspec.yaml`,
),
);
} else {
clack.log.success(
chalk.greenBright(
`${chalk.bold(
'sentry plugin configuration',
)} is already included in pubspec.yaml`,
),
);
}

fs.writeFileSync(pubspecFile, pubspecContent, 'utf8');

return true;
}

export function addProperties(pubspecFile: string | null, authToken: string) {
if (!pubspecFile || !fs.existsSync(pubspecFile)) {
clack.log.warn('No pubspec.yaml source file found in filesystem.');
Sentry.captureException('No pubspec.yaml source file');
return false;
}

try {
const pubspecDir = path.dirname(pubspecFile);
const sentryPropertiesFileName = 'sentry.properties';
const sentryPropertiesFile = path.join(pubspecDir, sentryPropertiesFileName);
const sentryPropertiesContent = sentryProperties(authToken);

fs.writeFileSync(sentryPropertiesFile, sentryPropertiesContent, 'utf8');

const gitignoreFile = path.join(pubspecDir, '.gitignore');
if (fs.existsSync(gitignoreFile)) {
fs.appendFileSync(gitignoreFile, `\n${sentryPropertiesFileName}\n`);
} else {
fs.writeFileSync(gitignoreFile, `${sentryPropertiesFileName}\n`, 'utf8');
}
return true;
} catch (e) {
return false;
}
}

export function patchMain(mainFile: string | null, dsn: string): boolean {
if (!mainFile || !fs.existsSync(mainFile)) {
clack.log.warn('No main.dart source file found in filesystem.');
Sentry.captureException('No main.dart source file');
return false;
}

let mainContent = fs.readFileSync(mainFile, 'utf8');

if (/import\s+['"]package[:]sentry_flutter\/sentry_flutter\.dart['"];?/i.test(mainContent)) {
// sentry is already configured
clack.log.success(
chalk.greenBright(
`${chalk.bold(
'main.dart',
)} is already patched with test error snippet.`,
),
);
return true;
}

mainContent = patchMainContent(dsn, mainContent);

fs.writeFileSync(mainFile, mainContent, 'utf8');

clack.log.success(
chalk.greenBright(
`Patched ${chalk.bold(
'main.dart',
)} with the Sentry setup and test error snippet.`,
),
);

return true;
}

export function patchMainContent(dsn: string, mainContent: string): string {

const importIndex = getLastImportLineLocation(mainContent);
mainContent = mainContent.slice(0, importIndex) +
sentryImport +
mainContent.slice(importIndex);

// Find and replace `runApp(...)`
mainContent = mainContent.replace(
/runApp\(([\s\S]*?)\);/g, // Match the `runApp(...)` invocation
(_, runAppArgs) => initSnippet(dsn, runAppArgs as string)
);

// Make the `main` function async if it's not already
mainContent = mainContent.replace(
/void\s+main\(\)\s*\{/g,
'Future<void> main() async {'
);

return mainContent;
}

export function getLastImportLineLocation(sourceCode: string): number {
const importRegex = /import\s+['"].*['"].*;/gim;
return getLastReqExpLocation(sourceCode, importRegex);
}

export function getDependenciesLocation(sourceCode: string): number {
const dependencyRegex = /^dependencies:\s*$/gim;
return getLastReqExpLocation(sourceCode, dependencyRegex);
}

export function getDevDependenciesLocation(sourceCode: string): number {
const dependencyRegex = /^dev_dependencies:\s*$/gim;
return getLastReqExpLocation(sourceCode, dependencyRegex);
}

// Helper

function getLastReqExpLocation(sourceCode: string, regExp: RegExp): number {
let match = regExp.exec(sourceCode);
let importIndex = 0;
while (match) {
importIndex = match.index + match[0].length + 1;
match = regExp.exec(sourceCode);
}
return importIndex;
}
115 changes: 115 additions & 0 deletions src/flutter/flutter-wizzard.ts
denrase marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { WizardOptions } from '../utils/types';
import * as Sentry from '@sentry/node';
import * as codetools from './code-tools';

// @ts-ignore - clack is ESM and TS complains about that. It works though
import * as clack from '@clack/prompts';
import chalk from 'chalk';

import {
confirmContinueIfNoOrDirtyGitRepo,
getOrAskForProjectData,
printWelcome,
} from '../utils/clack-utils';

import { traceStep, withTelemetry } from '../telemetry';
import { findFile } from './code-tools';

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

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

await confirmContinueIfNoOrDirtyGitRepo();

const { selectedProject, selfHosted, sentryUrl, authToken } =
await getOrAskForProjectData(options, 'flutter');

// const dsn = selectedProject.keys[0].dsn.public;
denrase marked this conversation as resolved.
Show resolved Hide resolved
const projectDir = process.cwd();
const pubspecFile = findFile(projectDir, 'pubspec.yaml');

// ======== STEP 1. Add sentry_flutter and sentry_dart_plugin to pubspec.yaml ============
clack.log.step(
`Adding ${chalk.bold('Sentry')} to your apps ${chalk.cyan('pubspec.yaml',)} file.`,
);
const pubspecPatched = await traceStep('Patch pubspec.yaml', () =>
codetools.patchPubspec(
pubspecFile,
selectedProject.slug,
selectedProject.organization.slug
),
);
if (!pubspecPatched) {
clack.log.warn(
"Could not add Sentry to your apps pubspec.yaml file. You'll have to add it manually.\nPlease follow the instructions at https://docs.sentry.io/platforms/flutter/#install",
);
}
Sentry.setTag('pubspec-patched', pubspecPatched);

// ======== STEP 2. Add sentry.properties with auth token ============

const propertiesAdded = traceStep('Add sentry.properties', () =>
codetools.addProperties(pubspecFile, authToken),
);
if (!propertiesAdded) {
clack.log.warn(
`We could not add "sentry.properties" file in your project directory in order to provide an auth token for Sentry CLI. You'll have to add it manually, or you can set the SENTRY_AUTH_TOKEN environment variable instead. See https://docs.sentry.io/cli/configuration/#auth-token for more information.`,
);
} else {
clack.log.info(
`We created "sentry.properties" file in your project directory in order to provide an auth token for Sentry CLI.\nIt was also added to your ".gitignore" file.\nAt your CI enviroment, you can set the SENTRY_AUTH_TOKEN environment variable instead. See https://docs.sentry.io/cli/configuration/#auth-token for more information.`,
);
}
Sentry.setTag('sentry-properties-added', pubspecPatched);

// ======== STEP 3. Patch main.dart with setup and a test error snippet ============
clack.log.step(
`Patching ${chalk.bold('main.dart')} with setup and test error snippet.`,
);

const mainFile = findFile(projectDir, 'main.dart');
const dsn = selectedProject.keys[0].dsn.public;

const mainPatched = traceStep('Patch main.dart', () =>
codetools.patchMain(mainFile, dsn),
);
if (!mainPatched) {
clack.log.warn(
"Could not patch main.dart file. You'll have to manually verify the setup.\nPlease follow the instructions at https://docs.sentry.io/platforms/flutter/#verify",
);
}
Sentry.setTag('main-patched', mainPatched);

// ======== OUTRO ========

const issuesPageLink = selfHosted
? `${sentryUrl}organizations/${selectedProject.organization.slug}/issues/?project=${selectedProject.id}`
: `https://${selectedProject.organization.slug}.sentry.io/issues/?project=${selectedProject.id}`;

clack.outro(`
${chalk.greenBright('Successfully installed the Sentry Flutter SDK!')}

${chalk.cyan(
`You can validate your setup by launching your application and checking Sentry issues page afterwards
denrase marked this conversation as resolved.
Show resolved Hide resolved
${issuesPageLink}`,
)}

Check out the SDK documentation for further configuration:
https://docs.sentry.io/platforms/flutter/
`);
};
Loading
Loading