Skip to content

fix(runner): attribute example.saved to its source example, batch rollup inserts (DEV-3146) - #400

Merged
demtario merged 2 commits into
masterfrom
worktree-dev-3146-example-analytics
Oct 1, 2026
Merged

demtario merged 2 commits into
masterfrom
worktree-dev-3146-example-analytics

Conversation

@demtario

@demtario demtario commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Context

DEV-3146 follow-ups from #371 (ADR-0042 example analytics). Of the three open items, two are addressed here and one is deliberately left open.

Funnel "saved" was always 0. A save carried kind=saved, ref=<demo id>, and the funnel panel filters blob17='docs'. The ticket offered a rollup lineage join or carrying the source on the event. A rollup join would not fix the panel, because the panel reads Analytics Engine, not D1, so the attribution goes on the example.saved point itself. The API worker resolves the row's forked_from to the source example (docs guide and area, starter, import, payload), following saved-demo hops (capped at 5, cycle-guarded). area is not derivable from the lineage, and the worker has no assets binding, so it bundles a compact generated docsPath -> [guide, area] map (workers/api/src/docs-taxonomy.generated.ts, about 1,600 paths) that the import-docs workflow regenerates, plus a freshness test. Anything that does not resolve (MCP demos, unknown docs paths, broken lineage, a failed lineage read) keeps the old kind=saved, ref=<demo id> shape and never fails the save. Saves counted before this change keep the old shape.

D1 per-invocation query limit. Confirmed from the D1 limits docs: every statement inside a batch() counts against the 1000-query limit (Workers Paid), and a statement is capped at 100 bound parameters. writeExampleDaily used one statement per row. It now uses 8-row multi-row INSERTs (12 columns, 96 parameters) in the same single batch as the DELETE. A day needing more than 500 statements (about 4,000 rows) throws before the DELETE, so the previous rows survive and Sentry gets the error. Splitting into several batches was rejected because it saves no queries and a failure part-way would persist a half-written day.

serve.share undercount: not changed. The ticket's condition ("if a server-side share route is ever added") is not met, since /share/:id is still served by the assets-only deploy. That checkbox stays open.

ADR-0042 §2 and §5 and observability-contract §5 are updated.

Types of changes

  • New example
  • Update to an existing example
  • README / documentation change
  • Demo runner (runner/) change
  • CI / tooling change

How was this verified?

  • node --experimental-strip-types --test pipeline/*.test.mjs run raw (not through an rtk filter): 2607 pass, 0 fail, 0 skipped.
  • pnpm --filter @handsontable/demo-api typecheck is clean.
  • Revert-check: with index.ts and reconcile.ts restored to master, 6 of the new tests fail (3 attribution, 3 statement budget); the other 4 new tests are guard cases that already pass on master.
  • New tests: example-saved-attribution.test.mjs (docs, starter, chained, looping/too-deep/dangling, MCP/unknown path, lineage-read failure, no read when no point is written), example-daily-statement-budget.test.mjs (real SQLite from the real migrations: statement count, parameter cap, throw-before-DELETE, exactly-at-budget), docs-taxonomy-generated.test.mjs (committed file equals what the manifests produce).
  • Not verified against production Analytics Engine: after deploy, a docs-fork Save should show up in the funnel's "saved" column for its area.

Checklist

  • New/renamed example: added to runner/config/frameworks.json (see CONTRIBUTING.md); otherwise it won't appear on demos.handsontable.com
  • New example: added a row to the tables in README.md
  • Ran pnpm build (and pnpm dev) in the affected example/server-example locally

None of the checklist items apply: no example is added or renamed, and this changes the runner only (typecheck and the pipeline suite stand in for the example build).

Related issue(s):

  1. DEV-3146

Note

Medium Risk
Changes server-side analytics attribution and nightly D1 rollup writes; mis-attribution or rollup failure would skew dashboards but saves and core API behavior are guarded with fallbacks and pre-DELETE budget checks.

Overview
Fixes DEV-3146 so the Examples funnel can count saves: example.saved no longer always emits kind=saved with the demo id. After a successful editor Save rebuild, the API worker walks forked_from (saved-demo hops, max 5, cycle-safe) inside waitUntil—after the D1 update— and attributes the point to the source example (docs guide/area/bucket, starter, import, or payload). Unresolvable lineages or failed reads keep the old shape and never block the save.

Docs guide/area for docs: lineages come from a new generated docs-taxonomy.generated.ts, built from docs-example manifests in import-docs.mjs (buildDocsTaxonomy / writeDocsTaxonomy). The import-docs workflow regenerates that file alongside the bucket index; a freshness test pins it to the manifests.

Separately, writeExampleDaily batches example_daily writes as multi-row INSERTs (8 rows × 12 columns) to stay under D1’s per-statement parameter cap and per-invocation statement budget; oversized days throw before the day’s DELETE so prior rollup rows remain. ADR-0042 and the observability contract document the new attribution and rollup behavior.

Reviewed by Cursor Bugbot for commit 3824cf1. Bugbot is set up for automated code reviews on this repo. Configure here.

demtario and others added 2 commits October 1, 2026 09:25
… rollup inserts (DEV-3146)

The Examples funnel's saved column was always 0: saves carried kind=saved with the demo id, and the panel filters kind=docs. The API worker now resolves the row's forked_from to the source example (docs guide/area from a bundled generated taxonomy, starter, import, payload), following saved-demo hops.

writeExampleDaily used one statement per row; D1 meters each batch() statement against its 1000-query limit. Rows now go in as 8-row INSERTs in the same batch, and an oversized day throws before the DELETE.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve the docs taxonomy per bucket (overrides over a newest-bucket default, numeric bucket order), run the lineage walk inside the waitUntil chain after the D1 update, fix the hop-cap off-by-one, and document that the funnel's saved stage counts descendant saves.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@demtario demtario self-assigned this Oct 1, 2026
@demtario
demtario merged commit e955461 into master Oct 1, 2026
10 checks passed
@demtario
demtario deleted the worktree-dev-3146-example-analytics branch October 1, 2026 07:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant