Why is counting GraphQL field usage from request document text unreliable?
answer
- Text says asked for, not answered
- Directives, fragments, nulls, rejections
- An abstract selection hides its implementer
- A hashed document has no text
- Hook resolution, not the parser
basics
~20 sDocument text shows what a client selected, not what the server resolved. Skipped branches, unmatched type conditions, null parents and rejected operations mean a selected field may never run, and a client sending only a document hash sends no text at all.
solid answer
~50 sParsing stored request bodies counts **selections**; the question you are trying to answer is about **resolutions**. The two diverge constantly. A field guarded by `@include(if: $withHistory)` is in the text of every request but resolves only when the variable is true. A selection inside an inline fragment on `Listing` never resolves when the object turns out to be a `Rental`. Anything under a field that resolved to null, or that raised a field error, is never reached. An operation rejected by validation, by a cost limit or by authorization contributes text and no execution at all. Text also cannot tell you which concrete type answered a selection on an interface, and a client that sends only a document hash sends no text to parse. Hooking execution and recording the coordinate at resolution time sidesteps all of it.
code
graphql · 15 linesquery ListingPanel($id: ID!, $withHistory: Boolean!) {
property(id: $id) {
address
... on Listing {
askingPrice
priceHistory @include(if: $withHistory) {
changedAt
amount
}
}
... on Rental {
monthlyRent
}
}
}go deeper
Remember the one-line distinction: the document says what a client asked for, execution says what the server actually answered, and only the second is usage. Be able to name one reason they differ.
Walk the mechanisms concretely - conditional directives evaluated at field collection, type conditions that do not match, branches abandoned under a null or an error, and requests rejected before execution ever starts.
Show you know the failure runs both ways: text overcounts, and execution undercounts a field whose parent is usually null, which is exactly the field someone will delete and break. Say how you would keep both signals.
Frame it as evidence quality for an irreversible action. Decide what standard of proof the organisation requires before a coordinate is treated as dead, and who pays for the instrumentation that produces it.
## Selection is not resolution It is tempting to answer "who still uses this field?" by grepping request bodies, or by walking the AST of every document a server received. It is cheap, it needs no execution instrumentation, and it is wrong often enough to be dangerous - in both directions. A document is a *request* for fields. Execution decides which of those requests are actually carried out. Five mechanisms drive the two apart. ### 1. Conditional inclusion `@skip(if:)` and `@include(if:)` are executable directives evaluated during field collection, using the operation's variable values. A field guarded by `@include(if: $withHistory)` appears in the document text of every single request, and resolves only in the subset where the client passed `true`. Parsing text says the field is used constantly; it may in fact never have run in production. ### 2. Type conditions that do not match A selection inside a fragment - named or inline - with a type condition only executes when the runtime object satisfies that condition. Over a real-estate graph where `Property` is an interface implemented by `Listing` and `Rental`, a document that selects both variants has both in its text on every request, while any given object satisfies exactly one. ### 3. Branches never reached Execution walks down from the root. If `Query.listing` resolves to `null`, or a resolver raises a field error and the error propagates, every selection beneath it is abandoned: those coordinates are in the text and were never resolved. The same happens, less dramatically, when a list resolves empty - a document selecting `PriceChange.amount` under a listing with no recorded price changes resolves the parent list field and never the child. ### 4. Operations that never execute A request can be rejected before execution begins: a parse error, a validation error, a depth or cost limit, a failed authorization check, a rate limit. The text arrived; nothing ran. A text-based counter treats the rejected request exactly like a successful one. ### 5. You may not have the text When a client sends only a hash of a previously registered document, the server has an identifier and no document body on that request. A text-scraping collector either misses that traffic entirely or has to resolve every hash back to a stored document, which is a second system to keep correct. ## The interface question There is one place where text is not merely unreliable but genuinely less informative. When a document selects `address` on the interface `Property`, execution resolves that field against the concrete object type it found - `Listing.address` or `Rental.address`. The text records the abstract selection; execution records who really answered. That distinction matters directly: a field declared on an interface can only be removed once every implementer's coordinate is quiet, and only execution data tells you which implementers were involved. ## The one case text is better Execution-derived usage undercounts in exactly one situation, and it is worth naming because it is the situation where deleting on evidence would break someone. A field whose parent resolves to null for almost all data never resolves, so it reads as dead - while clients still *select* it, and their documents would fail validation the moment the field is removed from the schema. Recording the coordinates that validated documents selected, as a second and weaker signal, covers that gap. The rule of thumb: resolved coordinates tell you what is *being read*; selected coordinates in validated documents tell you what would *break*. ## What to collect instead Hook field resolution, build the coordinate from the parent type name and the field name, and add it to a per-request set. That single change removes every problem above at once: skipped fields never fire the hook, unmatched fragments never fire it, unreached branches never fire it, rejected operations never reach execution, and hashed documents execute exactly like any other. Aliases are handled for free, since the alias lives in the response key and never touches the field being resolved. The cost is real - the hook runs on every resolved field, and a deep document over a list that grew without a bound can resolve thousands of them - which is why the accumulator is a set rather than a counter, and why the coordinate string is best interned once at schema build time rather than concatenated per resolution.
- A document selects a field on an interface. Which coordinate does execution record?The concrete type's. After the abstract type is resolved to an object type, the field is resolved against that object type, so a selection of `address` on the interface `Property` records `Listing.address` or `Rental.address` depending on what the object turned out to be. That is usually what you want, since each implementer can be retired separately; a collector can additionally record the abstract coordinate if it wants to see the selection as written.
- Is there a case where execution-derived usage reads lower than real dependence?Yes. A field whose parent resolves to null for nearly all data never resolves, so it looks dead while clients still select it in documents that would fail validation without it. The mitigation is to keep a second, weaker signal: the coordinates that validated documents selected, regardless of whether execution reached them. Resolved means read; selected means would break.
- Why does an alias not affect which coordinate is recorded?An alias renames the response key only. In `cheapest: listing(sort: PRICE_ASC)`, the response object gets a `cheapest` key, but the field being resolved is still `listing` on the query root type, so the coordinate is `Query.listing`. This is one more reason to record at resolution time: the executor already knows the real field, while a naive text scan sees an unfamiliar name.
saying these in an interview costs you the question
- Greps stored request bodies and calls the result usage
- Assumes a skipped field never appears in the document text
- Ignores that a null parent abandons every child selection
- Counts a fragment on a non-matching type as usage
- Believes the server always receives the document text
- Treats an aliased selection as a different schema field