Skip to content

Change probe is blind to a value that changes and changes back: compare the origin against the rendered page, not against the previous probe #138

Description

@harper-joseph

The gap

The change probe compares the origin's answer to its own previous answer. That bounds drift — a value changing after a render — but it is structurally blind to a value that changes and changes back between two probes. If a render lands inside that window, the cached page keeps the transient state and the probe never notices, because the signature it reads still equals the one it stored.

Measured on a production commerce deployment (n=143 sampled cached product pages, 112 of them inside the serve window):

  • price mismatches among served pages: 0/112 (0.0%) — the probe is doing its job on drift.
  • availability mismatches among served pages: 3/112 (2.7%), every one of them the same shape: the cached page says OutOfStock, the origin says available, and the price matches exactly.

One case traced end to end: the probe stored [39.99,35.99,35.99,true] (available) at 11:04:37Z; the page rendered at 11:10:33Z — six minutes later — capturing OutOfStock; the origin now returns exactly the stored signature again. The probe's next pass will compare equal and trigger nothing, so the page serves a wrong availability until its render interval expires (48h in that deployment).

Four alternative explanations were tested and eliminated:

hypothesis test result
endpoint is flaky 5 consecutive calls byte-identical signatures
answer varies per store 6 store identifiers incl. a nonsense one identical for every probed field
render captured a different variant compared SKUs cached page carries the same SKU as the origin's default
systematic render defect control pages controls render the correct availability

At ~180k product-page serves/hour on that deployment, 2.7% is ~5k serves/hour carrying a stale availability. A shorter sweepInterval shrinks the window a round trip must fit inside but cannot close the class, and the exposure scales with render interval — so it gets worse exactly when you stretch intervals on the strength of the probe.

Proposal: compare the origin against what the PAGE says, not against the last probe

Make the stored signature mean "what the cached page currently claims" rather than "what the origin said last time". Then the existing comparison — origin-now vs stored — becomes origin-now vs page, which is the question that actually matters for structured-data correctness, and any disagreement triggers regardless of history.

Mechanism, reusing what already exists:

  1. On render completion (RenderQueue.processJobResult), when a probe rule matches the URL and the new option is enabled, extract the rendered page's own structured-data values with the document-mode extractor already in changeProbeSpec (extractJsonLdOffers), normalize, and write that as the URL's ProbeState signature.
  2. The probe pass is then unchanged. It fetches the origin, compares to the stored signature, and triggers on difference — which now means "the page disagrees with reality".

This needs no schema change and no new storage: ProbeState already holds exactly one signature per URL, and the render result is processed on the owner node (claims are owner-scoped), which is the same node the owner-scoped sweep uses.

The one genuinely new piece: field mapping

The request-mode signature is an ordered extract array (e.g. [regularPrice, salePrice, lowestApplicablePrice, isAvailableForShip]); the page's structured data yields (price, availability) per offer. They are not directly comparable, and the correspondence is site-specific — so the rule must declare it:

pageCheck:
  enabled: true
  priceFrom: 2        # index into `extract` holding the price the page renders
  availableFrom: 3    # index into `extract` holding the availability verdict

Comparison: mismatch when the origin's price is absent from the page's offer prices, or when the origin's availability disagrees with whether any offer on the page is in stock (a page whose every SKU is out of stock while the origin says available is a mismatch — that is one of the three measured cases).

Cost

One structured-data extraction per render on pages matching a pageCheck rule — a bounded regex scan plus a JSON parse, order milliseconds against a render that already stores a ~1MB blob. Nothing added to the serve path, and nothing added to the probe path.

Design questions worth settling before implementation

  1. Device types. A URL renders per device and each render could capture different content, but ProbeState is one row per URL. Last-writer-wins, or only trust one device, or store per device?
  2. Failure semantics. If the page yields no structured data (extraction returns null), it must leave the stored signature untouched rather than write a null — same rule the probe already applies to failed probes, so a markup change cannot mass-trigger.
  3. Does this replace or supplement drift detection? Making the signature mean "what the page says" is strictly better for correctness, but it loses the ability to report a pure change rate from probe-to-probe deltas. If that metric matters, it needs its own counter.
  4. Interaction with seeding. A first observation currently seeds without triggering. Under this design a freshly rendered page always has a signature, so seeding should become rarer — worth confirming it does not mask a real disagreement on the first pass after a render.

Happy to implement once the shape is agreed — particularly the mapping surface in (the pageCheck block) and the device-type answer in (1).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions