Skip to content

Commit 2fc14a8

Browse files
authored
Merge branch 'main' into v8
2 parents f73c0ff + bb4ea33 commit 2fc14a8

File tree

22 files changed

+884
-76
lines changed

22 files changed

+884
-76
lines changed

.github/workflows/sample-application.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ concurrency:
1414
env:
1515
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
1616
MAESTRO_VERSION: '2.0.10'
17+
MAESTRO_DRIVER_STARTUP_TIMEOUT: 90000 # Increase timeout from default 30s to 90s for CI stability
1718
RN_SENTRY_POD_NAME: RNSentry
1819
IOS_APP_ARCHIVE_PATH: sentry-react-native-sample.app.zip
1920
ANDROID_APP_ARCHIVE_PATH: sentry-react-native-sample.apk.zip

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
### Fixes
1212

1313
- Fix for missing `replay_id` from metrics ([#5483](https://github.com/getsentry/sentry-react-native/pull/5483))
14+
- Skip span ID check when standalone mode is enabled ([#5493](https://github.com/getsentry/sentry-react-native/pull/5493))
1415

1516
### Dependencies
1617

packages/core/src/js/tracing/integrations/appStart.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -320,21 +320,26 @@ export const appStartIntegration = ({
320320
return;
321321
}
322322

323-
if (!firstStartedActiveRootSpanId) {
324-
debug.warn('[AppStart] No first started active root span id recorded. Can not attach app start.');
325-
return;
326-
}
327-
328323
if (!event.contexts?.trace) {
329324
debug.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.');
330325
return;
331326
}
332327

333-
if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) {
334-
debug.warn(
335-
'[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.',
336-
);
337-
return;
328+
// When standalone is true, we create our own transaction and don't need to verify
329+
// it matches the first navigation transaction. When standalone is false, we need to
330+
// ensure we're attaching app start to the first transaction (not a later one).
331+
if (!standalone) {
332+
if (!firstStartedActiveRootSpanId) {
333+
debug.warn('[AppStart] No first started active root span id recorded. Can not attach app start.');
334+
return;
335+
}
336+
337+
if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) {
338+
debug.warn(
339+
'[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.',
340+
);
341+
return;
342+
}
338343
}
339344

340345
const appStart = await NATIVE.fetchNativeAppStart();

packages/core/test/tracing/integrations/appStart.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
SEMANTIC_ATTRIBUTE_SENTRY_OP,
77
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
88
setCurrentClient,
9+
startInactiveSpan,
910
timestampInSeconds,
1011
} from '@sentry/core';
1112
import {
@@ -385,6 +386,69 @@ describe('App Start Integration', () => {
385386
expect(actualEvent).toStrictEqual(undefined);
386387
expect(NATIVE.fetchNativeAppStart).toHaveBeenCalledTimes(1);
387388
});
389+
390+
it('Attaches app start to standalone transaction even when navigation transaction starts first', async () => {
391+
// This test simulates the Android scenario where React Navigation auto-instrumentation
392+
// starts a navigation transaction before the standalone app start transaction is created.
393+
// The fix ensures that when standalone: true, the span ID check is skipped so app start
394+
// can be attached to the standalone transaction even if a navigation transaction started first.
395+
getCurrentScope().clear();
396+
getIsolationScope().clear();
397+
getGlobalScope().clear();
398+
399+
mockAppStart({ cold: true });
400+
401+
const integration = appStartIntegration({
402+
standalone: true,
403+
});
404+
const client = new TestClient({
405+
...getDefaultTestClientOptions(),
406+
enableAppStartTracking: true,
407+
tracesSampleRate: 1.0,
408+
});
409+
setCurrentClient(client);
410+
integration.setup(client);
411+
412+
// Simulate a navigation transaction starting first (like React Navigation auto-instrumentation)
413+
// This will set firstStartedActiveRootSpanId to the navigation span's ID
414+
const navigationSpan = startInactiveSpan({
415+
name: 'calendar/home',
416+
op: 'navigation',
417+
forceTransaction: true,
418+
});
419+
const navigationSpanId = navigationSpan?.spanContext().spanId;
420+
if (navigationSpan) {
421+
navigationSpan.end();
422+
}
423+
424+
// Now capture standalone app start - it should still work even though navigation span started first
425+
// The standalone transaction will have a different span ID, but the fix skips the check
426+
await integration.captureStandaloneAppStart();
427+
428+
const actualEvent = client.event as TransactionEvent | undefined;
429+
expect(actualEvent).toBeDefined();
430+
expect(actualEvent?.spans).toBeDefined();
431+
expect(actualEvent?.spans?.length).toBeGreaterThan(0);
432+
433+
// Verify that app start was attached successfully
434+
const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start');
435+
expect(appStartSpan).toBeDefined();
436+
expect(appStartSpan).toEqual(
437+
expect.objectContaining<Partial<SpanJSON>>({
438+
description: 'Cold Start',
439+
op: APP_START_COLD_OP,
440+
}),
441+
);
442+
443+
// Verify the standalone transaction has a different span ID than the navigation transaction
444+
// This confirms that the span ID check was skipped (otherwise app start wouldn't be attached)
445+
expect(actualEvent?.contexts?.trace?.span_id).toBeDefined();
446+
if (navigationSpanId) {
447+
expect(actualEvent?.contexts?.trace?.span_id).not.toBe(navigationSpanId);
448+
}
449+
450+
expect(actualEvent?.measurements?.[APP_START_COLD_MEASUREMENT]).toBeDefined();
451+
});
388452
});
389453

390454
describe('App Start Attached to the First Root Span', () => {

samples/expo/.eslintrc.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module.exports = {
2+
root: true,
3+
extends: ['expo', '@react-native'],
4+
parser: '@typescript-eslint/parser',
5+
plugins: ['@typescript-eslint'],
6+
settings: {
7+
'import/resolver': {
8+
typescript: {
9+
project: ['tsconfig.json'],
10+
},
11+
node: {
12+
extensions: ['.js', '.jsx', '.ts', '.tsx'],
13+
},
14+
},
15+
},
16+
overrides: [
17+
{
18+
files: ['*.ts', '*.tsx'],
19+
rules: {
20+
'@typescript-eslint/no-shadow': ['error'],
21+
'no-shadow': 'off',
22+
'no-undef': 'off',
23+
quotes: [2, 'single', { avoidEscape: true }],
24+
// Disable deprecated rules removed in @typescript-eslint v8
25+
'@typescript-eslint/func-call-spacing': 'off',
26+
'@typescript-eslint/ban-types': 'off',
27+
// Disable import/no-unresolved for workspace packages that may not be built yet
28+
'import/no-unresolved': ['error', { ignore: ['^@sentry/'] }],
29+
},
30+
},
31+
],
32+
ignorePatterns: ['/node_modules', '/ios', '/android', '/.expo'],
33+
};

samples/expo/.prettierignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
ios/
3+
android/
4+
.expo/
5+
*.md
6+

samples/expo/.prettierrc.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
arrowParens: 'avoid',
3+
bracketSameLine: true,
4+
bracketSpacing: true,
5+
singleQuote: true,
6+
trailingComma: 'all',
7+
};

samples/expo/app.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
"resizeMode": "contain",
1515
"backgroundColor": "#ffffff"
1616
},
17-
"assetBundlePatterns": [
18-
"**/*"
19-
],
17+
"assetBundlePatterns": ["**/*"],
2018
"ios": {
2119
"supportsTablet": true,
2220
"bundleIdentifier": "io.sentry.expo.sample",
@@ -90,4 +88,4 @@
9088
"url": "https://u.expo.dev/00000000-0000-0000-0000-000000000000"
9189
}
9290
}
93-
}
91+
}

