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

Merged
merged 24 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
56bdc03
Adds SAGP as an experimental expo plugin feature
antonis Jan 13, 2025
f168bd3
Updates changelog
antonis Jan 13, 2025
5984d12
Adds extra SAGP options
antonis Jan 14, 2025
6adb2c0
Adds tests
antonis Jan 14, 2025
d6ebdf3
Merge branch 'main' into antonis/experimental-expo-sagp
antonis Jan 14, 2025
4e0e78f
Fix configuration typo
antonis Jan 14, 2025
c153ed7
Remove autoInstallationEnabled
antonis Jan 16, 2025
2539a24
Hardcode SAGP version and use enableAndroidGradlePlugin
antonis Jan 16, 2025
6dbf9b5
Set tracingInstrumentation disabled as it is not currently supported …
antonis Jan 16, 2025
59e54ce
Deconstruct the options arguments to avoid duplication
antonis Jan 16, 2025
7b053f5
Use guard for checking groovy
antonis Jan 16, 2025
b1230bd
Use warnOnce instead of throwing an error
antonis Jan 16, 2025
9d6d9d2
Check that android/build.gradle uses Groovy
antonis Jan 16, 2025
cf55ce4
warn when dependency already exists
antonis Jan 16, 2025
f04a2c7
warnOnce if failed to modify build.gradle
antonis Jan 16, 2025
051f896
Merge branch 'main' into antonis/experimental-expo-sagp
antonis Jan 16, 2025
c430747
Add a changelog link to the documentation
antonis Jan 16, 2025
73f7b6b
Merge branch 'main' into antonis/experimental-expo-sagp
antonis Jan 24, 2025
2302390
Merge branch 'main' into antonis/experimental-expo-sagp
antonis Jan 30, 2025
56d4df1
Merge branch 'main' into antonis/experimental-expo-sagp
antonis Feb 4, 2025
75bd2f0
Update changelog
antonis Feb 4, 2025
e7e55c6
Merge branch 'main' into antonis/experimental-expo-sagp
antonis Feb 11, 2025
d501302
Fix changelog
antonis Feb 11, 2025
892437f
Merge branch 'main' into antonis/experimental-expo-sagp
antonis Feb 14, 2025
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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@

## 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 `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

- Various crashes and issues of Session Replay on Android. See the Android SDK version bump for more details. ([#4529](https://github.com/getsentry/sentry-react-native/pull/4529))
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) => {
// 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
}
}`;

// 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();
});
});
Loading
Loading