-
Notifications
You must be signed in to change notification settings - Fork 190
Expand file tree
/
Copy pathinstrumentMethod.ts
More file actions
183 lines (160 loc) · 6.14 KB
/
Copy pathinstrumentMethod.ts
File metadata and controls
183 lines (160 loc) · 6.14 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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import { setTimeout } from './timer'
import { callMonitored } from './monitor'
import { noop } from './utils/functionUtils'
import { createHandlingStack } from './stackTrace/handlingStack'
import { display } from './display'
/**
* Object passed to the callback of an instrumented method call. See `instrumentMethod` for more
* info.
*/
export interface InstrumentedMethodCall<TARGET extends { [key: string]: any }, METHOD extends keyof TARGET> {
/**
* The target object on which the method was called.
*/
target: TARGET
/**
* The parameters with which the method was called.
*
* Note: if needed, parameters can be mutated by the instrumentation
*/
parameters: Parameters<TARGET[METHOD]>
/**
* Registers a callback that will be called after the original method is called, with the method
* result passed as argument.
*/
onPostCall: (callback: PostCallCallback<TARGET, METHOD>) => void
/**
* The stack trace of the method call.
*/
handlingStack?: string
}
type PostCallCallback<TARGET extends { [key: string]: any }, METHOD extends keyof TARGET> = (
result: ReturnType<TARGET[METHOD]>
) => void
/**
* Instruments a method on a object, calling the given callback before the original method is
* invoked. The callback receives an object with information about the method call.
*
* This function makes sure that we are "good citizens" regarding third party instrumentations: when
* removing the instrumentation, the original method is usually restored, but if a third party
* instrumentation was set after ours, we keep it in place and just replace our instrumentation with
* a noop.
*
* Note: it is generally better to instrument methods that are "owned" by the object instead of ones
* that are inherited from the prototype chain. Example:
* * do: `instrumentMethod(Array.prototype, 'push', ...)`
* * don't: `instrumentMethod([], 'push', ...)`
*
* This method is also used to set event handler properties (ex: window.onerror = ...), as it has
* the same requirements as instrumenting a method:
* * if the event handler is already set by a third party, we need to call it and not just blindly
* override it.
* * if the event handler is set by a third party after us, we need to keep it in place when
* removing ours.
*
* @example
*
* instrumentMethod(window, 'fetch', ({ target, parameters, onPostCall }) => {
* console.log('Before calling fetch on', target, 'with parameters', parameters)
*
* onPostCall((result) => {
* console.log('After fetch calling on', target, 'with parameters', parameters, 'and result', result)
* })
* })
*/
export function instrumentMethod<TARGET extends { [key: string]: any }, METHOD extends keyof TARGET>(
targetPrototype: TARGET,
method: METHOD,
onPreCall: (this: null, callInfos: InstrumentedMethodCall<TARGET, METHOD>) => void,
{ computeHandlingStack }: { computeHandlingStack?: boolean } = {}
) {
let original = targetPrototype[method]
if (typeof original !== 'function') {
if (method in targetPrototype && typeof method === 'string' && method.startsWith('on')) {
original = noop as TARGET[METHOD]
} else {
return { stop: noop }
}
}
let stopped = false
const instrumentation = function (this: TARGET): ReturnType<TARGET[METHOD]> {
if (stopped) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return original.apply(this, arguments)
}
const parameters = Array.from(arguments) as Parameters<TARGET[METHOD]>
let postCallCallback: PostCallCallback<TARGET, METHOD> | undefined
callMonitored(onPreCall, null, [
{
target: this,
parameters,
onPostCall: (callback) => {
postCallCallback = callback
},
handlingStack: computeHandlingStack ? createHandlingStack('instrumented method') : undefined,
},
])
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const result = original.apply(this, parameters)
if (postCallCallback) {
callMonitored(postCallCallback, null, [result])
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return result
}
// Salesforce makes History.prototype.pushState and History.prototype.replaceState readonly. https://help.salesforce.com/s/articleView?id=release-notes.rn_lws_distortions_added.htm&release=238&type=5
try {
targetPrototype[method] = instrumentation as TARGET[METHOD]
} catch (error) {
display.error(error)
return { stop: noop }
}
return {
stop: () => {
stopped = true
// If the instrumentation has been removed by a third party, keep the last one
if (targetPrototype[method] === instrumentation) {
try {
targetPrototype[method] = original
} catch (error) {
display.error(error)
// Restore can be rejected by sandboxed runtimes; the instrumentation is already stopped.
}
}
},
}
}
export function instrumentSetter<TARGET extends { [key: string]: any }, PROPERTY extends keyof TARGET>(
targetPrototype: TARGET,
property: PROPERTY,
after: (target: TARGET, value: TARGET[PROPERTY]) => void
) {
const originalDescriptor = Object.getOwnPropertyDescriptor(targetPrototype, property)
if (!originalDescriptor || !originalDescriptor.set || !originalDescriptor.configurable) {
return { stop: noop }
}
const stoppedInstrumentation = noop
let instrumentation = (target: TARGET, value: TARGET[PROPERTY]) => {
// put hooked setter into event loop to avoid of set latency
setTimeout(() => {
if (instrumentation !== stoppedInstrumentation) {
after(target, value)
}
}, 0)
}
const instrumentationWrapper = function (this: TARGET, value: TARGET[PROPERTY]) {
originalDescriptor.set!.call(this, value)
instrumentation(this, value)
}
Object.defineProperty(targetPrototype, property, {
set: instrumentationWrapper,
})
return {
stop: () => {
if (Object.getOwnPropertyDescriptor(targetPrototype, property)?.set === instrumentationWrapper) {
Object.defineProperty(targetPrototype, property, originalDescriptor)
}
instrumentation = stoppedInstrumentation
},
}
}