Skip to content

fix(web): gate CLERK_WEBHOOK_SECRET, honest signup count, media backfill reach - #211

Merged
duyet merged 3 commits into
masterfrom
feat/extension-web-header
Sep 26, 2026
Merged

duyet merged 3 commits into
masterfrom
feat/extension-web-header

Conversation

@duyet

@duyet duyet commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

Rebased onto master (2b63ced, 43 commits) and fixes three defects found while auditing Clerk auth and the media backfill.

This branch changes deploy behaviour — read the sequencing note before merging.

1. CLERK_WEBHOOK_SECRET was never gated (the root cause)

Production probe:

POST https://aidr.today/api/webhooks/clerk  ->  503
{"error":"clerk webhook not configured"}

2b63ced shipped CLERK_WEBHOOK_SECRET as WORKER_OPTIONAL in scripts/sync-env.ts, so pnpm sync-env silently skipped it. 8554148 had already added exactly this gate for the sibling secret CLERK_SECRET_KEY (WORKER_REQUIRED + a fail-closed deploy smoke); the second Clerk secret never got one. The webhook fails closed before reading the body, so the only symptom was a silently dead signup sync while sign-in kept working.

  • sync-env.ts: CLERK_WEBHOOK_SECRET -> WORKER_REQUIRED
  • deploy-web.yml: fail-closed smoke asserting the endpoint is not 503/404, mirroring the /__clerk/v1/environment gate. Status only, never the body. The probe posts a non-user event type, which the handler ignores — so even a signature-verification regression could not write a clerk_users row and inflate the public count.

Verified red today (503) and passing for 400/401/405/200. Signup count confirmed unchanged at 0 after probing.

2. /data rendered a fabricated Signups 0

/api/system/accounts returned {"total":0,"status":"available"} for a mirror that had never received an event — indistinguishable from a real count on a public dashboard. An empty table cannot distinguish "Clerk has no accounts" from "no delivery yet", so an empty mirror is now unconfigured (total: null) and the UI says "Unavailable".

worker/README.md:93-96 already specified unconfigured for "table missing / no rows yet" — the code had drifted from its own documented contract. This closes that gap.

