Repository navigation
M2: read the Apollo cache the page already ships - #19
Merged
Merged
Conversation
Every Goodreads book page carries its whole normalised GraphQL cache
inline, 129 typed entities on the measured page. parse.go is a thousand
lines of CSS selectors reading the rendered output of that same data,
and it misses fields that are never rendered at all: ratingsCountDist,
textReviewsLanguageCounts, places, characters.
nextdata.go finds the script tag by id rather than by position, since
Goodreads ships several inline scripts and their order is not stable,
and hands the bytes to encoding/json with nothing evaluated.
apollo.go is the parser. Cache keys split on the first colon only. The
spec said ids are kca:// uris and that is only two thirds true: the
measured page has 69 kca ids, 29 bare integers on User, and 30 whole
JSON objects on Shelving, and those objects carry kca uris several
levels down. So the test asserts the round trip rather than the prefix,
which is the invariant that actually holds.
Field keys carry their GraphQL arguments, description({"stripped":true})
and quotes({"pagination":{"limit":1}}), and they get parsed to the
matching paren rather than string matched. TestParseFieldKeyOnEveryReal
Key runs the parser over every key in the capture, which is the test a
strings.Contains implementation fails.
Reading the limit out of the key is what makes the missed sentence
honest: the page asks for one quote, so the record says so and names
`goodread quotes 2792775` rather than implying it carries the set.
Ref resolution is depth bounded with a visited set, since the
book/work/bestBook triangle is on every page. The visited set unmarks on
the way out, because two fields pointing at the same author is a shared
author and not a loop. An unresolved ref becomes a stub with
resolved:false, which is the page not having asked for that entity
rather than anything going wrong.
Picking the entity the page is about goes through ROOT_QUERY. Taking
the first Book in the cache does not work: the measured page has three
and two are stubs holding nothing but __typename and id.
Level accounting lives in Extractor.set, so `goodread extraction` is
generated rather than maintained, and TestNoDirectFieldWrites fails on
any write that goes round it.
The spec was wrong about the ladder and the correction is worth
recording. Only the book page is a Next.js route. Author, series, list,
genre, editions and quotes are still Rails templates with no cache, and
four of them carry no og: tags either. So for those surfaces level 3 is
not debt to be driven to zero, it is the only source there is, and the
report says that rather than printing a zero that reads as good news.
TestNoNextDataOnRailsPages pins it, so if Goodreads does move them we
find out from a failing test and go delete selectors.
Seven golden captures with capture.txt as the ledger, gzipped, and
TestCaptureLedger fails on a file that does not say where it came from.
Verified against a freshly fetched live page as well as the captures:
32 fields at level 1, one at level 2, none on selectors.
Part of #8. Closes #10.
17 tasks done
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.
Closes #10. Part of #8.
What this replaces
parse.goreads rendered HTML with CSS selectors. The page it is reading rendered that HTML from a normalised GraphQL cache it also ships inline, 129 typed entities on the measured book page. So the selectors are reading the output of the data instead of the data, and they miss the fields that are never rendered at all:ratingsCountDist,textReviewsLanguageCounts,places,characters.What went in
nextdata.gofinds the script tag by id rather than by position, since Goodreads ships several inline scripts and their order is not stable, and hands the bytes toencoding/jsonwith nothing evaluated.apollo.goparses cache keys and field keys.ldjson.gois level 2, the schema.org block and theog:tags.extract.goholds the level accounting.extract_book.gois the book page extractor.verify.goandgoodread verifyrun it over the pinned captures.Three things measurement changed
Cache key ids come in three shapes, not one.
The spec said they are
kca://uris. The measured page has 69 of those, 29 bare integers onUser, and 30 whole JSON objects onShelving, and those objects containkca://uris several levels down. Splitting on the first colon still handles all three, but the test asserts the round trip rather than the prefix, because that is the invariant that actually holds.You cannot pick the entity by type.
The measured page carries three
Bookentities and two of them are stubs holding nothing but__typenameandid, because something on the page referenced them.Apollo.Rootgoes throughROOT_QUERY, which names the query the page ran.Only the book page is a Next.js route.
This is the big one and the spec got it wrong. Author, series, list, genre, editions and quotes are still Rails templates with no Apollo cache, and four of them carry no
og:tags either. So the ladder is per-surface: for those surfaces level 3 is not debt to be driven to zero, it is the only source there is.goodread extractionsays that rather than printing a zero that reads as good news, andTestNoNextDataOnRailsPagespins it so that if Goodreads does move them we find out from a failing test and go and delete selectors.The bits that look like details and are not
Field keys carry their arguments,
description({"stripped":true})andquotes({"pagination":{"limit":1}}), and they are parsed to the matching paren rather than string matched.TestParseFieldKeyOnEveryRealKeyruns the parser over every key in the capture, which is the test astrings.Containsimplementation fails.Reading the limit out of the key is what makes the missed sentence honest. The page asks for one quote, so the record says exactly that and names
goodread quotes 2792775, with the number and the id both coming from the page rather than from somebody's memory.Ref resolution is depth bounded with a visited set, since the book/work/bestBook triangle is on every page. The set unmarks on the way out, because two fields pointing at the same author is a shared author, not a loop, and a resolver that treats it as one silently drops the second.
Both descriptions are kept. The raw one is what a renderer wants and the stripped one is what a text pipeline wants, and deriving either from the other is lossy.
Level accounting
Every field write goes through
Extractor.set, so the report is generated rather than maintained, andTestNoDirectFieldWritesfails on any write that goes round it.A surface with no extractor yet is a gap in the work, not drift in the site, so it does not fail
--strict.Captures
Seven, gzipped, with
capture.txtas the ledger.TestCaptureLedgerfails on a file that does not say where it came from, which is the test that caught a hand-edited fixture inarxiv-cli.One capture the spec asked for is not here. A book page for a missing id returns 202 with an empty body and
x-amzn-waf-action: challenge, not a 404, so there is nothing to capture. The client already reports that as blocked rather than trying to solve it.Cross check
TestLevelsAgreeAcrossTheLaddercompares the cache againstld+jsonon title, author, image, ISBN, page count, format and language, and on rating counts with a one percent tolerance since the two blocks render a moment apart. That is what level 2 is for: when the rungs disagree, one of them is being read wrong.Verified live
Freshly fetched book page through the real client: 32 fields at level 1, one at level 2, none on selectors, with the ratings distribution summing to the published count and its weighted mean matching the published average to two decimal places. That last check is the one that catches a reversed slice, which no amount of eyeballing a bar chart would.
Gate
gofmt -l .clean,go vet ./...clean,go test ./...ok for both packages. 25 new tests, all against the captures.