Skip to content

fix(tracing): Avoid duplicate network requests (fetch, xhr) by default #4816

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

Merged
merged 3 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
- [changelog](https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/CHANGELOG.md#340)
- [diff](https://github.com/getsentry/sentry-javascript-bundler-plugins/compare/3.3.1...3.4.0)

### Fixes

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

## 6.13.1

### Fixes
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/js/tracing/reactnativetracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export interface ReactNativeTracingOptions {
/**
* Flag to disable patching all together for fetch requests.
*
* @default true
* Fetch in React Native is a `whatwg-fetch` polyfill which uses XHR under the hood.
* This causes duplicates when both `traceFetch` and `traceXHR` are enabled at the same time.
*
* @default false
*/
traceFetch: boolean;

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

export const defaultReactNativeTracingOptions: ReactNativeTracingOptions = {
traceFetch: true,
// Fetch in React Native is a `whatwg-fetch` polyfill which uses XHR under the hood.
// This causes duplicates when both `traceFetch` and `traceXHR` are enabled at the same time.
// https://github.com/facebook/react-native/blob/28945c68da056ab2ac01de7e542a845b2bca6096/packages/react-native/Libraries/Network/fetch.js
// (RN Web uses browsers native fetch implementation)
traceFetch: isWeb() ? true : false,
traceXHR: true,
enableHTTPTimings: true,
};
Expand Down
1 change: 1 addition & 0 deletions packages/core/test/tracing/timetodisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jest.mock('../../src/js/tracing/timetodisplaynative', () => mockedtimetodisplayn

import { isTurboModuleEnabled } from '../../src/js/utils/environment';
jest.mock('../../src/js/utils/environment', () => ({
isWeb: jest.fn().mockReturnValue(false),
isTurboModuleEnabled: jest.fn().mockReturnValue(false),
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,19 @@ describe('Capture Spaceflight News Screen Transaction', () => {
}),
);
});

it('contains exactly two articles requests spans', () => {
// This test ensures we are to tracing requests multiple times on different layers
// fetch > xhr > native

const item = getFirstNewsEventItem();
const spans = item?.[1].spans;

console.log(spans);

const httpSpans = spans?.filter(
span => span.data?.['sentry.op'] === 'http.client',
);
expect(httpSpans).toHaveLength(2);
});
});
1 change: 0 additions & 1 deletion samples/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"@sentry/core": "8.54.0",
"@sentry/react-native": "6.13.1",
"@shopify/flash-list": "^1.7.3",
"axios": "^1.8.3",
"delay": "^6.0.0",
"react": "18.3.1",
"react-native": "0.77.1",
Expand Down
14 changes: 7 additions & 7 deletions samples/react-native/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ Sentry.init({
Sentry.reactNativeTracingIntegration({
// The time to wait in ms until the transaction will be finished, For testing, default is 1000 ms
idleTimeoutMs: 5_000,
traceFetch: false, // Creates duplicate span for axios requests
}),
Sentry.httpClientIntegration({
// These options are effective only in JS.
Expand All @@ -107,15 +106,16 @@ Sentry.init({
standalone: false,
}),
Sentry.reactNativeErrorHandlersIntegration({
patchGlobalPromise: Platform.OS === 'ios' && isTurboModuleEnabled()
// The global patch doesn't work on iOS with the New Architecture in this Sample app
// In
? false
: true,
patchGlobalPromise:
Platform.OS === 'ios' && isTurboModuleEnabled()
? // The global patch doesn't work on iOS with the New Architecture in this Sample app
// In
false
: true,
}),
Sentry.feedbackIntegration({
imagePicker: ImagePicker,
styles:{
styles: {
submitButton: {
backgroundColor: '#6a1b9a',
paddingVertical: 15,
Expand Down
39 changes: 19 additions & 20 deletions samples/react-native/src/Screens/SpaceflightNewsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import React, { useState, useCallback } from 'react';
import { View, ActivityIndicator, StyleSheet, RefreshControl, Text, Pressable } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import axios from 'axios';
import { ArticleCard } from '../components/ArticleCard';
import type { Article } from '../types/api';
import { useFocusEffect } from '@react-navigation/native';
Expand All @@ -13,11 +12,7 @@ const API_URL = 'https://api.spaceflightnewsapi.net/v4/articles';

export const preloadArticles = async () => {
// Not actually preloading, just fetching for testing purposes
await axios.get(API_URL, {
params: {
limit: ITEMS_PER_PAGE,
},
});
await fetch(`${API_URL}/?limit=${ITEMS_PER_PAGE}`);
};

export default function NewsScreen() {
Expand All @@ -30,21 +25,21 @@ export default function NewsScreen() {

const fetchArticles = async (pageNumber: number, refresh = false) => {
try {
const response = await axios.get(API_URL, {
params: {
limit: ITEMS_PER_PAGE,
offset: (pageNumber - 1) * ITEMS_PER_PAGE,
},
});
const response = await fetch(
`${API_URL}/?limit=${ITEMS_PER_PAGE}&offset=${
(pageNumber - 1) * ITEMS_PER_PAGE
}`,
);
const data = await response.json();

const newArticles = response.data.results;
setHasMore(response.data.next !== null);
const newArticles = data.results;
setHasMore(data.next !== null);

if (refresh) {
setArticles(newArticles);
setAutoLoadCount(0);
} else {
setArticles((prev) => [...prev, ...newArticles]);
setArticles(prev => [...prev, ...newArticles]);
}
} catch (error) {
console.error('Error fetching articles:', error);
Expand All @@ -62,14 +57,14 @@ export default function NewsScreen() {
}

fetchArticles(1, true);
}, [articles])
}, [articles]),
);

const handleLoadMore = () => {
if (!loading && hasMore) {
setPage((prev) => prev + 1);
setPage(prev => prev + 1);
fetchArticles(page + 1);
setAutoLoadCount((prev) => prev + 1);
setAutoLoadCount(prev => prev + 1);
}
};

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

const LoadMoreButton = () => {
if (!hasMore) {return null;}
if (!hasMore) {
return null;
}
if (loading) {
return (
<View style={styles.loadMoreContainer}>
Expand Down Expand Up @@ -126,7 +123,9 @@ export default function NewsScreen() {
estimatedItemSize={350}
onEndReached={handleEndReached}
onEndReachedThreshold={0.5}
ListFooterComponent={autoLoadCount >= AUTO_LOAD_LIMIT ? LoadMoreButton : null}
ListFooterComponent={
autoLoadCount >= AUTO_LOAD_LIMIT ? LoadMoreButton : null
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
Expand Down
3 changes: 1 addition & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11113,7 +11113,7 @@ __metadata:
languageName: node
linkType: hard

"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":
"axios@npm:^1.4.0, axios@npm:^1.6.5, axios@npm:^1.6.7, axios@npm:^1.7.4, axios@npm:^1.x":
version: 1.8.4
resolution: "axios@npm:1.8.4"
dependencies:
Expand Down Expand Up @@ -25451,7 +25451,6 @@ __metadata:
"@types/react-test-renderer": ^18.0.0
"@typescript-eslint/eslint-plugin": ^7.18.0
"@typescript-eslint/parser": ^7.18.0
axios: ^1.8.3
babel-jest: ^29.6.3
babel-plugin-module-resolver: ^5.0.0
delay: ^6.0.0
Expand Down
Loading