{"version":3,"file":"instrumentMethod.mjs","names":[],"sources":["../../src/tools/instrumentMethod.ts"],"sourcesContent":["import { setTimeout } from './timer'\nimport { callMonitored } from './monitor'\nimport { noop } from './utils/functionUtils'\nimport { createHandlingStack } from './stackTrace/handlingStack'\n\n/**\n * Object passed to the callback of an instrumented method call. See `instrumentMethod` for more\n * info.\n */\nexport interface InstrumentedMethodCall<TARGET extends { [key: string]: any }, METHOD extends keyof TARGET> {\n  /**\n   * The target object on which the method was called.\n   */\n  target: TARGET\n\n  /**\n   * The parameters with which the method was called.\n   *\n   * Note: if needed, parameters can be mutated by the instrumentation\n   */\n  parameters: Parameters<TARGET[METHOD]>\n\n  /**\n   * Registers a callback that will be called after the original method is called, with the method\n   * result passed as argument.\n   */\n  onPostCall: (callback: PostCallCallback<TARGET, METHOD>) => void\n\n  /**\n   * The stack trace of the method call.\n   */\n  handlingStack?: string\n}\n\ntype PostCallCallback<TARGET extends { [key: string]: any }, METHOD extends keyof TARGET> = (\n  result: ReturnType<TARGET[METHOD]>\n) => void\n\ntype ConstructorParametersOf<CONSTRUCTOR> = CONSTRUCTOR extends new (...args: infer P) => any ? P : never\ntype ConstructorInstanceOf<CONSTRUCTOR> = CONSTRUCTOR extends new (...args: any[]) => infer R ? R : never\n\ntype AnyConstructor = abstract new (...args: any[]) => any\n\n/**\n * Object passed to the callback of an instrumented constructor call. See `instrumentConstructor`\n * for more info.\n */\nexport interface InstrumentedConstructorCall<CONSTRUCTOR> {\n  /**\n   * The parameters with which the constructor was called.\n   *\n   * Note: if needed, parameters can be mutated by the instrumentation\n   */\n  parameters: ConstructorParametersOf<CONSTRUCTOR>\n\n  /**\n   * Registers a callback that will be called after the original constructor is called, with the\n   * constructed instance passed as argument.\n   */\n  onPostCall: (callback: (result: ConstructorInstanceOf<CONSTRUCTOR>) => void) => void\n\n  /**\n   * The stack trace of the constructor call.\n   */\n  handlingStack?: string\n}\n\n/**\n * Instruments a method on a object, calling the given callback before the original method is\n * invoked. The callback receives an object with information about the method call.\n *\n * This function makes sure that we are \"good citizens\" regarding third party instrumentations: when\n * removing the instrumentation, the original method is usually restored, but if a third party\n * instrumentation was set after ours, we keep it in place and just replace our instrumentation with\n * a noop.\n *\n * Note: it is generally better to instrument methods that are \"owned\" by the object instead of ones\n * that are inherited from the prototype chain. Example:\n * * do:    `instrumentMethod(Array.prototype, 'push', ...)`\n * * don't: `instrumentMethod([], 'push', ...)`\n *\n * This method is also used to set event handler properties (ex: window.onerror = ...), as it has\n * the same requirements as instrumenting a method:\n * * if the event handler is already set by a third party, we need to call it and not just blindly\n * override it.\n * * if the event handler is set by a third party after us, we need to keep it in place when\n * removing ours.\n *\n * To instrument a constructor @see {@link instrumentConstructor}.\n *\n * @example\n *\n *  instrumentMethod(window, 'fetch', ({ target, parameters, onPostCall }) => {\n *    console.log('Before calling fetch on', target, 'with parameters', parameters)\n *\n *    onPostCall((result) => {\n *      console.log('After fetch calling on', target, 'with parameters', parameters, 'and result', result)\n *    })\n *  })\n */\nexport function instrumentMethod<TARGET extends { [key: string]: any }, METHOD extends keyof TARGET>(\n  targetPrototype: TARGET,\n  method: METHOD,\n  onPreCall: (this: null, callInfos: InstrumentedMethodCall<TARGET, METHOD>) => void,\n  { computeHandlingStack }: { computeHandlingStack?: boolean } = {}\n) {\n  let original = targetPrototype[method]\n\n  if (typeof original !== 'function') {\n    if (method in targetPrototype && typeof method === 'string' && method.startsWith('on')) {\n      original = noop as TARGET[METHOD]\n    } else {\n      return { stop: noop }\n    }\n  }\n\n  let stopped = false\n\n  const instrumentation = function (this: TARGET): ReturnType<TARGET[METHOD]> {\n    if (stopped) {\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call\n      return original.apply(this, arguments)\n    }\n\n    const parameters = Array.from(arguments) as Parameters<TARGET[METHOD]>\n\n    return notifyInstrumentation<InstrumentedMethodCall<TARGET, METHOD>, ReturnType<TARGET[METHOD]>>(\n      onPreCall,\n      {\n        target: this,\n        parameters,\n        handlingStack: computeHandlingStack ? createHandlingStack('instrumented method') : undefined,\n      },\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call -- TARGET[METHOD] is any under the index signature; value is verified as a function above\n      () => original.apply(this, parameters)\n    )\n  } as TARGET[METHOD]\n\n  const { stop: restoreOriginal } = replaceWithInstrumentation(targetPrototype, method, original, instrumentation)\n\n  return {\n    stop: () => {\n      stopped = true\n      restoreOriginal()\n    },\n  }\n}\n\n/**\n * Instruments a constructor on an object (typically a global, e.g. `window.WebSocket`), calling the\n * given callback before the original constructor is invoked. The callback receives an object with\n * information about the constructor call, and can register an `onPostCall` callback to be notified\n * with the constructed instance.\n *\n * Like `instrumentMethod`, this is a \"good citizen\" regarding third party instrumentations: stopping\n * restores the original constructor unless a third party replaced it afterwards.\n *\n * The wrapper preserves the original prototype (so `instanceof` keeps working), the original\n * `new.target` (so constructors that inspect it behave as if not instrumented), the original\n * static members (e.g. `WebSocket.OPEN`), and `instance.constructor ===` so checks like\n * `new WebSocket(url).constructor === WebSocket` stay true.\n *\n * @see {@link preserveConstructorShape} for limitations on static members preservation.\n * @example\n *\n *  instrumentConstructor(window, 'WebSocket', ({ parameters, onPostCall }) => {\n *    console.log('Before constructing WebSocket with parameters', parameters)\n *\n *    onPostCall((instance) => {\n *      console.log('Constructed WebSocket instance', instance)\n *    })\n *  })\n */\nexport function instrumentConstructor<CONTAINER extends { [key: string]: any }, CONSTRUCTOR extends keyof CONTAINER>(\n  container: CONTAINER,\n  constructor: CONSTRUCTOR,\n  onPreCall: (this: null, callInfos: InstrumentedConstructorCall<CONTAINER[CONSTRUCTOR]>) => void,\n  { computeHandlingStack }: { computeHandlingStack?: boolean } = {}\n) {\n  const original = container[constructor]\n\n  if (typeof original !== 'function') {\n    return { stop: noop }\n  }\n\n  let stopped = false\n\n  const instrumentation = function (this: unknown): ConstructorInstanceOf<CONTAINER[CONSTRUCTOR]> {\n    // Bare `[[Call]]` (no `new`): delegate through `[[Call]]` and skip `onPreCall`. Otherwise we\n    // would notify instrumentation before the original rejects or returns, unlike the native\n    // constructor (e.g. `WebSocket(url)` or `class {}()` without `new`).\n    if (!new.target) {\n      return Reflect.apply(original, this, Array.from(arguments)) as ConstructorInstanceOf<CONTAINER[CONSTRUCTOR]>\n    }\n\n    // When `new` is used on this instrumented property, `new.target` is this wrapper. Passing\n    // it through to Reflect.construct would expose the wrong new.target inside the original\n    // body. If a subclass extends the wrapper, or a third party wraps us and their class is\n    // instantiated, `new.target` is that outer constructor and must be preserved.\n    const newTarget = new.target === instrumentation ? original : new.target\n\n    if (stopped) {\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n      return Reflect.construct(\n        original,\n        arguments as unknown as ConstructorParametersOf<CONTAINER[CONSTRUCTOR]>,\n        newTarget\n      )\n    }\n\n    const parameters = Array.from(arguments) as ConstructorParametersOf<CONTAINER[CONSTRUCTOR]>\n\n    return notifyInstrumentation<\n      InstrumentedConstructorCall<CONTAINER[CONSTRUCTOR]>,\n      ConstructorInstanceOf<CONTAINER[CONSTRUCTOR]>\n    >(\n      onPreCall,\n      {\n        parameters,\n        handlingStack: computeHandlingStack ? createHandlingStack('instrumented constructor') : undefined,\n      },\n      () =>\n        // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n        Reflect.construct(original, parameters, newTarget)\n    )\n  } as CONTAINER[CONSTRUCTOR]\n\n  const restorePrototypeConstructor = preserveConstructorShape(instrumentation, original)\n  const { stop: restoreOriginal } = replaceWithInstrumentation(container, constructor, original, instrumentation)\n\n  return {\n    stop: () => {\n      stopped = true\n      restorePrototypeConstructor()\n      restoreOriginal()\n    },\n  }\n}\n\n/**\n * Replaces `targetPrototype[method]` with the provided instrumentation and\n * returns a `stop` function restoring the original (unless a third party replaced us afterwards).\n */\nfunction replaceWithInstrumentation<TARGET extends { [key: string]: any }, METHOD extends keyof TARGET>(\n  targetPrototype: TARGET,\n  method: METHOD,\n  original: TARGET[METHOD],\n  instrumentation: TARGET[METHOD]\n) {\n  targetPrototype[method] = instrumentation\n\n  return {\n    stop: () => {\n      // If the instrumentation has been removed by a third party, keep the last one\n      if (targetPrototype[method] === instrumentation) {\n        targetPrototype[method] = original\n      }\n    },\n  }\n}\n\n/**\n * Runs the pre-call callback (injecting the `onPostCall` registration), invokes the original\n * method/constructor, then runs the registered post-call callback with the result.\n *\n * Note: the handling stack must be computed by the caller (at the topmost position of the call\n * stack), so it is part of `baseCallInfo` rather than computed here.\n */\nfunction notifyInstrumentation<CALL_INFO extends InstrumentationCallbacks<RESULT>, RESULT>(\n  onPreCall: (this: null, callInfo: CALL_INFO) => void,\n  baseCallInfo: Omit<CALL_INFO, 'onPostCall'>,\n  invokeOriginal: () => RESULT\n): RESULT {\n  let postCallCallback: ((result: RESULT) => void) | undefined\n\n  callMonitored(onPreCall, null, [\n    {\n      ...baseCallInfo,\n      onPostCall: (callback: (result: RESULT) => void) => {\n        postCallCallback = callback\n      },\n    } as CALL_INFO,\n  ])\n\n  const result = invokeOriginal()\n\n  if (postCallCallback) {\n    callMonitored(postCallCallback, null, [result])\n  }\n\n  return result\n}\n\ninterface InstrumentationCallbacks<RESULT> {\n  onPostCall: (callback: (result: RESULT) => void) => void\n  handlingStack?: string\n}\n\n/**\n * Copies the original constructor prototype and other static members onto the instrumentation, so that\n * `instanceof` checks keep working and statics such as `WebSocket.OPEN` remain available while\n * instrumented.\n *\n * Returns a function that restores `prototype.constructor` to its prior descriptor; call it\n * before restoring the global property in `stop()`.\n */\nfunction preserveConstructorShape(instrumentation: AnyConstructor, original: AnyConstructor): () => void {\n  /**\n   * Limitation: Only the original's *own* static members are copied. Statics inherited through the\n   * constructor's prototype chain (e.g. `class Child extends Parent` where `Parent` defines statics)\n   * are not preserved, since the instrumentation still inherits from `Function.prototype`. This is\n   * acceptable for the globals we instrument (e.g. `WebSocket`, whose own statics are the only ones\n   * that matter), but we would need to delegate the static prototype chain to support subclassed\n   * constructors with meaningful inherited statics.\n   */\n  for (const key of ([] as PropertyKey[]).concat(\n    Object.getOwnPropertyNames(original),\n    Object.getOwnPropertySymbols(original)\n  )) {\n    const descriptor = Object.getOwnPropertyDescriptor(original, key) as PropertyDescriptor\n    Object.defineProperty(instrumentation, key, descriptor)\n  }\n\n  /*\n   * Limitation: Repointing `constructor` on this shared prototype affects every instance whose\n   * `[[Prototype]]` is this object, including instances created before instrumentation, because\n   * `constructor` is inherited rather than snapshotted per instance. Code that held `original` (or\n   * whatever was in `prototype.constructor` before) as the canonical constructor and expects reference\n   * equality to that value for the lifetime of the page can observe a behavior change while RUM is\n   * active; restoring on `stop()` fixes this. In practice the SDK initializes as early as possible,\n   * which narrows the window where pre-instrument instances exist.\n   */\n  const { stop: restoreOriginalConstructor } = replaceWithInstrumentation(\n    original.prototype,\n    'constructor',\n    original.prototype.constructor,\n    instrumentation\n  )\n\n  return restoreOriginalConstructor\n}\n\nexport function instrumentSetter<TARGET extends { [key: string]: any }, PROPERTY extends keyof TARGET>(\n  targetPrototype: TARGET,\n  property: PROPERTY,\n  after: (target: TARGET, value: TARGET[PROPERTY]) => void\n) {\n  const originalDescriptor = Object.getOwnPropertyDescriptor(targetPrototype, property)\n  if (!originalDescriptor?.set || !originalDescriptor.configurable) {\n    return { stop: noop }\n  }\n\n  const stoppedInstrumentation = noop\n  let instrumentation = (target: TARGET, value: TARGET[PROPERTY]) => {\n    // put hooked setter into event loop to avoid of set latency\n    setTimeout(() => {\n      if (instrumentation !== stoppedInstrumentation) {\n        after(target, value)\n      }\n    }, 0)\n  }\n\n  const instrumentationWrapper = function (this: TARGET, value: TARGET[PROPERTY]) {\n    originalDescriptor.set!.call(this, value)\n    instrumentation(this, value)\n  }\n\n  Object.defineProperty(targetPrototype, property, {\n    set: instrumentationWrapper,\n  })\n\n  return {\n    stop: () => {\n      if (Object.getOwnPropertyDescriptor(targetPrototype, property)?.set === instrumentationWrapper) {\n        Object.defineProperty(targetPrototype, property, originalDescriptor)\n      }\n      instrumentation = stoppedInstrumentation\n    },\n  }\n}\n"],"mappings":"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"}