Skip to content

Commit

Permalink
Merge 4e0e78f into 8b86336
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Jan 14, 2025
2 parents 8b86336 + 4e0e78f commit a05231a
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 1 deletion.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@
## Unreleased

### Features

- 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 `sentryAndroidGradlePluginVersion` in the `@sentry/react-native/expo` plugin.

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

### Fixes

- Use proper SDK name for Session Replay tags ([#4428](https://github.com/getsentry/sentry-react-native/pull/4428))
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 `sentryAndroidGradlePluginVersion` is provided configure the Sentry Android Gradle Plugin
if (props?.experimental_android && props?.experimental_android?.sentryAndroidGradlePluginVersion) {
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
96 changes: 96 additions & 0 deletions packages/core/plugin/src/withSentryAndroidGradlePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { withAppBuildGradle, withProjectBuildGradle } from '@expo/config-plugins';

export interface SentryAndroidGradlePluginOptions {
sentryAndroidGradlePluginVersion?: string;
includeProguardMapping?: boolean;
dexguardEnabled?: boolean;
autoUploadNativeSymbols?: boolean;
autoUploadProguardMapping?: boolean;
uploadNativeSymbols?: boolean;
includeNativeSources?: boolean;
includeSourceContext?: boolean;
autoInstallationEnabled?: 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, options: SentryAndroidGradlePluginOptions = {}): any {
const version = options.sentryAndroidGradlePluginVersion;
const includeProguardMapping = options.includeProguardMapping ?? true;
const dexguardEnabled = options.dexguardEnabled ?? false;
const autoUploadProguardMapping = options.autoUploadProguardMapping ?? true;
const uploadNativeSymbols = options.uploadNativeSymbols ?? true;
const autoUploadNativeSymbols = options.autoUploadNativeSymbols ?? true;
const includeNativeSources = options.includeNativeSources ?? true;
const includeSourceContext = options.includeSourceContext ?? false;
const autoInstallationEnabled = options.autoInstallationEnabled ?? false;

// Modify android/build.gradle
const withSentryProjectBuildGradle = (config: any): any => {
return withProjectBuildGradle(config, (projectBuildGradle: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!projectBuildGradle.modResults || !projectBuildGradle.modResults.contents) {
throw new Error('android/build.gradle content is missing or undefined.');
}

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)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
projectBuildGradle.modResults.contents = projectBuildGradle.modResults.contents.replace(
/dependencies\s*{/,
`dependencies {\n ${dependency}`,
);
}

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') {
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}
autoInstallation {
enabled = ${autoInstallationEnabled}
}
}`;

// 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;
} else {
throw new Error('Cannot configure Sentry in android/app/build.gradle because it is not in Groovy.');
}
return config;
});
};

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

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

jest.mock('@expo/config-plugins', () => ({
withProjectBuildGradle: jest.fn(),
withAppBuildGradle: 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', () => {
const version = '4.14.1';
const options: SentryAndroidGradlePluginOptions = { sentryAndroidGradlePluginVersion: version };

(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
const projectBuildGradle = {
modResults: { 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: { contents: mockedBuildGradle },
});

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

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,
autoInstallationEnabled: false,
};

// Mock withAppBuildGradle
(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
autoInstallation {
enabled = false
}
}`);
});

it('throws an error if modResults is missing in build.gradle', () => {
(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
expect(() => callback({})).toThrow('android/build.gradle content is missing or undefined.');
});

withSentryAndroidGradlePlugin(mockConfig, {});

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

it('throws an error if app/build.gradle is not Groovy', () => {
(withAppBuildGradle as jest.Mock).mockImplementation((config, callback) => {
expect(() => callback({ modResults: { language: 'kotlin', contents: mockedAppBuildGradle } })).toThrow(
'Cannot configure Sentry in android/app/build.gradle because it is not in Groovy.',
);
});

withSentryAndroidGradlePlugin(mockConfig, {});

expect(withAppBuildGradle).toHaveBeenCalled();
});
});
10 changes: 9 additions & 1 deletion samples/expo/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,15 @@
{
"url": "https://sentry.io/",
"project": "sentry-react-native",
"organization": "sentry-sdks"
"organization": "sentry-sdks",
"experimental_android": {
"sentryAndroidGradlePluginVersion": "4.14.1",
"autoUploadProguardMapping": true,
"uploadNativeSymbols": true,
"includeNativeSources": true,
"includeSourceContext": true,
"autoInstallationEnabled": false
}
}
],
[
Expand Down

0 comments on commit a05231a

Please # to comment.