UNPKG

@mtdt.temp/browser-core

Version:
114 lines 4.72 kB
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