fix(web): telemetry route runtime, cron facts validity, locale 404, usage copy - #6749
Merged
Merged
Conversation
@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
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
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.
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-telemetrydeclared the edge runtime, which the Cloudflare adapter does not supportProblem.
web/app/api/product-telemetry/route.tswas the only file inweb/appwithexport const runtime = "edge".@opennextjs/cloudflaredoes not support the edge runtime; its ownmigratecommand 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,RequestandTextDecoder, and all three work in the default runtime.Tests.
lib/product-telemetry-route.test.ts:app/opts into the edge runtime. Without the fix it failed and found the telemetry route.{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.
fetchLatestPublishedReleasestored GitHub'shtml_url. GitHub now returns the repo's canonical casing (Hmbown/Codewhale).isRepoFactsneeds the URL to equalhttps://github.com/Hmbown/CodeWhale/releases/tag/<tag>exactly, so it rejected every cron-written snapshot./api/factsthen reportedinvalid-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.mjsrequires.runFactsDriftalso runsisRepoFactson the derived facts and returnsok:falsewithout writing anything if they fail.Tests.
lib/facts-drift.test.ts:Hmbown/Codewhalenow yields the canonical URL and passesisRepoFacts.runFactsDriftagainst an in-memory KV writes afacts:currentvalue thatisRepoFactsaccepts.Both failed without the fix.
3. Facts drift dropped the ModelScope provider (merges two duplicate findings)
Problem.
ApiProvider::Modelscopeis mapped inweb/scripts/facts-lib.mjs(build facts have 50 providers), but the runtime copy infacts-drift.tsdid not have it.deriveProvidersFromConfiglogged a warning and dropped it. Fix 2 makes cron snapshots valid again, and a valid snapshot would have removed ModelScope from/models,/productand/api/facts. That is why these two fixes land together.Now.
PROVIDER_LABELSand includes ModelScope.facts-lib.mjsexportsPROVIDER_LABEL_MAP, with a smallfacts-lib.d.mtsdeclaration so TypeScript can import it.check-factsgate already fails whenfacts-libdoes not map aconfig.rsvariant.Tests. A test asserts that
PROVIDER_LABELSdeep-equalsPROVIDER_LABEL_MAP. It fails (diff shows theModelscopeentry) when the new line is removed.4. Dotted single-segment paths returned the home page with HTTP 200 and an invalid
langProblem. Middleware leaves any path containing
.alone, andapp/[locale]/layout.tsxnever checked itslocaleparam. So/wp-login.phprendered 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 samelocaleslist asgenerateStaticParamsand the middleware'spathLocale. The oldas Localecasts 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.tsrenders the layout directly (withnext/font/localstubbed):wp-login.phprejects with the 404 not-found digest. Without the fix the promise resolved.enstill renders withlang="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 recordsdownload. They left out thelogin/signuplink-click counters (data-usage) anderror_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).NOTICE_VERSION(bumping it would change ingest validation for every surface).Tests.
lib/usage-counting-copy.test.tsscansapp/andcomponents/forrecordUsage("…")anddata-usagemarkers. 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)
npx vitest run: 59 files, 517/517 passed.npx tsc --noEmit -p .: exit 0.eslinton touched files: exit 0.npm run checkinweb/(check:facts, check:latest-release, prebuild, check:docs, check:tokens, lint, tsc,next buildwith 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.tsxcannot catch anotFound()thrown by that same layout. So the not-found from fix 4 went to the root boundary. The repo had noapp/not-found.tsx, andapp/layout.tsxrenders no document. I rendered/wp-login.phpin headless Chrome: HTTP 404, title "404: This page could not be found.", emptylang, and no nav or footer. By contrast,/en/nonexistent-pagerenders the full site shell.Now.
app/not-found.tsxwraps the shared not-found state in the default locale's shell. It reuses[locale]/layout.tsx, so there is still only one shell.generateMetadatareturns the not-found metadata for a non-locale segment. Without that, the home page's title, canonical and hreflang were streamed into the 404. CallingnotFound()there instead would leave the page with no title.Evidence.
next starton the production build:/wp-login.phpreturns 404 withlang=en, title "Not found · Codewhale", nav, footer and the 404 heading. That matches/en/nonexistent-page./favicon.ico,/llms.txt,/robots.txtand a font.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.tsdiscards the result ofrunFactsDrift. So ifisRepoFactsrejected the derived facts, the cron stopped refreshing KV without any log line.Now.
runFactsDriftlogs aconsole.warnwith the reason and the snapshot'ssourceRevision,sourceCommittedAtandversion.Tests.
lib/facts-drift.test.ts: 7/7 passed. The new test uses a non-stringengines.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-edgeand the object formruntime: "edge". It also scans.js,.jsxand.mjsfiles underapp/.Tests.
lib/product-telemetry-route.test.ts: 3/3 passed. The new test failed with the old pattern onexperimental-edge.Rejected part of the finding. I did not widen the scan to
lib/,components/,middleware.tsorworker.ts. Route segmentruntimeconfig only takes effect in files underapp/. Middleware is not a route segment.Validation for the follow-ups (local)
npm test: 655 passed, 0 failed (68 + 16 + 50 node:test, 521 vitest).npm run check:web: exit 0, includingtsc,eslintandnext build.No Rust changes.
🤖 Generated with Claude Code
https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks