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(react-native): Wrap root app component #835

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- feat(react-native): Adds wrapping root app component with `Sentry.wrap` ([#835](https://github.com/getsentry/sentry-wizard/pull/835))

## 4.1.0

- feat(nuxt): More granular error catching while modifying config ([#833](https://github.com/getsentry/sentry-wizard/pull/833))
Expand Down
193 changes: 186 additions & 7 deletions src/react-native/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Check warning on line 19 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L19

Added line #L19 was not covered by tests
Copy link
Member

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.

Sentry.setTag('app-js-file-status', jsPath ? 'found' : 'not-found');
if (!jsPath) {
clack.log.warn(
Expand Down Expand Up @@ -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;

Check warning on line 105 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L99-L105

Added lines #L99 - L105 were not covered by tests
}

/**
* This step should be executed after `addSentryInit`
*/
export async function wrapRootComponent() {
const jsPath = getMainAppFilePath('find-app-js-file-to-wrap');

Check warning on line 112 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L112

Added line #L112 was not covered by tests
Sentry.setTag('app-js-file-status-to-wrap', jsPath ? 'found' : 'not-found');
if (!jsPath) {
clack.log.warn(

Check warning on line 115 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L115

Added line #L115 was not covered by tests
`Could not find main App file. Please wrap your App's Root component.`,
);
await showCopyPasteInstructions(

Check warning on line 118 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L118

Added line #L118 was not covered by tests
'App.js or _layout.tsx',
getSentryWrapColoredCodeSnippet(),
);
return;

Check warning on line 122 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L122

Added line #L122 was not covered by tests
}

const jsRelativePath = path.relative(process.cwd(), jsPath);

Check warning on line 125 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L125

Added line #L125 was not covered by tests

const js = fs.readFileSync(jsPath, 'utf-8');

Check warning on line 127 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L127

Added line #L127 was not covered by tests

const result = checkAndWrapRootComponent(js);

Check warning on line 129 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L129

Added line #L129 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Sentry.init used regex mainly because it was written before we started using AST manipulation.

Checkout

export async function patchMetroConfigWithSentrySerializer() {
or/and
export function getOrSetObjectProperty(

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(

Check warning on line 133 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L132-L133

Added lines #L132 - L133 were not covered by tests
`${chalk.cyan(
jsRelativePath,
)} already includes Sentry.wrap. We wont't add it again.`,
);
return;

Check warning on line 138 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L138

Added line #L138 was not covered by tests
}

if (result === SentryWrapError.NoImport) {
clack.log.warn(

Check warning on line 142 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L142

Added line #L142 was not covered by tests
`Please import '@sentry/react-native' and wrap your App's Root component manually.`,
);
await showCopyPasteInstructions(

Check warning on line 145 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L145

Added line #L145 was not covered by tests
'App.js or _layout.tsx',
getSentryWrapColoredCodeSnippet(),
);
return;

Check warning on line 149 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L149

Added line #L149 was not covered by tests
}

if (result === SentryWrapError.NotFound) {
clack.log.warn(

Check warning on line 153 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L153

Added line #L153 was not covered by tests
`Could not find your App's Root component. Please wrap your App's Root component manually.`,
);
await showCopyPasteInstructions(

Check warning on line 156 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L156

Added line #L156 was not covered by tests
'App.js or _layout.tsx',
getSentryWrapColoredCodeSnippet(),
);
return;

Check warning on line 160 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L160

Added line #L160 was not covered by tests
}

traceStep('add-sentry-wrap', () => {
clack.log.success(

Check warning on line 164 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L163-L164

Added lines #L163 - L164 were not covered by tests
`Added ${chalk.cyan('Sentry.wrap')} to ${chalk.cyan(jsRelativePath)}.`,
);

fs.writeFileSync(jsPath, result, 'utf-8');

Check warning on line 168 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L168

Added line #L168 was not covered by tests
});

Sentry.setTag('app-js-file-status', 'added-sentry-wrap');
clack.log.success(

Check warning on line 172 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L171-L172

Added lines #L171 - L172 were not covered by tests
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;

Check warning on line 273 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L273

Added line #L273 was not covered by tests
}

function getSentryWrapColoredCodeSnippet() {
return makeCodeSnippet(true, (_unchanged, plus, _minus) => {
return plus(`import * as Sentry from '@sentry/react-native';

Check warning on line 278 in src/react-native/javascript.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/javascript.ts#L276-L278

Added lines #L276 - L278 were not covered by tests

export default Sentry.wrap(App);`);
});
}
4 changes: 3 additions & 1 deletion src/react-native/react-native-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
import { runReactNativeUninstall } from './uninstall';
import { APP_BUILD_GRADLE, XCODE_PROJECT, getFirstMatchedPath } from './glob';
import { ReactNativeWizardOptions } from './options';
import { addSentryInit } from './javascript';
import { addSentryInit, wrapRootComponent } from './javascript';
import { traceStep, withTelemetry } from '../telemetry';
import * as Sentry from '@sentry/node';
import { fulfillsVersionRange } from '../utils/semver';
Expand Down Expand Up @@ -210,6 +210,8 @@
addSentryInit({ dsn: selectedProject.keys[0].dsn.public }),
);

await traceStep('patch-app-js-wrap', () => wrapRootComponent());

Check warning on line 213 in src/react-native/react-native-wizard.ts

View check run for this annotation

Codecov / codecov/patch

src/react-native/react-native-wizard.ts#L213

Added line #L213 was not covered by tests

if (isExpo) {
await traceStep('patch-expo-app-config', () =>
patchExpoAppConfig(cliConfig),
Expand Down
Loading
Loading