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: Adds SAGP as an experimental expo plugin feature #4440

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@
### Features

- Send Sentry react-native SDK version in the session replay event (#4450)
- Adds Sentry Android Gradle Plugin as an experimental Expo plugin feature ([#4440](https://github.com/getsentry/sentry-react-native/pull/4440))

To enable the plugin add the `enableAndroidGradlePlugin` in the `@sentry/react-native/expo` of the Expo application configuration.

```js
"plugins": [
[
"@sentry/react-native/expo",
{
"experimental_android": {
"enableAndroidGradlePlugin": true,
}
}
],
```

To learn more about the available configuration options visit [the documentation](https://docs.sentry.io/platforms/react-native/manual-setup/expo/expo-sagp/).

### Fixes

Expand Down
11 changes: 11 additions & 0 deletions packages/core/plugin/src/withSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { createRunOncePlugin } from 'expo/config-plugins';

import { bold, sdkPackage, warnOnce } from './utils';
import { withSentryAndroid } from './withSentryAndroid';
import type { SentryAndroidGradlePluginOptions } from './withSentryAndroidGradlePlugin';
import { withSentryAndroidGradlePlugin } from './withSentryAndroidGradlePlugin';
import { withSentryIOS } from './withSentryIOS';

interface PluginProps {
organization?: string;
project?: string;
authToken?: string;
url?: string;
experimental_android?: SentryAndroidGradlePluginOptions;
}

const withSentryPlugin: ConfigPlugin<PluginProps | void> = (config, props) => {
Expand All @@ -27,6 +30,14 @@ const withSentryPlugin: ConfigPlugin<PluginProps | void> = (config, props) => {
} catch (e) {
warnOnce(`There was a problem with configuring your native Android project: ${e}`);
}
// if `enableAndroidGradlePlugin` is provided configure the Sentry Android Gradle Plugin
if (props?.experimental_android && props?.experimental_android?.enableAndroidGradlePlugin) {
try {
cfg = withSentryAndroidGradlePlugin(cfg, props.experimental_android);
} catch (e) {
warnOnce(`There was a problem with configuring Sentry Android Gradle Plugin: ${e}`);
}
}
try {
cfg = withSentryIOS(cfg, sentryProperties);
} catch (e) {
Expand Down
122 changes: 122 additions & 0 deletions packages/core/plugin/src/withSentryAndroidGradlePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { withAppBuildGradle, withProjectBuildGradle } from '@expo/config-plugins';

import { warnOnce } from './utils';

export interface SentryAndroidGradlePluginOptions {
enableAndroidGradlePlugin?: boolean;
includeProguardMapping?: boolean;
dexguardEnabled?: boolean;
autoUploadNativeSymbols?: boolean;
autoUploadProguardMapping?: boolean;
uploadNativeSymbols?: boolean;
includeNativeSources?: boolean;
includeSourceContext?: boolean;
}

/**
* Adds the Sentry Android Gradle Plugin to the project.
* https://docs.sentry.io/platforms/react-native/manual-setup/manual-setup/#enable-sentry-agp
*/
export function withSentryAndroidGradlePlugin(
config: any,
{
includeProguardMapping = true,
dexguardEnabled = false,
autoUploadProguardMapping = true,
uploadNativeSymbols = true,
autoUploadNativeSymbols = true,
includeNativeSources = true,
includeSourceContext = false,
}: SentryAndroidGradlePluginOptions = {},
): any {
const version = '4.14.1';

// Modify android/build.gradle
const withSentryProjectBuildGradle = (config: any): any => {
return withProjectBuildGradle(config, (projectBuildGradle: any) => {
Copy link
Member

Choose a reason for hiding this comment

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

Should we also check if the lang is groovy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point 👍 Added a check with 9d6d9d2

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!projectBuildGradle.modResults || !projectBuildGradle.modResults.contents) {
warnOnce('android/build.gradle content is missing or undefined.');
return config;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (projectBuildGradle.modResults.language !== 'groovy') {
warnOnce('Cannot configure Sentry in android/build.gradle because it is not in Groovy.');
return config;
}

const dependency = `classpath("io.sentry:sentry-android-gradle-plugin:${version}")`;

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (projectBuildGradle.modResults.contents.includes(dependency)) {
warnOnce('sentry-android-gradle-plugin dependency in already in android/build.gradle.');
return config;
}

try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const updatedContents = projectBuildGradle.modResults.contents.replace(
/dependencies\s*{/,
`dependencies {\n ${dependency}`,
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (updatedContents === projectBuildGradle.modResults.contents) {
warnOnce('Failed to inject the dependency. Could not find `dependencies` in build.gradle.');
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
projectBuildGradle.modResults.contents = updatedContents;
}
} catch (error) {
warnOnce(`An error occurred while trying to modify build.gradle`);
}
return projectBuildGradle;
});
};

// Modify android/app/build.gradle
const withSentryAppBuildGradle = (config: any): any => {
return withAppBuildGradle(config, (config: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (config.modResults.language !== 'groovy') {
warnOnce('Cannot configure Sentry in android/app/build.gradle because it is not in Groovy.');
return config;
}
const sentryPlugin = `apply plugin: "io.sentry.android.gradle"`;
const sentryConfig = `
sentry {
autoUploadProguardMapping = ${autoUploadProguardMapping}
includeProguardMapping = ${includeProguardMapping}
dexguardEnabled = ${dexguardEnabled}
uploadNativeSymbols = ${uploadNativeSymbols}
autoUploadNativeSymbols = ${autoUploadNativeSymbols}
includeNativeSources = ${includeNativeSources}
includeSourceContext = ${includeSourceContext}
tracingInstrumentation {
enabled = false
}
autoInstallation {
enabled = false
}
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
}`;

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
let contents = config.modResults.contents;

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!contents.includes(sentryPlugin)) {
contents = `${sentryPlugin}\n${contents}`;
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!contents.includes('sentry {')) {
contents = `${contents}\n${sentryConfig}`;
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
config.modResults.contents = contents;
return config;
});
};

return withSentryAppBuildGradle(withSentryProjectBuildGradle(config));
}
196 changes: 196 additions & 0 deletions packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { withAppBuildGradle, withProjectBuildGradle } from '@expo/config-plugins';

import { warnOnce } from '../../plugin/src/utils';
import type { SentryAndroidGradlePluginOptions } from '../../plugin/src/withSentryAndroidGradlePlugin';
import { withSentryAndroidGradlePlugin } from '../../plugin/src/withSentryAndroidGradlePlugin';

jest.mock('@expo/config-plugins', () => ({
withProjectBuildGradle: jest.fn(),
withAppBuildGradle: jest.fn(),
}));

jest.mock('../../plugin/src/utils', () => ({
warnOnce: jest.fn(),
}));

const mockedBuildGradle = `
buildscript {
dependencies {
classpath('otherDependency')
}
}
`;

const mockedAppBuildGradle = `
apply plugin: "somePlugin"
react {
}
android {
}
dependencies {
}
`;

describe('withSentryAndroidGradlePlugin', () => {
const mockConfig = {
name: 'test-app',
slug: 'test-app',
modResults: { contents: '' },
};

beforeEach(() => {
jest.clearAllMocks();
});

it('adds the Sentry plugin to build.gradle when enableAndroidGradlePlugin is enabled', () => {
const version = '4.14.1';
const options: SentryAndroidGradlePluginOptions = { enableAndroidGradlePlugin: true };

(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
const projectBuildGradle = {
modResults: { language: 'groovy', contents: mockedBuildGradle },
};
const modified = callback(projectBuildGradle);
return modified;
});

withSentryAndroidGradlePlugin(mockConfig, options);

expect(withProjectBuildGradle).toHaveBeenCalled();
expect(withProjectBuildGradle).toHaveBeenCalledWith(expect.any(Object), expect.any(Function));

const calledCallback = (withProjectBuildGradle as jest.Mock).mock.calls[0][1];
const modifiedGradle = calledCallback({
modResults: { language: 'groovy', contents: mockedBuildGradle },
});

expect(modifiedGradle.modResults.contents).toContain(
`classpath("io.sentry:sentry-android-gradle-plugin:${version}")`,
);
});

it('warnOnce if the Sentry plugin is already included in build.gradle', () => {
const version = '4.14.1';
const includedBuildGradle = `dependencies { classpath("io.sentry:sentry-android-gradle-plugin:${version}")}`;
const options: SentryAndroidGradlePluginOptions = { enableAndroidGradlePlugin: true };

(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
callback({ modResults: { language: 'groovy', contents: includedBuildGradle } });
});

withSentryAndroidGradlePlugin(mockConfig, options);

expect(warnOnce).toHaveBeenCalledWith(
'sentry-android-gradle-plugin dependency in already in android/build.gradle.',
);
});

it('warnOnce if failed to modify build.gradle', () => {
const invalidBuildGradle = `android {}`;
const options: SentryAndroidGradlePluginOptions = { enableAndroidGradlePlugin: true };

(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
callback({ modResults: { language: 'groovy', contents: invalidBuildGradle } });
});

withSentryAndroidGradlePlugin(mockConfig, options);

expect(warnOnce).toHaveBeenCalledWith(
'Failed to inject the dependency. Could not find `dependencies` in build.gradle.',
);
});

it('adds the Sentry plugin configuration to app/build.gradle', () => {
const options: SentryAndroidGradlePluginOptions = {
autoUploadProguardMapping: true,
includeProguardMapping: true,
dexguardEnabled: false,
uploadNativeSymbols: true,
autoUploadNativeSymbols: true,
includeNativeSources: false,
includeSourceContext: true,
};
(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
const projectBuildGradle = {
modResults: { language: 'groovy', contents: mockedBuildGradle },
};
const modified = callback(projectBuildGradle);
return modified;
});
(withAppBuildGradle as jest.Mock).mockImplementation((config, callback) => {
const appBuildGradle = {
modResults: { language: 'groovy', contents: mockedAppBuildGradle },
};
const modified = callback(appBuildGradle);
return modified;
});

withSentryAndroidGradlePlugin(mockConfig, options);

expect(withAppBuildGradle).toHaveBeenCalled();
expect(withAppBuildGradle).toHaveBeenCalledWith(expect.any(Object), expect.any(Function));

const calledCallback = (withAppBuildGradle as jest.Mock).mock.calls[0][1];
const modifiedGradle = calledCallback({
modResults: { language: 'groovy', contents: mockedAppBuildGradle },
});

expect(modifiedGradle.modResults.contents).toContain('apply plugin: "io.sentry.android.gradle"');
expect(modifiedGradle.modResults.contents).toContain(`
sentry {
autoUploadProguardMapping = true
includeProguardMapping = true
dexguardEnabled = false
uploadNativeSymbols = true
autoUploadNativeSymbols = true
includeNativeSources = false
includeSourceContext = true
tracingInstrumentation {
enabled = false
}
autoInstallation {
enabled = false
}
}`);
});

it('warnOnce if modResults is missing in build.gradle', () => {
(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
callback({});
});

withSentryAndroidGradlePlugin(mockConfig, {});

expect(warnOnce).toHaveBeenCalledWith('android/build.gradle content is missing or undefined.');

expect(withProjectBuildGradle).toHaveBeenCalled();
});

it('warnOnce if android/build.gradle is not Groovy', () => {
(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
callback({ modResults: { language: 'kotlin', contents: mockedAppBuildGradle } });
});

withSentryAndroidGradlePlugin(mockConfig, {});

expect(warnOnce).toHaveBeenCalledWith(
'Cannot configure Sentry in android/build.gradle because it is not in Groovy.',
);

expect(withProjectBuildGradle).toHaveBeenCalled();
});

it('warnOnce if app/build.gradle is not Groovy', () => {
(withAppBuildGradle as jest.Mock).mockImplementation((config, callback) => {
callback({ modResults: { language: 'kotlin', contents: mockedAppBuildGradle } });
});

withSentryAndroidGradlePlugin(mockConfig, {});

expect(warnOnce).toHaveBeenCalledWith(
'Cannot configure Sentry in android/app/build.gradle because it is not in Groovy.',
);

expect(withAppBuildGradle).toHaveBeenCalled();
});
});
11 changes: 9 additions & 2 deletions samples/expo/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@
{
"url": "https://sentry.io/",
"project": "sentry-react-native",
"organization": "sentry-sdks"
"organization": "sentry-sdks",
"experimental_android": {
"enableAndroidGradlePlugin": true,
"autoUploadProguardMapping": true,
"uploadNativeSymbols": true,
"includeNativeSources": true,
"includeSourceContext": true,
}
}
],
[
Expand All @@ -58,4 +65,4 @@
]
]
}
}
}
Loading