Skip to content

fix(web): telemetry route runtime, cron facts validity, locale 404, usage copy - #6749

Merged
Hmbown merged 8 commits into
mainfrom
fix/bh2-web-site-runtime-facts
Sep 30, 2026
Merged

Hmbown merged 8 commits into
mainfrom
fix/bh2-web-site-runtime-facts

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

No-Issue: verified bug-hunt findings

Four fixes in the website's runtime-facts and usage-counting code. Each one was checked again on current main (6779519) before the fix, and each has a regression test that fails when the fix is taken out.

1. /api/product-telemetry declared the edge runtime, which the Cloudflare adapter does not support

Problem. web/app/api/product-telemetry/route.ts was the only file in web/app with export const runtime = "edge". @opennextjs/cloudflare does not support the edge runtime; its own migrate command warns you to remove that line. The adapter's server bundle also replaces Next's edge runtime with an empty shim (dist/cli/build/bundle-server.js). This PR does not show how the deployed route behaved while the line was there: no deployed request or ingest record was checked, and the tests call the handler directly. If the route did fail, the client drops failed flushes, so the failure would not have been visible.

Now. The line is removed, and a comment explains why. The route only uses fetch, Request and TextDecoder, and all three work in the default runtime.

Tests. lib/product-telemetry-route.test.ts:

  • A source check fails if any file under app/ opts into the edge runtime. Without the fix it failed and found the telemetry route.
  • An empty POST returns 200 {accepted:false,reason:"disabled"} when no ingest URL is configured. With the canonical ingest configured it returns 422 {reason:"schema"} and nothing is forwarded.

A deployed smoke check (POST {} should never return 5xx) still needs someone with deploy access. This PR does not deploy anything. (Wording narrowed in 3523cb5 after an independent review: the earlier text claimed a production 500 on every method and a complete telemetry outage. No receipt supports that claim, so it has been removed from this description and from the source comments.)

2. Facts drift wrote KV snapshots that its own validator rejects (release URL casing)

Problem. fetchLatestPublishedRelease stored GitHub's html_url. GitHub now returns the repo's canonical casing (Hmbown/Codewhale). isRepoFacts needs the URL to equal https://github.com/Hmbown/CodeWhale/releases/tag/<tag> exactly, so it rejected every cron-written snapshot. /api/facts then reported invalid-kv-snapshot, and the facts no longer updated themselves.

Now. The URL is built from the validated tag, the same way the build-time path in facts-lib.mjs requires. runFactsDrift also runs isRepoFacts on the derived facts and returns ok:false without writing anything if they fail.

Tests. lib/facts-drift.test.ts:

  • A fixture where GitHub answers with Hmbown/Codewhale now yields the canonical URL and passes isRepoFacts.
  • runFactsDrift against an in-memory KV writes a facts:current value that isRepoFacts accepts.

Both failed without the fix.

3. Facts drift dropped the ModelScope provider (merges two duplicate findings)

Problem. ApiProvider::Modelscope is mapped in web/scripts/facts-lib.mjs (build facts have 50 providers), but the runtime copy in facts-drift.ts did not have it. deriveProvidersFromConfig logged a warning and dropped it. Fix 2 makes cron snapshots valid again, and a valid snapshot would have removed ModelScope from /models, /product and /api/facts. That is why these two fixes land together.

Now.

  • The runtime map is now the module-level PROVIDER_LABELS and includes ModelScope.
  • facts-lib.mjs exports PROVIDER_LABEL_MAP, with a small facts-lib.d.mts declaration so TypeScript can import it.
  • The existing check-facts gate already fails when facts-lib does not map a config.rs variant.

Tests. A test asserts that PROVIDER_LABELS deep-equals PROVIDER_LABEL_MAP. It fails (diff shows the Modelscope entry) when the new line is removed.

4. Dotted single-segment paths returned the home page with HTTP 200 and an invalid lang

Problem. Middleware leaves any path containing . alone, and app/[locale]/layout.tsx never checked its locale param. So /wp-login.php rendered the home page with HTTP 200 and <html lang="wp-login.php">, a soft 404 that crawlers could index.

