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 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
db3f33d
add test to find last import line
denrase Nov 27, 2024
1ccd8cd
add basic wizzard and patch import in main.dart
denrase Nov 27, 2024
fd82fb5
patch pubspec.yaml
denrase Dec 2, 2024
7516db9
Merge branch 'master' into feat/flutter-support
denrase Dec 3, 2024
95a01e6
add sentry dart plugin options and propeties to pubspec file
denrase Dec 3, 2024
8a6839e
patch main with sentry init, fetch sentry_flutter version
denrase Dec 3, 2024
7844198
Merge branch 'master' into feat/flutter-support
denrase Dec 9, 2024
9e8fecd
update template, check already added to pubspec
denrase Dec 9, 2024
b6cd5db
order flutter behind reactNative
denrase Dec 9, 2024
d7e5f4a
cleanup
denrase Dec 9, 2024
cf0c522
update order
denrase Dec 9, 2024
7a10349
add cl entry
denrase Dec 9, 2024
c971115
Merge branch 'master' into feat/flutter-support
denrase Dec 16, 2024
23c3db4
fix typos
denrase Dec 16, 2024
51e0fd4
remove comment
denrase Dec 16, 2024
d3ae482
introduce options in flutter templates
denrase Dec 16, 2024
fce1ddd
options in setup, dont upload sources, upload debug symbols
denrase Dec 16, 2024
c15ca33
setup flutter e2e test
denrase Dec 16, 2024
e8c2f59
add flutter test app
denrase Dec 16, 2024
56b7889
fix Flutter e2e test
denrase Dec 16, 2024
17dcd69
update docs/readme
denrase Dec 17, 2024
1d55981
Merge branch 'master' of https://github.com/denrase/sentry-wizard int…
denrase Dec 17, 2024
4d78e0f
Merge branch 'master' into feat/flutter-support
denrase Dec 30, 2024
8cf3b5a
Disable replay option for now
denrase Dec 30, 2024
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