Show transaction status, action, and timestamp in social link previews - #3596
Show transaction status, action, and timestamp in social link previews#3596tom2drum wants to merge 25 commits into
Conversation
Medium task, five subtasks: make the `og` block a default/enhanced template layer, share the interpretation currency rounding and render summaries as plain text, derive the status/action/timestamp params, wire the bot-gated server-side fetch plus the /tx/[hash] templates, and verify on a demo (agent deploys, human confirms the real card). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. To trigger a review, include ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The `og` entry in the route template map was static data: `og.description` was passed through to the meta tag without ever reaching `compileValue`, so it could hold no placeholders, and the record required a description and an image together. That ruled out a per-route dynamic OG description. OG title and OG description now each accept the same `default`/`enhanced` shape as the metadata templates and are compiled against the same params; the image is independent of both. A route that declares no OG template falls back to the page title and description — written out explicitly so the resolution rule is uniform, which is why routes without an OG description now emit an `og:description` carrying the text crawlers previously inferred from `<meta name="description">`. The new `hash_short` param shortens a route's hash the way `truncation="constant"` does on the page. No route declares OG templates yet — `/tx/[hash]`'s land in the next steps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The OG description needs the transaction action to read exactly as the subheading on the page does, but the summary is a template of typed variables that only `TxInterpretation` knows how to turn into React elements. A plain-text renderer walks the same parsing pipeline and maps each variable type to the text its UI counterpart displays. Two pieces of that mapping were inline in components and are now shared, so the preview cannot drift from the page: the currency rounding ladder, which the interpretation component calls instead of holding its own copy, and the address name chain, extracted into the address slice and called by `AddressEntity` itself. Neither component's output changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turns the transaction and its interpretation summary into the three strings the OG description template compiles — status word, action, UTC timestamp — or nothing at all when any of them is missing, which is what keeps the template's all-or-nothing rule true at the source. The unit lives in the tx slice rather than beside `generate()`: metadata is regenerated on every client-side route change, so anything reachable from there ships to all users, and the responses would otherwise be serialized into `__NEXT_DATA__` whole. Three short strings instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A worktree under `.claude/worktrees/` is a full checkout of the repo, so every tool that walks the tree from the root sees each file twice: cspell and the two test runners duplicate their work, tsc type-checks a copy with no dependencies installed, and eslint exhausted the Node heap outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A shared transaction link produced a card titled with the full 66-character hash and described by the generic page copy, since the route declared no OG templates and crawlers never run the client-side metadata update. `/tx/[hash]` now carries a short-hash OG title for every crawler, and its `getServerSideProps` fetches the transaction and its interpretation summary for social-preview bots specifically, turning them into a status · action · timestamp description. Anything less than all three parts falls back to the generic description, so a pending transaction or a failed request degrades to exactly today's card. Both requests run in parallel with a 2 s timeout. That is longer than the other routes allow because both endpoints compute their response on the first request for a transaction and cache it afterwards, and a crawler is always that first request: on eth mainnet a cold summary averages 0.95 s against 0.30 s warm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TEAM.md mixed the roster with the rules for using it: who picks a task's contacts, that product questions go to a channel rather than a DM, which channel, and how to mention the addressee. Those already lived in `grill-the-task` and `to-spec`, so the file was a second, drifting copy — the skills are what an agent actually reads when routing a question. What stays in TEAM.md is the data plus what makes it readable: the meaning of the `default` marker, where IDs come from, and each team's ownership. One rule was only implied in the skill and is now explicit there: the roster default is what to record when the developer has no task-specific pick. The backend section also gains Slack group IDs beside its members, so a question can be addressed to a whole team once a skill has a rule for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The demo proved the wiring: every branch of the description was observed on a public URL, including the interpretation summary and the called-method fallback with a named address. It also showed the feature barely enhances on that instance, so the subtask now carries the sampling behind that — both transaction endpoints measured against a network control, and the ruling not to raise the timeout, since no crawler waits as long as the API takes. The latency goes to the backend team as Q2; it gates the release decision rather than any remaining work, because the code degrades to today's card wherever the API is slow. Also corrects a claim the spec made from the start: `logRequestFromBot` and `fetchApi` do record their metrics, but the SSR bundle keeps a registry of its own, so `/api/metrics` never exports them and the timeout evidence had to come from sampling the API directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `review` variant exports them, `review-2` did not, so `/api/metrics` answered 404 there — and the metrics are how a demo shows what the app did server-side, which is the point of deploying one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three open questions came back. The demo verification is done: the card was confirmed in Telegram and X on an eth-mainnet demo, and the timeout question was settled by the metrics once they worked — on the loaded instance the mandatory call aborted on every crawler request, so the answer is to keep 2 s and change where the data comes from. That leaves two pieces of work rather than one. The backend is building an endpoint shaped for this preview, with switchable ens/metadata/summary preloads, going to staging to be measured — subtask 6, a brief until its shape is agreed. And the Noves decision needs code: the product call is to emit no enhanced description there, but an instance running Noves currently still requests the Blockscout summary and lands on the called-method fallback, which is the option that was rejected — subtask 7, scoped and ready. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An instance runs a single interpretation provider. On a Noves one the transaction page renders Noves' prose and the Blockscout summary endpoint has nothing to serve, so the preview fell through to the `called … on …` line — the option the PM ruled against, since pointing crawler traffic at a third party's slow API is not ours to decide. Both gates now require the provider to be `blockscout`: the util has no action without it, and the page makes no request at all. That also drops the `core:tx` request on instances with the feature off entirely, where it could never produce a description — and `none` is the provider's default.
All three read OG tags, none of them was in `detectBotRequest`, so their crawlers were treated as ordinary visitors and every route that enhances its preview for social bots — transaction, address, token, NFT instance, stats — served them the generic description instead. Reported for WhatsApp from a real card. Discord and LinkedIn match the `…bot` suffix rather than the bare product name, because both also ship an in-app browser whose user agent carries that name and whose requests are real visitors.
Measured on the loaded instance: with `preload_ens`, `preload_metadata` and `decode_input` all on it answers in 0.66 s at p50 and never crossed 2 s, so status and timestamp now arrive inside the budget where they never did, and the preloads cost nothing worth trading away. The summary keeps its own request and its own timeout — it is still the slow one, and coupling it to the preview would trade a degraded description for no description.
The endpoint and the metadata fix both reached `dev`, so the response type can be published and the address tags survive the switch — the two things that were holding this subtask open. Latency was re-measured after the fix went live, since the earlier numbers were taken while the metadata preload silently did nothing: with all three preloads on the request costs the same as with none, so the spec asks for all of them.
The mandatory half of the description — status and timestamp — came from `core:tx`, which on a loaded instance answers too slowly for a crawler, so the card fell back to the generic text there. The new endpoint returns those fields alone and answers in half a second on that same instance. Its three preloads are all requested: measured together they cost what none of them cost, and without `decode_input` the fallback action line would name the method by its selector. The summary keeps its own request, so when it is slow the description degrades to that line instead of disappearing. `addressToPlainText` now takes the same name-source type `getAddressName` defines, since the preview's address carries only the fields needed to name it.
Both demos ran on this branch's image: the production instance the endpoint was built for enhances every crawler request, a dev instance enhances none. The spec records why — that instance answers in ~1.7 s against a 2 s budget, warm or cold — so the next person does not read a slow demo as a broken feature.
# Conflicts: # .agents/TEAM.md # .agents/skills/grill-the-task/SKILL.md # package.json # pnpm-lock.yaml
`Preview` and `PreviewAddress` said nothing about what they previewed once generated into a spec whose component names are global, so they were renamed upstream (blockscout/blockscout#14711) before anything depended on them.
Every page claimed `summary_large_image`, but the entity pages carry no image on purpose — a generic banner would push the description, which is the point of the card, below itself. X answers a promised-but-missing image with the compact card and a grey placeholder in the image's place, so the type now follows what the page has.
X keeps the slot whichever card type it is, and puts a grey placeholder in it when the page offers nothing, so an entity page gets the instance's generated icon — square, which is what a summary card's slot wants. Only the Twitter tag: `og:image` stays unset there, since Telegram and WhatsApp drop the image entirely rather than padding it, and their cards read better that way.
Three of the eight findings asked for code or record changes: `TxOgDescriptionParams` moves to the slice's `types/api.ts`, the path a cross-domain type import is required to go through. The status mapping's comment described a contract that no longer holds: `fetchApi` stopped parsing error bodies in #3623, which reached this branch through a `main` merge, so a miss now arrives as `undefined` rather than as `{ message }`. The distinction the code draws is still load-bearing — every 404, failure, and timeout would otherwise read as `Pending` — but its reason changed, and the test asserting the old contract is gone. The spec gains the two site-wide changes the PM asked for once real cards were in front of her, neither of which was an original AC: the wider crawler set, and the X card image.
`FAVICON_MASTER_URL` is read by the container start-up script rather than by app code, so it carries no `NEXT_PUBLIC_` prefix and `/node-api/config` never offered it. A preset could not copy it, and the favicon generator fell through to its documented default, `NEXT_PUBLIC_NETWORK_ICON` — which is a bare transparent glyph meant to sit on the app's own background, not the filled square an operator supplies. The difference is invisible in a browser tab and obvious in an X card, where the icon lands on the reader's theme background. Exposing it is safe: the image it points at is already public, and the envs validator filters to `NEXT_PUBLIC_` before its unknown-key check, so nothing downstream has to learn about the key. The allowlist is deliberately narrow — it may only ever name a value that is public regardless.
tom2drum
left a comment
There was a problem hiding this comment.
| Reviewed | 73171c99c382c3ff3e173f4784b2cd5d811f856c → b485abf0df8a55a4561d9ead87f91d0d4b9309cf |
| Round | 2 of 3 |
| Findings | 0 blocker · 0 major open · 3 nit deferred |
| By axis | Spec 0 open · Standards 0 open (1 reject accepted) · Correctness 0 open |
| Outcome | clear |
Arbitration
| Finding | Ruling |
|---|---|
| F1 PageMetadata twitter card / favicon image | fixed-verified — spec follow-ups + out-of-scope now own the behaviour |
| F2 WhatsApp / Discord / LinkedIn crawlers | fixed-verified — recorded in Follow-ups |
F3 TxOgDescriptionParams cross-domain import |
fixed-verified — now via slices/tx/types/api.ts |
| F4 slice → feature plain-text imports | rejected-accepted — compose rule is UI composition, not pure helpers |
F5 fetchApi error-body comment/test |
fixed-verified — comment + trap updated; dead test removed |
| F6–F8 nits | deferred (unchanged) |
No regressions from the fix commits. b485abf0d (expose FAVICON_MASTER_URL to presets) is adjacent tooling for the X card icon, not a fix regression.
— Reviewed by Cursor Grok 4.5
End-to-end proof that a preset carries `FAVICON_MASTER_URL` needs a source instance that already serves it, and none does yet — the variable only starts appearing once an instance runs a build with the config endpoint change. Setting it here stands in for that operator value so the demo can be checked now. Temporary, and it cannot escape the review environments: the entrypoint reads `.env.extra` only inside `load_envs_from_preset`, which returns early when no `ENVS_PRESET` is set — which is every non-preset deployment.
Description
Resolves #3593.
A shared transaction link produced a near-useless card: the OG title carried the full 66-character hash and
there was no
og:descriptionat all, so Telegram and X fell back to the generic page description. The card nowdescribes the transaction:
The action reads exactly as the transaction page's subheading, including its fallback chain and amount
rounding, because both now render from the same code.
Reaching that needed two structural changes. The
ogblock in the metadata template map became a realtemplate layer with
default/enhancedvariants (previouslyog.descriptionwas passed through raw and nevercompiled), and
/tx/[hash]'sgetServerSidePropsgained a bot-gated server-side fetch — crawlers don't run JS,and
metadata.update()only ever touches<title>and<meta name="description">.Decisions worth knowing, all recorded in the spec:
<title>keeps the full hash and<meta name="description">keeps the genericcopy; only social-preview crawlers get the enhanced description.
generate()has no notion of bot type — thepresence of
apiDatais the only signal.core:txsupplies the status and timestamp thedescription cannot do without, and on a loaded instance it answered too slowly for a crawler (aborted on 6 of
6 requests at a 2 s timeout). The Core API team added
/api/v2/transactions/:hash/previewfor this feature; against that same instance it answers in 0.51 s at p50and the card now enhances on 6 of 6. Raising the timeout instead was rejected: crawlers wait single-digit
seconds, and a card that fails to render is worse than a plain one.
/summaryis by far the slowest call, so coupling them would turn every slow summary into a lostdescription instead of one that degrades to
0x9a...71a3 called transfer on FiatTokenProxy.compileValuecontract. Accepted losses: a pending transaction (notimestamp), an unresolvable action, and any failed, timed-out or 404 request — each falls back to today's card.
running the Noves provider, makes no request at all: there is no action to be had, and the PM ruled that
quietly pointing crawler traffic at a third party's slow API isn't ours to do.
summary_large_image, but entitypages carry no image on purpose, and X answers a promised-but-missing image with the compact card and a grey
placeholder where the image would be. The type is now
summarywhen there is no image, so the card rendersas title and description alone. Applies to every imageless route, not just transactions.
back to the generic description on every OG-enhanced route (address, token, NFT, stats), not just this one.
Fixed here since WhatsApp is where it was noticed.
Also included: the currency rounding and summary-to-text rendering extracted from
TxInterpretationso thepreview and the page cannot drift, and
getAddressNameextracted fromAddressEntityso an address is labelledthe same way in both.
Environment variables
None. The feature reuses the existing
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED, which defaults to on.Minimum API version
The enhanced description needs
GET /api/v2/transactions/:hash/preview, added inblockscout/blockscout#14638 with a follow-up fix in #14703. Both are merged but not in a released version
yet (latest release is v11.2.6), so the requirement is the first Core API release that contains them.
This is not a hard requirement: on an instance whose backend predates the endpoint the request fails and the
card keeps the generic description, exactly as it does today when a request times out. Deliberately no
fallback to
core:tx— chaining a second attempt would spend a crawler's patience precisely on the instancesthat are already slow.
Breaking or incompatible changes
None for deployments.
One deliberate output change: routes with no OG description template now emit
og:descriptionexplicitly,carrying the same text crawlers already inferred from
<meta name="description">. This is visible insrc/shell/metadata/__snapshots__/generate.spec.ts.snapand was chosen over an implicit fallback so the tag isalways present. Every other route's
title/description/og:title/og:imageoutput is byte-identical.Additional information
@blockscout/api-typesis pinned to a beta (0.0.1-beta.eeda17a) because the endpoint's schema is not in astable release yet; it needs re-pinning to the stable version once the Core API release lands.
Preview/PreviewAddressupstream, which said nothing about what they previewedonce generated into a spec whose component names are global; renamed to
TransactionPreview/TransactionPreviewAddressin chore: Name the transaction preview schemas TransactionPreview blockscout#14711 before anything depended on them.🤖 Generated with Claude Code