autotel-mongoose 10.0.0 → 10.1.0

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.
package/dist/index.cjs CHANGED
@@ -106,6 +106,7 @@ const INSTRUMENTED_FLAG = "__autotelMongooseInstrumented";
106
106
  const WRAPPED_HOOK_FLAG = "__autotelWrappedHook";
107
107
  const WRAPPED_METHOD_FLAG = "__autotelWrappedMethod";
108
108
  const MODEL_PATCHED_FLAG = "__autotelModelPatched";
109
+ const PROXIED_COLLECTION_FLAG = "__autotelProxiedCollection";
109
110
  /**
110
111
  * Per-Mongoose-instance registry of the resolved tracer + config.
111
112
  *
@@ -161,6 +162,98 @@ function createSpan(tracer, operation, modelName, collectionName, config) {
161
162
  });
162
163
  }
163
164
  /**
165
+ * Returns an idempotent finalizer for a span. Every wrapper invocation may try
166
+ * to close its span from more than one place — a callback, an `exec()`
167
+ * continuation, a promise settlement, a synchronous return, or a thrown error —
168
+ * and the span must be ended exactly once. The first call wins; later calls are
169
+ * no-ops. A non-Error rejection is normalized to an `Error`.
170
+ */
171
+ function createSpanFinalizer(span) {
172
+ let done = false;
173
+ return (error) => {
174
+ if (done) return;
175
+ done = true;
176
+ (0, autotel_trace_helpers.finalizeSpan)(span, error === void 0 || error === null ? void 0 : error instanceof Error ? error : new Error(String(error)));
177
+ };
178
+ }
179
+ /**
180
+ * Closes `finalize` over whatever async shape an operation returns:
181
+ *
182
+ * - a Mongoose Query/Aggregate (`exec()`) → wrap `exec()` so the span spans the
183
+ * real DB round-trip, and return the Query unchanged for further chaining;
184
+ * - a Promise → finalize when it settles, and return the chained promise;
185
+ * - a synchronous value → finalize now, and return it.
186
+ *
187
+ * This is the single settlement ladder shared by every wrapper, so the rule
188
+ * "the span ends when the work ends" lives in exactly one place.
189
+ */
190
+ function settleSpan(result, finalize) {
191
+ if (result && typeof result.exec === "function") {
192
+ const originalExec = result.exec.bind(result);
193
+ result.exec = function wrappedExec() {
194
+ try {
195
+ return Promise.resolve(originalExec()).then((value) => {
196
+ finalize();
197
+ return value;
198
+ }, (error) => {
199
+ finalize(error);
200
+ throw error;
201
+ });
202
+ } catch (error) {
203
+ finalize(error);
204
+ throw error;
205
+ }
206
+ };
207
+ return result;
208
+ }
209
+ if (result && typeof result.then === "function") return Promise.resolve(result).then((value) => {
210
+ finalize();
211
+ return value;
212
+ }, (error) => {
213
+ finalize(error);
214
+ throw error;
215
+ });
216
+ finalize();
217
+ return result;
218
+ }
219
+ /**
220
+ * Node-convention callback support for custom methods: if the last argument is
221
+ * a function, replace it with one that (a) runs the original callback inside the
222
+ * span's context — so any DB calls the callback makes nest under this span — and
223
+ * (b) finalizes the span when the callback fires, treating a truthy first
224
+ * argument as the error. Older Mongoose code returns synchronously and does its
225
+ * real work in such a callback (e.g. `doc.checkValidationErrors(cb)`), so the
226
+ * span must outlive the synchronous return.
227
+ *
228
+ * Returns the args to call with and whether finalization was handed to a
229
+ * callback. If there is no trailing callback, the args pass through unchanged.
230
+ *
231
+ * NOTE: This is the Node trailing-callback convention. Mongoose *hooks* use a
232
+ * different convention — kareem's positional `next` — handled separately in
233
+ * wrapHookHandler. The two are intentionally not merged: a single "find the
234
+ * callback" rule across both would hide two genuinely different calling shapes.
235
+ */
236
+ function deferFinalizeToCallback(args, span, finalize) {
237
+ const lastIndex = args.length - 1;
238
+ const maybeCallback = lastIndex >= 0 ? args[lastIndex] : void 0;
239
+ if (typeof maybeCallback !== "function") return {
240
+ callArgs: args,
241
+ deferred: false
242
+ };
243
+ const callArgs = [...args];
244
+ callArgs[lastIndex] = function tracedCallback(...callbackArgs) {
245
+ try {
246
+ return (0, autotel_trace_helpers.runWithSpan)(span, () => maybeCallback.apply(this, callbackArgs));
247
+ } finally {
248
+ finalize(callbackArgs[0]);
249
+ }
250
+ };
251
+ return {
252
+ callArgs,
253
+ deferred: true
254
+ };
255
+ }
256
+ /**
164
257
  * Wraps Model methods that return Query objects (find, findOne, findById,
165
258
  * findOneAndUpdate, findOneAndDelete, findOneAndReplace, deleteOne, deleteMany,
166
259
  * updateOne, updateMany, countDocuments, estimatedDocumentCount).
@@ -174,41 +267,22 @@ function wrapQueryReturningMethod(target, methodName, operation, getCollectionNa
174
267
  target[methodName] = function instrumented(...args) {
175
268
  const collectionName = getCollectionName(this);
176
269
  const span = createSpan(tracer, operation, getModelName(this), collectionName, config);
270
+ const finalize = createSpanFinalizer(span);
177
271
  return (0, autotel_trace_helpers.runWithSpan)(span, () => {
178
272
  try {
179
273
  const result = original.apply(this, args);
180
- if (result && typeof result.exec === "function") {
181
- try {
182
- const payload = {};
183
- if (typeof result.getFilter === "function") payload.condition = result.getFilter();
184
- if (result._update !== void 0) payload.updates = result._update;
185
- if (typeof result.getOptions === "function") payload.options = result.getOptions();
186
- if (result._fields !== void 0) payload.fields = result._fields;
187
- const statementText = captureStatement(operation, payload);
188
- if (statementText) span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
189
- } catch {}
190
- const originalExec = result.exec.bind(result);
191
- result.exec = function wrappedExec() {
192
- try {
193
- const execPromise = originalExec();
194
- return Promise.resolve(execPromise).then((value) => {
195
- (0, autotel_trace_helpers.finalizeSpan)(span);
196
- return value;
197
- }).catch((error) => {
198
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
199
- throw error;
200
- });
201
- } catch (error) {
202
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
203
- throw error;
204
- }
205
- };
206
- return result;
207
- }
208
- (0, autotel_trace_helpers.finalizeSpan)(span);
209
- return result;
274
+ if (result && typeof result.exec === "function") try {
275
+ const payload = {};
276
+ if (typeof result.getFilter === "function") payload.condition = result.getFilter();
277
+ if (result._update !== void 0) payload.updates = result._update;
278
+ if (typeof result.getOptions === "function") payload.options = result.getOptions();
279
+ if (result._fields !== void 0) payload.fields = result._fields;
280
+ const statementText = captureStatement(operation, payload);
281
+ if (statementText) span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
282
+ } catch {}
283
+ return settleSpan(result, finalize);
210
284
  } catch (error) {
211
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
285
+ finalize(error);
212
286
  throw error;
213
287
  }
214
288
  });
@@ -250,39 +324,12 @@ function wrapStaticMethod(target, methodName, operation, getCollectionName, getM
250
324
  const statementText = captureStatement(operation, payload);
251
325
  if (statementText) span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
252
326
  } catch {}
327
+ const finalize = createSpanFinalizer(span);
253
328
  return (0, autotel_trace_helpers.runWithSpan)(span, () => {
254
329
  try {
255
- const result = original.apply(this, args);
256
- if (result && typeof result.exec === "function") {
257
- const originalExec = result.exec.bind(result);
258
- result.exec = function wrappedExec() {
259
- try {
260
- const execPromise = originalExec();
261
- return Promise.resolve(execPromise).then((value) => {
262
- (0, autotel_trace_helpers.finalizeSpan)(span);
263
- return value;
264
- }).catch((error) => {
265
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
266
- throw error;
267
- });
268
- } catch (error) {
269
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
270
- throw error;
271
- }
272
- };
273
- return result;
274
- }
275
- if (result && typeof result.then === "function") return Promise.resolve(result).then((value) => {
276
- (0, autotel_trace_helpers.finalizeSpan)(span);
277
- return value;
278
- }).catch((error) => {
279
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
280
- throw error;
281
- });
282
- (0, autotel_trace_helpers.finalizeSpan)(span);
283
- return result;
330
+ return settleSpan(original.apply(this, args), finalize);
284
331
  } catch (error) {
285
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
332
+ finalize(error);
286
333
  throw error;
287
334
  }
288
335
  });
@@ -310,20 +357,12 @@ function wrapInstanceMethod(target, methodName, operation, getCollectionName, ge
310
357
  const statementText = captureStatement(operation, payload);
311
358
  if (statementText) span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
312
359
  } catch {}
360
+ const finalize = createSpanFinalizer(span);
313
361
  return (0, autotel_trace_helpers.runWithSpan)(span, () => {
314
362
  try {
315
- const result = original.apply(this, args);
316
- if (result && typeof result.then === "function") return Promise.resolve(result).then((value) => {
317
- (0, autotel_trace_helpers.finalizeSpan)(span);
318
- return value;
319
- }).catch((error) => {
320
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
321
- throw error;
322
- });
323
- (0, autotel_trace_helpers.finalizeSpan)(span);
324
- return result;
363
+ return settleSpan(original.apply(this, args), finalize);
325
364
  } catch (error) {
326
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
365
+ finalize(error);
327
366
  throw error;
328
367
  }
329
368
  });
@@ -580,42 +619,24 @@ function wrapCustomFunction(original, methodName, methodType) {
580
619
  if (params !== void 0) span.setAttribute(ATTR_MONGOOSE_METHOD_PARAMETERS, params);
581
620
  } catch {}
582
621
  }
622
+ const finalize = createSpanFinalizer(span);
583
623
  return (0, autotel_trace_helpers.runWithSpan)(span, () => {
584
- try {
624
+ if (methodType === "query") try {
585
625
  const result = original.apply(this, args);
586
- if (methodType === "query") {
587
- (0, autotel_trace_helpers.finalizeSpan)(span);
588
- return result;
589
- }
590
- if (result && typeof result.exec === "function") {
591
- const originalExec = result.exec.bind(result);
592
- result.exec = function wrappedExec() {
593
- try {
594
- return Promise.resolve(originalExec()).then((value) => {
595
- (0, autotel_trace_helpers.finalizeSpan)(span);
596
- return value;
597
- }).catch((error) => {
598
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
599
- throw error;
600
- });
601
- } catch (error) {
602
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
603
- throw error;
604
- }
605
- };
606
- return result;
607
- }
608
- if (result && typeof result.then === "function") return Promise.resolve(result).then((value) => {
609
- (0, autotel_trace_helpers.finalizeSpan)(span);
610
- return value;
611
- }).catch((error) => {
612
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
613
- throw error;
614
- });
615
- (0, autotel_trace_helpers.finalizeSpan)(span);
626
+ finalize();
616
627
  return result;
617
628
  } catch (error) {
618
- (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
629
+ finalize(error);
630
+ throw error;
631
+ }
632
+ const { callArgs, deferred } = deferFinalizeToCallback(args, span, finalize);
633
+ try {
634
+ const result = original.apply(this, callArgs);
635
+ if (result && (typeof result.exec === "function" || typeof result.then === "function")) return settleSpan(result, finalize);
636
+ if (!deferred) finalize();
637
+ return result;
638
+ } catch (error) {
639
+ finalize(error);
619
640
  throw error;
620
641
  }
621
642
  });
@@ -653,20 +674,51 @@ const MONGOOSE_INTERNAL_FUNCTION_NAMES = new Set(["initializeTimestamps"]);
653
674
  function isMongooseInternalFunctionName(name) {
654
675
  return name.startsWith("$") || MONGOOSE_INTERNAL_FUNCTION_NAMES.has(name);
655
676
  }
677
+ /**
678
+ * Detects a compiled Mongoose Model assigned onto a schema collection — e.g.
679
+ * the `Patches` model attached to `schema.statics` by history/audit plugins
680
+ * (`schema.statics.Patches = mongoose.model(...)`). A Model is a constructor
681
+ * function carrying its own statics (`find`, `create`, …); wrapping it in a
682
+ * plain tracing function would drop those and break callers, so it must be
683
+ * skipped — both at the compile-time scan and on later assignment.
684
+ */
685
+ function isMongooseModelLike(fn) {
686
+ try {
687
+ return typeof fn === "function" && typeof fn.modelName === "string" && fn.schema != null;
688
+ } catch {
689
+ return false;
690
+ }
691
+ }
692
+ /** Whether a value assigned to a schema collection should be wrapped. */
693
+ function shouldWrapCustomFunction(name, value) {
694
+ return typeof value === "function" && !isMongooseInternalFunctionName(name) && !value[WRAPPED_METHOD_FLAG] && !isMongooseModelLike(value);
695
+ }
656
696
  function instrumentSchemaCustomFunctions(schema) {
657
697
  if (!schema) return;
658
698
  const wrapCollection = (collection, methodType) => {
659
- if (!collection) return;
699
+ if (!collection || collection[PROXIED_COLLECTION_FLAG]) return collection;
660
700
  for (const name of Object.keys(collection)) {
661
- if (isMongooseInternalFunctionName(name)) continue;
662
701
  const fn = collection[name];
663
- if (typeof fn !== "function" || fn[WRAPPED_METHOD_FLAG]) continue;
664
- collection[name] = wrapCustomFunction(fn, name, methodType);
702
+ if (isMongooseModelLike(fn)) continue;
703
+ if (shouldWrapCustomFunction(name, fn)) collection[name] = wrapCustomFunction(fn, name, methodType);
704
+ }
705
+ try {
706
+ Object.defineProperty(collection, PROXIED_COLLECTION_FLAG, {
707
+ value: true,
708
+ enumerable: false,
709
+ configurable: true
710
+ });
711
+ } catch {
712
+ return collection;
665
713
  }
714
+ return new Proxy(collection, { set(target, prop, value) {
715
+ target[prop] = typeof prop === "string" && shouldWrapCustomFunction(prop, value) ? wrapCustomFunction(value, prop, methodType) : value;
716
+ return true;
717
+ } });
666
718
  };
667
- wrapCollection(schema.statics, "static");
668
- wrapCollection(schema.methods, "instance");
669
- wrapCollection(schema.query, "query");
719
+ schema.statics = wrapCollection(schema.statics, "static");
720
+ schema.methods = wrapCollection(schema.methods, "instance");
721
+ schema.query = wrapCollection(schema.query, "query");
670
722
  }
671
723
  /**
672
724
  * Patches `mongoose.model()` (and `Connection.prototype.model()`) so custom