From 8f732488b7c02bbd5e9e8ae35a6b54643737c3fb Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 1 Jul 2024 15:50:00 +0200 Subject: [PATCH 01/16] feat: add intrumentation handler for long lived fetch requests --- packages/browser/src/tracing/request.ts | 37 +++++++++++++ packages/utils/src/instrument/fetch.ts | 65 +++++++++++++++++++++++ packages/utils/src/instrument/handlers.ts | 10 +++- packages/utils/src/instrument/index.ts | 3 +- 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index b7b01b5e8ce3..017079b9a8b3 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -23,6 +23,7 @@ import { import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/types'; import { BAGGAGE_HEADER_NAME, + addFetchEndInstrumentationHandler, addFetchInstrumentationHandler, browserPerformanceTimeOrigin, dynamicSamplingContextToSentryBaggageHeader, @@ -93,6 +94,9 @@ export interface RequestInstrumentationOptions { shouldCreateSpanForRequest?(this: void, url: string): boolean; } +const responseToSpanId = new WeakMap(); +const spanIdToEndTimestamp = new Map(); + export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions = { traceFetch: true, traceXHR: true, @@ -113,10 +117,43 @@ export function instrumentOutgoingRequests(_options?: Partial shouldAttachHeaders(url, tracePropagationTargets); const spans: Record = {}; + const client = getClient(); if (traceFetch) { + if (client) { + // Keeping track of http requests, whose body payloads resolved later than the intial resolved request (e.g. SSE) + client.addEventProcessor(event => { + if (event.type === 'transaction' && event.spans) { + event.spans.forEach(span => { + if (span.op === 'http.client') { + const updatedTimestamp = spanIdToEndTimestamp.get(span.span_id); + if (updatedTimestamp !== undefined) { + span.timestamp = updatedTimestamp / 1000; + spanIdToEndTimestamp.delete(span.span_id); + } + } + }); + } + return event; + }); + } + + addFetchEndInstrumentationHandler(handlerData => { + if (handlerData.response) { + const span = responseToSpanId.get(handlerData.response); + if (span && handlerData.endTimestamp) { + spanIdToEndTimestamp.set(span, handlerData.endTimestamp); + } + } + }); + addFetchInstrumentationHandler(handlerData => { const createdSpan = instrumentFetchRequest(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans); + + if (handlerData.response && handlerData.fetchData.__span) { + responseToSpanId.set(handlerData.response, handlerData.fetchData.__span); + } + // We cannot use `window.location` in the generic fetch instrumentation, // but we need it for reliable `server.address` attribute. // so we extend this in here diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index 24c435fdf07c..2299afa1f1f8 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -1,7 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { HandlerDataFetch } from '@sentry/types'; +import { DEBUG_BUILD } from '../debug-build'; import { isError } from '../is'; +import { logger } from '../logger'; import { addNonEnumerableProperty, fill } from '../object'; import { supportsNativeFetch } from '../supports'; import { timestampInSeconds } from '../time'; @@ -24,6 +26,20 @@ export function addFetchInstrumentationHandler(handler: (data: HandlerDataFetch) maybeInstrument(type, instrumentFetch); } +/** + * Add an instrumentation handler for long-lived fetch requests, like consuming SSE via fetch. + * The handler will resolve the request body and emit the actual `endTimestamp`, so that the + * span can be updated accordingly. + * + * Only used internally + * @hidden + */ +export function addFetchEndInstrumentationHandler(handler: (data: HandlerDataFetch) => void): void { + const type = 'fetch-body-resolved'; + addHandler(type, handler); + maybeInstrument(type, instrumentFetchBodyReceived); +} + function instrumentFetch(): void { if (!supportsNativeFetch()) { return; @@ -95,6 +111,55 @@ function instrumentFetch(): void { }); } +function instrumentFetchBodyReceived(): void { + if (!supportsNativeFetch()) { + return; + } + + fill(GLOBAL_OBJ, 'fetch', function (originalFetch: () => void): () => void { + return function (...args: any[]): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return originalFetch.apply(GLOBAL_OBJ, args).then(async (response: Response) => { + // clone response for awaiting stream + let clonedResponseForResolving: Response | undefined; + try { + clonedResponseForResolving = response.clone(); + } catch (e) { + // noop + DEBUG_BUILD && logger.warn('Failed to clone response body.'); + } + + if (clonedResponseForResolving && clonedResponseForResolving.body) { + const responseReader = clonedResponseForResolving.body.getReader(); + + // eslint-disable-next-line no-inner-declarations + function consumeChunks({ done }: { done: boolean }): Promise { + if (!done) { + return responseReader.read().then(consumeChunks); + } else { + return Promise.resolve(); + } + } + + responseReader + .read() + .then(consumeChunks) + .then(() => { + triggerHandlers('fetch-body-resolved', { + endTimestamp: timestampInSeconds() * 1000, + response, + }); + }) + .catch(() => { + // noop + }); + } + return response; + }); + }; + }); +} + function hasProp(obj: unknown, prop: T): obj is Record { return !!obj && typeof obj === 'object' && !!(obj as Record)[prop]; } diff --git a/packages/utils/src/instrument/handlers.ts b/packages/utils/src/instrument/handlers.ts index 520d258ae1ac..b22f01318b0a 100644 --- a/packages/utils/src/instrument/handlers.ts +++ b/packages/utils/src/instrument/handlers.ts @@ -2,7 +2,15 @@ import { DEBUG_BUILD } from '../debug-build'; import { logger } from '../logger'; import { getFunctionName } from '../stacktrace'; -export type InstrumentHandlerType = 'console' | 'dom' | 'fetch' | 'history' | 'xhr' | 'error' | 'unhandledrejection'; +export type InstrumentHandlerType = + | 'console' + | 'dom' + | 'fetch' + | 'fetch-body-resolved' + | 'history' + | 'xhr' + | 'error' + | 'unhandledrejection'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type InstrumentHandlerCallback = (data: any) => void; diff --git a/packages/utils/src/instrument/index.ts b/packages/utils/src/instrument/index.ts index ee84f1375522..8a83959c636b 100644 --- a/packages/utils/src/instrument/index.ts +++ b/packages/utils/src/instrument/index.ts @@ -1,5 +1,5 @@ import { addConsoleInstrumentationHandler } from './console'; -import { addFetchInstrumentationHandler } from './fetch'; +import { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './fetch'; import { addGlobalErrorInstrumentationHandler } from './globalError'; import { addGlobalUnhandledRejectionInstrumentationHandler } from './globalUnhandledRejection'; import { addHandler, maybeInstrument, resetInstrumentationHandlers, triggerHandlers } from './handlers'; @@ -14,4 +14,5 @@ export { triggerHandlers, // Only exported for tests resetInstrumentationHandlers, + addFetchEndInstrumentationHandler, }; From edd558371044277b9227e1ba2bac3632adc701cf Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 2 Jul 2024 14:17:51 +0200 Subject: [PATCH 02/16] test: add e2e test for sse stream --- .../react-router-6/package.json | 4 +- .../react-router-6/server/app.js | 42 ++++++++++++++++++ .../react-router-6/src/index.tsx | 2 + .../react-router-6/src/pages/SSE.tsx | 43 +++++++++++++++++++ .../react-router-6/tests/sse.test.ts | 35 +++++++++++++++ 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-6/server/app.js create mode 100644 dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/package.json b/dev-packages/e2e-tests/test-applications/react-router-6/package.json index 3053b0a7c137..bc6903b3e11b 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6/package.json @@ -6,6 +6,7 @@ "@sentry/react": "latest || *", "@types/react": "18.0.0", "@types/react-dom": "18.0.0", + "express": "4.19.2", "react": "18.2.0", "react-dom": "18.2.0", "react-router-dom": "^6.4.1", @@ -14,7 +15,7 @@ }, "scripts": { "build": "react-scripts build", - "start": "serve -s build", + "start": "concurrently \"node server/app.js\" \"serve -s build\"", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:build": "pnpm install && npx playwright install && pnpm build", @@ -43,6 +44,7 @@ "devDependencies": { "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils", + "concurrently": "^8.2.2", "serve": "14.0.1" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/server/app.js b/dev-packages/e2e-tests/test-applications/react-router-6/server/app.js new file mode 100644 index 000000000000..d0ece10b907b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6/server/app.js @@ -0,0 +1,42 @@ +const express = require('express'); + +const app = express(); +const PORT = 8080; + +const wait = time => { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, time); + }); +}; + +async function eventsHandler(request, response) { + response.headers = { + 'Content-Type': 'text/event-stream', + Connection: 'keep-alive', + 'Cache-Control': 'no-cache', + 'Access-Control-Allow-Origin': '*', + }; + + response.setHeader('Cache-Control', 'no-cache'); + response.setHeader('Content-Type', 'text/event-stream'); + response.setHeader('Access-Control-Allow-Origin', '*'); + response.setHeader('Connection', 'keep-alive'); + + response.flushHeaders(); + + await wait(2000); + + for (let index = 0; index < 10; index++) { + response.write(`data: ${new Date().toISOString()}\n\n`); + } + + response.end(); +} + +app.get('/sse', eventsHandler); + +app.listen(PORT, () => { + console.log(`SSE service listening at http://localhost:${PORT}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx index 601ac10a084b..434c1677bf88 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6/src/index.tsx @@ -11,6 +11,7 @@ import { useNavigationType, } from 'react-router-dom'; import Index from './pages/Index'; +import SSE from './pages/SSE'; import User from './pages/User'; const replay = Sentry.replayIntegration(); @@ -48,6 +49,7 @@ root.render( } /> } /> + } /> , ); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx b/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx new file mode 100644 index 000000000000..877d9f4298c8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx @@ -0,0 +1,43 @@ +import * as Sentry from '@sentry/react'; +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX +import * as React from 'react'; + +const fetchSSE = async () => { + Sentry.startSpanManual({ name: 'sse stream using fetch' }, async span => { + const res = await Sentry.startSpan({ name: 'sse fetch call' }, async () => { + return await fetch('http://localhost:8080/sse'); + }); + + const stream = res.body; + const reader = stream?.getReader(); + + const readChunk = async () => { + const readRes = await reader?.read(); + if (readRes?.done) { + return; + } + + new TextDecoder().decode(readRes?.value); + + await readChunk(); + }; + + try { + await readChunk(); + } catch (error) { + console.error('Could not fetch sse', error); + } + + span.end(); + }); +}; + +const SSE = () => { + return ( + + ); +}; + +export default SSE; diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts new file mode 100644 index 000000000000..a27db8264d6d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SpanJSON } from '@sentry/types'; + +test('Waits for sse streaming when creating spans', async ({ page }) => { + await page.goto('/sse'); + + const transactionPromise = waitForTransaction('react-router-6', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const fetchButton = page.locator('id=fetch-button'); + await fetchButton.click(); + + const rootSpan = await transactionPromise; + const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0] as SpanJSON; + const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0] as SpanJSON; + + expect(sseFetchCall).not.toBeUndefined(); + expect(httpGet).not.toBeUndefined(); + + expect(sseFetchCall?.timestamp).not.toBeUndefined(); + expect(sseFetchCall?.start_timestamp).not.toBeUndefined(); + expect(httpGet?.timestamp).not.toBeUndefined(); + expect(httpGet?.start_timestamp).not.toBeUndefined(); + + // http headers get sent instantly from the server + const resolveDuration = Math.round((sseFetchCall.timestamp as number) - sseFetchCall.start_timestamp); + + // body streams after 2s + const resolveBodyDuration = Math.round((httpGet.timestamp as number) - httpGet.start_timestamp); + + expect(resolveDuration).toBe(0); + expect(resolveBodyDuration).toBe(2); +}); From 1b529d8aa946c90d046ab3d0972edb6e89fddf8e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 3 Jul 2024 14:28:21 +0200 Subject: [PATCH 03/16] refactor: update undefined check Co-authored-by: Francesco Novy --- packages/browser/src/tracing/request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 017079b9a8b3..e64302d46a95 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -127,7 +127,7 @@ export function instrumentOutgoingRequests(_options?: Partial { if (span.op === 'http.client') { const updatedTimestamp = spanIdToEndTimestamp.get(span.span_id); - if (updatedTimestamp !== undefined) { + if (updatedTimestamp) { span.timestamp = updatedTimestamp / 1000; spanIdToEndTimestamp.delete(span.span_id); } From c4104af1686bf05a3eb9d7c57f9095dadb77a79b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 3 Jul 2024 15:42:00 +0200 Subject: [PATCH 04/16] chore: clarify sse term --- packages/browser/src/tracing/request.ts | 3 ++- packages/utils/src/instrument/fetch.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index e64302d46a95..30f03dbddec0 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -121,7 +121,8 @@ export function instrumentOutgoingRequests(_options?: Partial { if (event.type === 'transaction' && event.spans) { event.spans.forEach(span => { diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index 2299afa1f1f8..9aee85c1e589 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -27,7 +27,7 @@ export function addFetchInstrumentationHandler(handler: (data: HandlerDataFetch) } /** - * Add an instrumentation handler for long-lived fetch requests, like consuming SSE via fetch. + * Add an instrumentation handler for long-lived fetch requests, like consuming server-sent events (SSE) via fetch. * The handler will resolve the request body and emit the actual `endTimestamp`, so that the * span can be updated accordingly. * From d13349e870deff5cf08a0a369ef04960885761ae Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 5 Jul 2024 11:26:28 +0200 Subject: [PATCH 05/16] feat: timeout stream reading + small refactor --- packages/utils/src/instrument/fetch.ts | 162 +++++++++++++------------ 1 file changed, 86 insertions(+), 76 deletions(-) diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index 9aee85c1e589..ef1fc7999bc0 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -23,7 +23,7 @@ type FetchResource = string | { toString(): string } | { url: string }; export function addFetchInstrumentationHandler(handler: (data: HandlerDataFetch) => void): void { const type = 'fetch'; addHandler(type, handler); - maybeInstrument(type, instrumentFetch); + maybeInstrument(type, () => instrumentFetch(type)); } /** @@ -37,10 +37,10 @@ export function addFetchInstrumentationHandler(handler: (data: HandlerDataFetch) export function addFetchEndInstrumentationHandler(handler: (data: HandlerDataFetch) => void): void { const type = 'fetch-body-resolved'; addHandler(type, handler); - maybeInstrument(type, instrumentFetchBodyReceived); + maybeInstrument(type, () => instrumentFetch(type)); } -function instrumentFetch(): void { +function instrumentFetch(handlerType: 'fetch' | 'fetch-body-resolved'): void { if (!supportsNativeFetch()) { return; } @@ -48,7 +48,6 @@ function instrumentFetch(): void { fill(GLOBAL_OBJ, 'fetch', function (originalFetch: () => void): () => void { return function (...args: any[]): void { const { method, url } = parseFetchArgs(args); - const handlerData: HandlerDataFetch = { args, fetchData: { @@ -58,9 +57,11 @@ function instrumentFetch(): void { startTimestamp: timestampInSeconds() * 1000, }; - triggerHandlers('fetch', { - ...handlerData, - }); + if (handlerType === 'fetch') { + triggerHandlers('fetch', { + ...handlerData, + }); + } // We capture the stack right here and not in the Promise error callback because Safari (and probably other // browsers too) will wipe the stack trace up to this point, only leaving us with this file which is useless. @@ -73,91 +74,100 @@ function instrumentFetch(): void { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return originalFetch.apply(GLOBAL_OBJ, args).then( - (response: Response) => { - const finishedHandlerData: HandlerDataFetch = { - ...handlerData, - endTimestamp: timestampInSeconds() * 1000, - response, - }; - - triggerHandlers('fetch', finishedHandlerData); + async (response: Response) => { + if (handlerType === 'fetch-body-resolved') { + // clone response for awaiting stream + let clonedResponseForResolving: Response | undefined; + try { + clonedResponseForResolving = response.clone(); + } catch (e) { + // noop + DEBUG_BUILD && logger.warn('Failed to clone response body.'); + } + + await resolveResponse(clonedResponseForResolving, () => { + triggerHandlers('fetch-body-resolved', { + endTimestamp: timestampInSeconds() * 1000, + response, + }); + }); + } else { + const finishedHandlerData: HandlerDataFetch = { + ...handlerData, + endTimestamp: timestampInSeconds() * 1000, + response, + }; + triggerHandlers('fetch', finishedHandlerData); + } + return response; }, (error: Error) => { - const erroredHandlerData: HandlerDataFetch = { - ...handlerData, - endTimestamp: timestampInSeconds() * 1000, - error, - }; - - triggerHandlers('fetch', erroredHandlerData); + if (handlerType === 'fetch') { + const erroredHandlerData: HandlerDataFetch = { + ...handlerData, + endTimestamp: timestampInSeconds() * 1000, + error, + }; + + triggerHandlers('fetch', erroredHandlerData); + + if (isError(error) && error.stack === undefined) { + // NOTE: If you are a Sentry user, and you are seeing this stack frame, + // it means the error, that was caused by your fetch call did not + // have a stack trace, so the SDK backfilled the stack trace so + // you can see which fetch call failed. + error.stack = virtualStackTrace; + addNonEnumerableProperty(error, 'framesToPop', 1); + } - if (isError(error) && error.stack === undefined) { // NOTE: If you are a Sentry user, and you are seeing this stack frame, - // it means the error, that was caused by your fetch call did not - // have a stack trace, so the SDK backfilled the stack trace so - // you can see which fetch call failed. - error.stack = virtualStackTrace; - addNonEnumerableProperty(error, 'framesToPop', 1); + // it means the sentry.javascript SDK caught an error invoking your application code. + // This is expected behavior and NOT indicative of a bug with sentry.javascript. + throw error; } - - // NOTE: If you are a Sentry user, and you are seeing this stack frame, - // it means the sentry.javascript SDK caught an error invoking your application code. - // This is expected behavior and NOT indicative of a bug with sentry.javascript. - throw error; }, ); }; }); } -function instrumentFetchBodyReceived(): void { - if (!supportsNativeFetch()) { - return; - } +function resolveResponse(res: Response | undefined, onFinishedResolving: () => void): void { + if (res && res.body) { + const responseReader = res.body.getReader(); - fill(GLOBAL_OBJ, 'fetch', function (originalFetch: () => void): () => void { - return function (...args: any[]): void { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return originalFetch.apply(GLOBAL_OBJ, args).then(async (response: Response) => { - // clone response for awaiting stream - let clonedResponseForResolving: Response | undefined; + // eslint-disable-next-line no-inner-declarations + async function consumeChunks({ done }: { done: boolean }): Promise { + if (!done) { try { - clonedResponseForResolving = response.clone(); - } catch (e) { - // noop - DEBUG_BUILD && logger.warn('Failed to clone response body.'); - } - - if (clonedResponseForResolving && clonedResponseForResolving.body) { - const responseReader = clonedResponseForResolving.body.getReader(); - - // eslint-disable-next-line no-inner-declarations - function consumeChunks({ done }: { done: boolean }): Promise { - if (!done) { - return responseReader.read().then(consumeChunks); - } else { - return Promise.resolve(); - } - } - - responseReader - .read() - .then(consumeChunks) - .then(() => { - triggerHandlers('fetch-body-resolved', { - endTimestamp: timestampInSeconds() * 1000, - response, - }); - }) - .catch(() => { - // noop - }); + // abort reading if read op takes more than 5s + const result = await Promise.race([ + responseReader.read(), + new Promise<{ done: boolean }>(res => { + setTimeout(() => { + res({ done: true }); + }, 5000); + }), + ]); + await consumeChunks(result); + } catch (error) { + // handle error if needed } - return response; + } else { + return Promise.resolve(); + } + } + + responseReader + .read() + .then(consumeChunks) + .then(() => { + onFinishedResolving(); + }) + .catch(() => { + // noop }); - }; - }); + } } function hasProp(obj: unknown, prop: T): obj is Record { From 43fd9183f83023d94b5c4702fee03a935ae6dbe3 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 5 Jul 2024 11:26:51 +0200 Subject: [PATCH 06/16] test: add e2e test for stream timeouts --- .../react-router-6/server/app.js | 9 +++-- .../react-router-6/src/pages/SSE.tsx | 16 ++++++--- .../react-router-6/tests/sse.test.ts | 34 +++++++++++++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/server/app.js b/dev-packages/e2e-tests/test-applications/react-router-6/server/app.js index d0ece10b907b..5a8cdb3929a1 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/server/app.js +++ b/dev-packages/e2e-tests/test-applications/react-router-6/server/app.js @@ -11,7 +11,7 @@ const wait = time => { }); }; -async function eventsHandler(request, response) { +async function sseHandler(request, response, timeout = false) { response.headers = { 'Content-Type': 'text/event-stream', Connection: 'keep-alive', @@ -30,12 +30,17 @@ async function eventsHandler(request, response) { for (let index = 0; index < 10; index++) { response.write(`data: ${new Date().toISOString()}\n\n`); + if (timeout) { + await wait(10000); + } } response.end(); } -app.get('/sse', eventsHandler); +app.get('/sse', (req, res) => sseHandler(req, res)); + +app.get('/sse-timeout', (req, res) => sseHandler(req, res, true)); app.listen(PORT, () => { console.log(`SSE service listening at http://localhost:${PORT}`); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx b/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx index 877d9f4298c8..49e53b09cfa2 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx @@ -2,10 +2,11 @@ import * as Sentry from '@sentry/react'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; -const fetchSSE = async () => { +const fetchSSE = async ({ timeout }: { timeout: boolean }) => { Sentry.startSpanManual({ name: 'sse stream using fetch' }, async span => { const res = await Sentry.startSpan({ name: 'sse fetch call' }, async () => { - return await fetch('http://localhost:8080/sse'); + const endpoint = `http://localhost:8080/${timeout ? 'sse-timeout' : 'sse'}`; + return await fetch(endpoint); }); const stream = res.body; @@ -34,9 +35,14 @@ const fetchSSE = async () => { const SSE = () => { return ( - + <> + + + ); }; diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts index a27db8264d6d..44612566051c 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts @@ -33,3 +33,37 @@ test('Waits for sse streaming when creating spans', async ({ page }) => { expect(resolveDuration).toBe(0); expect(resolveBodyDuration).toBe(2); }); + +test('Aborts when stream takes longer than 5s', async ({ page }) => { + await page.goto('/sse'); + + const transactionPromise = waitForTransaction('react-router-6', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const fetchButton = page.locator('id=fetch-timeout-button'); + await fetchButton.click(); + + const rootSpan = await transactionPromise; + const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0] as SpanJSON; + const httpGet = rootSpan.spans?.filter( + span => span.description === 'GET http://localhost:8080/sse-timeout', + )[0] as SpanJSON; + + expect(sseFetchCall).not.toBeUndefined(); + expect(httpGet).not.toBeUndefined(); + + expect(sseFetchCall?.timestamp).not.toBeUndefined(); + expect(sseFetchCall?.start_timestamp).not.toBeUndefined(); + expect(httpGet?.timestamp).not.toBeUndefined(); + expect(httpGet?.start_timestamp).not.toBeUndefined(); + + // http headers get sent instantly from the server + const resolveDuration = Math.round((sseFetchCall.timestamp as number) - sseFetchCall.start_timestamp); + + // body streams after 10s but client should abort reading after 5s + const resolveBodyDuration = Math.round((httpGet.timestamp as number) - httpGet.start_timestamp); + + expect(resolveDuration).toBe(0); + expect(resolveBodyDuration).toBe(7); +}); From 5883aca587a74875a22c956d38a70418a2ceef6b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 24 Jul 2024 16:53:51 +0200 Subject: [PATCH 07/16] test: update sse tests + update start script --- .../react-router-6/package.json | 5 ++-- .../react-router-6/tests/sse.test.ts | 24 +++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/package.json b/dev-packages/e2e-tests/test-applications/react-router-6/package.json index bc6903b3e11b..628c4e129024 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6/package.json @@ -15,7 +15,9 @@ }, "scripts": { "build": "react-scripts build", - "start": "concurrently \"node server/app.js\" \"serve -s build\"", + "start": "run-p start:client start:server", + "start:client": "node server/app.js", + "start:server": "serve -s build", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:build": "pnpm install && npx playwright install && pnpm build", @@ -44,7 +46,6 @@ "devDependencies": { "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils", - "concurrently": "^8.2.2", "serve": "14.0.1" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts index 44612566051c..5d4533726e36 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts @@ -16,13 +16,13 @@ test('Waits for sse streaming when creating spans', async ({ page }) => { const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0] as SpanJSON; const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0] as SpanJSON; - expect(sseFetchCall).not.toBeUndefined(); - expect(httpGet).not.toBeUndefined(); + expect(sseFetchCall).toBeDefined(); + expect(httpGet).toBeDefined(); - expect(sseFetchCall?.timestamp).not.toBeUndefined(); - expect(sseFetchCall?.start_timestamp).not.toBeUndefined(); - expect(httpGet?.timestamp).not.toBeUndefined(); - expect(httpGet?.start_timestamp).not.toBeUndefined(); + expect(sseFetchCall?.timestamp).toBeDefined(); + expect(sseFetchCall?.start_timestamp).toBeDefined(); + expect(httpGet?.timestamp).toBeDefined(); + expect(httpGet?.start_timestamp).toBeDefined(); // http headers get sent instantly from the server const resolveDuration = Math.round((sseFetchCall.timestamp as number) - sseFetchCall.start_timestamp); @@ -50,13 +50,13 @@ test('Aborts when stream takes longer than 5s', async ({ page }) => { span => span.description === 'GET http://localhost:8080/sse-timeout', )[0] as SpanJSON; - expect(sseFetchCall).not.toBeUndefined(); - expect(httpGet).not.toBeUndefined(); + expect(sseFetchCall).toBeDefined(); + expect(httpGet).toBeDefined(); - expect(sseFetchCall?.timestamp).not.toBeUndefined(); - expect(sseFetchCall?.start_timestamp).not.toBeUndefined(); - expect(httpGet?.timestamp).not.toBeUndefined(); - expect(httpGet?.start_timestamp).not.toBeUndefined(); + expect(sseFetchCall?.timestamp).toBeDefined(); + expect(sseFetchCall?.start_timestamp).toBeDefined(); + expect(httpGet?.timestamp).toBeDefined(); + expect(httpGet?.start_timestamp).toBeDefined(); // http headers get sent instantly from the server const resolveDuration = Math.round((sseFetchCall.timestamp as number) - sseFetchCall.start_timestamp); From 50b1c321a133bb421bf484e32d2d2be8b6ddf9aa Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 24 Jul 2024 17:07:28 +0200 Subject: [PATCH 08/16] refactor: extract client from method --- .../src/tracing/browserTracingIntegration.ts | 2 +- packages/browser/src/tracing/request.ts | 35 +++++++++---------- .../browser/test/unit/tracing/request.test.ts | 18 ++++++++-- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 9d5421f697cd..d8f6796b5b1b 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -398,7 +398,7 @@ export const browserTracingIntegration = ((_options: Partial): void { +export function instrumentOutgoingRequests(client: Client, _options?: Partial): void { const { traceFetch, traceXHR, shouldCreateSpanForRequest, enableHTTPTimings, tracePropagationTargets } = { traceFetch: defaultRequestInstrumentationOptions.traceFetch, traceXHR: defaultRequestInstrumentationOptions.traceXHR, @@ -117,27 +117,24 @@ export function instrumentOutgoingRequests(_options?: Partial shouldAttachHeaders(url, tracePropagationTargets); const spans: Record = {}; - const client = getClient(); if (traceFetch) { - if (client) { - // Keeping track of http requests, whose body payloads resolved later than the intial resolved request - // e.g. streaming using server sent events (SSE) - client.addEventProcessor(event => { - if (event.type === 'transaction' && event.spans) { - event.spans.forEach(span => { - if (span.op === 'http.client') { - const updatedTimestamp = spanIdToEndTimestamp.get(span.span_id); - if (updatedTimestamp) { - span.timestamp = updatedTimestamp / 1000; - spanIdToEndTimestamp.delete(span.span_id); - } + // Keeping track of http requests, whose body payloads resolved later than the intial resolved request + // e.g. streaming using server sent events (SSE) + client.addEventProcessor(event => { + if (event.type === 'transaction' && event.spans) { + event.spans.forEach(span => { + if (span.op === 'http.client') { + const updatedTimestamp = spanIdToEndTimestamp.get(span.span_id); + if (updatedTimestamp) { + span.timestamp = updatedTimestamp / 1000; + spanIdToEndTimestamp.delete(span.span_id); } - }); - } - return event; - }); - } + } + }); + } + return event; + }); addFetchEndInstrumentationHandler(handlerData => { if (handlerData.response) { diff --git a/packages/browser/test/unit/tracing/request.test.ts b/packages/browser/test/unit/tracing/request.test.ts index 47174c7d9016..384d62bf52e2 100644 --- a/packages/browser/test/unit/tracing/request.test.ts +++ b/packages/browser/test/unit/tracing/request.test.ts @@ -1,4 +1,5 @@ import * as browserUtils from '@sentry-internal/browser-utils'; +import type { Client } from '@sentry/types'; import * as utils from '@sentry/utils'; import { WINDOW } from '../../../src/helpers'; @@ -10,16 +11,27 @@ beforeAll(() => { global.Request = {}; }); +class MockClient implements Partial { + public addEventProcessor: () => void; + constructor() { + // Mock addEventProcessor function + this.addEventProcessor = jest.fn(); + } +} + describe('instrumentOutgoingRequests', () => { + let client: Client; + beforeEach(() => { jest.clearAllMocks(); + client = new MockClient() as unknown as Client; }); it('instruments fetch and xhr requests', () => { const addFetchSpy = jest.spyOn(utils, 'addFetchInstrumentationHandler'); const addXhrSpy = jest.spyOn(browserUtils, 'addXhrInstrumentationHandler'); - instrumentOutgoingRequests(); + instrumentOutgoingRequests(client); expect(addFetchSpy).toHaveBeenCalledWith(expect.any(Function)); expect(addXhrSpy).toHaveBeenCalledWith(expect.any(Function)); @@ -28,7 +40,7 @@ describe('instrumentOutgoingRequests', () => { it('does not instrument fetch requests if traceFetch is false', () => { const addFetchSpy = jest.spyOn(utils, 'addFetchInstrumentationHandler'); - instrumentOutgoingRequests({ traceFetch: false }); + instrumentOutgoingRequests(client, { traceFetch: false }); expect(addFetchSpy).not.toHaveBeenCalled(); }); @@ -36,7 +48,7 @@ describe('instrumentOutgoingRequests', () => { it('does not instrument xhr requests if traceXHR is false', () => { const addXhrSpy = jest.spyOn(browserUtils, 'addXhrInstrumentationHandler'); - instrumentOutgoingRequests({ traceXHR: false }); + instrumentOutgoingRequests(client, { traceXHR: false }); expect(addXhrSpy).not.toHaveBeenCalled(); }); From 08b5da66c6b6e9732ce92ebbabad151ef8f1a87d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 24 Jul 2024 17:09:54 +0200 Subject: [PATCH 09/16] feat: remove log from fetch instrumentation handler --- packages/utils/src/instrument/fetch.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index ef1fc7999bc0..e0b300b86ce1 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -1,9 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { HandlerDataFetch } from '@sentry/types'; -import { DEBUG_BUILD } from '../debug-build'; import { isError } from '../is'; -import { logger } from '../logger'; import { addNonEnumerableProperty, fill } from '../object'; import { supportsNativeFetch } from '../supports'; import { timestampInSeconds } from '../time'; @@ -82,7 +80,6 @@ function instrumentFetch(handlerType: 'fetch' | 'fetch-body-resolved'): void { clonedResponseForResolving = response.clone(); } catch (e) { // noop - DEBUG_BUILD && logger.warn('Failed to clone response body.'); } await resolveResponse(clonedResponseForResolving, () => { From 7bfae277d34b57781e4bc7188f36df42c3cc0b93 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 24 Jul 2024 17:42:31 +0200 Subject: [PATCH 10/16] fix: add missing dev dependency --- .../e2e-tests/test-applications/react-router-6/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/package.json b/dev-packages/e2e-tests/test-applications/react-router-6/package.json index 628c4e129024..5171a89eadb3 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6/package.json @@ -46,7 +46,8 @@ "devDependencies": { "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils", - "serve": "14.0.1" + "serve": "14.0.1", + "npm-run-all2": "^6.2.0" }, "volta": { "extends": "../../package.json" From dfa262e93461003961f32a147a84979b82669151 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 25 Jul 2024 09:55:32 +0200 Subject: [PATCH 11/16] test: update fakeClient for sveltekit --- .../sveltekit/test/client/browserTracingIntegration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sveltekit/test/client/browserTracingIntegration.test.ts b/packages/sveltekit/test/client/browserTracingIntegration.test.ts index a8fbaa815af2..6d052cc77b50 100644 --- a/packages/sveltekit/test/client/browserTracingIntegration.test.ts +++ b/packages/sveltekit/test/client/browserTracingIntegration.test.ts @@ -60,7 +60,7 @@ describe('browserTracingIntegration', () => { return createdRootSpan as Span; }); - const fakeClient = { getOptions: () => ({}), on: () => {} }; + const fakeClient = { getOptions: () => ({}), on: () => {}, addEventProcessor: () => {} }; const mockedRoutingSpan = { end: () => {}, From 72ea4b4fc6b90a47d176b595c8caed8850593cde Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 26 Jul 2024 13:43:57 +0200 Subject: [PATCH 12/16] feat: make instrumentFetch treeshakable --- packages/utils/src/instrument/fetch.ts | 46 +++++++++++++++----------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index e0b300b86ce1..d373b60603d1 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -21,7 +21,7 @@ type FetchResource = string | { toString(): string } | { url: string }; export function addFetchInstrumentationHandler(handler: (data: HandlerDataFetch) => void): void { const type = 'fetch'; addHandler(type, handler); - maybeInstrument(type, () => instrumentFetch(type)); + maybeInstrument(type, () => instrumentFetch()); } /** @@ -35,10 +35,11 @@ export function addFetchInstrumentationHandler(handler: (data: HandlerDataFetch) export function addFetchEndInstrumentationHandler(handler: (data: HandlerDataFetch) => void): void { const type = 'fetch-body-resolved'; addHandler(type, handler); - maybeInstrument(type, () => instrumentFetch(type)); + maybeInstrument(type, () => instrumentFetch(streamHandler)); } -function instrumentFetch(handlerType: 'fetch' | 'fetch-body-resolved'): void { +// function instrumentFetch(handlerType: 'fetch' | 'fetch-body-resolved'): void { +function instrumentFetch(onFetchResolved?: (response: Response) => void): void { if (!supportsNativeFetch()) { return; } @@ -55,7 +56,8 @@ function instrumentFetch(handlerType: 'fetch' | 'fetch-body-resolved'): void { startTimestamp: timestampInSeconds() * 1000, }; - if (handlerType === 'fetch') { + // if there is no callback, fetch is instrumented + if (!onFetchResolved) { triggerHandlers('fetch', { ...handlerData, }); @@ -73,21 +75,8 @@ function instrumentFetch(handlerType: 'fetch' | 'fetch-body-resolved'): void { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return originalFetch.apply(GLOBAL_OBJ, args).then( async (response: Response) => { - if (handlerType === 'fetch-body-resolved') { - // clone response for awaiting stream - let clonedResponseForResolving: Response | undefined; - try { - clonedResponseForResolving = response.clone(); - } catch (e) { - // noop - } - - await resolveResponse(clonedResponseForResolving, () => { - triggerHandlers('fetch-body-resolved', { - endTimestamp: timestampInSeconds() * 1000, - response, - }); - }); + if (onFetchResolved) { + onFetchResolved(response); } else { const finishedHandlerData: HandlerDataFetch = { ...handlerData, @@ -100,7 +89,7 @@ function instrumentFetch(handlerType: 'fetch' | 'fetch-body-resolved'): void { return response; }, (error: Error) => { - if (handlerType === 'fetch') { + if (!onFetchResolved) { const erroredHandlerData: HandlerDataFetch = { ...handlerData, endTimestamp: timestampInSeconds() * 1000, @@ -167,6 +156,23 @@ function resolveResponse(res: Response | undefined, onFinishedResolving: () => v } } +async function streamHandler(response: Response): Promise { + // clone response for awaiting stream + let clonedResponseForResolving: Response | undefined; + try { + clonedResponseForResolving = response.clone(); + } catch (e) { + // noop + } + + await resolveResponse(clonedResponseForResolving, () => { + triggerHandlers('fetch-body-resolved', { + endTimestamp: timestampInSeconds() * 1000, + response, + }); + }); +} + function hasProp(obj: unknown, prop: T): obj is Record { return !!obj && typeof obj === 'object' && !!(obj as Record)[prop]; } From afeb3e7887d5e51ab3e47d2219c154b4c8bebf67 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 26 Jul 2024 13:45:34 +0200 Subject: [PATCH 13/16] remove comment --- packages/utils/src/instrument/fetch.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index d373b60603d1..ed879a4c9088 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -38,7 +38,6 @@ export function addFetchEndInstrumentationHandler(handler: (data: HandlerDataFet maybeInstrument(type, () => instrumentFetch(streamHandler)); } -// function instrumentFetch(handlerType: 'fetch' | 'fetch-body-resolved'): void { function instrumentFetch(onFetchResolved?: (response: Response) => void): void { if (!supportsNativeFetch()) { return; From 970b3df3d4782d1c3e96fe52a04212e0e3ea00dc Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 29 Jul 2024 12:55:40 +0200 Subject: [PATCH 14/16] ci: raise package limits --- .size-limit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 2e7899cb934a..4ccb90d3679c 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -48,14 +48,14 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '76 KB', + limit: '77 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback)', path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'), gzip: true, - limit: '89 KB', + limit: '90 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback, metrics)', @@ -107,7 +107,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '38 KB', + limit: '39 KB', }, // Vue SDK (ESM) { From b6f4df0a3809837f167a4120a5ebb3b2d49047d2 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 29 Jul 2024 14:43:20 +0200 Subject: [PATCH 15/16] Update packages/utils/src/instrument/fetch.ts Co-authored-by: Francesco Novy --- packages/utils/src/instrument/fetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index ed879a4c9088..a4df9bd6f807 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -55,7 +55,7 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void): void { startTimestamp: timestampInSeconds() * 1000, }; - // if there is no callback, fetch is instrumented + // if there is no callback, fetch is instrumented directly if (!onFetchResolved) { triggerHandlers('fetch', { ...handlerData, From 377ab3b8575a9acde9b865e5f991b214a481ac22 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 29 Jul 2024 15:32:27 +0200 Subject: [PATCH 16/16] ci: raise limits once again --- .size-limit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 0653991d1d32..dc85fffe40af 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -15,7 +15,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '35 KB', + limit: '36 KB', }, { name: '@sentry/browser (incl. Tracing, Replay)', @@ -29,7 +29,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration'), gzip: true, - limit: '65 KB', + limit: '66 KB', modifyWebpackConfig: function (config) { const webpack = require('webpack'); config.plugins.push(