diff --git a/src/flutter/code-tools.ts b/src/flutter/code-tools.ts index 8fe2e3e6..9073d124 100644 --- a/src/flutter/code-tools.ts +++ b/src/flutter/code-tools.ts @@ -8,8 +8,10 @@ import { sentryImport, pubspecOptions, sentryProperties, + initSnippet, // testErrorSnippet, } from './templates'; +import { fetchSdkVersion } from '../utils/release-registry'; /** * Recursively finds a file per name in subfolders. @@ -37,7 +39,7 @@ export function findFile(dir: string, name: string): string | null { return null; } -export function patchPubspec(pubspecFile: string | null, project: string, org: string): boolean { +export async function patchPubspec(pubspecFile: string | null, project: string, org: string): Promise { if (!pubspecFile || !fs.existsSync(pubspecFile)) { clack.log.warn('No pubspec.yaml source file found in filesystem.'); Sentry.captureException('No pubspec.yaml source file'); @@ -49,16 +51,16 @@ export function patchPubspec(pubspecFile: string | null, project: string, org: s // TODO: Check if already added sentry: + const sentryFlutterVersion = await fetchSdkVersion("sentry.dart.flutter") ?? "any"; pubspecContent = pubspecContent.slice(0, dependenciesIndex) + - ' sentry:\n' + + ` sentry_flutter: ${sentryFlutterVersion ? `^${sentryFlutterVersion}` : "any"}\n` + pubspecContent.slice(dependenciesIndex); const devDependenciesIndex = getDevDependenciesLocation(pubspecContent); // TODO: Check if already added sentry-dart-plugin: - pubspecContent = pubspecContent.slice(0, devDependenciesIndex) + - ' sentry-dart-plugin:\n' + + ' sentry_dart_plugin: any\n' + // TODO: There is no sentry dart plugin in https://release-registry.services.sentry.io/sdks pubspecContent.slice(devDependenciesIndex); // TODO: Check if already added sentry: @@ -92,21 +94,20 @@ export function addProperties(pubspecFile: string | null, authToken: string) { } else { fs.writeFileSync(gitignoreFile, `${sentryPropertiesFileName}\n`, 'utf8'); } - return true; } catch (e) { return false; } } -export function patchMain(mainFile: string | null): boolean { +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; } - const mainContent = fs.readFileSync(mainFile, 'utf8'); + let mainContent = fs.readFileSync(mainFile, 'utf8'); if (/import\s+['"]package[:]sentry_flutter\/sentry_flutter\.dart['"];?/i.test(mainContent)) { // sentry is already configured @@ -120,16 +121,9 @@ export function patchMain(mainFile: string | null): boolean { return true; } - const importIndex = getLastImportLineLocation(mainContent); - const newActivityContent = mainContent.slice(0, importIndex) + - sentryImport + - mainContent.slice(importIndex); - - // TODO: @denis setup + mainContent = patchMainContent(dsn, mainContent); - // TODO: @denis snippet - - fs.writeFileSync(mainFile, newActivityContent, 'utf8'); + fs.writeFileSync(mainFile, mainContent, 'utf8'); clack.log.success( chalk.greenBright( @@ -142,6 +136,28 @@ export function patchMain(mainFile: string | null): boolean { 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 main() async {' + ); + + return mainContent; +} + export function getLastImportLineLocation(sourceCode: string): number { const importRegex = /import\s+['"].*['"].*;/gim; return getLastReqExpLocation(sourceCode, importRegex); diff --git a/src/flutter/flutter-wizzard.ts b/src/flutter/flutter-wizzard.ts index c80111b3..415dc98c 100644 --- a/src/flutter/flutter-wizzard.ts +++ b/src/flutter/flutter-wizzard.ts @@ -51,11 +51,13 @@ async function runFlutterWizzardWithTelemetry( clack.log.step( `Adding ${chalk.bold('Sentry')} to your apps ${chalk.cyan('pubspec.yaml',)} file.`, ); - const pubspecPatched = codetools.patchPubspec( - pubspecFile, - selectedProject.slug, - selectedProject.organization.slug - ) + 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", @@ -65,7 +67,9 @@ async function runFlutterWizzardWithTelemetry( // ======== STEP X. Add sentry.properties with auth token ============ - const propertiesAdded = codetools.addProperties(pubspecFile, authToken); + 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.`, @@ -81,8 +85,12 @@ async function runFlutterWizzardWithTelemetry( 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(findFile(projectDir, 'main.dart')), + codetools.patchMain(mainFile, dsn), ); if (!mainPatched) { clack.log.warn( diff --git a/src/flutter/templates.ts b/src/flutter/templates.ts index 28e37ee7..23a36df6 100644 --- a/src/flutter/templates.ts +++ b/src/flutter/templates.ts @@ -12,3 +12,22 @@ export function pubspecOptions(project: string, org: string): string { export function sentryProperties(authToken: string): string { return `auth_token=${authToken}`; } + +export function initSnippet(dsn: string, runApp: string): string { + return `await SentryFlutter.init( + (options) { + options.dsn = '${dsn}'; + // Set tracesSampleRate to 1.0 to capture 100% of transactions for tracing. + // We recommend adjusting this value in production. + options.tracesSampleRate = 1.0; + // The sampling rate for profiling is relative to tracesSampleRate + // Setting to 1.0 will profile 100% of sampled transactions: + // Note: Profiling alpha is available for iOS and macOS since SDK version 7.12.0 + options.profilesSampleRate = 1.0; + }, + appRunner: () => runApp(${runApp}), + ); + // TODO: Remove this line after sending the first sample event to sentry. + Sentry.captureMessage('This is a sample exception.'); +` +} diff --git a/test/flutter/code-tools.test.ts b/test/flutter/code-tools.test.ts index b0c19fcb..7aab39f8 100644 --- a/test/flutter/code-tools.test.ts +++ b/test/flutter/code-tools.test.ts @@ -1,5 +1,7 @@ //@ts-ignore -import { getDependenciesLocation, getDevDependenciesLocation, getLastImportLineLocation } from '../../src/flutter/code-tools'; +import { patchMainContent, getDependenciesLocation, getDevDependenciesLocation, getLastImportLineLocation } from '../../src/flutter/code-tools'; +//@ts-ignore +import { initSnippet } from '../../src/flutter/templates'; describe('code-tools', () => { const pubspec = `name: flutter_example @@ -19,6 +21,94 @@ dev_dependencies: flutter_lints: ^2.0.0 `; + const simpleRunApp = `import 'package:flutter/widgets.dart'; + +void main() { + runApp(const MyApp()); +} +`; + + const asyncRunApp = `import 'package:flutter/widgets.dart'; + +void main() { + runApp(const MyApp()); +} +`; + + const simpleRunAppPatched = `import 'package:flutter/widgets.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +Future main() async { + ${initSnippet('dsn', 'const MyApp()')} +} +`; + + const paramRunApp = `import 'package:flutter/widgets.dart'; + +Future main() async { + await someFunction(); + runApp(MyApp(param: SomeParam())); + await anotherFunction(); +} +`; + + const paramRunAppPatched = `import 'package:flutter/widgets.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +Future main() async { + await someFunction(); + ${initSnippet('dsn', 'MyApp(param: SomeParam())')} + await anotherFunction(); +} +`; + + const multilineRunApp = `import 'package:flutter/widgets.dart'; + +void main() { + runApp( + MyApp( + param: Param(), + multi: Another(1), + line: await bites(the: "dust"), + ), + ); + anotherFunction(); +} +`; + + const multilineRunAppPatched = `import 'package:flutter/widgets.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +Future main() async { + ${initSnippet('dsn', ` + MyApp( + param: Param(), + multi: Another(1), + line: await bites(the: "dust"), + ), + `)} + anotherFunction(); +} +`; + + describe('patchMainContent', () => { + it('wraps simple runApp', () => { + expect(patchMainContent('dsn', simpleRunApp)).toBe(simpleRunAppPatched); + }); + + it('wraps async runApp', () => { + expect(patchMainContent('dsn', asyncRunApp)).toBe(simpleRunAppPatched); + }); + + it('wraps runApp with parameterized app', () => { + expect(patchMainContent('dsn', paramRunApp)).toBe(paramRunAppPatched); + }); + + it('wraps multiline runApp', () => { + expect(patchMainContent('dsn', multilineRunApp)).toBe(multilineRunAppPatched); + }); + }); + describe('pubspec', () => { it('returns proper line index for dependencies', () => { expect(getDependenciesLocation(pubspec)).toBe(