Skip to content

fix: keep an instance without a constructor working - #10

Open
johnnyhuirilef wants to merge 1 commit into
nestjs:masterfrom
johnnyhuirilef:fix/provider-without-constructor
Open

johnnyhuirilef wants to merge 1 commit into
nestjs:masterfrom
johnnyhuirilef:fix/provider-without-constructor

Conversation

@johnnyhuirilef

Copy link
Copy Markdown
Contributor

PR Checklist

PR Type

[x] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Other... Please describe:

What is the current behavior?

Issue Number: N/A (issues are disabled on this repo)

Hi! 馃憢 With instrumentation on, a method call on a provider that has no constructor function throws TypeError: Cannot read properties of undefined (reading 'name'). In a route, this is a 500.

The span class name comes from constructor.name. The code reads it in two places in src/instrument/create-instance-decorator.instrument.ts: the get trap (line 271) and generateSpanId (line 570). A null-prototype object has no constructor. An ES module namespace has none either, so { provide: 'UTILS', useValue: utils } with import * as utils from './utils.js' triggers it. The call fails in a trace and outside a trace. The same provider works without instrumentation.

I ran the repro on 0.3.7:

--- control: no instrument option ---
HTTP status:   200
HTTP body:     Hello, Ana!
logged errors: 0
first error:   none

--- with instrument: ObserveInstrument ---
HTTP status:   500
HTTP body:     {"statusCode":500,"message":"Internal server error"}
logged errors: 1
first error:   Cannot read properties of undefined (reading 'name')

RESULT: BUG

Until a release has the fix, skipInstrumentation: (instance) => instance === greetings keeps the provider working. The repro README runs it.

What is the new behavior?

  • A small helper, classNameOf, gives the class name of an instance. Both places use it.
  • When constructor is a function with a name, the span keeps that name, as before.
  • When constructor is missing or is not a function, the span class name is Object.
  • A constructor that is not a function used to name the span undefined.fn for { constructor: 'x' }, or Fake.fn for { constructor: { name: 'Fake' } }. Both now read Object.fn.

Does this PR introduce a breaking change?

[ ] Yes
[x] No

Providers with a named constructor keep their span names. Only the shapes that threw or had no usable constructor function change. A constructor that is not a function now gives Object, so { constructor: { name: 'Fake' } } reads Object.fn instead of Fake.fn.

Other information

The new unit specs call the decorator on these instances, with the same helpers as the other specs:

  • A null-prototype object, in a trace and outside a trace.
  • A real ES module namespace. It comes from a data: module, because a file in the repo goes through the Vitest transform and is no longer a native namespace.
  • A constructor that is not a function, with a name property. This case isolates the typeof check.

They check the returned value, the started step and the ended span id, so both places are covered.

The new int spec boots a real Nest app with instrument: ObserveInstrument. It registers a namespace as a useValue provider and checks that GET /greet returns 200.

All the new specs fail on master and pass with the fix. I also reverted each changed line one at a time, and a spec failed every time.

Separate from this fix: a frozen object with methods also fails under instrumentation, plain or null-prototype. The get trap returns a wrapper for a read-only, non-configurable property, and the proxy invariant check rejects it. I ran it and left it out of this PR. I am happy to open a follow-up for it. 馃檪

An instance with no `constructor` function threw a TypeError on its
first method access once instrumentation was on. A null-prototype
object and an ES module namespace are two such instances.

The class name for the span came from `constructor.name` in two places:
the `get` trap and `generateSpanId`. Both now read it through one
helper. The helper falls back to `Object` when `constructor` is not a
function.
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.

1 participant