-
Notifications
You must be signed in to change notification settings - Fork 529
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
fix(instrumentation-dataloader): Patch batchLoadFn
without creating an instance
#2498
base: main
Are you sure you want to change the base?
fix(instrumentation-dataloader): Patch batchLoadFn
without creating an instance
#2498
Conversation
batchLoadFn
without creating an instancebatchLoadFn
without creating an instance
6110fa8
to
acfeb34
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2498 +/- ##
==========================================
- Coverage 90.75% 90.74% -0.02%
==========================================
Files 169 169
Lines 8018 8020 +2
Branches 1632 1633 +1
==========================================
+ Hits 7277 7278 +1
- Misses 741 742 +1
|
acfeb34
to
60d8499
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly looks good, just a couple edge cases.
return this.load('test'); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think this class and the getMd5HashFromIdx function could move inside the test case to be closer to the only place they are used:
--- a/plugins/node/instrumentation-dataloader/test/dataloader.test.ts
+++ b/plugins/node/instrumentation-dataloader/test/dataloader.test.ts
@@ -33,23 +33,10 @@ import * as assert from 'assert';
import * as Dataloader from 'dataloader';
import * as crypto from 'crypto';
-const getMd5HashFromIdx = (idx: number) =>
- crypto.createHash('md5').update(String(idx)).digest('hex');
-
describe('DataloaderInstrumentation', () => {
let dataloader: Dataloader<string, number>;
let contextManager: AsyncHooksContextManager;
- class CustomDataLoader extends Dataloader<string, string> {
- constructor() {
- super(async keys => keys.map((_, idx) => getMd5HashFromIdx(idx)));
- }
-
- public async customLoad() {
- return this.load('test');
- }
- }
-
const memoryExporter = new InMemorySpanExporter();
const provider = new NodeTracerProvider();
const tracer = provider.getTracer('default');
@@ -350,6 +337,19 @@ describe('DataloaderInstrumentation', () => {
});
it('should not prune custom methods', async () => {
+ const getMd5HashFromIdx = (idx: number) =>
+ crypto.createHash('md5').update(String(idx)).digest('hex');
+
+ 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();
const instrumentation = this; | ||
const prototype = constructor.prototype; | ||
|
||
if (!instrumentation.isEnabled() || !instrumentation.shouldCreateSpans()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!instrumentation.isEnabled() || !instrumentation.shouldCreateSpans()) { | |
if (!instrumentation.isEnabled()) { |
This if-block is at init-time before any spans are being created. This !instrumentation.shouldCreateSpans()
is only true by fluke because config.requireParentSpan
is undefined in the tests.
|
||
if (isWrapped(inst._batchLoadFn)) { | ||
instrumentation._unwrap(inst, '_batchLoadFn'); | ||
// BatchLoadFn is the first constructor argument |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a guard that args[0]
is actually a function before wrapping it. Something like (untested):
if (typeof args[0] !== 'function') {
return constructor.apply(this, args);
}
or perhaps only do the unwrap and wrap if it is a function.
The current behaviour breaks the guard in Dataloader itself:
foo.js
if (process.env.INSTRUMENT_IT) {
const { DataloaderInstrumentation } = require('./');
const { NodeSDK, tracing } = require('@opentelemetry/sdk-node');
const instr = new DataloaderInstrumentation();
const sdk = new NodeSDK({
spanProcessor: new tracing.SimpleSpanProcessor(new tracing.ConsoleSpanExporter()),
serviceName: 'foo',
instrumentations: [instr]
});
sdk.start();
}
const Dataloader = require('dataloader');
const dl = new Dataloader(); // oops, no batchLoadFn passed in
console.log('the end');
Run that without and with instrumentation shows a changed behaviour:
% node foo.js
/Users/trentm/tm/opentelemetry-js-contrib10/node_modules/dataloader/index.js:32
throw new TypeError('DataLoader must be constructed with a function which accepts ' + ("Array<key> and returns Promise<Array<value>>, but got: " + batchLoadFn + "."));
^
TypeError: DataLoader must be constructed with a function which accepts Array<key> and returns Promise<Array<value>>, but got: undefined.
at new DataLoader (/Users/trentm/tm/opentelemetry-js-contrib10/node_modules/dataloader/index.js:32:13)
at Object.<anonymous> (/Users/trentm/tm/opentelemetry-js-contrib10/plugins/node/instrumentation-dataloader/foo.js:14:12)
at Module._compile (node:internal/modules/cjs/loader:1364:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1422:10)
at Module.load (node:internal/modules/cjs/loader:1203:32)
at Module._load (node:internal/modules/cjs/loader:1019:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:128:12)
at node:internal/main/run_main_module:28:49
Node.js v18.20.4
% INSTRUMENT_IT=true node foo.js
the end
Please add a test case for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a guard to check if batchLoadFn
is a function. However, I could not reproduce the problem where Dataloader
instance is created without a batchLoadFn
(with or without the instrumentation).
It fails with:
TypeError: DataLoader must be constructed with a function which accepts Array<key> and returns Promise<Array<value>>, but got: undefined.
for both cases.
Related: getsentry/sentry-javascript#13869
Which problem is this PR solving?
DataLoader
constructor forbatchLoadFn
. This breaks the usage where theDataLoader
class is extended.Short description of the changes
DataLoader
constructor (which isbatchLoadFn
) to patch to avoid creating an instance.