feat(deprecation): collect deprecated schema element usage - #146
Merged
Conversation
Adds `collectDeprecatedElementUsage`, a pure collector reporting every `@deprecated` element an operation uses, so teams can tell whether a deprecated element is safe to remove. Output fields, arguments and directive arguments are named in the document, so an AST walk finds them. Input fields and enum values are data the caller supplies rather than selects, and can arrive either as a document literal or inside a variable where the name appears nowhere in the AST, so the supplied variable values are walked too, against their declared input types. Ported from an app-local Apollo plugin. Fixed on the way in: - The AST pass walked every operation in the document, reporting elements from operations that never ran, and fragments only those operations could reach. Now scoped via `separateOperations`. - Directive arguments were attributed to the enclosing field, naming a field argument that does not exist. TypeInfo resolves an argument against the enclosing directive when there is one, so these are now reported as `directive-argument`. - Only input fields and enum values were deduplicated, so an aliased document could emit one record per occurrence. All kinds now dedupe on kind and name, and results are sorted. - The walk was bounded by depth but not breadth or result size, adding `maxVariableNodes` and `maxElements`. - `path` is now best-effort and documented as such: it is fragment-relative for elements inside a fragment definition, so it is a debugging aid rather than an aggregation key. Consumers get the result via `deprecatedElements` on the operation log entry, which is omitted when empty, and via `includeDeprecatedElements` on `useSubscriptionsServer` — collected when the subscription is established rather than per emitted payload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`npm run audit` gates CI and had started failing on seven advisories published since the last run on main — none introduced here, and none reaching consumers: this package has no runtime dependencies, so every affected path is a devDependency or a peer. `fast-uri` arrives via better-npm-audit's own dependency tree. `npm audit fix` resolved all seven within the existing semver ranges, so only the lockfile changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a reusable deprecated GraphQL schema element collector for operation telemetry and the linked Apollo Server integration.
Changes:
- Collects and deduplicates deprecated fields, arguments, input fields, and enum values.
- Integrates collection into subscription operation logging.
- Adds tests, documentation, release metadata, and audit dependency updates.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/deprecation.ts |
Implements the collector and traversal limits. |
src/deprecation.spec.ts |
Tests collection, scoping, resilience, and limits. |
src/logging.ts |
Adds deprecated elements to operation logs. |
src/logging.spec.ts |
Tests log entry inclusion and omission. |
src/subscriptions/server.ts |
Exposes subscription collection option. |
src/index.ts |
Exports the collector API. |
README.md |
Documents usage and limitations. |
package.json |
Bumps the package to 3.3.0. |
package-lock.json |
Updates version and audited dependencies. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The recursive call returned early once the budget was exhausted, but the loop around it did not: list length comes from the payload, and each step built a path string before making the no-op call. A large flat array therefore still cost work proportional to its length, which is what `maxVariableNodes` exists to prevent. Measured on a 5,000 element list with a budget of 10: 5,000 element reads before, fewer than 50 after. The input-object loop gets the same guard. It is bounded by schema size rather than payload size so the cost was never unbounded there, but it makes the rule uniform: once the budget is spent, traversal stops. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lement cap
Collection stops at three limits, but only `maxElements` was visible to
callers — and only by inferring it from the result length, which also
misreads an exactly-at-the-cap complete result as truncated. Hitting
`maxVariableDepth` or `maxVariableNodes` produced a short list that
looked complete.
That is the dangerous direction for this telemetry. Its purpose is
answering "does anything still use this element", so a silently
truncated result invites the answer "no", which is how a still-used
element gets deleted. `maxVariableNodes` is reachable by a legitimate
request: a large batch mutation is shallow but wide.
`collectDeprecatedElementUsage` now returns `{ elements, truncated }`,
with `truncated` set by any of the three limits. Deduplication and null
values do not set it — neither loses anything.
Consumers see it as `deprecatedElementsTruncated` on the log entry,
omitted when false.
BREAKING CHANGE: collectDeprecatedElementUsage returns
{ elements, truncated } rather than an array. Unreleased, so no
published version is affected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mderriey
reviewed
Aug 18, 2026
…constants Per review: the reason adds noise without earning it. It is static schema data, identical on every record for a given element, so it inflates every log entry to repeat something the schema already answers — and anyone acting on this telemetry has the schema in front of them. `kind` and `name` identify the element; that is what usage queries aggregate on. Also renames the exported defaults, which said nothing about what they limit once imported from the package root: DEFAULT_MAX_VARIABLE_DEPTH -> DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH DEFAULT_MAX_VARIABLE_NODES -> DEFAULT_DEPRECATION_MAX_VARIABLE_NODES DEFAULT_MAX_ELEMENTS -> DEFAULT_DEPRECATION_MAX_ELEMENTS The option names on `collectDeprecatedElementUsage` keep their short forms: the function name already scopes them. The test that asserted the reason for each kind now asserts the elements themselves, so it still covers all five kinds from one operation. BREAKING CHANGE: DeprecatedElementUsage no longer carries deprecationReason, and the three DEFAULT_MAX_* constants are renamed. Unreleased, so no published version is affected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Consumer: MakerXStudio/graphql-apollo-server#157 (draft) wires this collector into Apollo Server via a new
includeDeprecatedElementsoption ongraphqlOperationLoggingPlugin.That PR is blocked on this one: it imports
collectDeprecatedElementUsage, so its CI stays red until this merges and publishes3.3.0. Reviewing the two together shows the full intent — this PR is the mechanism, that one is the usage.Adds
collectDeprecatedElementUsage— a pure, server-agnostic collector that reports every@deprecatedschema element an operation uses, so we can tell whether a deprecated element is actually safe to remove.This is the first half of porting an app-local Apollo plugin into the shared libraries; see the linked consumer PR above for the second half.
Why here rather than in graphql-apollo-server
The collector is pure —
(schema, document, operation, variables) -> DeprecatedElementUsage[], no logger, server, or clock — andgraphqlis already a required peer, so this adds no dependency, peer, or subpath export. Putting it here also means the subscription path gets covered:onOperationalready receives everything the collector needs, and subscriptions are invisible to this telemetry otherwise.What consumers get
deprecatedElementson the operation log entry, omitted when empty.includeDeprecatedElementsonuseSubscriptionsServer— collected once when the subscription is established, not per emitted payload, since the usage is a property of the operation rather than of each event.Fixes made during the port
The behaviour is not a straight lift-and-shift. Each of these is pinned by a test that fails against the old behaviour:
separateOperations. The old code was scrupulous about this for variables and then did the opposite for literals.TypeInforesolves an argument against the enclosing directive when there is one, so a deprecated directive argument was reported asQuery.field(arg)— a field argument that doesn't exist. Now reported asdirective-argument/@directive(arg). This also explains the old<unknown>fallbacks, which were papering over the unhandled case.maxVariableNodes(10,000) andmaxElements(50) alongsidemaxVariableDepth(25).pathdemoted to best-effort. It's fragment-relative for elements inside a fragment definition, which is the common case for clients using generated fragments. Documented as a debugging aid; aggregate onname.graphql/utilitiesandgraphql/language/astimports flattened to the package root, which the dual ESM/CJS build andattwcheck require.Testing
148 tests pass. Roughly 400 lines of the original 512-line suite port across as pure-function tests — no Apollo Server boot, no logger spy — using
buildSchema+parse, so no@graphql-tools/schemadependency. The fixture-integrity guard comes across too: it stops the suite going vacuous if a fixture loses its@deprecateddirective, and confirmsbuildSchemapreserves@deprecatedon argument and input-field definitions.Full
npm run buildis green, includingattwon all four entry points, and I verifiedcollectDeprecatedElementUsageresolves from both the CJS and ESM builds.Limitations, documented in the README
pathis not an aggregation key, per above.Second commit: audit fixes
CI gates on
npm run audit, which had started failing on seven advisories published since main last ran — unrelated to this change. This package declares no runtime dependencies, so every affected path is a devDependency or peer and none reach consumers (fast-uriarrives viabetter-npm-audit's own tree).npm audit fixcleared all seven within the existing semver ranges, so only the lockfile changes.🤖 Generated with Claude Code