@mtdt.temp/browser-core
Version:
Datadog browser core utilities.
114 lines • 4.72 kB
JavaScript
import { setTimeout } from './timer';
import { callMonitored } from './monitor';
import { noop } from './utils/functionUtils';
import { createHandlingStack } from './stackTrace/handlingStack';
/**
* 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(targetPrototype, method, onPreCall, { computeHandlingStack } = {}) {
let original = targetPrototype[method];
if (typeof original !== 'function') {
if (method in targetPrototype && method.startsWith('on')) {
original = noop;
}
else {
return { stop: noop };
}
}
let stopped = false;
const instrumentation = function () {
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);
let postCallCallback;
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;
};
targetPrototype[method] = instrumentation;
return {
stop: () => {
stopped = true;
// If the instrumentation has been removed by a third party, keep the last one
if (targetPrototype[method] === instrumentation) {
targetPrototype[method] = original;
}
},
};
}
export function instrumentSetter(targetPrototype, property, after) {
const originalDescriptor = Object.getOwnPropertyDescriptor(targetPrototype, property);
if (!originalDescriptor || !originalDescriptor.set || !originalDescriptor.configurable) {
return { stop: noop };
}
const stoppedInstrumentation = noop;
let instrumentation = (target, value) => {
// put hooked setter into event loop to avoid of set latency
setTimeout(() => {
if (instrumentation !== stoppedInstrumentation) {
after(target, value);
}
}, 0);
};
const instrumentationWrapper = function (value) {
originalDescriptor.set.call(this, value);
instrumentation(this, value);
};
Object.defineProperty(targetPrototype, property, {
set: instrumentationWrapper,
});
return {
stop: () => {
var _a;
if (((_a = Object.getOwnPropertyDescriptor(targetPrototype, property)) === null || _a === void 0 ? void 0 : _a.set) === instrumentationWrapper) {
Object.defineProperty(targetPrototype, property, originalDescriptor);
}
instrumentation = stoppedInstrumentation;
},
};
}
//# sourceMappingURL=instrumentMethod.js.map