✨ Add constructor support to instrumentMethod#4714
Conversation
Bundles Sizes Evolution
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c445e3c783
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 0f0b7a0 | Docs | Datadog PR Page | Give us feedback! |
Co-authored-by: Cursor <cursoragent@cursor.com>
c445e3c to
e2bdea2
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e2bdea2f62
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| import { createHandlingStack } from './stackTrace/handlingStack' | ||
|
|
||
| /* Resolves parameters from either a call or construct signature, so instrumentMethod can handle | ||
| * both regular methods and constructors (e.g. `new WebSocket()`). |
There was a problem hiding this comment.
🥜 nitpick: What about replacing WebSocket references by something like MyClass?
No need to reference the usages in utility functions.
| const instrumentation = function (this: TARGET): MethodReturnType<TARGET[METHOD]> { | ||
| if (stopped) { | ||
| if (new.target) { | ||
| return Reflect.construct(original, arguments as unknown as MethodParameters<TARGET[METHOD]>) |
There was a problem hiding this comment.
❓ question: the other construct call uses a third parameter new.target, should it be used in this call as well?
There was a problem hiding this comment.
No, this is called when the instrumentation has been stopped, but the consumer might still have a reference to it, then we want to behave as if we're not here.
| * Note: if needed, parameters can be mutated by the instrumentation | ||
| */ | ||
| parameters: Parameters<TARGET[METHOD]> | ||
| parameters: MethodParameters<TARGET[METHOD]> |
There was a problem hiding this comment.
❓ question: the TARGET[METHOD] type or even the target property do not really make sense in a constructor context, no?
Could it be worth considering an instrumentConstructor function instead with some shared helpers?
There was a problem hiding this comment.
They don't in a regular constructor context, but all the constructor we're likely to instrument will be instrumented on the globalObject (window.WebSocket for example).
I will try to make this split and see if it reads better.
| if (typeof original === 'function' && original.prototype) { | ||
| instrumentation.prototype = original.prototype | ||
| } | ||
|
|
||
| // Preserve static members (class fields, static methods, etc.) on the instrumented function. | ||
| if (typeof original === 'function') { |
There was a problem hiding this comment.
🥜 nitpick: what about moving the prototype logic inside the other function guard below?
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b52da5151
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| // Preserve static members (class fields, static methods, etc.) on the instrumented function. | ||
| const excludedKeys = new Set(['prototype', 'length', 'name', 'arguments', 'caller']) | ||
| for (const key of Object.getOwnPropertyNames(original)) { |
There was a problem hiding this comment.
Preserve inherited constructor statics
issue: This only copies own static descriptors, but ES class constructors inherit static APIs through their constructor prototype chain (class Child extends Parent). When such a constructor is instrumented, static methods or constants that previously resolved from Parent become undefined because the wrapper still inherits from Function.prototype; preserve the original constructor's prototype chain (or otherwise delegate inherited statics) in addition to copying own descriptors.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good point, but the usage of instrumentConstructor is the instrumentation of WebSocket and it doesn't have any inherited static members.
Plus the fix would be to use Object.setPrototypeOf which might degrade performance.
I've chosen to document the limitation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f0b7a0fda
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| // Preserve the original prototype so that instanceof checks against the instrumented global work. | ||
| // e.g. `new MyClass() instanceof MyClass` must remain true. | ||
| instrumentation.prototype = original.prototype |
There was a problem hiding this comment.
Preserve the prototype constructor link
issue: When this wraps a global constructor such as WebSocket, the wrapper reuses the original prototype object but leaves that prototype's constructor property pointing at the pre-instrumentation function. While the SDK is active, code that checks new WebSocket(url).constructor === WebSocket now gets false because WebSocket is the wrapper and the instance inherits constructor from the original prototype, which is a backwards-compatibility break for constructor identity checks.
Useful? React with 👍 / 👎.
| return notifyInstrumentation<InstrumentedConstructorCall<TARGET[NAME]>, ConstructorInstanceOf<TARGET[NAME]>>( | ||
| onPreCall, | ||
| { | ||
| parameters, | ||
| handlingStack: computeHandlingStack ? createHandlingStack('instrumented method') : undefined, |
There was a problem hiding this comment.
Avoid instrumenting failed bare constructor calls
issue: If the instrumented constructor is invoked without new (for example WebSocket(url) or a class constructor called as a function), the original would throw before any constructor work happens, but this wrapper calls onPreCall first and only fails later because new.target is undefined. That means the SDK can record or mutate a constructor attempt that never actually reaches the original constructor; preserve the original call semantics for bare calls before notifying instrumentation.
Useful? React with 👍 / 👎.
|
|
||
| // Preserve the original prototype so that instanceof checks against the instrumented global work. | ||
| // e.g. `new MyClass() instanceof MyClass` must remain true. | ||
| instrumentation.prototype = original.prototype |
There was a problem hiding this comment.
Preserve the prototype property's descriptor
issue: Assigning instrumentation.prototype copies only the value, leaving the wrapper's own prototype descriptor writable even when the original constructor's descriptor is not (as with ES class constructors and many DOM-style constructors). While the SDK is active, code can replace WebSocket.prototype/MyClass.prototype successfully where the original constructor would reject or ignore the assignment, which changes observable constructor semantics; define the original prototype descriptor instead of assigning just the value.
Useful? React with 👍 / 👎.
| // body. If a subclass extends the wrapper, or a third party wraps us and their class is | ||
| // `new`ed, `new.target` is that outer constructor and must be preserved. |
There was a problem hiding this comment.
🥜 nitpick: it is what you mean, right?
| // body. If a subclass extends the wrapper, or a third party wraps us and their class is | |
| // `new`ed, `new.target` is that outer constructor and must be preserved. | |
| // body. If a subclass extends the wrapper, or a third party wraps us and their class is | |
| // instantiated, `new.target` is that outer constructor and must be preserved. |
| onPreCall, | ||
| { | ||
| parameters, | ||
| handlingStack: computeHandlingStack ? createHandlingStack('instrumented method') : undefined, |
There was a problem hiding this comment.
🥜 nitpick: it could be handy for debugging
| handlingStack: computeHandlingStack ? createHandlingStack('instrumented method') : undefined, | |
| handlingStack: computeHandlingStack ? createHandlingStack('instrumented constructor') : undefined, |
| } | ||
| }) | ||
|
|
||
| describe('instrumentConstructor', () => { |
There was a problem hiding this comment.
💬 suggestion: comparing with instrumentMethod tests, it could be nice to have some equivalents on:
- computeHandlingStack
- third party compatibility
| } | ||
|
|
||
| return replaceWithInstrumentation(target, name, original, (isStopped) => { | ||
| const instrumentation = function (this: TARGET): ConstructorInstanceOf<TARGET[NAME]> { |
There was a problem hiding this comment.
💬 suggestion: this is actually not TARGET here but since we don't use it, we could just remove it
| const instrumentation = function (this: TARGET): ConstructorInstanceOf<TARGET[NAME]> { | |
| const instrumentation = function (): ConstructorInstanceOf<TARGET[NAME]> { |
| // Preserve own static members (class fields, static methods, etc.) on the instrumented function. | ||
| const excludedKeys = new Set(['prototype', 'length', 'name', 'arguments', 'caller']) |
There was a problem hiding this comment.
💬 suggestion: Not sure to get why each of them are excluded, especially name/length, it could be worth adding a comment.
There was a problem hiding this comment.
Sure! It's because we don't want to overwrite the ones of the subclass.

Motivation
The WebSocket instrumentation added higher in this stack needs to wrap the
WebSocketconstructor, butinstrumentMethodonly supported instrumenting regular methods.Changes
instrumentMethod: handlenew.target, call the original viaReflect.construct, and preserve the originalprototypesoinstanceofchecks against the instrumented global keep working.MethodParameters/MethodReturnTypetype helpers that resolve the parameters/return type from either a call or a construct signature.Test instructions
yarn test:unit --spec packages/core/src/tools/instrumentMethod.spec.tsChecklist