3. Media backfill could not reach rows with a legacy image_url (closes half of #207)

buildMissingMediaQuery gated on image_url IS NULL OR image_url = '', excluding every published row that already carried a legacy og:image — precisely the rows needing a media_manifest. Only media_manifest gates now; image_url is still selected as manifest input. The stale-predicate assertion in backfill.test.ts is replaced by a regression test.

The other half of #207 (stale 0025 gate name) already landed in cc460b3.

Rebase notes

8 conflicts, all resolved toward master's current direction and verified individually:

  • smoke.ts — took master byte-identical; the branch's naive body.includes("/og-home.jpg") is subsumed by bb5c0cb's real <meta> lookup compared against the SITE_OG_HOME_IMAGE_URL constant, plus a JPEG-magic-bytes fetch check
  • newtab.js — kept master's language-aware tagSiteLinks(document, {}, uiLang(settings)); folding the branch's trackChannelClick into master's re-sorted import block avoided a duplicate track binding (SyntaxError)
  • GetAIDRMenu.tsx (add/add) — master + three trackChannelClick handlers; preserved aa12d43 (fix(web): remove mobile Get AI;DR trigger circle background #206) and 080c9bb
  • PhoneMenu.tsx — master's Radix dialog; the branch's hand-rolled portal/Escape/scroll-lock dropped as superseded
  • NewsFooter.tsx — kept master's getCachedFeedFreshness and the Plan 022: A rejected Telegram digest tail is not stored as sent #418 root-cause fix (useState<number | null>(null), cache read inside useEffect), so master's suppressHydrationWarning band-aid is now removable
  • feed-queries.ts — master + the 3-line setLearnedKeywords removal, which is still correct: feed-cache.ts:86 is the live path feeding highlight keywords

Two stale source-grep tests were retargeted rather than deleted (chrome.test.ts, chrome-copy.test.ts) — both pinned class/API names that master refactored while preserving the behaviour. Each now asserts the composition guarantee too.

Verification

  • pnpm exec biome lint — 542 files clean
  • pnpm run check-types — exit 0
  • web suite — 161/161 files, 1656/1656 tests
  • extension suite — 65/65
  • node --check on the merged extension bundle

⚠️ Sequencing: set the secret before merging

This branch makes the deploy fail closed. If CLERK_WEBHOOK_SECRET is still unset, the new smoke step fails and blocks the release. That is the intended trade (loud failure over a silently dead sync), but it means the secret must be pushed first:

pnpm sync-env --workers

and the endpoint registered in Clerk → Webhooks against https://aidr.today/api/webhooks/clerk for user.created / user.updated / user.deleted.

Test plan

  • lint, typecheck, full web + extension suites green
  • webhook gate confirmed red against production today (503)
  • gate logic confirmed to pass on 400/401/405/200
  • probe confirmed to write nothing (count stayed 0)
  • operator: push CLERK_WEBHOOK_SECRET and register the Clerk endpoint
  • after deploy: POST /api/admin/clerk-sync once, then confirm /data shows a real count

🤖 Generated with Claude Code

Summary by Sourcery

Harden Clerk signup synchronization and media backfill while making signup reporting honest and expanding cross-channel analytics, navigation, and verification coverage.

New Features:

  • Add analytics attribution for page views, campaign landings, and Chrome, Telegram, and email channel interactions across the website and extension.
  • Expand verification tooling with analytics, Telegram, and extension-package validation workflows.
  • Refresh extension and mobile navigation with shared channel menus, improved accessibility, and the 0.1.18 release.

Bug Fixes:

  • Require CLERK_WEBHOOK_SECRET during environment synchronization and fail deployments when the Clerk webhook endpoint is unavailable.
  • Report an empty Clerk mirror as unavailable instead of presenting a potentially fabricated zero signup count.
  • Allow media backfill to process published items that have legacy image URLs but no media manifest.
  • Prevent hydration mismatches in feed freshness rendering and avoid mutating highlight state during server-side feed generation.

Enhancements:

  • Make initial page-view tracking explicit and enrich subscription, campaign, and channel events with normalized attribution data.
  • Improve desktop and mobile extension header layouts, menu behavior, and channel navigation.

CI:

  • Add full-suite validation for the new extension navigation, analytics, attribution, hydration, and media-backfill behavior.

Deployment:

  • Add a fail-closed post-deployment smoke check for the Clerk webhook configuration.

Documentation:

  • Document analytics attribution, Telegram workflows, extension package verification, and their associated proof requirements.

Tests:

  • Add regression coverage for Clerk account-count semantics, media backfill reach, analytics events, campaign attribution, extension navigation, and hydration-safe rendering.

…olish

Work in progress, captured before rebasing onto master (43 commits behind).

- extension: web header + newtab polish, settings panel, manifest 0.1.18
- ui: add trackChannelClick / TrackChannel for chrome|telegram|email funnels
- web: stop double-counting page views (gtag send_page_view: false)
- web: header menu split (GetAIDRMenu, PhoneMenu, Compact/Wide rows)
- verify-aidr skill: analytics + telegram features, refreshed driver
- tests: feed-queries, tldr-section, chrome copy, campaign, analytics

Excluded QA artifacts (.playwright-mcp/, design-preview.html).
Production probe: POST /api/webhooks/clerk returns 503
{"error":"clerk webhook not configured"} on every delivery, and
/api/system/accounts returns {"total":0,"status":"available"} — so /data
renders "Signups 0" for a mirror that has never received an event.

2b63ced shipped CLERK_WEBHOOK_SECRET as WORKER_OPTIONAL, so `pnpm sync-env`
silently skipped it. 8554148 had already added exactly this gate for the
sibling secret CLERK_SECRET_KEY (WORKER_REQUIRED + a fail-closed deploy
smoke); the new secret never got one.

- sync-env.ts: CLERK_WEBHOOK_SECRET -> WORKER_REQUIRED
- deploy-web.yml: fail-closed smoke asserting the endpoint is not 503/404,
  mirroring the /__clerk/v1/environment gate. Asserts status only, never the
  body. The probe posts a non-user event type, which the handler ignores, so
  even a signature-verification regression could not write a clerk_users row
  and inflate the public count.
- account-count.ts: an empty mirror is now `unconfigured` (total: null) rather
  than a real 0. An empty table cannot distinguish "Clerk has no accounts"
  from "no webhook delivery yet", and worker/README.md:93-96 already
  specified `unconfigured` for "table missing / no rows yet" — the code had
  drifted from its own documented contract. /data now says "Unavailable"
  instead of a fabricated count, and becomes available once a row lands.

Verified: 1656 web tests, 65 extension tests, tsc --noEmit clean, biome clean.
Live gate confirmed red today (503) and passing for 400/401/405/200.
… image_url

Closes the remaining half of #207.

buildMissingMediaQuery gated candidates on
`image_url IS NULL OR image_url = ''`, so every published row that already
carried a legacy og:image was excluded — precisely the rows that most need a
media_manifest, and the reason the backfill "cannot enrich rows with a legacy
image_url". The only remaining gate is media_manifest being absent/empty/[].

`image_url` is still selected so the manifest can be built from it.

The old predicate was pinned by an assertion in backfill.test.ts; it is
replaced by a regression test asserting the clause stays gone and the column
stays selected.

The other half of #207 (the stale 0025 migration-gate assertion) already
landed in cc460b3 and needed no change here.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @duyet, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 4 days and 1 hour by commenting @sourcery-ai review. Upgrade to get a review now.

@coderabbitai

coderabbitai Bot commented Sep 26, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 95671f36-f0f0-4978-a217-5bd744b6e77b


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Sep 26, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR makes Clerk webhook setup fail closed, reports unknown rather than fabricated zero signups, and fixes media backfill reachability, while also adding cross-surface analytics/Telegram attribution, refreshing extension navigation and accessibility, and hardening hydration, feed, and verification tests. Review the deploy sequencing carefully: CLERK_WEBHOOK_SECRET must be provisioned and the Clerk endpoint registered before merging.

Sequence diagram for Clerk webhook deployment gate and signup sync

sequenceDiagram
    participant Deploy as GitHub Actions deploy
    participant Worker as Worker /api/webhooks/clerk
    participant Clerk as Clerk
    participant D1 as clerk_users

    Deploy->>Worker: POST webhook smoke with clerk.webhook.smoke
    alt CLERK_WEBHOOK_SECRET missing
        Worker-->>Deploy: 503 clerk webhook not configured
        Deploy-->>Deploy: Fail deployment
    else Secret configured
        Worker-->>Deploy: 4xx unsigned probe rejected
        Deploy->>Clerk: Register user.created/user.updated/user.deleted endpoint
        Clerk->>Worker: Signed user event
        Worker->>D1: Sync Clerk user
    end
Loading

Sequence diagram for cross-surface channel attribution

sequenceDiagram
    actor User
    participant Surface as Web or Chrome extension
    participant Analytics as track or trackChannelClick
    participant Endpoint as /api/extension
    participant GA as gtag

    User->>Surface: Open campaign URL
    Surface->>Analytics: track page_view and landing event
    Analytics->>GA: Send sanitized event
    User->>Surface: Click Chrome, Telegram, or Email link
    Surface->>Analytics: trackChannelClick(channel, to)
    Analytics->>GA: Send channel_click
    Analytics->>Endpoint: GET with campaign-tagged parameters
Loading

State diagram for honest Clerk signup count

stateDiagram-v2
    [*] --> Unconfigured
    Unconfigured --> Available: First Clerk user syncs
    Available --> Available: Additional users sync
    Unconfigured --> Error: Count query fails
    Available --> Error: Count query fails

    state Unconfigured {
        [*] --> Unknown
        Unknown: total = null
        Unknown: status = unconfigured
    }

    state Available {
        [*] --> Counted
        Counted: total > 0
        Counted: status = available
    }

    state Error {
        [*] --> Failed
        Failed: status = error
    }
Loading

Flow diagram for media backfill candidate selection

flowchart LR
    Item[Published item with summary] --> Manifest{media_manifest missing or empty?}
    Manifest -->|No| Skip[Skip item]
    Manifest -->|Yes| Input[Select image_url as manifest input]
    Input --> Backfill[Build media manifest]
    Backfill --> Persist[Persist enriched media_manifest]
Loading

File-Level Changes

Change Details Files
Make Clerk webhook configuration fail closed during environment sync and deployment.
  • Promote CLERK_WEBHOOK_SECRET to the required Worker secret set.
  • Add a deployment smoke probe that rejects missing or absent webhook routes without sending a user event.
  • Document the operator sequencing requirement to provision the secret and register the Clerk endpoint before merging.
scripts/sync-env.ts
.github/workflows/deploy-web.yml
Represent an empty Clerk account mirror as unknown instead of reporting a fabricated signup count.
  • Return total: null and status: unconfigured when the Clerk mirror has no rows.
  • Update account-count tests to enforce the distinction between no delivery and a real zero count.
apps/web/worker/account-count.ts
apps/web/worker/__tests__/account-count.test.ts
Allow media backfill to reach published items that already have legacy image URLs.
  • Remove image_url emptiness from the candidate predicate while retaining image_url as manifest input.
  • Replace the stale predicate assertion with regression coverage for legacy-image rows.
apps/web/worker/backfill.ts
apps/web/worker/__tests__/backfill.test.ts
Expand analytics and attribution coverage across web, extension, email, and Telegram workflows.
  • Add typed channel-click tracking and explicit page-view handling, including Telegram campaign landing attribution.
  • Annotate web and extension channel links and email subscription outcomes with channel/source metadata.
  • Extend verification documentation and the verify-aidr driver with analytics and Telegram paths while keeping credentialed delivery checks gated.
packages/ui/track.ts
packages/ui/Analytics.tsx
apps/web/src/components/PageViewTracker.tsx
apps/web/src/components/EmailSubscribeForm.tsx
apps/web/src/components/GetAIDRMenu.tsx
apps/web/src/components/NewsFooter.tsx
apps/web/src/components/header/GetAIDRMenu.tsx
apps/web/src/components/header/PhoneMenu.tsx
apps/web/src/components/header/lib.ts
apps/web/src/components/subscribe/DeliverPage.tsx
apps/web/src/lib/campaign.ts
apps/web/src/lib/analytics.test.ts
apps/web/src/lib/campaign.test.ts
.cursor/skills/verify-aidr/SKILL.md
.cursor/skills/verify-aidr/bin/verify-aidr
.cursor/skills/verify-aidr/features/README.md
.cursor/skills/verify-aidr/features/analytics.md
.cursor/skills/verify-aidr/features/telegram.md
Refresh extension navigation, accessibility, layout, and release verification for the shared Get AI;DR menu.
  • Replace direct header channel icons with a keyboard- and pointer-accessible dropdown, and add explicit phone-menu close behavior.
  • Expose Submit regardless of sign-in state and update touch-target, spacing, and responsive header styles.
  • Add extension page-view/channel tracking, package verification assertions, and bump the extension version to 0.1.18.
apps/extension/newtab.html
apps/extension/js/newtab.js
apps/extension/js/settings-panel.js
apps/extension/js/track.js
apps/extension/css/newtab.css
apps/extension/scripts/verify-newtab.mjs
apps/extension/js/chrome-copy.test.js
apps/extension/js/track.test.js
apps/extension/manifest.json
apps/extension/package.json
apps/web/src/lib/extension-release.ts
Remove hydration-sensitive feed state mutation and make freshness navigation explicit.
  • Defer footer cache reads until after mount and remove the hydration-warning workaround.
  • Link the rendered freshness timestamp to /data while preserving SSR-safe fallback text.
  • Stop mutating highlight keyword state during feed generation and add source-level regression coverage.
apps/web/src/components/NewsFooter.tsx
apps/web/src/components/TldrSection.tsx
apps/web/src/lib/feed-queries.ts
apps/web/src/lib/chrome-copy.test.ts
apps/web/src/lib/feed-queries.test.ts
apps/web/src/lib/tldr-section.test.ts
Retarget source-contract tests to the rebased header and extension implementations.
  • Assert shared menu composition and 44px compact-header tap targets.
  • Replace obsolete sign-in and stale class/API expectations with current navigation and hydration guarantees.
apps/web/src/lib/chrome.test.ts
apps/web/src/lib/chrome-copy.test.ts
apps/extension/js/chrome-copy.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@duyet
duyet merged commit 60ce9ec into master Sep 26, 2026
5 checks passed
@duyet
duyet deleted the feat/extension-web-header branch September 26, 2026 12:43
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