Repository navigation
chore: sync upstream deco-cx/deco 1.211.2 into main-alt - #3
Merged
Merged
Conversation
This wrapper already measured the duration of every outgoing fetch — it just
discarded it unless a logger happened to be installed, and `logger` is null in
production:
const start = logger && performance.now();
So there was no metric answering "is the external API slow, or are we making
too many calls to it?". The only alternative was `otel_traces`, which is
tail-sampled at ~1.7% and carries no client spans for these calls at all. In
practice that meant a `load-data` span of 157s could not be attributed to
anything, and diagnosis fell back to guessing.
Adds an `outgoing_fetch_duration` histogram, dimensioned by external host and
status class.
Notes on the design, both copied from what already works in this repo:
- `unit: "ms"`. The meter provider in `observability/otel/metrics.ts` selects
bucket boundaries by unit, so "ms" picks up
`[10, 100, 500, 1000, 5000, 10000, 15000]` automatically. Recording seconds
would put every observation in the first bucket — which is exactly the bug
the @decocms/start runtime currently has on its four duration metrics
(99.8%-99.95% in bucket 1, measured).
- Low cardinality by construction: `server.address` is the host, never the path,
and status is bucketed into a class rather than the raw code. Measured on a
large VTEX storefront, a site talks to 6 distinct hosts, so this is ~6 x 5
series per site. For comparison, `loader_cache` reaches 3684 distinct label
values on a single site because it uses the full resolver chain as a label.
- Failures are recorded, not dropped. A call that hangs for 60s and then aborts
is the sample you most want and the one a success-only path loses. The error
is re-thrown untouched.
- `hostOf` returns null instead of throwing on a malformed input, and a null
host skips the sample. A metric must not be able to break the fetch path.
The logger behaviour is unchanged.
Verified: `deno check runtime/fetch/fetchLog.ts` clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`URL.host` appends a non-default port, so `example.com:8080` and `example.com` would become two distinct `server.address` label values for the same host — inflating exactly the cardinality this metric is careful about, and undercutting the "6 hosts x 5 classes per site" claim in its own docstring. `hostname` also matches semconv, where `server.address` is the address alone and `server.port` is a separate attribute. Caught by CodeRabbit on deco-cx#1218. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Authority-less schemes — `data:`, `blob:`, `file:` — parse fine but have no hostname, so they returned "" and `record` only skipped on null. That would have created a meaningless `server.address=""` series for calls that never crossed the network. Also collapses the three-branch return into a single URL construction. Verified: https://a.com/x -> a.com https://a.com:8080/x -> a.com data:text/plain,hi -> null file:///tmp/x -> null nonsense -> null Caught by cubic on deco-cx#1218. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
deco-cx#1222) * feat(preview): sandbox-less Fast Preview via pull-based draft decofile Port of decocms/blocks#463 to the Deno runtime. A production site can render a draft by PULLING the draft decofile from Studio's decofile API and rendering its own real pages against it, as a request-scoped snapshot — no sandbox daemon. - engine/decofile/draft.ts: `?__draft=<authority><path>?token=…@<version>` pointer parsing (last-`@` split), authority validation against preview API domains (scheme derived, no SSRF), host gating (DECO_ALLOWED_PREVIEW_HOSTS / site-declared previewHosts), version-bounded cache, cookie helper. - runtime/mod.ts (prepareState): the single per-request seam covering all routes. When a draft resolves, swap `state.release` for `fromJSON(draft)` + rebind the resolver (`resolver.with({release})`) — snapshot semantics for free (deletions work). Force no-store + noindex; persist the pointer cookie so /deco/render and /deco/invoke partials carry the same draft. - engine/mod.ts: re-export the public API. - engine/decofile/draft.test.ts: 9 tests / 31 steps mirroring the Node suite. Verified end-to-end against a real Fresh site (demo-linkedin): draft renders the pulled snapshot, gates (host/origin/off/cookie-nav) all behave, published traffic is untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(preview): key draft cache by host+path+version; harden cookie decode Addresses review of deco-cx#1222: - draftSource cache was keyed by `version` alone; a runtime pointed at more than one draft source that reuse a version label (`v1`, a branch id) could serve the wrong snapshot. Key on `<host><path>@<version>` so a hit is always the right source. `version` is a sha in production, but the type accepts any short label — so this closes a real collision class. - `readCookieValue` now degrades a malformed `%`-sequence cookie to null instead of letting `decodeURIComponent` throw at direct callers of the exported `draftPointerFromRequest`. - Tests: cross-source cache non-collision + malformed-cookie null (33 steps). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(preview): draft badge — always-on "you are viewing a draft" signal Follow-up to Fast Preview (deco-cx#1222). Fast Preview carries the draft pointer in a cookie across navigation, so after the first click the URL no longer shows `?__draft=` — a reviewer can forget they're on unpublished content. This adds a floating badge whenever a draft is bound, with "leave preview" and "copy a link to this exact draft version". - runtime/draftBadge.ts: self-contained HTML+JS snippet (inline styles, no site CSS/island dependency, very high z-index), on-brand (deco lime/green). Renders nothing-first and reveals only once confirmed unframed — hidden inside Studio's own preview iframe, never flashes. Pointer embedded only in the copy-link handler (script-escaped), never as raw HTML. - runtime/mod.ts (prepareState): stash the active pointer in the request bag (server-side; the cookie is HttpOnly) when a draft resolves. - runtime/middleware.ts: inject the badge before </body> on draft-bound HTML responses, alongside the existing cookie-script injection (single body buffer). - runtime/draftBadge.test.ts: builder + injector unit tests (escaping, iframe gate, first-</body> injection). Verified in-browser against demo-linkedin: badge floats bottom-center on a draft render, absent on published traffic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(preview): match the Node draft badge exactly (pill + popover, deco mark) Rework the badge to be a faithful vanilla port of @decocms/blocks's React DraftPreviewBadge instead of the ad-hoc pill: - bottom-left "Preview mode" pill with the inlined deco mark (runtime/decoMark.ts, self-contained data URI — no asset pipeline / 404 risk). - click opens a white popover above it with "Exit preview" (arrow icon) and "Share preview" (three-node icon, → "Copied!"), matching colours, paddings, radii and shadows. - outside-click / Escape dismissal; exit → ?__draft=off; share copies ?__draft=<pointer> (prompt fallback when clipboard is blocked). - English copy, same as Node. Verified in-browser against demo-linkedin: pill bottom-left, popover on click. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…tion-metric feat(fetch): record outgoing fetch duration by external host
…eco-cx#1225) Fast Preview renders the page shell against the draft, but deferred sections are lazy-loaded client-side via a separate `/deco/render` fetch that resolves its draft from the `__deco_draft` cookie. That cookie is `SameSite=Lax`, so inside Studio's cross-site preview iframe the browser does not attach it to the subrequest — the section renders against the PUBLISHED release while the shell rendered the draft, producing duplicated or stale sections (a removed section reappears). Opening the same URL in a top-level tab works because the cookie is sent in first-party context. `useSection` runs server-side and the runtime already stashes the active pointer in the request bag during `prepareState`. Carry it forward as an explicit top-level `?__draft=` param on the render URL: `draftPointerFromRequest` prefers the param over the cookie, so lazy rendering stays on the draft regardless of frame context. The param is only appended when a draft is actually bound, so ordinary traffic is untouched. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
deco-cx#1224) * feat(preview): infer <site>.deco.site as an allowed draft preview host The draft-preview allowlist (DECO_ALLOWED_PREVIEW_HOSTS / site-block previewHosts) required per-site opt-in, but DECO_SITE_NAME isn't always in the env. Derive the deco-hosted preview domain from the site name the runtime already resolves (opts.site ?? DECO_SITE_NAME ?? …) and register it via setDecoSiteHost at Deco.init. <site>.deco.site is merged ON TOP of the env/site-block list (never replacing it) so a signed draft grant can preview on deco-operated infra out of the box. The random dev fallback registers nothing, and a custom production domain is never inferred — it stays inert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(preview): add kill switch and document threat model for inferred host Address review of the `<site>.deco.site` inference: - `DECO_ALLOWED_PREVIEW_HOSTS=none` is now a kill switch that disables preview entirely (inferred host + site block included), restoring the env var's "stop a bad rollout without a deploy" escape hatch — which the merge-on-top had removed for the inferred host. - Document the post-change threat model: a named site is no longer inert by default, the request host is spoofable, so the signed `?__draft=` grant is the sole remaining gate; host-scoping only bounds blast radius. - Fix the now-stale `isDraftPreviewEnabled` docstring. - Tests: kill switch, and undefined (random dev fallback) registering no host. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…eco-cx#1227) The `deviceHint` query param is an explicit override, only ever present when a tool (e.g. the Studio preview device toggle) forces a device. Real client traffic never carries it, yet `deviceOf` evaluated `cf-device-type` first — so behind Cloudflare the requesting browser's real device (desktop) always won and the forced `deviceHint=mobile` was silently ignored, both in sandbox preview and Fast Preview. Move `deviceHint` to the top of the resolution chain so the override is honored when present; production traffic (no param) still falls through to cf-device-type / user-agent unchanged. Adds utils/userAgent.test.ts covering the priority order.
…aft preview host (deco-cx#1226) Extends the deco-hosted preview inference (<site>.deco.site, deco-cx#1224) to the per-deploy preview CDN. Unlike the stable .deco.site domain, the deploy URL carries a fresh <hash> label every deploy (envs-als-storefront--4l18ts.decocdn.com), so it can't be a fixed allowlist entry — it's matched as a PATTERN instead. - setDecoSiteHost now stores the raw site name and derives both deco-operated hosts from it: <site>.deco.site (exact) and envs-<site>--<hash>.decocdn.com (pattern). - previewConfig centralises the exact-host list, the deploy-preview prefix, and the kill switch in one read. - matchesDeployPreview constrains <hash> to a SINGLE DNS label (no dots), so nothing under an attacker-controlled *.decocdn.com subdomain (envs-<site>--x.evil.decocdn.com) can widen the match. Merged ON TOP of the env/site-block list, killed by DECO_ALLOWED_PREVIEW_HOSTS=none, and only inferred for a named site — same guarantees as the .deco.site host. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…emo (deco-cx#1230) Resolvables (named decofile blocks) are memoized per request by key in `context.memo`. The cached value is the *promise* returned by the first consumer to resolve that key, and every other reference to the block awaits that same promise. `website/sections/Rendering/Lazy.tsx` resolves its inner section under an already-aborted `AbortController` to render a loading fallback fast. If a section resolved this way is a named block that is also referenced elsewhere on the page, the aborted resolution's rejected promise gets cached and served to those other references — they all fail with `AbortError` and the surrounding render collapses to empty. This surfaces on multivariate pages (`website/flags/multivariate.ts`): adding a second page variant is enough to change resolution ordering/concurrency so the aborted lazy read wins the memo race, and the page renders blank ~80% of the time (flaky, since it's a race). The abort belongs to one consumer, not to the block, so it must not be cached. Fix: when a memoized resolvable rejects with an abort, evict the cache entry so later reads re-resolve; and if a consumer whose own signal is still live inherited a co-scheduled aborted promise, re-resolve fresh under that live caller. Consumers that are themselves aborted still reject, as before. Adds sequential and concurrent regression tests. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…cx#1232) Resolve hints are cached by resolveType (block id) and derived from the release's resolvables (`context.resolveHints[resolveType] ??= traverseAny(...)` in engine/core/resolver.ts). Fast Preview binds a request-scoped draft by swapping the release via `resolver.with({ release: draftProvider })`, but `.with` copied the base resolver's hints (`{ ...this.resolveHints }`). On a long-running server that serves the published release, the hint cache for a block gets populated with the published shape. When the draft is then resolved, it reuses those stale hints — so a block whose shape changed between releases (e.g. a page whose `sections` was a plain array when published but a `website/flags/multivariate` flag in the draft) resolves against the old shape and silently drops everything the old hints don't cover. Symptom: the draft renders a blank page (only globally-injected sections survive; the whole page body, footer included, is gone), while a fresh process that saw the draft first renders correctly. Fix: when `.with` swaps the release, start from empty hints instead of inheriting them. Hints belong to a release's resolvables, so a different release invalidates them — this mirrors the `release.onChange` invariant in the constructor, which already clears hints on every release change. Only the two preview/release-swap call sites are affected (Fast Preview draft, block preview); normal traffic and the setup path (which pass no `release`) are unchanged. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…1233) Newer Deno (the CI pulls `deno-version: v2.x`) rejects a bare `file:` as a reload specifier: `error: invalid reload URL: 'file:'`, failing the "Setup deno" job on every run. The flag is also redundant — the preceding "Build Deno Module" step already runs `deno run --reload mod.ts`, so all deps are cached by the time this step runs; `deno cache ./mod.ts` just fetches anything still missing, which matches the step name. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
deco-cx#1234) * fix(engine): dispose superseded ReleaseResolver's release subscription Every `new ReleaseResolver` subscribes to its release in the constructor and the returned Disposable was dropped. `installApps` rebuilds the resolver on each decofile change (`currentResolver = currentResolver.with(...)`), so every publish left one more superseded resolver permanently reachable from the provider's listener list, retaining its whole resolvables/resolvers/ resolveHints graph. Measured on a production replica (same runner image, Deno 1.44.4, real decofile): 8.5 MB of heap retained per publish, after two forced GCs, linear over 10 reloads — same with concurrent load (8.8 MB). A site publishing 24-36 times a day accumulated ~300 MB of unreclaimable heap in 44h. Keep the Disposable, expose `dispose()` (idempotent, also via Symbol.dispose), and drop the superseded resolver's subscription in `installApps`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(decofile): make the resolver's release subscription actually disposable The previous commit kept the Disposable returned by `release.onChange` and dropped it when a resolver was superseded, but that was inert in production and incomplete across publishes. Review of deco-cx#1234 surfaced both: - `fromEndpoint` (engine/decofile/fetcher.ts) subscribed through `decofileProviderPromise.then((r) => r.onChange(cb))` and returned a disposable whose `[Symbol.dispose]` was empty, discarding the real inner subscription. `getProvider()` wraps every `folder://`, `file://`, `deconfig://` and `http(s)://` release in it, and `compose()` returns a single provider unchanged, so every real deployment disposed nothing. It now proxies the eventual subscription, including disposal that happens before the provider promise settles. - `fulfillContext` rebuilds the runtime resolver from the same stable base on every publish and overwrites `ctx.runtime` without disposing the outgoing one, so one resolver graph per publish stayed reachable — the very scenario the fix targets. Track the installed resolver and dispose it once its replacement is live. On the zero-apps path `runtimePromise` only takes the first resolution, so a later publish there discards the resolver it just built: dispose that one instead. Also guard `splice(indexOf(cb), 1)` in the four `onChange` implementations (fs, fsFolder, realtime, fromJSON): now that disposal actually runs, a second call would find -1 and drop the last, still-live callback. `Symbol.dispose` becomes an alias of the bound `dispose` field instead of a prototype method that breaks when detached, and two dead optional chains are removed. The new test counts live subscriptions through `fromEndpoint` by way of the `deco:hmr` event; it fails (26 live subscriptions instead of 1) without the fetcher fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…deco-cx#1238) * feat(draft): allow fast preview on local dev hosts and the dev tunnel The `?__draft=` fast preview was inert during local development: it only rendered drafts on hosts named in `previewHosts` / `DECO_ALLOWED_PREVIEW_HOSTS` or the deco-hosted `<site>.deco.site` / `envs-<site>--<hash>.decocdn.com` domains. But `deno task start` serves on `localhost:<port>` and on the per-developer dev tunnel `<env>--<site>.deco.{host,site}` (daemon/tunnel.ts), so a draft never previewed locally without extra config. Now local dev hosts (`localhost`, `127.0.0.1`, `*.localhost`, any port) are always allowed, and the dev tunnel is inferred from the resolved site name with its `<env>` label pinned to a single DNS label — the same anti-widening constraint as the per-deploy `<hash>`. `DECO_ALLOWED_PREVIEW_HOSTS=none` still kills everything, local and tunnel included. The signed `?__draft=` grant remains the actual capability; host-scoping only bounds blast radius. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(draft): address review — drop .deco.site tunnel apex, match IPv6 loopback Two issues from PR review: 1. The dev-tunnel match no longer accepts the `<env>--<site>.deco.site` simpletunnel fallback: `.deco.site` is also the stable production apex, so matching it widened the draft gate on production, not just dev machines. Only `<env>--<site>.deco.host` (the `DECO_HOST` default) is matched now; the rare `DECO_HOST=false` opt-out previews via localhost or an explicit allowlist. 2. `isLocalDevHost` now treats the IPv6 loopback as local — `::1`, `[::1]`, and `[::1]:port` — which the port-splitting previously mangled to a non-match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.
O que é
Merge (merge commit) de
deco-cx/deco@main(1.211.2, 28 commits) emmain-alt. Base comum: 1.203.0 (40762b5).O que vem do upstream
ReleaseResolversubstituído é descartada.?__draft=/ cookie__deco_draft, badge) — inerte sem pointer e em host de produção custom.deviceHint(query) passa a vencercf-device-type— nenhum código nosso nem do app usa.fetchLog: duração de fetch externo por host.Revisão contra as nossas regras
engine/core/resolver.tsehooks/useSection.ts— hunks disjuntos.blocks/loader.tssegueresolver: blockKey).computeRenderCb: byte-idênticos;/deco/rendergera a mesma URL (o__draftsó entra com rascunho ativo).__deco_draft(HttpOnly, só com?__draft=em host permitido);stripFrameworkSetCookiesno mesmo gate.engine/core,hooks,blocks,utils/userAgent— 12 passed (22 steps), deno 2.8.2.Merge: merge commit (não squash) — o site pina o SHA deste branch.
🤖 Generated with Claude Code