Now. The layout calls notFound() when !isValidLocale(locale). This check uses the same locales list as generateStaticParams and the middleware's pathLocale. The old as Locale casts are gone because the check narrows the type.

I chose not to add dynamicParams = false. That setting is inherited by child segments and would also change the dynamic [...rest] catch-all.

Tests. lib/locale-layout.test.ts renders the layout directly (with next/font/local stubbed):

  • wp-login.php rejects with the 404 not-found digest. Without the fix the promise resolved.
  • en still renders with lang="en".

5. Usage-counting copy did not match the counters the site sends

Problem. The privacy-page summary (lib/content/usage-counting.ts, en and zh) and the privacy policy's "Anonymous usage counting" section (lib/legal-copy.ts) listed "downloads", but no call site records download. They left out the login/signup link-click counters (data-usage) and error_shown (the error boundary), which are recorded and sent.

Now. Both texts list page views, documentation views, install-command copies, sign-in and sign-up link clicks, and error pages shown. "Downloads" is removed.

Not changed here, for a human to decide:

  • LEGAL_UPDATED (a legal date that a test pins).
  • The telemetry NOTICE_VERSION (bumping it would change ingest validation for every surface).

Tests. lib/usage-counting-copy.test.ts scans app/ and components/ for recordUsage("…") and data-usage markers. It checks that each counter is named in both copies exactly when the site records it. Without the fix, 4 cases failed: download, login, signup, error_shown.

Validation (local)

  • New and touched test files: 4 files, 18/18 passed. With the source changes stashed: 8 failed, 10 passed. The ModelScope parity test was checked separately: 1 failed, 5 passed.
  • Full web suite, npx vitest run: 59 files, 517/517 passed.
  • npx tsc --noEmit -p .: exit 0. eslint on touched files: exit 0.
  • npm run check in web/ (check:facts, check:latest-release, prebuild, check:docs, check:tokens, lint, tsc, next build with 840/840 static pages): exit 0.

No Rust changes, so no cargo build.

Review follow-ups (3 more commits)

A review of this PR raised three findings. I checked each one again, and all three hold. Each has its own commit.

6. The locale 404 from fix 4 rendered the framework's bare page

Problem. A layout's own not-found.tsx cannot catch a notFound() thrown by that same layout. So the not-found from fix 4 went to the root boundary. The repo had no app/not-found.tsx, and app/layout.tsx renders no document. I rendered /wp-login.php in headless Chrome: HTTP 404, title "404: This page could not be found.", empty lang, and no nav or footer. By contrast, /en/nonexistent-page renders the full site shell.

Now.

  • A new app/not-found.tsx wraps the shared not-found state in the default locale's shell. It reuses [locale]/layout.tsx, so there is still only one shell.
  • generateMetadata returns the not-found metadata for a non-locale segment. Without that, the home page's title, canonical and hreflang were streamed into the 404. Calling notFound() there instead would leave the page with no title.

Evidence.

  • Headless Chrome against next start on the production build: /wp-login.php returns 404 with lang=en, title "Not found · Codewhale", nav, footer and the 404 heading. That matches /en/nonexistent-page.
  • Static files still return 200: /favicon.ico, /llms.txt, /robots.txt and a font.
  • I have not run this through the OpenNext worker or a preview deploy.
  • lib/locale-layout.test.ts: 4/4 passed. The 2 new tests each failed without their fix.

7. A failed facts validation left no signal

Problem. The scheduled handler in worker.ts discards the result of runFactsDrift. So if isRepoFacts rejected the derived facts, the cron stopped refreshing KV without any log line.

Now. runFactsDrift logs a console.warn with the reason and the snapshot's sourceRevision, sourceCommittedAt and version.

Tests. lib/facts-drift.test.ts: 7/7 passed. The new test uses a non-string engines.node, which gets through derivation but fails validation. It asserts that nothing is written and the warning is logged. It failed without the change.

8. The edge-runtime scan missed other spellings

Now. The pattern also matches experimental-edge and the object form runtime: "edge". It also scans .js, .jsx and .mjs files under app/.

Tests. lib/product-telemetry-route.test.ts: 3/3 passed. The new test failed with the old pattern on experimental-edge.

