Skip to content

Commit 8120f0c

Browse files
fix(tracing): Avoid duplicate network requests (fetch, xhr) by default (#4816)
1 parent 1d75738 commit 8120f0c

File tree

8 files changed

+57
-32
lines changed

8 files changed

+57
-32
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
- [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8500)
2525
- [diff](https://github.com/getsentry/sentry-cocoa/compare/8.49.2...8.50.0)
2626

27+
### Fixes
28+
29+
- Avoid duplicate network requests (fetch, xhr) by default ([#4816](https://github.com/getsentry/sentry-react-native/pull/4816))
30+
- `traceFetch` is disabled by default on mobile as RN uses a polyfill which will be traced by `traceXHR`
31+
2732
## 6.13.1
2833

2934
### Fixes

packages/core/src/js/tracing/reactnativetracing.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export interface ReactNativeTracingOptions {
2929
/**
3030
* Flag to disable patching all together for fetch requests.
3131
*
32-
* @default true
32+
* Fetch in React Native is a `whatwg-fetch` polyfill which uses XHR under the hood.
33+
* This causes duplicates when both `traceFetch` and `traceXHR` are enabled at the same time.
34+
*
35+
* @default false
3336
*/
3437
traceFetch: boolean;
3538

@@ -70,7 +73,11 @@ function getDefaultTracePropagationTargets(): RegExp[] | undefined {
7073
}
7174

7275
export const defaultReactNativeTracingOptions: ReactNativeTracingOptions = {
73-
traceFetch: true,
76+
// Fetch in React Native is a `whatwg-fetch` polyfill which uses XHR under the hood.
77+
// This causes duplicates when both `traceFetch` and `traceXHR` are enabled at the same time.
78+
// https://github.com/facebook/react-native/blob/28945c68da056ab2ac01de7e542a845b2bca6096/packages/react-native/Libraries/Network/fetch.js
79+
// (RN Web uses browsers native fetch implementation)
80+
traceFetch: isWeb() ? true : false,
7481
traceXHR: true,
7582
enableHTTPTimings: true,
7683
};

packages/core/test/tracing/timetodisplay.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ jest.mock('../../src/js/tracing/timetodisplaynative', () => mockedtimetodisplayn
99

1010
import { isTurboModuleEnabled } from '../../src/js/utils/environment';
1111
jest.mock('../../src/js/utils/environment', () => ({
12+
isWeb: jest.fn().mockReturnValue(false),
1213
isTurboModuleEnabled: jest.fn().mockReturnValue(false),
1314
}));
1415

samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,19 @@ describe('Capture Spaceflight News Screen Transaction', () => {
108108
}),
109109
);
110110
});
111+
112+
it('contains exactly two articles requests spans', () => {
113+
// This test ensures we are to tracing requests multiple times on different layers
114+
// fetch > xhr > native
115+
116+
const item = getFirstNewsEventItem();
117+
const spans = item?.[1].spans;
118+
119+
console.log(spans);
120+
121+
const httpSpans = spans?.filter(
122+
span => span.data?.['sentry.op'] === 'http.client',
123+
);
124+
expect(httpSpans).toHaveLength(2);
125+
});
111126
});

samples/react-native/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
"@sentry/core": "8.54.0",
3131
"@sentry/react-native": "6.13.1",
3232
"@shopify/flash-list": "^1.7.3",
33-
"axios": "^1.8.3",
3433
"delay": "^6.0.0",
3534
"react": "18.3.1",
3635
"react-native": "0.77.1",

samples/react-native/src/App.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ Sentry.init({
8484
Sentry.reactNativeTracingIntegration({
8585
// The time to wait in ms until the transaction will be finished, For testing, default is 1000 ms
8686
idleTimeoutMs: 5_000,
87-
traceFetch: false, // Creates duplicate span for axios requests
8887
}),
8988
Sentry.httpClientIntegration({
9089
// These options are effective only in JS.
@@ -107,15 +106,16 @@ Sentry.init({
107106
standalone: false,
108107
}),
109108
Sentry.reactNativeErrorHandlersIntegration({
110-
patchGlobalPromise: Platform.OS === 'ios' && isTurboModuleEnabled()
111-
// The global patch doesn't work on iOS with the New Architecture in this Sample app
112-
// In
113-
? false
114-
: true,
109+
patchGlobalPromise:
110+
Platform.OS === 'ios' && isTurboModuleEnabled()
111+
? // The global patch doesn't work on iOS with the New Architecture in this Sample app
112+
// In
113+
false
114+
: true,
115115
}),
116116
Sentry.feedbackIntegration({
117117
imagePicker: ImagePicker,
118-
styles:{
118+
styles: {
119119
submitButton: {
120120
backgroundColor: '#6a1b9a',
121121
paddingVertical: 15,

samples/react-native/src/Screens/SpaceflightNewsScreen.tsx

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import React, { useState, useCallback } from 'react';
33
import { View, ActivityIndicator, StyleSheet, RefreshControl, Text, Pressable } from 'react-native';
44
import { FlashList } from '@shopify/flash-list';
5-
import axios from 'axios';
65
import { ArticleCard } from '../components/ArticleCard';
76
import type { Article } from '../types/api';
87
import { useFocusEffect } from '@react-navigation/native';
@@ -13,11 +12,7 @@ const API_URL = 'https://api.spaceflightnewsapi.net/v4/articles';
1312

1413
export const preloadArticles = async () => {
1514
// Not actually preloading, just fetching for testing purposes
16-
await axios.get(API_URL, {
17-
params: {
18-
limit: ITEMS_PER_PAGE,
19-
},
20-
});
15+
await fetch(`${API_URL}/?limit=${ITEMS_PER_PAGE}`);
2116
};
2217

2318
export default function NewsScreen() {
@@ -30,21 +25,21 @@ export default function NewsScreen() {
3025

3126
const fetchArticles = async (pageNumber: number, refresh = false) => {
3227
try {
33-
const response = await axios.get(API_URL, {
34-
params: {
35-
limit: ITEMS_PER_PAGE,
36-
offset: (pageNumber - 1) * ITEMS_PER_PAGE,
37-
},
38-
});
28+
const response = await fetch(
29+
`${API_URL}/?limit=${ITEMS_PER_PAGE}&offset=${
30+
(pageNumber - 1) * ITEMS_PER_PAGE
31+
}`,
32+
);
33+
const data = await response.json();
3934

40-
const newArticles = response.data.results;
41-
setHasMore(response.data.next !== null);
35+
const newArticles = data.results;
36+
setHasMore(data.next !== null);
4237

4338
if (refresh) {
4439
setArticles(newArticles);
4540
setAutoLoadCount(0);
4641
} else {
47-
setArticles((prev) => [...prev, ...newArticles]);
42+
setArticles(prev => [...prev, ...newArticles]);
4843
}
4944
} catch (error) {
5045
console.error('Error fetching articles:', error);
@@ -62,14 +57,14 @@ export default function NewsScreen() {
6257
}
6358

6459
fetchArticles(1, true);
65-
}, [articles])
60+
}, [articles]),
6661
);
6762

6863
const handleLoadMore = () => {
6964
if (!loading && hasMore) {
70-
setPage((prev) => prev + 1);
65+
setPage(prev => prev + 1);
7166
fetchArticles(page + 1);
72-
setAutoLoadCount((prev) => prev + 1);
67+
setAutoLoadCount(prev => prev + 1);
7368
}
7469
};
7570

@@ -90,7 +85,9 @@ export default function NewsScreen() {
9085
};
9186

9287
const LoadMoreButton = () => {
93-
if (!hasMore) {return null;}
88+
if (!hasMore) {
89+
return null;
90+
}
9491
if (loading) {
9592
return (
9693
<View style={styles.loadMoreContainer}>
@@ -126,7 +123,9 @@ export default function NewsScreen() {
126123
estimatedItemSize={350}
127124
onEndReached={handleEndReached}
128125
onEndReachedThreshold={0.5}
129-
ListFooterComponent={autoLoadCount >= AUTO_LOAD_LIMIT ? LoadMoreButton : null}
126+
ListFooterComponent={
127+
autoLoadCount >= AUTO_LOAD_LIMIT ? LoadMoreButton : null
128+
}
130129
refreshControl={
131130
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
132131
}

yarn.lock

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11113,7 +11113,7 @@ __metadata:
1111311113
languageName: node
1111411114
linkType: hard
1111511115

11116-
"axios@npm:^1.4.0, axios@npm:^1.6.5, axios@npm:^1.6.7, axios@npm:^1.7.4, axios@npm:^1.8.3, axios@npm:^1.x":
11116+
"axios@npm:^1.4.0, axios@npm:^1.6.5, axios@npm:^1.6.7, axios@npm:^1.7.4, axios@npm:^1.x":
1111711117
version: 1.8.4
1111811118
resolution: "axios@npm:1.8.4"
1111911119
dependencies:
@@ -25451,7 +25451,6 @@ __metadata:
2545125451
"@types/react-test-renderer": ^18.0.0
2545225452
"@typescript-eslint/eslint-plugin": ^7.18.0
2545325453
"@typescript-eslint/parser": ^7.18.0
25454-
axios: ^1.8.3
2545525454
babel-jest: ^29.6.3
2545625455
babel-plugin-module-resolver: ^5.0.0
2545725456
delay: ^6.0.0

0 commit comments

Comments
 (0)