Skip to content

feat(deprecation): collect deprecated schema element usage - #146

Merged
cuzzlor merged 5 commits into
mainfrom
feat/deprecated-element-usage
Aug 18, 2026
Merged

feat(deprecation): collect deprecated schema element usage#146
cuzzlor merged 5 commits into
mainfrom
feat/deprecated-element-usage

Conversation

@cuzzlor

@cuzzlor cuzzlor commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Note

Consumer: MakerXStudio/graphql-apollo-server#157 (draft) wires this collector into Apollo Server via a new includeDeprecatedElements option on graphqlOperationLoggingPlugin.
That PR is blocked on this one: it imports collectDeprecatedElementUsage, so its CI stays red until this merges and publishes 3.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 @deprecated schema 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 — and graphql is already a required peer, so this adds no dependency, peer, or subpath export. Putting it here also means the subscription path gets covered: onOperation already receives everything the collector needs, and subscriptions are invisible to this telemetry otherwise.

What consumers get

  • deprecatedElements on the operation log entry, omitted when empty.
  • includeDeprecatedElements on useSubscriptionsServer — 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:

  • Operation scoping. The AST pass walked the whole document, so a multi-operation request reported elements from operations that never ran — including fragments only those operations could reach. Now scoped with separateOperations. The old code was scrupulous about this for variables and then did the opposite for literals.
  • Directive arguments. TypeInfo resolves an argument against the enclosing directive when there is one, so a deprecated directive argument was reported as Query.field(arg) — a field argument that doesn't exist. Now reported as directive-argument / @directive(arg). This also explains the old <unknown> fallbacks, which were papering over the unhandled case.
  • Uniform dedupe. Only input fields and enum values deduped, so an aliased document could emit one record per occurrence. All kinds now dedupe on kind + name, and results are sorted for stable, diffable records.
  • Bounded work. The walk was depth-capped but unbounded in breadth and result size — a large flat array of input objects is shallow but arbitrarily wide. Adds maxVariableNodes (10,000) and maxElements (50) alongside maxVariableDepth (25).
  • path demoted 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 on name.
  • Deep graphql/utilities and graphql/language/ast imports flattened to the package root, which the dual ESM/CJS build and attw check 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/schema dependency. The fixture-integrity guard comes across too: it stops the suite going vacuous if a fixture loses its @deprecated directive, and confirms buildSchema preserves @deprecated on argument and input-field definitions.

Full npm run build is green, including attw on all four entry points, and I verified collectDeprecatedElementUsage resolves from both the CJS and ESM builds.

Limitations, documented in the README

  • Request-side only. It observes values a caller sends, never values the server returns — a deprecated enum value appearing solely in responses reads as unused.
  • path is 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-uri arrives via better-npm-audit's own tree). npm audit fix cleared all seven within the existing semver ranges, so only the lockfile changes.

🤖 Generated with Claude Code

cuzzlor and others added 2 commits August 17, 2026 12:07
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/deprecation.ts
cuzzlor and others added 2 commits August 17, 2026 12:39
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>
@cuzzlor
cuzzlor requested a review from mderriey August 18, 2026 01:02

@mderriey mderriey left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, great work!

Comment thread src/deprecation.ts Outdated
…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>
@cuzzlor
cuzzlor merged commit 77a7956 into main Aug 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants