Skip to content

Commit 5682481

Browse files
antonisclaude
andauthored
test(profiling): pin Android RNSentryStart wiring of _experiments.profilingOptions (#6060)
Add JUnit coverage for `RNSentryStart.configureAndroidProfiling` to catch a future refactor moving it out of the live `getSentryAndroidOptions` call site — the same regression shape as the iOS bug fixed in #6012, which slipped through because the Android side had zero coverage for this wiring. Closes #6016. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3b471f5 commit 5682481

1 file changed

Lines changed: 230 additions & 0 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package io.sentry.react
2+
3+
import com.facebook.react.bridge.JavaOnlyMap
4+
import io.sentry.ILogger
5+
import io.sentry.ProfileLifecycle
6+
import io.sentry.SentryLevel
7+
import io.sentry.android.core.SentryAndroidOptions
8+
import org.junit.Assert.assertEquals
9+
import org.junit.Assert.assertFalse
10+
import org.junit.Assert.assertNull
11+
import org.junit.Assert.assertTrue
12+
import org.junit.Before
13+
import org.junit.Test
14+
import org.junit.runner.RunWith
15+
import org.junit.runners.JUnit4
16+
import org.mockito.ArgumentMatchers.anyString
17+
import org.mockito.ArgumentMatchers.eq
18+
import org.mockito.Mockito.mock
19+
import org.mockito.Mockito.never
20+
import org.mockito.Mockito.times
21+
import org.mockito.Mockito.verify
22+
23+
/**
24+
* Pins the wiring of `_experiments.profilingOptions` from the JS bridge into
25+
* `SentryAndroidOptions` via `RNSentryStart.configureAndroidProfiling`.
26+
*
27+
* The Android side is correctly wired today (unlike the iOS bug fixed in #6012). These tests
28+
* ensure that a future refactor moving `configureAndroidProfiling` out of the live init path
29+
* — the same shape of regression as the iOS one — fails loudly.
30+
*/
31+
@RunWith(JUnit4::class)
32+
class RNSentryProfilingOptionsTest {
33+
private lateinit var logger: ILogger
34+
35+
@Before
36+
fun setUp() {
37+
logger = mock(ILogger::class.java)
38+
}
39+
40+
@Test
41+
fun `profilingOptions wires sessionSampleRate, trace lifecycle, and startOnAppStart`() {
42+
val rnOptions =
43+
JavaOnlyMap.of(
44+
"_experiments",
45+
JavaOnlyMap.of(
46+
"profilingOptions",
47+
JavaOnlyMap.of(
48+
"profileSessionSampleRate",
49+
1.0,
50+
"lifecycle",
51+
"trace",
52+
"startOnAppStart",
53+
true,
54+
),
55+
),
56+
)
57+
val options = SentryAndroidOptions()
58+
59+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
60+
61+
assertEquals(1.0, options.profileSessionSampleRate!!, 0.0)
62+
assertEquals(ProfileLifecycle.TRACE, options.profileLifecycle)
63+
assertTrue(options.isStartProfilerOnAppStart)
64+
}
65+
66+
@Test
67+
fun `profilingOptions lifecycle manual maps to ProfileLifecycle MANUAL`() {
68+
val rnOptions =
69+
JavaOnlyMap.of(
70+
"_experiments",
71+
JavaOnlyMap.of(
72+
"profilingOptions",
73+
JavaOnlyMap.of("lifecycle", "manual"),
74+
),
75+
)
76+
val options = SentryAndroidOptions()
77+
78+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
79+
80+
assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle)
81+
}
82+
83+
@Test
84+
fun `profilingOptions lifecycle is case-insensitive`() {
85+
val rnOptions =
86+
JavaOnlyMap.of(
87+
"_experiments",
88+
JavaOnlyMap.of(
89+
"profilingOptions",
90+
JavaOnlyMap.of("lifecycle", "TRACE"),
91+
),
92+
)
93+
val options = SentryAndroidOptions()
94+
95+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
96+
97+
assertEquals(ProfileLifecycle.TRACE, options.profileLifecycle)
98+
}
99+
100+
@Test
101+
fun `profilingOptions startOnAppStart false is honored`() {
102+
val rnOptions =
103+
JavaOnlyMap.of(
104+
"_experiments",
105+
JavaOnlyMap.of(
106+
"profilingOptions",
107+
JavaOnlyMap.of("startOnAppStart", false),
108+
),
109+
)
110+
val options = SentryAndroidOptions()
111+
112+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
113+
114+
assertFalse(options.isStartProfilerOnAppStart)
115+
}
116+
117+
@Test
118+
fun `no _experiments key leaves profiling defaults untouched`() {
119+
val rnOptions = JavaOnlyMap()
120+
val options = SentryAndroidOptions()
121+
122+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
123+
124+
assertNull(options.profileSessionSampleRate)
125+
assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle)
126+
assertFalse(options.isStartProfilerOnAppStart)
127+
}
128+
129+
@Test
130+
fun `_experiments without profilingOptions leaves profiling defaults untouched`() {
131+
val rnOptions = JavaOnlyMap.of("_experiments", JavaOnlyMap())
132+
val options = SentryAndroidOptions()
133+
134+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
135+
136+
assertNull(options.profileSessionSampleRate)
137+
assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle)
138+
assertFalse(options.isStartProfilerOnAppStart)
139+
}
140+
141+
@Test
142+
fun `empty profilingOptions leaves profiling defaults untouched`() {
143+
val rnOptions =
144+
JavaOnlyMap.of(
145+
"_experiments",
146+
JavaOnlyMap.of("profilingOptions", JavaOnlyMap()),
147+
)
148+
val options = SentryAndroidOptions()
149+
150+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
151+
152+
assertNull(options.profileSessionSampleRate)
153+
assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle)
154+
assertFalse(options.isStartProfilerOnAppStart)
155+
}
156+
157+
@Test
158+
fun `non-number profileSessionSampleRate logs a warning and is ignored`() {
159+
val rnOptions =
160+
JavaOnlyMap.of(
161+
"_experiments",
162+
JavaOnlyMap.of(
163+
"profilingOptions",
164+
JavaOnlyMap.of("profileSessionSampleRate", "not-a-number"),
165+
),
166+
)
167+
val options = SentryAndroidOptions()
168+
169+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
170+
171+
assertNull(options.profileSessionSampleRate)
172+
verify(logger, times(1)).log(eq(SentryLevel.WARNING), anyString())
173+
}
174+
175+
@Test
176+
fun `non-string lifecycle logs a warning and is ignored`() {
177+
val rnOptions =
178+
JavaOnlyMap.of(
179+
"_experiments",
180+
JavaOnlyMap.of(
181+
"profilingOptions",
182+
JavaOnlyMap.of("lifecycle", 1),
183+
),
184+
)
185+
val options = SentryAndroidOptions()
186+
187+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
188+
189+
assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle)
190+
verify(logger, times(1)).log(eq(SentryLevel.WARNING), anyString())
191+
}
192+
193+
@Test
194+
fun `non-boolean startOnAppStart logs a warning and is ignored`() {
195+
val rnOptions =
196+
JavaOnlyMap.of(
197+
"_experiments",
198+
JavaOnlyMap.of(
199+
"profilingOptions",
200+
JavaOnlyMap.of("startOnAppStart", "yes"),
201+
),
202+
)
203+
val options = SentryAndroidOptions()
204+
205+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
206+
207+
assertFalse(options.isStartProfilerOnAppStart)
208+
verify(logger, times(1)).log(eq(SentryLevel.WARNING), anyString())
209+
}
210+
211+
@Test
212+
fun `unknown lifecycle string is silently ignored without warning`() {
213+
val rnOptions =
214+
JavaOnlyMap.of(
215+
"_experiments",
216+
JavaOnlyMap.of(
217+
"profilingOptions",
218+
JavaOnlyMap.of("lifecycle", "not-a-real-mode"),
219+
),
220+
)
221+
val options = SentryAndroidOptions()
222+
223+
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)
224+
225+
// Implementation only logs a warning for type mismatches, not unknown string values.
226+
// Lifecycle stays at the default.
227+
assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle)
228+
verify(logger, never()).log(eq(SentryLevel.WARNING), anyString())
229+
}
230+
}

0 commit comments

Comments
 (0)