Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
dbb1ebd
feat(diagnostics): add enableDiagnosticsChannel and channel registry
logaretm Apr 17, 2026
d5cffe8
feat(language): publish on graphql:parse tracing channel
logaretm Apr 17, 2026
79522a3
test(integration): exercise graphql tracing channels on real node:dia…
logaretm Apr 17, 2026
b225a77
feat(validation): publish on graphql:validate tracing channel
logaretm Apr 17, 2026
3fba340
feat(execution): publish on graphql:execute tracing channel
logaretm Apr 17, 2026
3e8c37f
refactor(diagnostics): align async lifecycle with Node's tracePromise…
logaretm Apr 17, 2026
219fe29
feat(execution): publish on graphql:subscribe tracing channel
logaretm Apr 17, 2026
3c42a11
feat(execution): publish on graphql:resolve tracing channel
logaretm Apr 17, 2026
8c72f1d
fix(diagnostics): preserve AsyncLocalStorage across async lifecycle
logaretm Apr 17, 2026
b166ec2
fix(diagnostics): fire asyncStart synchronously, asyncEnd in finally
logaretm Apr 17, 2026
97d05bb
chore: remove old comments no longer apply
logaretm Apr 18, 2026
7762b61
ref: remove tracePromise as it was not needed with runStores
logaretm Apr 20, 2026
ae47506
ref(perf): cache publish decision in excutor
logaretm Apr 20, 2026
3e15aea
test: coverage and cleanup unused type
logaretm Apr 20, 2026
b6767f5
feat(diagnostics): throw on re-registration with different dc module
logaretm Apr 21, 2026
f7adfdb
feat(diagnostics): allow re-registration with equivalent tracingChann…
logaretm Apr 21, 2026
3eb557b
ref: autoload tracing channels
logaretm Apr 24, 2026
5244975
ref: create direct pointers to tracing channels
logaretm Apr 24, 2026
08ecca0
ref(perf): inline no-subscriber fast path at tracing emission sites
logaretm Apr 24, 2026
78e0b07
test: cover executeIgnoringIncremental traced path and drop unused tr…
logaretm Apr 24, 2026
1b39ae6
ref(perf): keep executeField inlinable by extracting traced path
logaretm Apr 24, 2026
fc07d8f
rename from isTrivialResolver => isDefaultResolver
yaacovCR Apr 25, 2026
b44c511
move helpers into class
yaacovCR Apr 25, 2026
5a65b99
condense
yaacovCR Apr 25, 2026
1caeb34
move subscription event tracing to mapSourceToResponse
yaacovCR Apr 25, 2026
630e828
fixes failing test
yaacovCR Apr 25, 2026
37ea332
rename test files
yaacovCR Apr 25, 2026
8f3105c
merge resolve tracing tests into execution tracing file
yaacovCR Apr 25, 2026
199df94
use per-test rootValue in execution tracing tests
yaacovCR Apr 25, 2026
3689278
test no tracing activity
yaacovCR Apr 26, 2026
93db9be
add c8 ignore directive
yaacovCR Apr 26, 2026
8383717
move tracing back to executeField
yaacovCR Apr 26, 2026
4585d02
add globalThis to protect against potentially missing process symbol
yaacovCR Apr 26, 2026
19137cb
rename test files (yet again!)
yaacovCR Apr 26, 2026
8e74a35
prune required MinimalChannelInterface
yaacovCR Apr 26, 2026
48d8b1d
move function helpers to follow their use
yaacovCR Apr 26, 2026
33e92c4
revamp diagnostics tests with full subscription data
yaacovCR Apr 26, 2026
7c18788
make sure variableValues in context for execution and subscription ar…
yaacovCR Apr 26, 2026
e2f638c
separate out diagnostics for execute/subscribe/resolve
yaacovCR Apr 26, 2026
45273a3
fix coverage
yaacovCR Apr 27, 2026
73a5d6f
expand integration tests for non latest node
yaacovCR Apr 27, 2026
935a1b7
support runtimes without TracingChannel.hasSubscribers aggregate
logaretm Apr 27, 2026
dc3b80f
cover shouldTrace: real-channel test plus c8 ignore for Bun fallback
logaretm Apr 27, 2026
78f3c31
restore aggregate-first branch order in shouldTrace
logaretm Apr 27, 2026
290af56
use helpers extracted from this PR now on 17.x.x
yaacovCR Apr 27, 2026
b9bd5cc
narrow ignore
yaacovCR Apr 27, 2026
778df89
just iterate
yaacovCR Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions integrationTests/diagnostics-bun/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"description": "graphql-js tracing channels should publish on node:diagnostics_channel (Bun)",
"private": true,
"scripts": {
"test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app oven/bun:\"$BUN_VERSION\"-slim bun test.js"
},
"dependencies": {
"graphql": "file:../graphql.tgz"
}
}
288 changes: 288 additions & 0 deletions integrationTests/diagnostics-bun/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
// TracingChannel is marked experimental in Node's docs but is shipped on
// every runtime graphql-js supports. This test exercises it directly.
/* eslint-disable n/no-unsupported-features/node-builtins */

