diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a6d06e2e4..bc94d31fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/core/src/js/tracing/reactnativetracing.ts b/packages/core/src/js/tracing/reactnativetracing.ts index 30beeaec07..8874f6769f 100644 --- a/packages/core/src/js/tracing/reactnativetracing.ts +++ b/packages/core/src/js/tracing/reactnativetracing.ts @@ -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; @@ -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, }; diff --git a/packages/core/test/tracing/timetodisplay.test.tsx b/packages/core/test/tracing/timetodisplay.test.tsx index 5bd812651f..43413abcbc 100644 --- a/packages/core/test/tracing/timetodisplay.test.tsx +++ b/packages/core/test/tracing/timetodisplay.test.tsx @@ -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), })); diff --git a/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts b/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts index 3a91eb3a4b..2bdd30486e 100644 --- a/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts +++ b/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts @@ -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); + }); }); diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 84b261bced..deaf8cdebd 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -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", diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 46a489952a..6d0d4aa437 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -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. @@ -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, diff --git a/samples/react-native/src/Screens/SpaceflightNewsScreen.tsx b/samples/react-native/src/Screens/SpaceflightNewsScreen.tsx index 399a7265f7..f33bcfb80e 100644 --- a/samples/react-native/src/Screens/SpaceflightNewsScreen.tsx +++ b/samples/react-native/src/Screens/SpaceflightNewsScreen.tsx @@ -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'; @@ -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() { @@ -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); @@ -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); } }; @@ -90,7 +85,9 @@ export default function NewsScreen() { }; const LoadMoreButton = () => { - if (!hasMore) {return null;} + if (!hasMore) { + return null; + } if (loading) { return ( @@ -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={ } diff --git a/yarn.lock b/yarn.lock index ce86fd55fd..e7ec1eed4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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: @@ -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