-
Notifications
You must be signed in to change notification settings - Fork 190
Expand file tree
/
Copy pathfetchObservable.ts
More file actions
166 lines (140 loc) · 4.81 KB
/
Copy pathfetchObservable.ts
File metadata and controls
166 lines (140 loc) · 4.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import type { ClocksState } from '@datadog/js-core/time'
import { clocksNow } from '@datadog/js-core/time'
import { normalizeUrl, globalObject } from '@datadog/js-core/util'
import type { GlobalObject } from '@datadog/js-core/util'
import type { InstrumentedMethodCall } from '../tools/instrumentMethod'
import { instrumentMethod } from '../tools/instrumentMethod'
import { monitorError } from '../tools/monitor'
import { Observable } from '../tools/observable'
import { readBytesFromStream } from '../tools/readBytesFromStream'
import { tryToClone } from '../tools/utils/responseUtils'
interface FetchContextBase {
method: string
startClocks: ClocksState
input: unknown
init?: RequestInit
url: string
handlingStack?: string
isAbortedOnStart: boolean
}
export interface FetchStartContext extends FetchContextBase {
state: 'start'
}
export interface FetchResolveContext extends FetchContextBase {
state: 'resolve'
status: number
response?: Response
responseBody?: string
responseType?: string
isAborted: boolean
error?: Error
}
export type FetchContext = FetchStartContext | FetchResolveContext
type ResponseBodyActionGetter = (context: FetchResolveContext) => ResponseBodyAction
/**
* Action to take with the response body of a fetch request.
* Values are ordered by priority: higher values take precedence when multiple actions are requested.
*/
export const enum ResponseBodyAction {
IGNORE = 0,
COLLECT = 1,
}
let fetchObservable: Observable<FetchContext> | undefined
const responseBodyActionGetters: ResponseBodyActionGetter[] = []
export function initFetchObservable({ responseBodyAction }: { responseBodyAction?: ResponseBodyActionGetter } = {}) {
if (responseBodyAction) {
responseBodyActionGetters.push(responseBodyAction)
}
if (!fetchObservable) {
fetchObservable = createFetchObservable()
}
return fetchObservable
}
export function resetFetchObservable() {
fetchObservable = undefined
responseBodyActionGetters.length = 0
}
function createFetchObservable() {
return new Observable<FetchContext>((observable) => {
// eslint-disable-next-line local-rules/disallow-zone-js-patched-values
if (!globalObject.fetch) {
return
}
const { stop } = instrumentMethod(globalObject, 'fetch', (call) => beforeSend(call, observable), {
computeHandlingStack: true,
})
return stop
})
}
function beforeSend(
{ parameters, onPostCall, handlingStack }: InstrumentedMethodCall<GlobalObject, 'fetch'>,
observable: Observable<FetchContext>
) {
const [input, init] = parameters
let methodFromParams = init?.method
if (methodFromParams === undefined && input instanceof Request) {
methodFromParams = input.method
}
const method = methodFromParams !== undefined ? String(methodFromParams).toUpperCase() : 'GET'
const url = input instanceof Request ? input.url : normalizeUrl(String(input))
const startClocks = clocksNow()
const context: FetchStartContext = {
state: 'start',
init,
input,
method,
startClocks,
url,
handlingStack,
isAbortedOnStart: (input instanceof Request && input.signal?.aborted) || init?.signal?.aborted || false,
}
observable.notify(context)
// Those properties can be changed by observable subscribers
parameters[0] = context.input as RequestInfo | URL
parameters[1] = context.init
onPostCall((responsePromise) => {
afterSend(observable, responsePromise, context).catch(monitorError)
})
}
async function afterSend(
observable: Observable<FetchContext>,
responsePromise: Promise<Response>,
startContext: FetchStartContext
) {
const context = startContext as unknown as FetchResolveContext
let response: Response
try {
response = await responsePromise
} catch (error) {
observable.notify({
...context,
state: 'resolve',
status: 0,
isAborted:
context.init?.signal?.aborted || (error instanceof DOMException && error.code === DOMException.ABORT_ERR),
error: error as Error,
})
return
}
context.response = response
context.status = response.status
context.responseType = response.type
context.isAborted = false
const responseBodyCondition: ResponseBodyAction = responseBodyActionGetters.reduce<number>(
(action, getter) => Math.max(action, getter(context)),
ResponseBodyAction.IGNORE
)
if (responseBodyCondition === ResponseBodyAction.COLLECT) {
const clonedResponse = tryToClone(response)
if (clonedResponse?.body) {
try {
const bytes = await readBytesFromStream(clonedResponse.body)
context.responseBody = new TextDecoder().decode(bytes)
} catch {
// Ignore errors when reading the response body (e.g., stream aborted, network errors)
// This is not critical and should not be reported as an SDK error
}
}
}
observable.notify({ ...context, state: 'resolve' })
}