-
-
Notifications
You must be signed in to change notification settings - Fork 49
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
base: master
Are you sure you want to change the base?
Changes from all commits
db3f33d
1ccd8cd
fd82fb5
7516db9
95a01e6
8a6839e
7844198
9e8fecd
b6cd5db
d7e5f4a
cf0c522
7a10349
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we remove this comment? |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should it make sense to give more specific instructions? Since we set up the plugin as well the app should be able to have symbolicated stacktraces. Telling the user to start the app I assume most people will just run it in dev mode There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw @denrase wizard filename is wrong -> |
||
${issuesPageLink}`, | ||
)} | ||
|
||
Check out the SDK documentation for further configuration: | ||
https://docs.sentry.io/platforms/flutter/ | ||
`); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
flutter-wizard.ts not flutter-wizzard.ts
just a typo