Skip to content

✨ Add constructor support to instrumentMethod#4714

Open
bdibon wants to merge 10 commits into
mainfrom
boris.dibon/ws-1-instrument-method-constructor
Open

✨ Add constructor support to instrumentMethod#4714
bdibon wants to merge 10 commits into
mainfrom
boris.dibon/ws-1-instrument-method-constructor

Conversation

@bdibon
Copy link
Copy Markdown
Contributor

@bdibon bdibon commented Jun 3, 2026

Motivation

The WebSocket instrumentation added higher in this stack needs to wrap the WebSocket constructor, but instrumentMethod only supported instrumenting regular methods.

Changes

  • Add support for instrumenting constructors to instrumentMethod: handle new.target, call the original via Reflect.construct, and preserve the original prototype so instanceof checks against the instrumented global keep working.
  • Introduce MethodParameters / MethodReturnType type 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.ts

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.
  • Updated documentation and/or relevant AGENTS.md file

Copy link
Copy Markdown
Contributor Author

bdibon commented Jun 3, 2026

@bdibon bdibon marked this pull request as ready for review June 3, 2026 11:36
@bdibon bdibon requested a review from a team as a code owner June 3, 2026 11:36
@cit-pr-commenter-54b7da
Copy link
Copy Markdown

cit-pr-commenter-54b7da Bot commented Jun 3, 2026

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 171.78 KiB 171.88 KiB +101 B +0.06%
Rum Profiler 7.88 KiB 7.88 KiB 0 B 0.00%
Rum Recorder 21.21 KiB 21.22 KiB +9 B +0.04%
Logs 54.30 KiB 54.39 KiB +89 B +0.16%
Rum Slim 129.68 KiB 129.77 KiB +89 B +0.07%
Worker 22.96 KiB 22.96 KiB 0 B 0.00%

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/core/src/tools/instrumentMethod.ts Outdated
Comment thread packages/browser-core/src/tools/instrumentMethod.ts Outdated
@datadog-official
Copy link
Copy Markdown

datadog-official Bot commented Jun 3, 2026

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 76.74%
Overall Coverage: 76.69% (-0.01%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 0f0b7a0 | Docs | Datadog PR Page | Give us feedback!

@bdibon bdibon force-pushed the boris.dibon/ws-1-instrument-method-constructor branch from c445e3c to e2bdea2 Compare June 4, 2026 09:16
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/browser-core/src/tools/instrumentMethod.ts Outdated
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()`).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥜 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]>)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ question: ‏the other construct call uses a third parameter new.target, should it be used in this call as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +136 to +141
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') {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥜 nitpick: ‏what about moving the prototype logic inside the other function guard below?

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/browser-core/src/tools/instrumentMethod.ts Outdated

// 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)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +196 to +200
return notifyInstrumentation<InstrumentedConstructorCall<TARGET[NAME]>, ConstructorInstanceOf<TARGET[NAME]>>(
onPreCall,
{
parameters,
handlingStack: computeHandlingStack ? createHandlingStack('instrumented method') : undefined,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +181 to +182
// 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥜 nitpick: it is what you mean, right?

Suggested change
// 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,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥜 nitpick: it could be handy for debugging‏

Suggested change
handlingStack: computeHandlingStack ? createHandlingStack('instrumented method') : undefined,
handlingStack: computeHandlingStack ? createHandlingStack('instrumented constructor') : undefined,

}
})

describe('instrumentConstructor', () => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 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]> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion:this is actually not TARGET here but since we don't use it, we could just remove it

Suggested change
const instrumentation = function (this: TARGET): ConstructorInstanceOf<TARGET[NAME]> {
const instrumentation = function (): ConstructorInstanceOf<TARGET[NAME]> {

Comment on lines +303 to +304
// Preserve own static members (class fields, static methods, etc.) on the instrumented function.
const excludedKeys = new Set(['prototype', 'length', 'name', 'arguments', 'caller'])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion: ‏Not sure to get why each of them are excluded, especially name/length, it could be worth adding a comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! It's because we don't want to overwrite the ones of the subclass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants