-
-
Notifications
You must be signed in to change notification settings - Fork 60
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(react-native): Wrap root app component #835
base: master
Are you sure you want to change the base?
Changes from all commits
3121e60
5fc92e5
2fd9e0a
9776481
ef2a70e
7dcb9e0
fd75f41
dbceb6f
a77a242
6c3864b
8bfe1f2
f30cfee
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 | ||||
---|---|---|---|---|---|---|
|
@@ -16,13 +16,7 @@ | |||||
import { RN_SDK_PACKAGE } from './react-native-wizard'; | ||||||
|
||||||
export async function addSentryInit({ dsn }: { dsn: string }) { | ||||||
const prefixGlob = '{.,./src,./app}'; | ||||||
const suffixGlob = '@(j|t|cj|mj)s?(x)'; | ||||||
const universalGlob = `@(App|_layout).${suffixGlob}`; | ||||||
const jsFileGlob = `${prefixGlob}/+(${universalGlob})`; | ||||||
const jsPath = traceStep('find-app-js-file', () => | ||||||
getFirstMatchedPath(jsFileGlob), | ||||||
); | ||||||
const jsPath = getMainAppFilePath('find-app-js-file'); | ||||||
Sentry.setTag('app-js-file-status', jsPath ? 'found' : 'not-found'); | ||||||
if (!jsPath) { | ||||||
clack.log.warn( | ||||||
|
@@ -101,3 +95,188 @@ | |||||
// spotlight: __DEV__, | ||||||
});`; | ||||||
} | ||||||
|
||||||
function getMainAppFilePath(stepToTrace: string): string | undefined { | ||||||
const prefixGlob = '{.,./src,./app}'; | ||||||
const suffixGlob = '@(j|t|cj|mj)s?(x)'; | ||||||
const universalGlob = `@(App|_layout).${suffixGlob}`; | ||||||
const jsFileGlob = `${prefixGlob}/+(${universalGlob})`; | ||||||
const jsPath = traceStep(stepToTrace, () => getFirstMatchedPath(jsFileGlob)); | ||||||
return jsPath; | ||||||
} | ||||||
|
||||||
/** | ||||||
* This step should be executed after `addSentryInit` | ||||||
*/ | ||||||
export async function wrapRootComponent() { | ||||||
const jsPath = getMainAppFilePath('find-app-js-file-to-wrap'); | ||||||
Sentry.setTag('app-js-file-status-to-wrap', jsPath ? 'found' : 'not-found'); | ||||||
if (!jsPath) { | ||||||
clack.log.warn( | ||||||
`Could not find main App file. Please wrap your App's Root component.`, | ||||||
); | ||||||
await showCopyPasteInstructions( | ||||||
'App.js or _layout.tsx', | ||||||
getSentryWrapColoredCodeSnippet(), | ||||||
); | ||||||
return; | ||||||
} | ||||||
|
||||||
const jsRelativePath = path.relative(process.cwd(), jsPath); | ||||||
|
||||||
const js = fs.readFileSync(jsPath, 'utf-8'); | ||||||
|
||||||
const result = checkAndWrapRootComponent(js); | ||||||
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. The Checkout sentry-wizard/src/react-native/metro.ts Line 114 in fe849fe
sentry-wizard/src/utils/ast-utils.ts Line 85 in f30cfee
Using the AST manipulation will be more safe to adjust the code. |
||||||
|
||||||
if (result === SentryWrapError.AlreadyWrapped) { | ||||||
Sentry.setTag('app-js-file-status', 'already-includes-sentry-wrap'); | ||||||
clack.log.warn( | ||||||
`${chalk.cyan( | ||||||
jsRelativePath, | ||||||
)} already includes Sentry.wrap. We wont't add it again.`, | ||||||
); | ||||||
return; | ||||||
} | ||||||
|
||||||
if (result === SentryWrapError.NoImport) { | ||||||
clack.log.warn( | ||||||
`Please import '@sentry/react-native' and wrap your App's Root component manually.`, | ||||||
); | ||||||
await showCopyPasteInstructions( | ||||||
'App.js or _layout.tsx', | ||||||
getSentryWrapColoredCodeSnippet(), | ||||||
); | ||||||
return; | ||||||
} | ||||||
|
||||||
if (result === SentryWrapError.NotFound) { | ||||||
clack.log.warn( | ||||||
`Could not find your App's Root component. Please wrap your App's Root component manually.`, | ||||||
); | ||||||
await showCopyPasteInstructions( | ||||||
'App.js or _layout.tsx', | ||||||
getSentryWrapColoredCodeSnippet(), | ||||||
); | ||||||
return; | ||||||
} | ||||||
|
||||||
traceStep('add-sentry-wrap', () => { | ||||||
clack.log.success( | ||||||
`Added ${chalk.cyan('Sentry.wrap')} to ${chalk.cyan(jsRelativePath)}.`, | ||||||
); | ||||||
|
||||||
fs.writeFileSync(jsPath, result, 'utf-8'); | ||||||
}); | ||||||
|
||||||
Sentry.setTag('app-js-file-status', 'added-sentry-wrap'); | ||||||
clack.log.success( | ||||||
chalk.green(`${chalk.cyan(jsRelativePath)} changes saved.`), | ||||||
); | ||||||
} | ||||||
|
||||||
export enum SentryWrapError { | ||||||
NotFound = 'RootComponentNotFound', | ||||||
AlreadyWrapped = 'AlreadyWrapped', | ||||||
NoImport = 'NoImport', | ||||||
} | ||||||
|
||||||
export function checkAndWrapRootComponent( | ||||||
js: string, | ||||||
): string | SentryWrapError { | ||||||
if (doesContainSentryWrap(js)) { | ||||||
return SentryWrapError.AlreadyWrapped; | ||||||
} | ||||||
|
||||||
if (!foundRootComponent(js)) { | ||||||
return SentryWrapError.NotFound; | ||||||
} | ||||||
|
||||||
if ( | ||||||
!doesJsCodeIncludeSdkSentryImport(js, { sdkPackageName: RN_SDK_PACKAGE }) | ||||||
) { | ||||||
return SentryWrapError.NoImport; | ||||||
} | ||||||
|
||||||
return addSentryWrap(js); | ||||||
} | ||||||
|
||||||
function doesContainSentryWrap(js: string): boolean { | ||||||
return js.includes('Sentry.wrap'); | ||||||
} | ||||||
|
||||||
// Matches simple named exports like `export default App;` | ||||||
const SIMPLE_EXPORT_REGEX = /export default (\w+);/; | ||||||
|
||||||
/* | ||||||
Matches named function exports like: | ||||||
|
||||||
export default function RootLayout() { | ||||||
// function body | ||||||
} | ||||||
*/ | ||||||
const NAMED_FUNCTION_REGEX = | ||||||
/export default function (\w+)\s*\([^)]*\)\s*\{([\s\S]*)\}\s*$/; | ||||||
|
||||||
/* | ||||||
Matches anonymous function exports like: | ||||||
|
||||||
export default () => { | ||||||
// function body | ||||||
} | ||||||
*/ | ||||||
const ANONYMOUS_FUNCTION_REGEX = | ||||||
/export default\s*\(\s*\)\s*=>\s*\{([\s\S]*)\}\s*$/; | ||||||
|
||||||
// Matches simple wrapped exports like `export default Another.wrapper(App);` | ||||||
const SIMPLE_WRAPPED_COMPONENT_REGEX = /export default (\w+\.\w+)\((\w+)\);/; | ||||||
|
||||||
function foundRootComponent(js: string): boolean { | ||||||
return ( | ||||||
SIMPLE_EXPORT_REGEX.test(js) || | ||||||
NAMED_FUNCTION_REGEX.test(js) || | ||||||
ANONYMOUS_FUNCTION_REGEX.test(js) || | ||||||
SIMPLE_WRAPPED_COMPONENT_REGEX.test(js) | ||||||
); | ||||||
} | ||||||
|
||||||
function addSentryWrap(js: string): string { | ||||||
if (SIMPLE_EXPORT_REGEX.test(js)) { | ||||||
return js.replace(SIMPLE_EXPORT_REGEX, 'export default Sentry.wrap($1);'); | ||||||
} | ||||||
|
||||||
if (NAMED_FUNCTION_REGEX.test(js)) { | ||||||
return js.replace( | ||||||
NAMED_FUNCTION_REGEX, | ||||||
(_match: string, funcName: string, body: string) => { | ||||||
return `export default Sentry.wrap(function ${funcName}() {${body}});`; | ||||||
}, | ||||||
); | ||||||
} | ||||||
|
||||||
if (ANONYMOUS_FUNCTION_REGEX.test(js)) { | ||||||
return js.replace( | ||||||
ANONYMOUS_FUNCTION_REGEX, | ||||||
(_match: string, body: string) => { | ||||||
return `export default Sentry.wrap(() => {${body}});`; | ||||||
}, | ||||||
); | ||||||
} | ||||||
|
||||||
if (SIMPLE_WRAPPED_COMPONENT_REGEX.test(js)) { | ||||||
return js.replace( | ||||||
SIMPLE_WRAPPED_COMPONENT_REGEX, | ||||||
(_match: string, wrapper: string, component: string) => | ||||||
`export default Sentry.wrap(${wrapper}(${component}));`, | ||||||
); | ||||||
} | ||||||
|
||||||
return js; | ||||||
} | ||||||
|
||||||
function getSentryWrapColoredCodeSnippet() { | ||||||
return makeCodeSnippet(true, (_unchanged, plus, _minus) => { | ||||||
return plus(`import * as Sentry from '@sentry/react-native'; | ||||||
|
||||||
export default Sentry.wrap(App);`); | ||||||
}); | ||||||
} |
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.
The
traceStep
function creates a span, and since those have parent-child relations we can reuse the same name. No need to pass unique for different steps.