import assert from 'node:assert/strict';
import { AsyncLocalStorage } from 'node:async_hooks';
import dc from 'node:diagnostics_channel';

import { buildSchema, execute, parse, subscribe, validate } from 'graphql';

function runParseCases() {
// graphql:parse - synchronous.
{
const events = [];
const handler = {
start: (msg) => events.push({ kind: 'start', source: msg.source }),
end: (msg) => events.push({ kind: 'end', source: msg.source }),
asyncStart: (msg) =>
events.push({ kind: 'asyncStart', source: msg.source }),
asyncEnd: (msg) => events.push({ kind: 'asyncEnd', source: msg.source }),
error: (msg) =>
events.push({ kind: 'error', source: msg.source, error: msg.error }),
};

const channel = dc.tracingChannel('graphql:parse');
channel.subscribe(handler);

try {
const doc = parse('{ field }');
assert.equal(doc.kind, 'Document');
assert.deepEqual(
events.map((e) => e.kind),
['start', 'end'],
);
assert.equal(events[0].source, '{ field }');
assert.equal(events[1].source, '{ field }');
} finally {
channel.unsubscribe(handler);
}
}

// graphql:parse - error path fires start, error, end.
{
const events = [];
const handler = {
start: (msg) => events.push({ kind: 'start', source: msg.source }),
end: (msg) => events.push({ kind: 'end', source: msg.source }),
error: (msg) =>
events.push({ kind: 'error', source: msg.source, error: msg.error }),
};

const channel = dc.tracingChannel('graphql:parse');
channel.subscribe(handler);

try {
assert.throws(() => parse('{ '));
assert.deepEqual(
events.map((e) => e.kind),
['start', 'error', 'end'],
);
assert.ok(events[1].error instanceof Error);
} finally {
channel.unsubscribe(handler);
}
}
}

function runValidateCase() {
const schema = buildSchema(`type Query { field: String }`);
const doc = parse('{ field }');

const events = [];
const handler = {
start: (msg) =>
events.push({
kind: 'start',
schema: msg.schema,
document: msg.document,
}),
end: () => events.push({ kind: 'end' }),
error: (msg) => events.push({ kind: 'error', error: msg.error }),
};

const channel = dc.tracingChannel('graphql:validate');
channel.subscribe(handler);

try {
const errors = validate(schema, doc);
assert.deepEqual(errors, []);
assert.deepEqual(
events.map((e) => e.kind),
['start', 'end'],
);
assert.equal(events[0].schema, schema);
assert.equal(events[0].document, doc);
} finally {
channel.unsubscribe(handler);
}
}

function runExecuteCase() {
const schema = buildSchema(`type Query { hello: String }`);
const document = parse('query Greeting { hello }');

const events = [];
const handler = {
start: (msg) =>
events.push({
kind: 'start',
operationType: msg.operationType,
operationName: msg.operationName,
document: msg.document,
schema: msg.schema,
}),
end: () => events.push({ kind: 'end' }),
asyncStart: () => events.push({ kind: 'asyncStart' }),
asyncEnd: () => events.push({ kind: 'asyncEnd' }),
error: (msg) => events.push({ kind: 'error', error: msg.error }),
};

const channel = dc.tracingChannel('graphql:execute');
channel.subscribe(handler);

try {
const result = execute({
schema,
document,
rootValue: { hello: 'world' },
});
assert.equal(result.data.hello, 'world');
assert.deepEqual(
events.map((e) => e.kind),
['start', 'end'],
);
assert.equal(events[0].operationType, 'query');
assert.equal(events[0].operationName, 'Greeting');
assert.equal(events[0].document, document);
assert.equal(events[0].schema, schema);
} finally {
channel.unsubscribe(handler);
}
}

