Skip to content

Commit 9de1a8f

Browse files
authored
Merge pull request #13861 from getsentry/prepare-release/8.33.1
2 parents 4604f3e + 2903036 commit 9de1a8f

File tree

7 files changed

+69
-42
lines changed

7 files changed

+69
-42
lines changed

.github/workflows/build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ jobs:
437437
with:
438438
node-version-file: 'package.json'
439439
- name: Set up Deno
440-
uses: denoland/setup-deno@v1.4.1
440+
uses: denoland/setup-deno@v1.5.1
441441
with:
442442
deno-version: v1.38.5
443443
- name: Restore caches

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010

1111
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1212

13+
## 8.33.1
14+
15+
- fix(core): Update trpc middleware types ([#13859](https://github.com/getsentry/sentry-javascript/pull/13859))
16+
- fix(fetch): Fix memory leak when handling endless streaming
17+
([#13809](https://github.com/getsentry/sentry-javascript/pull/13809))
18+
19+
Work in this release was contributed by @soapproject. Thank you for your contribution!
20+
1321
## 8.33.0
1422

1523
### Important Changes

dev-packages/e2e-tests/test-applications/nextjs-t3/src/server/api/trpc.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ export const createCallerFactory = t.createCallerFactory;
7070
*/
7171
export const createTRPCRouter = t.router;
7272

73-
const sentryMiddleware = Sentry.trpcMiddleware({
74-
attachRpcInput: true,
75-
});
76-
77-
export const publicProcedure = t.procedure.use(async opts => sentryMiddleware(opts));
73+
export const publicProcedure = t.procedure.use(
74+
Sentry.trpcMiddleware({
75+
attachRpcInput: true,
76+
}),
77+
);

dev-packages/e2e-tests/test-applications/node-express/src/app.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,7 @@ Sentry.addEventProcessor(event => {
105105

106106
export const t = initTRPC.context<Context>().create();
107107

108-
const sentryMiddleware = Sentry.trpcMiddleware({ attachRpcInput: true });
109-
110-
const procedure = t.procedure.use(async opts => sentryMiddleware(opts));
108+
const procedure = t.procedure.use(Sentry.trpcMiddleware({ attachRpcInput: true }));
111109

112110
export const appRouter = t.router({
113111
getSomething: procedure.input(z.string()).query(opts => {

dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ test('Waits for sse streaming when sse has been explicitly aborted', async ({ pa
4545
await fetchButton.click();
4646

4747
const rootSpan = await transactionPromise;
48-
console.log(JSON.stringify(rootSpan, null, 2));
4948
const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0] as SpanJSON;
5049
const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0] as SpanJSON;
5150

@@ -71,7 +70,7 @@ test('Waits for sse streaming when sse has been explicitly aborted', async ({ pa
7170
expect(consoleBreadcrumb?.message).toBe('Could not fetch sse AbortError: BodyStreamBuffer was aborted');
7271
});
7372

74-
test('Aborts when stream takes longer than 5s', async ({ page }) => {
73+
test('Aborts when stream takes longer than 5s, by not updating the span duration', async ({ page }) => {
7574
await page.goto('/sse');
7675

7776
const transactionPromise = waitForTransaction('react-router-6', async transactionEvent => {
@@ -102,5 +101,5 @@ test('Aborts when stream takes longer than 5s', async ({ page }) => {
102101
const resolveBodyDuration = Math.round((httpGet.timestamp as number) - httpGet.start_timestamp);
103102

104103
expect(resolveDuration).toBe(0);
105-
expect(resolveBodyDuration).toBe(7);
104+
expect(resolveBodyDuration).toBe(0);
106105
});

packages/core/src/trpc.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,15 @@ function captureIfError(nextResult: unknown): void {
3333
}
3434
}
3535

36+
type SentryTrpcMiddleware<T> = T extends Promise<unknown> ? T : Promise<T>;
37+
3638
/**
3739
* Sentry tRPC middleware that captures errors and creates spans for tRPC procedures.
3840
*/
3941
export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) {
40-
return async function <T>(opts: SentryTrpcMiddlewareArguments<T>): Promise<T> {
42+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
43+
// @ts-ignore
44+
return async function <T>(opts: SentryTrpcMiddlewareArguments<T>): SentryTrpcMiddleware<T> {
4145
const { path, type, next, rawInput, getRawInput } = opts;
4246

4347
const client = getClient();
@@ -85,6 +89,6 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) {
8589
throw e;
8690
}
8791
},
88-
);
92+
) as SentryTrpcMiddleware<T>;
8993
};
9094
}

packages/utils/src/instrument/fetch.ts

+46-28
Original file line numberDiff line numberDiff line change
@@ -116,40 +116,57 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat
116116
}
117117

118118
async function resolveResponse(res: Response | undefined, onFinishedResolving: () => void): Promise<void> {
119-
if (res && res.body && res.body.getReader) {
120-
const responseReader = res.body.getReader();
121-
122-
// eslint-disable-next-line no-inner-declarations
123-
async function consumeChunks({ done }: { done: boolean }): Promise<void> {
124-
if (!done) {
125-
try {
126-
// abort reading if read op takes more than 5s
127-
const result = await Promise.race([
128-
responseReader.read(),
129-
new Promise<{ done: boolean }>(res => {
130-
setTimeout(() => {
131-
res({ done: true });
132-
}, 5000);
133-
}),
134-
]);
135-
await consumeChunks(result);
136-
} catch (error) {
137-
// handle error if needed
119+
if (res && res.body) {
120+
const body = res.body;
121+
const responseReader = body.getReader();
122+
123+
// Define a maximum duration after which we just cancel
124+
const maxFetchDurationTimeout = setTimeout(
125+
() => {
126+
body.cancel().then(null, () => {
127+
// noop
128+
});
129+
},
130+
90 * 1000, // 90s
131+
);
132+
133+
let readingActive = true;
134+
while (readingActive) {
135+
let chunkTimeout;
136+
try {
137+
// abort reading if read op takes more than 5s
138+
chunkTimeout = setTimeout(() => {
139+
body.cancel().then(null, () => {
140+
// noop on error
141+
});
142+
}, 5000);
143+
144+
// This .read() call will reject/throw when we abort due to timeouts through `body.cancel()`
145+
const { done } = await responseReader.read();
146+
147+
clearTimeout(chunkTimeout);
148+
149+
if (done) {
150+
onFinishedResolving();
151+
readingActive = false;
138152
}
139-
} else {
140-
return Promise.resolve();
153+
} catch (error) {
154+
readingActive = false;
155+
} finally {
156+
clearTimeout(chunkTimeout);
141157
}
142158
}
143159

144-
return responseReader
145-
.read()
146-
.then(consumeChunks)
147-
.then(onFinishedResolving)
148-
.catch(() => undefined);
160+
clearTimeout(maxFetchDurationTimeout);
161+
162+
responseReader.releaseLock();
163+
body.cancel().then(null, () => {
164+
// noop on error
165+
});
149166
}
150167
}
151168

152-
async function streamHandler(response: Response): Promise<void> {
169+
function streamHandler(response: Response): void {
153170
// clone response for awaiting stream
154171
let clonedResponseForResolving: Response;
155172
try {
@@ -158,7 +175,8 @@ async function streamHandler(response: Response): Promise<void> {
158175
return;
159176
}
160177

161-
await resolveResponse(clonedResponseForResolving, () => {
178+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
179+
resolveResponse(clonedResponseForResolving, () => {
162180
triggerHandlers('fetch-body-resolved', {
163181
endTimestamp: timestampInSeconds() * 1000,
164182
response,

0 commit comments

Comments
 (0)