Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
7b2fcf5
feat(diagnostics): add enableDiagnosticsChannel and channel registry
logaretm Apr 17, 2026
c24243f
feat(language): publish on graphql:parse tracing channel
logaretm Apr 17, 2026
8cd0119
test(integration): exercise graphql tracing channels on real node:dia…
logaretm Apr 17, 2026
ae06f91
feat(validation): publish on graphql:validate tracing channel
logaretm Apr 17, 2026
56d94c1
feat(execution): publish on graphql:execute tracing channel
logaretm Apr 17, 2026
6a16403
refactor(diagnostics): align async lifecycle with Node's tracePromise…
logaretm Apr 17, 2026
37d8b2c
feat(execution): publish on graphql:subscribe tracing channel
logaretm Apr 17, 2026
67c9625
feat(execution): publish on graphql:resolve tracing channel
logaretm Apr 17, 2026
85ea8df
fix(diagnostics): preserve AsyncLocalStorage across async lifecycle
logaretm Apr 17, 2026
1a24257
fix(diagnostics): fire asyncStart synchronously, asyncEnd in finally
logaretm Apr 17, 2026
bc48419
chore: remove old comments no longer apply
logaretm Apr 18, 2026
405051f
ref: remove tracePromise as it was not needed with runStores
logaretm Apr 20, 2026
9289307
ref(perf): cache publish decision in excutor
logaretm Apr 20, 2026
0fccb89
test: coverage and cleanup unused type
logaretm Apr 20, 2026
27b8246
feat(diagnostics): throw on re-registration with different dc module
logaretm Apr 21, 2026
9ee85ad
feat(diagnostics): allow re-registration with equivalent tracingChann…
logaretm Apr 21, 2026
2dc17d3
ref: autoload tracing channels
logaretm Apr 24, 2026
fe46d17
ref: create direct pointers to tracing channels
logaretm Apr 24, 2026
f57fb9f
ref(perf): inline no-subscriber fast path at tracing emission sites
logaretm Apr 24, 2026
3414e38
test: cover executeIgnoringIncremental traced path and drop unused tr…
logaretm Apr 24, 2026
566e0ad
ref(perf): keep executeField inlinable by extracting traced path
logaretm Apr 24, 2026
7b6fa15
rename from isTrivialResolver => isDefaultResolver
yaacovCR Apr 25, 2026
27abe1d
move helpers into class
yaacovCR Apr 25, 2026
8ae296e
condense
yaacovCR Apr 25, 2026
ff66d39
move subscription event tracing to mapSourceToResponse
yaacovCR Apr 25, 2026
3e07b44
fixes failing test
yaacovCR Apr 25, 2026
12afc6d
rename test files
yaacovCR Apr 25, 2026
140d0ff
merge resolve tracing tests into execution tracing file
yaacovCR Apr 25, 2026
fad92a6
use per-test rootValue in execution tracing tests
yaacovCR Apr 25, 2026
ce0d1f0
test no tracing activity
yaacovCR Apr 26, 2026
238608d
add c8 ignore directive
yaacovCR Apr 26, 2026
3389f51
move tracing back to executeField
yaacovCR Apr 26, 2026
a2cb53e
add globalThis to protect against potentially missing process symbol
yaacovCR Apr 26, 2026
2fd340e
rename test files (yet again!)
yaacovCR Apr 26, 2026
f45427b
prune required MinimalChannelInterface
yaacovCR Apr 26, 2026
dd4b56a
move function helpers to follow their use
yaacovCR Apr 26, 2026
36eb11f
revamp diagnostics tests with full subscription data
yaacovCR Apr 26, 2026
74b136c
make sure variableValues in context for execution and subscription ar…
yaacovCR Apr 26, 2026
47e494f
separate out diagnostics for execute/subscribe/resolve
yaacovCR Apr 26, 2026
fe67b0e
fix coverage
yaacovCR Apr 27, 2026
f27be45
expand integration tests for non latest node
yaacovCR Apr 27, 2026
d7b5e63
support runtimes without TracingChannel.hasSubscribers aggregate
logaretm Apr 27, 2026
878feb9
cover shouldTrace: real-channel test plus c8 ignore for Bun fallback
logaretm Apr 27, 2026
4d4a138
restore aggregate-first branch order in shouldTrace
logaretm Apr 27, 2026
b3fd003
use helpers extracted from this PR now on 17.x.x
yaacovCR Apr 27, 2026
72398eb
narrow ignore
yaacovCR Apr 27, 2026
47cfa62
just iterate
yaacovCR Apr 27, 2026
f5f7772
simplify context build
yaacovCR May 5, 2026
d922bdd
revert "simplification"
yaacovCR May 5, 2026
f8a3d9c
introduce execute:rootSelectionSet and subscribe:perEventExecutor
yaacovCR May 5, 2026
bd295d4
chore(diagnostics): standardize context field ordering
yaacovCR May 5, 2026
cbeace1
feat(diagnostics): add strict types for all Ctx types
yaacovCR May 5, 2026
674b94d
rename ctx => context
yaacovCR May 5, 2026
4d28cce
fix: adapt diagnostics to v17 after rebase
logaretm May 14, 2026
1fcccd8
refactor: store rawVariableValues on ValidatedExecutionArgs
logaretm May 15, 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"
}
}
342 changes: 342 additions & 0 deletions integrationTests/diagnostics-bun/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
// 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',
schema: msg.schema,
document: msg.document,
variableValues: msg.variableValues,
operationName: msg.operationName,
operationType: msg.operationType,
}),
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);
}
}

function runExecuteRootSelectionSetCase() {
const schema = buildSchema(`type Query { hello: String }`);
const document = parse('query Greeting { hello }');
const operation = document.definitions[0];

const events = [];
const handler = {
start: (msg) =>
events.push({
kind: 'start',
schema: msg.schema,
operation: msg.operation,
variableValues: msg.variableValues,
operationName: msg.operationName,
operationType: msg.operationType,
}),
end: (msg) => events.push({ kind: 'end', result: msg.result }),
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:rootSelectionSet');
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].operation, operation);
assert.equal(events[0].schema, schema);
assert.equal(events[1].result, result);
} 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',
schema: msg.schema,
document: msg.document,
variableValues: msg.variableValues,
operationName: msg.operationName,
operationType: msg.operationType,
}),
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,
args: msg.args,
isDefaultResolver: msg.isDefaultResolver,
fieldPath: msg.fieldPath,
}),
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 context object.
const als = new AsyncLocalStorage();
const channel = dc.tracingChannel('graphql:execute');
channel.start.bindStore(als, (context) => ({
operationName: context.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();
runExecuteRootSelectionSetCase();
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