Rejected part of the finding. I did not widen the scan to lib/, components/, middleware.ts or worker.ts. Route segment runtime config only takes effect in files under app/. Middleware is not a route segment.

Validation for the follow-ups (local)

  • Repo-root npm test: 655 passed, 0 failed (68 + 16 + 50 node:test, 521 vitest).
  • npm run check:web: exit 0, including tsc, eslint and next build.

No Rust changes.

🤖 Generated with Claude Code

https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks

Hmbown and others added 4 commits September 29, 2026 04:32
@opennextjs/cloudflare does not support the edge runtime, and the deployed
route answered every method with a 500, so website usage counts never
reached the ingest. The route only needs fetch, Request, and TextDecoder.

Test: product-telemetry-route.test.ts 2/2 (edge-runtime scan failed
without the fix).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
Build the release URL from the tag instead of GitHub's html_url, whose
repo casing (Hmbown/Codewhale) failed isRepoFacts and invalidated every
KV snapshot. runFactsDrift now refuses to write facts that fail
isRepoFacts. Add the missing ModelScope provider to the runtime label map
and pin it equal to facts-lib's PROVIDER_LABEL_MAP.

Test: facts-drift.test.ts 6/6 (3 new; each failed without its fix).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
Middleware leaves dotted paths alone, so /wp-login.php reached the locale
layout and rendered the home page with HTTP 200 and an invalid html lang.

Test: locale-layout.test.ts 2/2 (not-found case failed without the fix).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
The privacy copy listed downloads, which nothing records, and omitted the
sign-in/sign-up link clicks and error pages the site does count.

Test: usage-counting-copy.test.ts 8/8 (4 failed without the fix). Web
suite 517/517; npm run check (web) exit 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
Copilot AI balanced review requested due to automatic review settings September 29, 2026 11:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Hmbown and others added 3 commits September 29, 2026 04:41
The layout's notFound() for a first segment that is not a locale cannot be
caught by the locale's own not-found boundary, so /wp-login.php answered
with the framework's bare 404: no lang, nav, footer, or site copy. A root
not-found now wraps the not-found state in the default locale's shell, and
generateMetadata returns the not-found metadata for that case instead of
streaming the home page's title, canonical, and hreflang into the 404.

Checked in headless Chrome against next start (production build):
/wp-login.php -> 404, lang=en, title "Not found · Codewhale", nav+footer,
404 heading; same as /en/nonexistent-page. Before: title
"404: This page could not be found.", empty lang, no nav or footer.

Test: locale-layout.test.ts 4/4 (2 new; each failed without its fix).
Gate: npm test 655 passed / 0 failed (68+16+50 node, 521 vitest);
npm run check:web passed (incl. next build).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
The scheduled handler discards runFactsDrift's result, so a snapshot that
failed isRepoFacts stopped the KV refresh with no signal. It now warns with
the reason and the snapshot's provenance and version.

Test: facts-drift.test.ts 7/7 (1 new; failed without the warning).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
The scan matched only `export const runtime = "edge"` in .ts/.tsx files.
It now also matches `experimental-edge` and `runtime: "edge"` object
forms, and scans .js/.jsx/.mjs files under app/.

Test: product-telemetry-route.test.ts 3/3 (1 new; failed with the old
pattern on experimental-edge).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
Independent review of accf2cf: the route and test comments said the
deployed telemetry route returned 500 for every method while it declared
the edge runtime. Nothing in this PR observed the deployed route; the tests
call handleProductTelemetry directly. The verified reason is the adapter
source: @opennextjs/cloudflare's migrate command warns the edge runtime is
unsupported and its server bundle swaps next/dist/compiled/edge-runtime for
an empty shim. The comments now say that, and that deployed behavior was
not checked. The runtime removal and its regression tests are unchanged.

Tests: product-telemetry-route, usage-counting-copy, facts-drift,
locale-layout: 4 files, 22 passed. tsc --noEmit exit 0; eslint on touched
files exit 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
@Hmbown
Hmbown merged commit a2be484 into main Sep 30, 2026
33 checks passed
@Hmbown
Hmbown deleted the fix/bh2-web-site-runtime-facts branch September 30, 2026 00:49
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.

2 participants