async function runSubscribeCase() {
async function* ticks() {
yield { tick: 'one' };
}

const schema = buildSchema(`
type Query { dummy: String }
type Subscription { tick: String }
`);
// buildSchema doesn't attach a subscribe resolver to fields; inject one.
schema.getSubscriptionType().getFields().tick.subscribe = () => ticks();

const document = parse('subscription Tick { tick }');

const events = [];
const handler = {
start: (msg) =>
events.push({
kind: 'start',
operationType: msg.operationType,
operationName: msg.operationName,
}),
end: () => events.push({ kind: 'end' }),
asyncStart: () => events.push({ kind: 'asyncStart' }),
asyncEnd: () => events.push({ kind: 'asyncEnd' }),
error: (msg) => events.push({ kind: 'error', error: msg.error }),
};

const channel = dc.tracingChannel('graphql:subscribe');
channel.subscribe(handler);

try {
const result = subscribe({ schema, document });
const stream = typeof result.then === 'function' ? await result : result;
if (stream[Symbol.asyncIterator]) {
await stream.return?.();
}
// Subscription setup is synchronous here; start/end fire, no async tail.
assert.deepEqual(
events.map((e) => e.kind),
['start', 'end'],
);
assert.equal(events[0].operationType, 'subscription');
assert.equal(events[0].operationName, 'Tick');
} finally {
channel.unsubscribe(handler);
}
}

function runResolveCase() {
const schema = buildSchema(
`type Query { hello: String nested: Nested } type Nested { leaf: String }`,
);
const document = parse('{ hello nested { leaf } }');

const events = [];
const handler = {
start: (msg) =>
events.push({
kind: 'start',
fieldName: msg.fieldName,
parentType: msg.parentType,
fieldType: msg.fieldType,
fieldPath: msg.fieldPath,
isDefaultResolver: msg.isDefaultResolver,
}),
end: () => events.push({ kind: 'end' }),
asyncStart: () => events.push({ kind: 'asyncStart' }),
asyncEnd: () => events.push({ kind: 'asyncEnd' }),
error: (msg) => events.push({ kind: 'error', error: msg.error }),
};

const channel = dc.tracingChannel('graphql:resolve');
channel.subscribe(handler);

try {
const rootValue = { hello: () => 'world', nested: { leaf: 'leaf-value' } };
execute({ schema, document, rootValue });

const starts = events.filter((e) => e.kind === 'start');
const paths = starts.map((e) => e.fieldPath);
assert.deepEqual(paths, ['hello', 'nested', 'nested.leaf']);

const hello = starts.find((e) => e.fieldName === 'hello');
assert.equal(hello.parentType, 'Query');
assert.equal(hello.fieldType, 'String');
// buildSchema never attaches field.resolve; all fields report as trivial.
assert.equal(hello.isDefaultResolver, true);
} finally {
channel.unsubscribe(handler);
}
}

function runNoSubscriberCase() {
const doc = parse('{ field }');
assert.equal(doc.kind, 'Document');
}

async function runAlsPropagationCase() {
// A subscriber that binds a store on the `start` sub-channel should be able
// to read it in every lifecycle handler (start, end, asyncStart, asyncEnd).
// This is what APMs use to parent child spans to the current operation
// without threading state through the ctx object.
const als = new AsyncLocalStorage();
const channel = dc.tracingChannel('graphql:execute');
channel.start.bindStore(als, (ctx) => ({ operationName: ctx.operationName }));

const seen = {};
const handler = {
start: () => (seen.start = als.getStore()),
end: () => (seen.end = als.getStore()),
asyncStart: () => (seen.asyncStart = als.getStore()),
asyncEnd: () => (seen.asyncEnd = als.getStore()),
};
channel.subscribe(handler);

try {
const schema = buildSchema(`type Query { slow: String }`);
const document = parse('query Slow { slow }');
const rootValue = { slow: () => Promise.resolve('done') };

await execute({ schema, document, rootValue });

assert.deepEqual(seen.start, { operationName: 'Slow' });
assert.deepEqual(seen.end, { operationName: 'Slow' });
assert.deepEqual(seen.asyncStart, { operationName: 'Slow' });
assert.deepEqual(seen.asyncEnd, { operationName: 'Slow' });
} finally {
channel.unsubscribe(handler);
channel.start.unbindStore(als);
}
}

async function main() {
runParseCases();
runValidateCase();
runExecuteCase();
await runSubscribeCase();
runResolveCase();
await runAlsPropagationCase();
runNoSubscriberCase();
console.log('diagnostics integration test passed');
}

main();
6 changes: 6 additions & 0 deletions integrationTests/diagnostics-deno-with-deno-build/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"imports": {
"graphql": "../graphql-deno-dist/index.ts",
"graphql/": "../graphql-deno-dist/"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"description": "graphql-js tracing channels should publish on node:diagnostics_channel (Deno with deno build)",
"private": true,
"scripts": {
"test": "docker run --rm --volume \"$PWD/..\":/usr/src/app -w /usr/src/app/diagnostics-deno-with-deno-build denoland/deno:alpine-\"$DENO_VERSION\" deno run test.js"
}
}
Loading
Loading