Skip to content

Commit b44e053

Browse files
chargomemydea
authored andcommitted
feat: timeout stream reading + small refactor
1 parent 2bfd2dc commit b44e053

File tree

1 file changed

+86
-76
lines changed

1 file changed

+86
-76
lines changed

packages/utils/src/instrument/fetch.ts

Lines changed: 86 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type FetchResource = string | { toString(): string } | { url: string };
2323
export function addFetchInstrumentationHandler(handler: (data: HandlerDataFetch) => void): void {
2424
const type = 'fetch';
2525
addHandler(type, handler);
26-
maybeInstrument(type, instrumentFetch);
26+
maybeInstrument(type, () => instrumentFetch(type));
2727
}
2828

2929
/**
@@ -37,18 +37,17 @@ export function addFetchInstrumentationHandler(handler: (data: HandlerDataFetch)
3737
export function addFetchEndInstrumentationHandler(handler: (data: HandlerDataFetch) => void): void {
3838
const type = 'fetch-body-resolved';
3939
addHandler(type, handler);
40-
maybeInstrument(type, instrumentFetchBodyReceived);
40+
maybeInstrument(type, () => instrumentFetch(type));
4141
}
4242

43-
function instrumentFetch(): void {
43+
function instrumentFetch(handlerType: 'fetch' | 'fetch-body-resolved'): void {
4444
if (!supportsNativeFetch()) {
4545
return;
4646
}
4747

4848
fill(GLOBAL_OBJ, 'fetch', function (originalFetch: () => void): () => void {
4949
return function (...args: any[]): void {
5050
const { method, url } = parseFetchArgs(args);
51-
5251
const handlerData: HandlerDataFetch = {
5352
args,
5453
fetchData: {
@@ -58,9 +57,11 @@ function instrumentFetch(): void {
5857
startTimestamp: timestampInSeconds() * 1000,
5958
};
6059

61-
triggerHandlers('fetch', {
62-
...handlerData,
63-
});
60+
if (handlerType === 'fetch') {
61+
triggerHandlers('fetch', {
62+
...handlerData,
63+
});
64+
}
6465

6566
// We capture the stack right here and not in the Promise error callback because Safari (and probably other
6667
// 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 {
7374

7475
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
7576
return originalFetch.apply(GLOBAL_OBJ, args).then(
76-
(response: Response) => {
77-
const finishedHandlerData: HandlerDataFetch = {
78-
...handlerData,
79-
endTimestamp: timestampInSeconds() * 1000,
80-
response,
81-
};
82-
83-
triggerHandlers('fetch', finishedHandlerData);
77+
async (response: Response) => {
78+
if (handlerType === 'fetch-body-resolved') {
79+
// clone response for awaiting stream
80+
let clonedResponseForResolving: Response | undefined;
81+
try {
82+
clonedResponseForResolving = response.clone();
83+
} catch (e) {
84+
// noop
85+
DEBUG_BUILD && logger.warn('Failed to clone response body.');
86+
}
87+
88+
await resolveResponse(clonedResponseForResolving, () => {
89+
triggerHandlers('fetch-body-resolved', {
90+
endTimestamp: timestampInSeconds() * 1000,
91+
response,
92+
});
93+
});
94+
} else {
95+
const finishedHandlerData: HandlerDataFetch = {
96+
...handlerData,
97+
endTimestamp: timestampInSeconds() * 1000,
98+
response,
99+
};
100+
triggerHandlers('fetch', finishedHandlerData);
101+
}
102+
84103
return response;
85104
},
86105
(error: Error) => {
87-
const erroredHandlerData: HandlerDataFetch = {
88-
...handlerData,
89-
endTimestamp: timestampInSeconds() * 1000,
90-
error,
91-
};
92-
93-
triggerHandlers('fetch', erroredHandlerData);
106+
if (handlerType === 'fetch') {
107+
const erroredHandlerData: HandlerDataFetch = {
108+
...handlerData,
109+
endTimestamp: timestampInSeconds() * 1000,
110+
error,
111+
};
112+
113+
triggerHandlers('fetch', erroredHandlerData);
114+
115+
if (isError(error) && error.stack === undefined) {
116+
// NOTE: If you are a Sentry user, and you are seeing this stack frame,
117+
// it means the error, that was caused by your fetch call did not
118+
// have a stack trace, so the SDK backfilled the stack trace so
119+
// you can see which fetch call failed.
120+
error.stack = virtualStackTrace;
121+
addNonEnumerableProperty(error, 'framesToPop', 1);
122+
}
94123

95-
if (isError(error) && error.stack === undefined) {
96124
// NOTE: If you are a Sentry user, and you are seeing this stack frame,
97-
// it means the error, that was caused by your fetch call did not
98-
// have a stack trace, so the SDK backfilled the stack trace so
99-
// you can see which fetch call failed.
100-
error.stack = virtualStackTrace;
101-
addNonEnumerableProperty(error, 'framesToPop', 1);
125+
// it means the sentry.javascript SDK caught an error invoking your application code.
126+
// This is expected behavior and NOT indicative of a bug with sentry.javascript.
127+
throw error;
102128
}
103-
104-
// NOTE: If you are a Sentry user, and you are seeing this stack frame,
105-
// it means the sentry.javascript SDK caught an error invoking your application code.
106-
// This is expected behavior and NOT indicative of a bug with sentry.javascript.
107-
throw error;
108129
},
109130
);
110131
};
111132
});
112133
}
113134

114-
function instrumentFetchBodyReceived(): void {
115-
if (!supportsNativeFetch()) {
116-
return;
117-
}
135+
function resolveResponse(res: Response | undefined, onFinishedResolving: () => void): void {
136+
if (res && res.body) {
137+
const responseReader = res.body.getReader();
118138

119-
fill(GLOBAL_OBJ, 'fetch', function (originalFetch: () => void): () => void {
120-
return function (...args: any[]): void {
121-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
122-
return originalFetch.apply(GLOBAL_OBJ, args).then(async (response: Response) => {
123-
// clone response for awaiting stream
124-
let clonedResponseForResolving: Response | undefined;
139+
// eslint-disable-next-line no-inner-declarations
140+
async function consumeChunks({ done }: { done: boolean }): Promise<void> {
141+
if (!done) {
125142
try {
126-
clonedResponseForResolving = response.clone();
127-
} catch (e) {
128-
// noop
129-
DEBUG_BUILD && logger.warn('Failed to clone response body.');
130-
}
131-
132-
if (clonedResponseForResolving && clonedResponseForResolving.body) {
133-
const responseReader = clonedResponseForResolving.body.getReader();
134-
135-
// eslint-disable-next-line no-inner-declarations
136-
function consumeChunks({ done }: { done: boolean }): Promise<void> {
137-
if (!done) {
138-
return responseReader.read().then(consumeChunks);
139-
} else {
140-
return Promise.resolve();
141-
}
142-
}
143-
144-
responseReader
145-
.read()
146-
.then(consumeChunks)
147-
.then(() => {
148-
triggerHandlers('fetch-body-resolved', {
149-
endTimestamp: timestampInSeconds() * 1000,
150-
response,
151-
});
152-
})
153-
.catch(() => {
154-
// noop
155-
});
143+
// abort reading if read op takes more than 5s
144+
const result = await Promise.race([
145+
responseReader.read(),
146+
new Promise<{ done: boolean }>(res => {
147+
setTimeout(() => {
148+
res({ done: true });
149+
}, 5000);
150+
}),
151+
]);
152+
await consumeChunks(result);
153+
} catch (error) {
154+
// handle error if needed
156155
}
157-
return response;
156+
} else {
157+
return Promise.resolve();
158+
}
159+
}
160+
161+
responseReader
162+
.read()
163+
.then(consumeChunks)
164+
.then(() => {
165+
onFinishedResolving();
166+
})
167+
.catch(() => {
168+
// noop
158169
});
159-
};
160-
});
170+
}
161171
}
162172

163173
function hasProp<T extends string>(obj: unknown, prop: T): obj is Record<string, string> {

0 commit comments

Comments
 (0)