Skip to content

Commit cec511e

Browse files
authored
feat: Adds SAGP as an experimental expo plugin feature (#4440)
1 parent 686b3bc commit cec511e

File tree

5 files changed

+358
-2
lines changed

5 files changed

+358
-2
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@
88
99
## Unreleased
1010

11+
### Features
12+
13+
- Adds Sentry Android Gradle Plugin as an experimental Expo plugin feature ([#4440](https://github.com/getsentry/sentry-react-native/pull/4440))
14+
15+
To enable the plugin add the `enableAndroidGradlePlugin` in the `@sentry/react-native/expo` of the Expo application configuration.
16+
17+
```js
18+
"plugins": [
19+
[
20+
"@sentry/react-native/expo",
21+
{
22+
"experimental_android": {
23+
"enableAndroidGradlePlugin": true,
24+
}
25+
}
26+
],
27+
```
28+
29+
To learn more about the available configuration options visit [the documentation](https://docs.sentry.io/platforms/react-native/manual-setup/expo/expo-sagp/).
30+
1131
### Fixes
1232

1333
- 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))

packages/core/plugin/src/withSentry.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import { createRunOncePlugin } from 'expo/config-plugins';
33

44
import { bold, sdkPackage, warnOnce } from './utils';
55
import { withSentryAndroid } from './withSentryAndroid';
6+
import type { SentryAndroidGradlePluginOptions } from './withSentryAndroidGradlePlugin';
7+
import { withSentryAndroidGradlePlugin } from './withSentryAndroidGradlePlugin';
68
import { withSentryIOS } from './withSentryIOS';
79

810
interface PluginProps {
911
organization?: string;
1012
project?: string;
1113
authToken?: string;
1214
url?: string;
15+
experimental_android?: SentryAndroidGradlePluginOptions;
1316
}
1417

1518
const withSentryPlugin: ConfigPlugin<PluginProps | void> = (config, props) => {
@@ -27,6 +30,14 @@ const withSentryPlugin: ConfigPlugin<PluginProps | void> = (config, props) => {
2730
} catch (e) {
2831
warnOnce(`There was a problem with configuring your native Android project: ${e}`);
2932
}
33+
// if `enableAndroidGradlePlugin` is provided configure the Sentry Android Gradle Plugin
34+
if (props?.experimental_android && props?.experimental_android?.enableAndroidGradlePlugin) {
35+
try {
36+
cfg = withSentryAndroidGradlePlugin(cfg, props.experimental_android);
37+
} catch (e) {
38+
warnOnce(`There was a problem with configuring Sentry Android Gradle Plugin: ${e}`);
39+
}
40+
}
3041
try {
3142
cfg = withSentryIOS(cfg, sentryProperties);
3243
} catch (e) {
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { withAppBuildGradle, withProjectBuildGradle } from '@expo/config-plugins';
2+
3+
import { warnOnce } from './utils';
4+
5+
export interface SentryAndroidGradlePluginOptions {
6+
enableAndroidGradlePlugin?: boolean;
7+
includeProguardMapping?: boolean;
8+
dexguardEnabled?: boolean;
9+
autoUploadNativeSymbols?: boolean;
10+
autoUploadProguardMapping?: boolean;
11+
uploadNativeSymbols?: boolean;
12+
includeNativeSources?: boolean;
13+
includeSourceContext?: boolean;
14+
}
15+
16+
/**
17+
* Adds the Sentry Android Gradle Plugin to the project.
18+
* https://docs.sentry.io/platforms/react-native/manual-setup/manual-setup/#enable-sentry-agp
19+
*/
20+
export function withSentryAndroidGradlePlugin(
21+
config: any,
22+
{
23+
includeProguardMapping = true,
24+
dexguardEnabled = false,
25+
autoUploadProguardMapping = true,
26+
uploadNativeSymbols = true,
27+
autoUploadNativeSymbols = true,
28+
includeNativeSources = true,
29+
includeSourceContext = false,
30+
}: SentryAndroidGradlePluginOptions = {},
31+
): any {
32+
const version = '4.14.1';
33+
34+
// Modify android/build.gradle
35+
const withSentryProjectBuildGradle = (config: any): any => {
36+
return withProjectBuildGradle(config, (projectBuildGradle: any) => {
37+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
38+
if (!projectBuildGradle.modResults || !projectBuildGradle.modResults.contents) {
39+
warnOnce('android/build.gradle content is missing or undefined.');
40+
return config;
41+
}
42+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
43+
if (projectBuildGradle.modResults.language !== 'groovy') {
44+
warnOnce('Cannot configure Sentry in android/build.gradle because it is not in Groovy.');
45+
return config;
46+
}
47+
48+
const dependency = `classpath("io.sentry:sentry-android-gradle-plugin:${version}")`;
49+
50+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
51+
if (projectBuildGradle.modResults.contents.includes(dependency)) {
52+
warnOnce('sentry-android-gradle-plugin dependency in already in android/build.gradle.');
53+
return config;
54+
}
55+
56+
try {
57+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
58+
const updatedContents = projectBuildGradle.modResults.contents.replace(
59+
/dependencies\s*{/,
60+
`dependencies {\n ${dependency}`,
61+
);
62+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
63+
if (updatedContents === projectBuildGradle.modResults.contents) {
64+
warnOnce('Failed to inject the dependency. Could not find `dependencies` in build.gradle.');
65+
} else {
66+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
67+
projectBuildGradle.modResults.contents = updatedContents;
68+
}
69+
} catch (error) {
70+
warnOnce(`An error occurred while trying to modify build.gradle`);
71+
}
72+
return projectBuildGradle;
73+
});
74+
};
75+
76+
// Modify android/app/build.gradle
77+
const withSentryAppBuildGradle = (config: any): any => {
78+
return withAppBuildGradle(config, (config: any) => {
79+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
80+
if (config.modResults.language !== 'groovy') {
81+
warnOnce('Cannot configure Sentry in android/app/build.gradle because it is not in Groovy.');
82+
return config;
83+
}
84+
const sentryPlugin = `apply plugin: "io.sentry.android.gradle"`;
85+
const sentryConfig = `
86+
sentry {
87+
autoUploadProguardMapping = ${autoUploadProguardMapping}
88+
includeProguardMapping = ${includeProguardMapping}
89+
dexguardEnabled = ${dexguardEnabled}
90+
uploadNativeSymbols = ${uploadNativeSymbols}
91+
autoUploadNativeSymbols = ${autoUploadNativeSymbols}
92+
includeNativeSources = ${includeNativeSources}
93+
includeSourceContext = ${includeSourceContext}
94+
tracingInstrumentation {
95+
enabled = false
96+
}
97+
autoInstallation {
98+
enabled = false
99+
}
100+
}`;
101+
102+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
103+
let contents = config.modResults.contents;
104+
105+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
106+
if (!contents.includes(sentryPlugin)) {
107+
contents = `${sentryPlugin}\n${contents}`;
108+
}
109+
110+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
111+
if (!contents.includes('sentry {')) {
112+
contents = `${contents}\n${sentryConfig}`;
113+
}
114+
115+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
116+
config.modResults.contents = contents;
117+
return config;
118+
});
119+
};
120+
121+
return withSentryAppBuildGradle(withSentryProjectBuildGradle(config));
122+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { withAppBuildGradle, withProjectBuildGradle } from '@expo/config-plugins';
2+
3+
import { warnOnce } from '../../plugin/src/utils';
4+
import type { SentryAndroidGradlePluginOptions } from '../../plugin/src/withSentryAndroidGradlePlugin';
5+
import { withSentryAndroidGradlePlugin } from '../../plugin/src/withSentryAndroidGradlePlugin';
6+
7+
jest.mock('@expo/config-plugins', () => ({
8+
withProjectBuildGradle: jest.fn(),
9+
withAppBuildGradle: jest.fn(),
10+
}));
11+
12+
jest.mock('../../plugin/src/utils', () => ({
13+
warnOnce: jest.fn(),
14+
}));
15+
16+
const mockedBuildGradle = `
17+
buildscript {
18+
dependencies {
19+
classpath('otherDependency')
20+
}
21+
}
22+
`;
23+
24+
const mockedAppBuildGradle = `
25+
apply plugin: "somePlugin"
26+
react {
27+
}
28+
android {
29+
}
30+
dependencies {
31+
}
32+
`;
33+
34+
describe('withSentryAndroidGradlePlugin', () => {
35+
const mockConfig = {
36+
name: 'test-app',
37+
slug: 'test-app',
38+
modResults: { contents: '' },
39+
};
40+
41+
beforeEach(() => {
42+
jest.clearAllMocks();
43+
});
44+
45+
it('adds the Sentry plugin to build.gradle when enableAndroidGradlePlugin is enabled', () => {
46+
const version = '4.14.1';
47+
const options: SentryAndroidGradlePluginOptions = { enableAndroidGradlePlugin: true };
48+
49+
(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
50+
const projectBuildGradle = {
51+
modResults: { language: 'groovy', contents: mockedBuildGradle },
52+
};
53+
const modified = callback(projectBuildGradle);
54+
return modified;
55+
});
56+
57+
withSentryAndroidGradlePlugin(mockConfig, options);
58+
59+
expect(withProjectBuildGradle).toHaveBeenCalled();
60+
expect(withProjectBuildGradle).toHaveBeenCalledWith(expect.any(Object), expect.any(Function));
61+
62+
const calledCallback = (withProjectBuildGradle as jest.Mock).mock.calls[0][1];
63+
const modifiedGradle = calledCallback({
64+
modResults: { language: 'groovy', contents: mockedBuildGradle },
65+
});
66+
67+
expect(modifiedGradle.modResults.contents).toContain(
68+
`classpath("io.sentry:sentry-android-gradle-plugin:${version}")`,
69+
);
70+
});
71+
72+
it('warnOnce if the Sentry plugin is already included in build.gradle', () => {
73+
const version = '4.14.1';
74+
const includedBuildGradle = `dependencies { classpath("io.sentry:sentry-android-gradle-plugin:${version}")}`;
75+
const options: SentryAndroidGradlePluginOptions = { enableAndroidGradlePlugin: true };
76+
77+
(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
78+
callback({ modResults: { language: 'groovy', contents: includedBuildGradle } });
79+
});
80+
81+
withSentryAndroidGradlePlugin(mockConfig, options);
82+
83+
expect(warnOnce).toHaveBeenCalledWith(
84+
'sentry-android-gradle-plugin dependency in already in android/build.gradle.',
85+
);
86+
});
87+
88+
it('warnOnce if failed to modify build.gradle', () => {
89+
const invalidBuildGradle = `android {}`;
90+
const options: SentryAndroidGradlePluginOptions = { enableAndroidGradlePlugin: true };
91+
92+
(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
93+
callback({ modResults: { language: 'groovy', contents: invalidBuildGradle } });
94+
});
95+
96+
withSentryAndroidGradlePlugin(mockConfig, options);
97+
98+
expect(warnOnce).toHaveBeenCalledWith(
99+
'Failed to inject the dependency. Could not find `dependencies` in build.gradle.',
100+
);
101+
});
102+
103+
it('adds the Sentry plugin configuration to app/build.gradle', () => {
104+
const options: SentryAndroidGradlePluginOptions = {
105+
autoUploadProguardMapping: true,
106+
includeProguardMapping: true,
107+
dexguardEnabled: false,
108+
uploadNativeSymbols: true,
109+
autoUploadNativeSymbols: true,
110+
includeNativeSources: false,
111+
includeSourceContext: true,
112+
};
113+
(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
114+
const projectBuildGradle = {
115+
modResults: { language: 'groovy', contents: mockedBuildGradle },
116+
};
117+
const modified = callback(projectBuildGradle);
118+
return modified;
119+
});
120+
(withAppBuildGradle as jest.Mock).mockImplementation((config, callback) => {
121+
const appBuildGradle = {
122+
modResults: { language: 'groovy', contents: mockedAppBuildGradle },
123+
};
124+
const modified = callback(appBuildGradle);
125+
return modified;
126+
});
127+
128+
withSentryAndroidGradlePlugin(mockConfig, options);
129+
130+
expect(withAppBuildGradle).toHaveBeenCalled();
131+
expect(withAppBuildGradle).toHaveBeenCalledWith(expect.any(Object), expect.any(Function));
132+
133+
const calledCallback = (withAppBuildGradle as jest.Mock).mock.calls[0][1];
134+
const modifiedGradle = calledCallback({
135+
modResults: { language: 'groovy', contents: mockedAppBuildGradle },
136+
});
137+
138+
expect(modifiedGradle.modResults.contents).toContain('apply plugin: "io.sentry.android.gradle"');
139+
expect(modifiedGradle.modResults.contents).toContain(`
140+
sentry {
141+
autoUploadProguardMapping = true
142+
includeProguardMapping = true
143+
dexguardEnabled = false
144+
uploadNativeSymbols = true
145+
autoUploadNativeSymbols = true
146+
includeNativeSources = false
147+
includeSourceContext = true
148+
tracingInstrumentation {
149+
enabled = false
150+
}
151+
autoInstallation {
152+
enabled = false
153+
}
154+
}`);
155+
});
156+
157+
it('warnOnce if modResults is missing in build.gradle', () => {
158+
(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
159+
callback({});
160+
});
161+
162+
withSentryAndroidGradlePlugin(mockConfig, {});
163+
164+
expect(warnOnce).toHaveBeenCalledWith('android/build.gradle content is missing or undefined.');
165+
166+
expect(withProjectBuildGradle).toHaveBeenCalled();
167+
});
168+
169+
it('warnOnce if android/build.gradle is not Groovy', () => {
170+
(withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => {
171+
callback({ modResults: { language: 'kotlin', contents: mockedAppBuildGradle } });
172+
});
173+
174+
withSentryAndroidGradlePlugin(mockConfig, {});
175+
176+
expect(warnOnce).toHaveBeenCalledWith(
177+
'Cannot configure Sentry in android/build.gradle because it is not in Groovy.',
178+
);
179+
180+
expect(withProjectBuildGradle).toHaveBeenCalled();
181+
});
182+
183+
it('warnOnce if app/build.gradle is not Groovy', () => {
184+
(withAppBuildGradle as jest.Mock).mockImplementation((config, callback) => {
185+
callback({ modResults: { language: 'kotlin', contents: mockedAppBuildGradle } });
186+
});
187+
188+
withSentryAndroidGradlePlugin(mockConfig, {});
189+
190+
expect(warnOnce).toHaveBeenCalledWith(
191+
'Cannot configure Sentry in android/app/build.gradle because it is not in Groovy.',
192+
);
193+
194+
expect(withAppBuildGradle).toHaveBeenCalled();
195+
});
196+
});

0 commit comments

Comments
 (0)