nestjs-otel 8.0.2 → 8.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,9 +44,7 @@ function Span(nameOrOptions, maybeOptions) {
44
44
  return tracer.startActiveSpan(name, otelOptions, (span) => {
45
45
  try {
46
46
  const result = originalFunction.apply(this, args);
47
- if (result &&
48
- typeof result.then === "function" &&
49
- typeof result.catch === "function") {
47
+ if (result instanceof Promise) {
50
48
  return result
51
49
  .then((res) => {
52
50
  handleOnResult(span, onResult, res);
@@ -40,6 +40,20 @@ class TestSpan {
40
40
  errorInOnResult() {
41
41
  return "success";
42
42
  }
43
+ async asyncError() {
44
+ throw new Error("async hello world");
45
+ }
46
+ lastLazyThenable;
47
+ returnsLazyThenable() {
48
+ this.lastLazyThenable = makeLazyThenable("lazy result");
49
+ return this.lastLazyThenable;
50
+ }
51
+ returnsFailingLazyThenable() {
52
+ return makeFailingLazyThenable(new Error("lazy rejection"));
53
+ }
54
+ lazyThenableOnResult() {
55
+ return makeLazyThenable({ rows: [1, 2, 3] });
56
+ }
43
57
  }
44
58
  __decorate([
45
59
  (0, span_1.Span)(),
@@ -122,6 +136,72 @@ __decorate([
122
136
  __metadata("design:paramtypes", []),
123
137
  __metadata("design:returntype", void 0)
124
138
  ], TestSpan.prototype, "errorInOnResult", null);
139
+ __decorate([
140
+ (0, span_1.Span)(),
141
+ __metadata("design:type", Function),
142
+ __metadata("design:paramtypes", []),
143
+ __metadata("design:returntype", Promise)
144
+ ], TestSpan.prototype, "asyncError", null);
145
+ __decorate([
146
+ (0, span_1.Span)(),
147
+ __metadata("design:type", Function),
148
+ __metadata("design:paramtypes", []),
149
+ __metadata("design:returntype", void 0)
150
+ ], TestSpan.prototype, "returnsLazyThenable", null);
151
+ __decorate([
152
+ (0, span_1.Span)(),
153
+ __metadata("design:type", Function),
154
+ __metadata("design:paramtypes", []),
155
+ __metadata("design:returntype", void 0)
156
+ ], TestSpan.prototype, "returnsFailingLazyThenable", null);
157
+ __decorate([
158
+ (0, span_1.Span)({
159
+ onResult: (_result) => ({ attributes: { count: _result.rows.length } }),
160
+ }),
161
+ __metadata("design:type", Function),
162
+ __metadata("design:paramtypes", []),
163
+ __metadata("design:returntype", void 0)
164
+ ], TestSpan.prototype, "lazyThenableOnResult", null);
165
+ function makeLazyThenable(value) {
166
+ const thenable = {
167
+ triggered: false,
168
+ // biome-ignore lint/suspicious/noThenProperty: <We are testing the behavior of the thenable>
169
+ then(onFulfilled, onRejected) {
170
+ thenable.triggered = true;
171
+ try {
172
+ const result = onFulfilled ? onFulfilled(value) : value;
173
+ return Promise.resolve(result);
174
+ }
175
+ catch (error) {
176
+ if (onRejected) {
177
+ return Promise.resolve(onRejected(error));
178
+ }
179
+ return Promise.reject(error);
180
+ }
181
+ },
182
+ catch(onRejected) {
183
+ return thenable.then(undefined, onRejected);
184
+ },
185
+ };
186
+ return thenable;
187
+ }
188
+ function makeFailingLazyThenable(error) {
189
+ const thenable = {
190
+ triggered: false,
191
+ // biome-ignore lint/suspicious/noThenProperty: <We are testing the behavior of the thenable>
192
+ then(_onFulfilled, onRejected) {
193
+ thenable.triggered = true;
194
+ if (onRejected) {
195
+ return Promise.resolve(onRejected(error));
196
+ }
197
+ return Promise.reject(error);
198
+ },
199
+ catch(onRejected) {
200
+ return thenable.then(undefined, onRejected);
201
+ },
202
+ };
203
+ return thenable;
204
+ }
125
205
  describe("Span", () => {
126
206
  let instance;
127
207
  let traceExporter;
@@ -249,4 +329,71 @@ describe("Span", () => {
249
329
  expect(spans[0].events).toHaveLength(1);
250
330
  expect(spans[0].events[0].name).toBe("exception");
251
331
  });
332
+ it("should call onResult with the thenable (not resolved value) for lazy thenables", () => {
333
+ // The decorator hands the lazy thenable to onResult untouched (it cannot
334
+ // know it is a deferred query). Accessing fields on the resolved shape
335
+ // (e.g. `.rows`) therefore throws synchronously, and the decorator records
336
+ // that as an exception on the span.
337
+ const returned = instance.lazyThenableOnResult();
338
+ // Caller still receives the untouched thenable.
339
+ expect(returned.triggered).toBe(false);
340
+ const spans = traceExporter.getFinishedSpans();
341
+ expect(spans).toHaveLength(1);
342
+ expect(spans[0].status.code).toBe(api_1.SpanStatusCode.ERROR);
343
+ expect(spans[0].status.message).toMatch("Cannot read properties of undefined (reading 'length')");
344
+ expect(spans[0].events).toHaveLength(1);
345
+ expect(spans[0].events[0].name).toBe("exception");
346
+ expect(spans[0].events[0].attributes?.["exception.type"]).toBe("TypeError");
347
+ });
348
+ it("should not trigger a lazy thenable returned by the wrapped method", () => {
349
+ const returned = instance.returnsLazyThenable();
350
+ // The decorator must hand the lazy thenable back to the caller untouched.
351
+ // It must NOT subscribe via .then(), which would force-execute query
352
+ // builders such as Knex, Mongoose Query, or Drizzle queries that the
353
+ // caller intended to defer (or compose further) before awaiting.
354
+ expect(instance.lastLazyThenable?.triggered).toBe(false);
355
+ expect(returned).toBe(instance.lastLazyThenable);
356
+ });
357
+ it("should end the span synchronously before the caller awaits a lazy thenable", () => {
358
+ instance.returnsLazyThenable();
359
+ // The span closes as soon as the decorated method returns, before the
360
+ // caller subscribes to the thenable, because the decorator cannot know
361
+ // whether the caller will compose it further or await it at all.
362
+ const spans = traceExporter.getFinishedSpans();
363
+ expect(spans).toHaveLength(1);
364
+ expect(spans[0].name).toBe("TestSpan.returnsLazyThenable");
365
+ });
366
+ it("should not record errors from a lazy thenable that rejects after the span has ended", async () => {
367
+ const returned = instance.returnsFailingLazyThenable();
368
+ // Span is already closed — no error recorded yet.
369
+ const spans = traceExporter.getFinishedSpans();
370
+ expect(spans).toHaveLength(1);
371
+ expect(spans[0].status.code).toBe(api_1.SpanStatusCode.UNSET);
372
+ expect(spans[0].events).toHaveLength(0);
373
+ // Caller awaits the thenable now — it rejects — but the span is gone.
374
+ await expect(Promise.resolve(returned)).rejects.toThrow("lazy rejection");
375
+ // Known limitation: the rejection is invisible to the span.
376
+ expect(traceExporter.getFinishedSpans()[0].events).toHaveLength(0);
377
+ });
378
+ it("should still track results from async (real Promise) methods", async () => {
379
+ const result = await instance.asyncMethod();
380
+ expect(result).toBe("async success");
381
+ const spans = traceExporter.getFinishedSpans();
382
+ expect(spans).toHaveLength(1);
383
+ expect(spans[0].name).toBe("TestSpan.asyncMethod");
384
+ expect(spans[0].status.code).toBe(api_1.SpanStatusCode.UNSET);
385
+ expect(spans[0].attributes).toEqual({ result: "async success" });
386
+ });
387
+ it("should still track errors thrown by async (real Promise) methods", async () => {
388
+ await expect(instance.asyncError()).rejects.toThrow("async hello world");
389
+ const spans = traceExporter.getFinishedSpans();
390
+ expect(spans).toHaveLength(1);
391
+ expect(spans[0].name).toBe("TestSpan.asyncError");
392
+ expect(spans[0].status).toEqual({
393
+ code: api_1.SpanStatusCode.ERROR,
394
+ message: "async hello world",
395
+ });
396
+ expect(spans[0].events).toHaveLength(1);
397
+ expect(spans[0].events[0].name).toBe("exception");
398
+ });
252
399
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nestjs-otel",
3
- "version": "8.0.2",
3
+ "version": "8.0.3",
4
4
  "description": "NestJS OpenTelemetry Library",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index",