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
|
});
|