Skip to content

fix(instrumentation-dataloader): Patch batchLoadFn without creating an instance #2498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 26, 2025
105 changes: 58 additions & 47 deletions plugins/node/instrumentation-dataloader/src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,64 +92,75 @@
return `${MODULE_NAME}.${operation} ${dataloaderName}`;
}

private _wrapBatchLoadFn(
batchLoadFn: Dataloader.BatchLoadFn<unknown, unknown>
): Dataloader.BatchLoadFn<unknown, unknown> {
const instrumentation = this;

return function patchedBatchLoadFn(
this: DataloaderInternal,
...args: Parameters<Dataloader.BatchLoadFn<unknown, unknown>>
) {
if (
!instrumentation.isEnabled() ||
!instrumentation.shouldCreateSpans()
) {
return batchLoadFn.call(this, ...args);
}

const parent = context.active();
const span = instrumentation.tracer.startSpan(
instrumentation.getSpanName(this, 'batch'),
{ links: this._batch?.spanLinks as Link[] | undefined },
parent
);

return context.with(trace.setSpan(parent, span), () => {
return (batchLoadFn.apply(this, args) as Promise<unknown[]>)
.then(value => {
span.end();
return value;
})
.catch(err => {
span.recordException(err);
span.setStatus({
code: SpanStatusCode.ERROR,
message: err.message,
});
span.end();
throw err;
});
});
};
}

private _getPatchedConstructor(
constructor: typeof Dataloader
): typeof Dataloader {
const prototype = constructor.prototype;
const instrumentation = this;
const prototype = constructor.prototype;

if (!instrumentation.isEnabled()) {
return constructor;

Check warning on line 144 in plugins/node/instrumentation-dataloader/src/instrumentation.ts

View check run for this annotation

Codecov / codecov/patch

plugins/node/instrumentation-dataloader/src/instrumentation.ts#L144

Added line #L144 was not covered by tests
}

function PatchedDataloader(
this: DataloaderInternal,
...args: ConstructorParameters<typeof constructor>
) {
const inst = new constructor(...args) as DataloaderInternal;

if (!instrumentation.isEnabled()) {
return inst;
}
// BatchLoadFn is the first constructor argument
// https://github.com/graphql/dataloader/blob/77c2cd7ca97e8795242018ebc212ce2487e729d2/src/index.js#L47
if (typeof args[0] === 'function') {
if (isWrapped(args[0])) {
instrumentation._unwrap(args, 0);

Check warning on line 155 in plugins/node/instrumentation-dataloader/src/instrumentation.ts

View check run for this annotation

Codecov / codecov/patch

plugins/node/instrumentation-dataloader/src/instrumentation.ts#L155

Added line #L155 was not covered by tests
}

if (isWrapped(inst._batchLoadFn)) {
instrumentation._unwrap(inst, '_batchLoadFn');
args[0] = instrumentation._wrapBatchLoadFn(
args[0]
) as Dataloader.BatchLoadFn<unknown, unknown>;
}

instrumentation._wrap(inst, '_batchLoadFn', original => {
return function patchedBatchLoadFn(
this: DataloaderInternal,
...args: Parameters<Dataloader.BatchLoadFn<unknown, unknown>>
) {
if (
!instrumentation.isEnabled() ||
!instrumentation.shouldCreateSpans()
) {
return original.call(this, ...args);
}

const parent = context.active();
const span = instrumentation.tracer.startSpan(
instrumentation.getSpanName(inst, 'batch'),
{ links: this._batch?.spanLinks as Link[] | undefined },
parent
);

return context.with(trace.setSpan(parent, span), () => {
return (original.apply(this, args) as Promise<unknown[]>)
.then(value => {
span.end();
return value;
})
.catch(err => {
span.recordException(err);
span.setStatus({
code: SpanStatusCode.ERROR,
message: err.message,
});
span.end();
throw err;
});
});
};
});

return inst;
return constructor.apply(this, args);
}

PatchedDataloader.prototype = prototype;
Expand Down
21 changes: 21 additions & 0 deletions plugins/node/instrumentation-dataloader/test/dataloader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,4 +619,25 @@ describe('DataloaderInstrumentation', () => {
assert.deepStrictEqual(await alternativeDataloader.loadMany(['test']), [1]);
assert.strictEqual(memoryExporter.getFinishedSpans().length, 5);
});

it('should not prune custom methods', async () => {
class CustomDataLoader extends Dataloader<string, string> {
constructor() {
super(async keys => keys.map((_, idx) => getMd5HashFromIdx(idx)));
}

public async customLoad() {
return this.load('test');
}
}

const customDataloader = new CustomDataLoader();
await customDataloader.customLoad();

assert.strictEqual(memoryExporter.getFinishedSpans().length, 2);
const [batchSpan, loadSpan] = memoryExporter.getFinishedSpans();

assert.strictEqual(loadSpan.name, 'dataloader.load');
assert.strictEqual(batchSpan.name, 'dataloader.batch');
});
});