samples/expo/app/(tabs)/_layout.tsx

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,40 @@ function TabBarIcon(props: {
1212
name: React.ComponentProps<typeof FontAwesome>['name'];
1313
color: string;
1414
}) {
15+
// eslint-disable-next-line react-native/no-inline-styles
1516
return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} />;
1617
}
1718

19+
function CodeIcon({ color }: { color: string }) {
20+
return <TabBarIcon name="code" color={color} />;
21+
}
22+
23+
function InfoButton({ colorScheme }: { colorScheme: 'light' | 'dark' | null }) {
24+
return (
25+
<Link href="/modal" asChild>
26+
<Pressable>
27+
{({ pressed }) => (
28+
<FontAwesome
29+
name="info-circle"
30+
size={25}
31+
color={Colors[colorScheme ?? 'light'].text}
32+
// eslint-disable-next-line react-native/no-inline-styles
33+
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
34+
/>
35+
)}
36+
</Pressable>
37+
</Link>
38+
);
39+
}
40+
1841
export default function TabLayout() {
1942
const colorScheme = useColorScheme();
2043

44+
const renderInfoButton = React.useCallback(
45+
() => <InfoButton colorScheme={colorScheme} />,
46+
[colorScheme],
47+
);
48+
2149
return (
2250
<Tabs
2351
screenOptions={{
@@ -30,28 +58,15 @@ export default function TabLayout() {
3058
name="index"
3159
options={{
3260
title: 'Tab One',
33-
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
34-
headerRight: () => (
35-
<Link href="/modal" asChild>
36-
<Pressable>
37-
{({ pressed }) => (
38-
<FontAwesome
39-
name="info-circle"
40-
size={25}
41-
color={Colors[colorScheme ?? 'light'].text}
42-
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
43-
/>
44-
)}
45-
</Pressable>
46-
</Link>
47-
),
61+
tabBarIcon: CodeIcon,
62+
headerRight: renderInfoButton,
4863
}}
4964
/>
5065
<Tabs.Screen
5166
name="two"
5267
options={{
5368
title: 'Tab Two',
54-
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
69+
tabBarIcon: CodeIcon,
5570
}}
5671
/>
5772
</Tabs>

samples/expo/app/(tabs)/index.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ export default function TabOneScreen() {
7979
title="Native Crash"
8080
onPress={() => {
8181
if (isRunningInExpoGo()) {
82-
console.warn('Not supported in Expo Go. Build the application to test this feature.');
82+
console.warn(
83+
'Not supported in Expo Go. Build the application to test this feature.',
84+
);
8385
return;
8486
}
8587
Sentry.nativeCrash();
@@ -138,7 +140,9 @@ export default function TabOneScreen() {
138140
<Button
139141
title="Send count metric with attributes"
140142
onPress={() => {
141-
Sentry.metrics.count('count_metric', 1, { attributes: { from_test_app: true } });
143+
Sentry.metrics.count('count_metric', 1, {
144+
attributes: { from_test_app: true },
145+
});
142146
}}
143147
/>
144148
</View>
@@ -184,7 +188,9 @@ export default function TabOneScreen() {
184188
string: 'string',
185189
bigint: BigInt(123),
186190
});
187-
Sentry.captureMessage('Message with different types of tags globally');
191+
Sentry.captureMessage(
192+
'Message with different types of tags globally',
193+
);
188194
Sentry.setTags({
189195
number: undefined,
190196
boolean: undefined,
@@ -226,7 +232,11 @@ export default function TabOneScreen() {
226232
Sentry.logger.warn('expo warn log');
227233
Sentry.logger.error('expo error log');
228234

229-
Sentry.logger.info('expo info log with data', { database: 'admin', number: 123, obj: { password: 'admin' } });
235+
Sentry.logger.info('expo info log with data', {
236+
database: 'admin',
237+
number: 123,
238+
obj: { password: 'admin' },
239+
});
230240
}}
231241
/>
232242
</View>

0 commit comments

Comments
 (0)