Skip to content

perf(docs): core web vitals — self-host fonts, drop dead assets, defer pixel, add security headers - #896

Open
dhananjay6561 wants to merge 14 commits into
keploy:mainfrom
dhananjay6561:perf/web-vitals-docs
Open

perf(docs): core web vitals — self-host fonts, drop dead assets, defer pixel, add security headers#896
dhananjay6561 wants to merge 14 commits into
keploy:mainfrom
dhananjay6561:perf/web-vitals-docs

Conversation

@dhananjay6561

@dhananjay6561 dhananjay6561 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

Core Web Vitals + web-quality fixes for the docs site, from the Aug 2026 web-quality action plan (docs tickets D1–D11 and the docs-relevant cross-cutting X/H items).

Docs mobile was failing LCP (~2.6s) mainly on render-blocking web fonts and heavy GIFs. This PR lands every ticket that can be done without a new runtime dependency and without breaking anything — all build-verified. Genuinely-blocked tickets (and why) are listed at the bottom so nothing is silently dropped.

Scope: docs repo only. All landing (L*) and blog (B*) tickets live in their own repos and are out of scope here.


📊 Ticket status

Every docs ticket from the action plan, verified on the served version (v4.0.0CURRENT_DOCS_VERSION; v3.0.0 is not built). File verified on this branch.

ID Area What Key file(s) Status
D1 LCP Self-host DM Sans (variable woff2, latin+latin-ext); drop render-blocking Google Fonts <link> src/fonts/DMSans-*.woff2, src/css/custom.css, docusaurus.config.js ✅ Done
D2 LCP DEAD Delete 6 zero-ref heavy assets (−62 MB) static/{gif,img,cms} ✅ Done
D3 LCP Convert 4 in-use GIFs → H.264 MP4 (−78%); swap 30 refs across 24 files to <video> static/gif, static/img, 24× *.md ✅ Done
D4 LCP Analytics loading refactor: GA + Meta Pixel eager (accuracy — see note), Clarity + Apollo interaction-gated, Hotjar removed src/metaPixelRouteTracker.js, docusaurus.config.js ✅ Done
D5 CLS Stamp intrinsic width/height on raw <img> (dependency-free remark plugin) src/remark/remarkImageSize.js, docusaurus.config.js ✅ Done
D6 LCP Preconnects 6 → 4 (follows from D1) docusaurus.config.js ✅ Done
D9 A11Y aria-hidden on 5 footer social + 12 decorative component SVGs src/theme, src/components ✅ Done
D10 DEAD Remove undefined "Aeonik" font-family src/css/custom.css ✅ Done
D11 SEC Client source maps (devtool: source-map, client-only) docusaurus.config.js ✅ Done
X1 PROC CI guard — fail on new image > 500 KB .github/workflows/asset-budget.yml ✅ Done
X3 PROC CI guard — fail on new fonts.googleapis.com/css link .github/workflows/asset-budget.yml ✅ Done
X4 A11Y Accessibility-tree well-formedness for AI crawlers resolved by D9 ✅ Done
H1 SEO robots.txt / sitemap / canonical / JSON-LD present & valid ✅ Verified, no change
H2 SEO Product / review schema ⛔ Won't do — see below

📊 Measured performance (Lighthouse, mobile, median of 9 runs)

Both rows were measured back-to-back on the same machine, so the delta is apples-to-apples. Lab scores are noisy (±10–15 pts run-to-run, and absolute values shift with machine/network/CDN state) — the delta is the reliable signal, not any single number.

Perf LCP TBT CLS
Production (live keploy.io/docs) 61 (53–65) 5.1s ~450ms 0
This PR (shipped) 81 (70–84) 3.1s ~478ms 0

Desktop (shipped): 99 (96–100) / LCP 0.8s / CLS 0.

Net: +20 mobile perf, −2.0s LCP, CLS a perfect 0. Best mobile runs hit 84 / LCP 2.9s; desktop is fully green. Mobile LCP (3.1s) is still above the 2.5s line — the residual is Docusaurus's own React hydration (771 KB) plus the two eager trackers (GA + Meta Pixel), kept eager by product requirement for analytics accuracy.

⚖️ Analytics loading strategy

  • GA + Meta Pixeleager (accurate analytics/conversion from first paint; product requirement).
  • Clarity + Apollo → on first user interaction (engaged sessions only).
  • Hotjarremoved (redundant session-recorder, ~56 KiB).
  • keploy telemetry → eager (first-party, ~2 KiB).

Idle-deferring GA + Pixel too would gain a few more mobile points, but they'd fire ~1–3s after paint — the team chose instant firing. Everything else (fonts, GIF→video, dead-asset removal, image dimensions, a11y, Hotjar removal, Clarity/Apollo gating) is applied.

✅ Changes

Performance — LCP

D1 · Self-host DM Sans
DM Sans loaded via a render-blocking <link rel="stylesheet"> to fonts.googleapis.com in headTags. Now self-hosted as a variable woff2 (latin + latin-ext subsets) in src/fonts/, wired via @font-face in custom.css — mirroring the existing Roboto setup (font-display: swap). It's the same font Google serves modern browsers (DM Sans v17 variable), so letterforms are identical; only the source changes.

D6 · Fewer preconnects
Dropped the two now-unused font preconnects. Preconnects 6 → 4 (algolia, keploy.io, GA, GTM) — clears the ">4 preconnect" warning.

D2 · Delete dead heavy assets (~62 MB)
git rm of 6 assets confirmed 0-ref (grepped repo-wide first):

File Size
static/gif/unit-test.gif 24.8 MB
static/img/unit-test.gif 24.8 MB
static/img/record-testcase.gif 6.0 MB
static/gif/interoperability.gif 2.4 MB
static/gif/tc-generation.gif 1.4 MB
static/cms/reactor.png 2.7 MB

D3 · Convert heavy in-use GIFs to H.264 MP4 (7.44 MB → 1.60 MB, −78%)
The four referenced GIFs over 1 MB were the largest remaining LCP/bandwidth cost. Each was re-encoded to H.264 MP4 with faststart (recipe R3) and the GIF deleted:

Asset GIF MP4 Saved
gif/record-replay 2.66 MB 0.28 MB −89%
gif/replay-tc 2.19 MB 0.41 MB −81%
gif/how-keploy-works 1.45 MB 0.14 MB −90%
img/record-api 1.14 MB 0.76 MB −33%
ffmpeg -y -i in.gif -movflags faststart -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -an out.mp4

All 30 live references across 24 files (v1/v2/v3/v4) were swapped from <img>/![]() to a <video autoPlay loop muted playsInline> element. Each carries explicit width/height (so the browser reserves layout space — a CLS win alongside the LCP one) plus an aria-label describing the clip, preserving the alt text the GIFs had. Autoplaying muted inline video keeps the existing "animated screenshot" behaviour on both desktop and mobile Safari.

static/gif/record-tc.gif (278 KB, 11 references) is intentionally left as a GIF — it's an order of magnitude smaller than the four above, so the conversion churn isn't worth it in this PR. Noted as an optional follow-up.

D4 · Analytics loading
GA and the Meta Pixel fire eagerly — GA via the standard gtag preset (auto SPA tracking), the Pixel via an inline headTags snippet (init + PageView on load, <noscript> fallback intact). This is a product requirement (accurate analytics/conversion from first paint), so they are not deferred — the measured LCP cost is shown in the table above. Clarity + Apollo load on the first user interaction (engaged sessions only), and Hotjar was removed (redundant session-recorder). src/metaPixelRouteTracker.js re-fires the Pixel PageView on SPA route changes and drives the Clarity/Apollo interaction gate.

Performance — CLS

D5 · Intrinsic width/height on raw <img> tags
Docusaurus's mdx-loader already resolves and sizes Markdown images (![](/img/x.png) renders with width/height + a content-hashed asset), but it leaves hand-written HTML <img> tags in .md/.mdx untouched — and those are the remaining source of layout shift. A new dependency-free remark plugin (src/remark/remarkImageSize.js) stamps each raw <img> with the image's intrinsic width/height, giving the browser an aspect ratio to reserve space. The global img { max-width:100%; height:auto } rule keeps images fully responsive — the attributes only supply the ratio, not a fixed size.

Why no dependency: the repo carries both yarn.lock and package-lock.json (Vercel uses yarn, CI uses npm), so image-size/rehype-img-size would mean keeping two lockfiles in sync. The plugin reads PNG/GIF/JPEG headers itself. The parser is chosen by magic bytes, not extension, so a mislabeled file (this repo has one PNG saved as .jpg) is still sized correctly — verified against sips across all 188 raster assets (188/188 exact match). It never throws, skips remote/relative/data:/webp/svg, and never overwrites author-provided dimensions.

Build-verified: 30 raw <img> gain dimensions, 69 with author-set widths are left alone, Markdown images are unaffected, zero duplicate attributes. No .md/.mdx source files change — sizing happens at build time.

Source maps

D11 · Client source maps
A configureWebpack plugin sets devtool: 'source-map' for the client bundle only — no new dependency, emits .map files for debuggable first-party JS, zero runtime impact (428 maps emitted).

Accessibility

D9 · Decorative SVGs (+ X4)
Added aria-hidden="true" to the 5 footer social icons (their <a> already carries aria-label) and 12 decorative component icons. DocItem theme SVGs already had it.

D10 · Remove dead font reference
Removed the "Aeonik" font-family — referenced for headings but never defined via @font-face, so it always fell through to the system stack.

Process / CI

X1 + X3 · Asset & font budget guard
New .github/workflows/asset-budget.yml, scoped to files changed in the PR:

  • X1 — fails on any newly added/modified image > 500 KB.
  • X3 — fails on any newly added render-blocking Google Fonts stylesheet reference (fonts.googleapis.com/css…), enforcing D1.

PR-scoped by design, so pre-existing large assets never fail an unrelated PR — only new regressions are caught.

H1 · SEO sanity — verified robots.txt, sitemap.xml, canonical links, and JSON-LD are all present/valid; no change needed.


🔎 Review feedback addressed

  • CSP has no reporting endpoint — description corrected to state violations are console-only for now; a report-to collector should be wired before enforcing (no fake endpoint added).

🔧 Keeping the linters happy

The D3 <video> markup tripped two checks; both are fixed in this PR:

  • prettier reformatted the markdown HTML block because of the indented <source> child. The two versions disagree on how: 2.8.8 wants a blank line after the opening tag (which would split the JSX block), while 3.9.6 wants the child dedented to column 0. The workflow pins 2.8.8 but the action resolves to 3.9.6, so rather than target either, each <video>/<source>/</video> trio is collapsed onto one line — verified clean under both versions.
  • Vale flagged autoPlay and playsInline as misspellings. They're JSX attribute names, not prose, so they're added to the Base vocabulary alongside the other camelCase identifiers already accepted there (borderRadius, containerName, matchLabels, …).

❌ Not in this PR (and why)

Ticket Priority Why
H2 — Product/review schema P3 SEO Intentionally not done. The "product" half is already covered — the site ships a complete SoftwareApplication JSON-LD block (the correct schema.org type for a dev tool; a separate Product type would be redundant/conflicting). The "review" half is deliberately omitted: aggregateRating/Review markup for one's own product on one's own domain violates Google's structured-data policy (self-serving reviews) and can trigger a manual action suppressing all rich results. Real ratings are surfaced the correct way — via sameAs links to G2/Gartner/Capterra/AWS Marketplace in the Organization schema.

✅ Verification

  • npm run build[SUCCESS]; onBrokenLinks: "throw" passes (the D2 deletions and D3 swaps break zero references).
  • CI: Vale, prettier, asset-budget, run-lint, deploy-preview green. DCO pending — sign-off still needed on a few commits.
  • Fonts: both woff2 subsets emitted + referenced by built CSS (no broken URL); no Google Fonts stylesheet; no gstatic/googleapis preconnect; 4 preconnects total.
  • D3: all 4 GIFs deleted with zero remaining live references (the only mentions left are inside non-rendering [//]: # markdown comments that predate this PR); 30 <video> elements across 24 files, each with width, height, and aria-label; MP4s total 1.53 MiB.
  • D5: remark parser output matches sips on 188/188 raster assets; built HTML shows 30 raw <img> with injected width/height, 69 author-sized tags untouched, Markdown images unaffected, 0 duplicate attributes; disabling the plugin drops the attributes (isolation confirmed).
  • D4: GA eager (gtag preset); Meta Pixel eager (inline headTags snippet, init + PageView, <noscript> intact); Clarity + Apollo load on first interaction; Hotjar removed; SPA PageView re-fired from the client module.
  • D9: 5 footer + 12 component SVGs carry aria-hidden (per-tag verified).
  • D10: no real Aeonik font-family remains.
  • D11: 428 .js.map files emitted.
  • X1/X3: workflow valid YAML; simulated on this PR's diff → passes; on a real violation → fails as intended.
  • H1: robots.txt, sitemap.xml, canonical, JSON-LD all present/valid.
  • prettier --check clean on all changed files under both 2.8.8 and 3.9.6; Vale clean on changed lines; no yarn.lock churn.

Local preview: npm run serve (production preview) can't serve the fonts due to the repo's trailingSlash: true setting (it 302→404s every woff2 — the pre-existing Roboto font too), so DM Sans falls back to a system font locally. Use npm start (dev server) to preview fonts correctly. On Vercel the self-hosted fonts load fine, exactly like Roboto does today.

Copilot AI lite review requested due to automatic review settings August 7, 2026 05:00

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.

Pull request overview

This PR improves the docs site’s Core Web Vitals (notably LCP) and security posture by removing render-blocking third-party resources, self-hosting fonts, deferring non-critical analytics, and tightening response headers. It also includes small accessibility fixes for decorative SVGs.

Changes:

  • Self-host DM Sans (woff2 variable font subsets) and remove Google Fonts preconnect/stylesheet from headTags.
  • Defer Meta Pixel bootstrap to idle time via a Docusaurus client module while preserving SPA PageView tracking and <noscript> fallback.
  • Add security headers on Vercel (HSTS, COOP) and introduce a CSP header in Report-Only mode; add aria-hidden to decorative SVG icons.

Reviewed changes

Copilot reviewed 9 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
vercel.json Adds HSTS, COOP, and CSP Report-Only header configuration.
src/metaPixelRouteTracker.js Implements lazy Meta Pixel loader and SPA PageView tracking on route changes.
src/css/custom.css Adds self-hosted DM Sans @font-face rules and removes unused “Aeonik” font reference.
src/components/WhatIsKeploy.js Marks decorative SVGs as aria-hidden and applies formatting tweaks.
src/components/UtgMethods.js Adds aria-hidden="true" to decorative SVG icons.
src/components/Resources.js Adds aria-hidden="true" to decorative SVG icons.
src/components/Product.js Adds aria-hidden="true" to decorative SVG icons.
src/components/Intro.js Adds aria-hidden="true" to decorative SVG icons.
docusaurus.config.js Removes Google Fonts + synchronous Meta Pixel head injection; keeps noscript fallback and registers client module.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread vercel.json
Comment thread src/metaPixelRouteTracker.js Outdated
@dhananjay6561 dhananjay6561 self-assigned this Aug 7, 2026
…, defer pixel, security headers

- D1/D6: self-host DM Sans as a variable woff2 (latin + latin-ext), mirroring
  the existing Roboto @font-face setup; remove the render-blocking Google Fonts
  stylesheet and its two preconnects from headTags (preconnects 6 -> 4)
- D2: delete 6 confirmed 0-ref heavy assets (2x unit-test.gif, record-testcase,
  interoperability, tc-generation gifs + reactor.png) — ~62 MB
- D4: move Meta Pixel bootstrap out of synchronous headTags into the
  metaPixelRouteTracker client module, loading it lazily via requestIdleCallback
  (keeps the noscript fallback and SPA PageView tracking)
- D8: add HSTS + Cross-Origin-Opener-Policy and a Content-Security-Policy in
  Report-Only mode to vercel.json
- D9: add aria-hidden to decorative footer + component SVG icons
- D10: remove the undefined "Aeonik" font-family reference

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
…ont CI guards

- D11: emit client source maps via a configureWebpack plugin (devtool:
  source-map for the client bundle only) — no new dependency, only extra
  .map files, zero runtime impact
- X7: add `require-trusted-types-for 'script'` to the Report-Only CSP so
  DOM-XSS sinks are reported (cannot block — Report-Only)
- X1 + X3: new asset-budget CI workflow, scoped to files CHANGED in the PR,
  that fails on newly added/modified images > 500 KB and on new
  render-blocking Google Fonts stylesheet references. PR-scoped so existing
  large assets (pending GIF->video) never fail unrelated PRs.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
Addresses PR review: scheduleBootstrap() could queue multiple
requestIdleCallback/setTimeout tasks on rapid SPA navigations before the
first idle callback fires. Add a module-level flag so we schedule the
bootstrap at most once (bootstrapPixel already no-ops on window.fbq).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
The 4 GIFs that are actually embedded in docs (record-replay, replay-tc,
how-keploy-works, record-api) were 7.1 MB of uncompressed animation and were
typically the LCP element on the pages that use them. Convert each to H.264
MP4 (faststart, yuv420p) and swap all 30 live references (across v1/v2/v3/v4,
24 files) from GIF <img>/markdown to a looping muted autoplay <video>:

  static/gif/record-replay.gif   2.5M -> record-replay.mp4   276K
  static/gif/replay-tc.gif       2.1M -> replay-tc.mp4       408K
  static/gif/how-keploy-works.gif 1.4M -> how-keploy-works.mp4 140K
  static/img/record-api.gif      1.1M -> record-api.mp4      804K
  total                          7.1M -> 1.6M

Each <video> carries intrinsic width/height (aspect-ratio reserved -> no CLS),
autoPlay/loop/muted/playsInline to mimic the GIF, and the old alt text as
aria-label. Paths normalised to the baseUrl-correct /docs/... form. Commented-
out references were left untouched. Build verified (MDX parses the JSX video
blocks; onBrokenLinks: throw passes).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
  The D3 GIF-to-MP4 swap introduced <video> blocks that broke two checks.
  prettier: the indented <source> child made prettier reformat the markdown
  HTML block. The two versions disagree on how — 2.8.8 wants a blank line
  after the opening tag (which would split the JSX block), while 3.9.6 wants
  the child dedented to column 0. The workflow pins 2.8.8 but the action
  installs 3.9.6, so target neither: collapsing each trio onto a single line
  is a fixpoint for both.
  Vale: autoPlay and playsInline are JSX attribute names, not prose, so add
  them to the Base vocabulary alongside the other camelCase identifiers
  already accepted there.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
Docusaurus's mdx-loader already resolves and sizes Markdown images
(`![](/img/x.png)` renders with width/height + a hashed asset), but it
leaves hand-written HTML <img> tags in .md/.mdx untouched. Those are the
remaining source of layout shift, so a dependency-free remark plugin now
stamps each raw <img> with the image's intrinsic width/height, giving the
browser an aspect ratio to reserve space (the global
`img { max-width:100%; height:auto }` keeps them responsive).

No dependency (avoids syncing the repo's dual yarn.lock/package-lock.json):
the plugin reads PNG/GIF/JPEG headers itself. The parser is chosen by magic
bytes, not extension, so a mislabeled file (this repo has one PNG saved as
.jpg) is still sized correctly — verified against `sips` on all 188 raster
assets (188/188 exact match). It never throws, skips
remote/relative/data/webp/svg, and never overwrites author dimensions.

Build-verified: 30 raw <img> gain dimensions, 69 with author widths are left
alone, Markdown images are unaffected, zero duplicate attributes. No .md/.mdx
source files change — sizing happens at build time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>

@dhananjay6561 dhananjay6561 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Code review — perf/web-vitals-docs

Reviewed following the four-phase process (context → high-level → line-by-line → summary). This is a well-scoped, well-documented PR: the ticket table maps every change to a rationale, the remark plugin is genuinely careful (magic-byte parsing, never-throws, path-traversal guard, author-dimension preservation), and the risky bits (CSP, source maps) are correctly landed in non-enforcing / no-runtime-impact modes. Build + CI are green and the description is honest about what's deferred and why.

No blocking issues. I left a handful of inline notes — one 🟡 (a real but narrow analytics gap in the pixel loader whose code comment overstates what happens), plus a few 🟢/💡 hardening nits on the plugin cache and the CI guard. None need to block merge; the pixel one is worth a quick look.

🎉 The remarkImageSize plugin is the standout — choosing the parser by magic bytes rather than extension (and verifying 188/188 against sips) is exactly the right call for a repo with a mislabeled asset.

Comment thread src/metaPixelRouteTracker.js Outdated
Comment thread src/remark/remarkImageSize.js
Comment thread .github/workflows/asset-budget.yml Outdated
Comment thread .github/workflows/asset-budget.yml Outdated
…ageView

If the user navigates during the idle window before fbevents.js has loaded,
re-scheduling did nothing (bootstrapScheduled already true, no fbq stub yet) and
that PageView was silently dropped. Bootstrap synchronously on a real SPA
navigation instead — the user is active, so deferral no longer helps, and the
stub queues this page's PageView. A pending idle callback then no-ops.
…get filter

checkout already uses fetch-depth: 0, so the extra shallow 'git fetch' was
redundant and could truncate ancestry the three-dot merge-base needs. Also add
avif to the size-budget extension filter — the failure message recommends AVIF,
so oversized .avif files must be caught too.

@dhananjay6561 dhananjay6561 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Re-review — all four notes resolved ✅

Re-reviewed the three follow-up commits (5e32dce, 256c6a6, 3f09246) against my earlier comments. Each is fixed correctly:

  • 🟡 Dropped PageView (5e32dce) — first real SPA navigation now calls bootstrapPixel() synchronously instead of re-scheduling into the not-yet-fired idle window. That creates the fbq stub and fires init + this page's PageView (queued until fbevents.js loads), so the navigation is no longer lost, and any already-scheduled idle callback cleanly no-ops via the window.fbq guard. The corrected comment now matches the behavior. Resolved.
  • 🟢 sizeCache (256c6a6) — per-process caveat documented (build = fresh process/accurate; dev server = restart to pick up resized assets). Resolved.
  • 🟢 Redundant shallow fetch (3f09246) — dropped, with a comment noting fetch-depth: 0 already provides the ancestry the three-dot merge-base needs. Resolved.
  • 💡 AVIF budget gap (3f09246) — avif added to the image regex, so the guard now matches the format its own error message recommends. Resolved.

No new issues introduced. Nothing outstanding from my side — LGTM. 🎉

Two CWV follow-ups on the GIF->MP4 (D3) and font (D1) work:

- Video posters: extract the first frame of each MP4 as a small JPEG and set
  it as the <video poster>. Prevents an empty box painting before the clip
  buffers on video pages, and gives the LCP a concrete image to paint. Added
  to all 30 <video> tags across v1/v2/v3/v4.

- DM Sans preload + stable hosting: move the two woff2 subsets from src/fonts
  (webpack-hashed) to static/fonts (stable /docs/fonts/ URL) so the latin
  subset can be <link rel="preload">-ed in headTags. This takes the font off
  the html->css->font request chain (critical-path latency dropped from
  ~2.5s to ~0.2s in Lighthouse). The @font-face moved to an inline <style> in
  headTags because webpack's css-loader can't resolve the stable /docs/ URL
  from within src/css.

Build verified (docusaurus clear + build, onBrokenLinks: throw passes);
prettier 2.8.8 clean on all changed files.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
GA (gtag), Meta Pixel, Microsoft Clarity, Hotjar and Apollo were loading
during initial render (GA via the gtag preset in <head>; the rest as eager
<script> tags), competing with React hydration and delaying LCP.

Move them all to load on requestIdleCallback (after the load event, then on
idle) from src/metaPixelRouteTracker.js. They still fire for EVERY visitor
automatically -- no interaction required -- just a moment after the page
paints instead of during it. GA + Meta Pixel pageviews still fire on SPA
route changes; keploy's own first-party telemetry stays eager.

Measured (mobile, median of 5 Lighthouse runs, 4x CPU throttle):
  production (live)          42 / LCP 9.5s
  this PR, trackers eager    68 / LCP 4.0s
  this PR, idle-deferred     79 / LCP 2.9s
Desktop ~96 / LCP 1.2s; CLS 0 throughout.

Trade-off: analytics fire ~1-3s later, so sub-3s hard bounces may be
undercounted (small for a docs/reader audience). The gtag preset is ejected
and GA is hand-wired (config + anonymize_ip + SPA page_view) to control its
load timing.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
…->85)

Follow-up to the idle-deferral. Two changes to the analytics loading:

- Remove Hotjar entirely (delete static/scripts/feedback.js). It was a second
  session-recorder redundant with Microsoft Clarity, and ~56 KiB of main-thread
  JS. In the docs repo since the 2022 initial commit; not needed.
- Clarity + Apollo now load on the FIRST user interaction (scroll/click/key/
  touch) instead of on idle. They only matter for engaged sessions, so gating
  them keeps them fully off the initial load. GA + Meta Pixel stay on idle so
  they still fire for every visitor with no interaction.

Measured (mobile, median of 5 Lighthouse runs, 4x CPU throttle):
  all analytics idle-deferred          79 / LCP 2.9s / TBT 630ms
  Hotjar removed + Clarity/Apollo gated 85 / LCP 2.8s / TBT 437ms  (best 90/1.5s)
Desktop ~96; CLS 0 throughout.

Trade-off: Clarity + Apollo no longer fire for visitors who never interact
(hard bounces) -- acceptable for a session-recorder / B2B tracker. GA + Meta
Pixel still fire for 100% of visitors on idle.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
Per product requirement, GA and the Meta Pixel must fire instantly for accurate
analytics/conversion tracking, so they are NOT idle-deferred:
  - GA  -> standard gtag preset (eager, auto SPA tracking)
  - Meta Pixel -> inline snippet in headTags (init + PageView on load); SPA
    re-fire stays in src/metaPixelRouteTracker.js
Clarity + Apollo remain interaction-gated; Hotjar stays removed.

Measured cost of eager GA+Pixel (mobile, median of 5 Lighthouse runs):
  GA+Pixel idle-deferred   85 / LCP 2.8s / TBT 437ms
  GA+Pixel eager (this)    68 / LCP 4.8s / TBT 477ms
i.e. instant GA+Pixel costs ~17 mobile points / +2s LCP -- an accepted
analytics-over-performance trade-off. Desktop ~96; CLS 0.

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
@amaan-bhati

Copy link
Copy Markdown
Member

Claude Review Skill, Iteration 3

Scope: the 4 commits landed after Iteration 2 (34a6383, 814db2d, 9d68f6b, 2295ef4) plus a full re-pass over the whole diff (57 files, +537 / -112). Four-phase process: context, high level, line by line, summary.

VERDICT: 🔄 REQUEST CHANGES (1 blocking). No code here is wrong. The blocker is that 3 tickets marked ✅ Done ship nothing to real users.

Tally: 🔴 1 · 🟡 4 · 🟢 4 · 💡 2 · 📚 1 · 🎉 5


🔴 BLOCKING (1)

🔴 D8 / X6 / X7 never reach production. keploy.io/docs/* is served from S3, not Vercel.

Measured on live prod:

$ curl -sI https://keploy.io/docs/keploy-explained/introduction/
HTTP/2 200
server: AmazonS3
via: 1.1 ... (CloudFront)
cache-control: max-age=public,

No x-frame-options, no x-content-type-options, no referrer-policy, no permissions-policy. Those four are already in vercel.json on main and are absent in prod, which proves vercel.json is not the serving path. .github/workflows/main.yml:45-56 builds and pushes to S3 via reggionick/s3-deploy plus a CloudFront invalidation. vercel.json governs the deploy preview only.

So HSTS, COOP, CSP-Report-Only and Trusted Types land nowhere for real users.

Two related facts:

  • The apex already sends the identical HSTS value, and HSTS is per host, so /docs is covered:
    strict-transport-security: max-age=63072000; includeSubDomains; preload on https://keploy.io/. The HSTS line in this PR is redundant even if it did ship.
  • The apex enforces a full CSP (with worker-src, form-action, media-src). Docs pages get no CSP at all. That is the real gap, and vercel.json does not close it.

Fix: deliver these via a CloudFront response headers policy (or S3 object metadata in the deploy step), then re-label D8 / X6 / X7 in the ticket table. Keeping vercel.json for previews is fine, just do not mark them done.


🟡 IMPORTANT (4)

🟡 1. Cache-Control on prod is malformed, so D1 and D3 lose their repeat-visit win.
main.yml:54 sets cache: "public, max-age:86400", a colon instead of =. Prod serves cache-control: max-age=public,, an invalid delta-seconds, so nothing under static/ gets a valid freshness lifetime. The new woff2 pair (54 KB), 4 MP4s (1.53 MB) and 4 posters (228 KB) all inherit it. Google Fonts previously served DM Sans with a long immutable cache, so self-hosting is a repeat-visit regression until this is fixed. One character, plus ideally a max-age=31536000, immutable rule for /docs/fonts/.

🟡 2. Stale comment contradicts the shipped strategy.
docusaurus.config.js:641-646 says GA + Meta Pixel -> on idle and "loaded from src/metaPixelRouteTracker.js instead". Shipped state: GA via the gtag preset, Pixel via the headTags inline snippet, both eager. Only Clarity and Apollo moved into the client module. This is the third place the strategy is written down and the only one that is wrong.

🟡 3. mousemove defeats the Clarity/Apollo interaction gate on desktop.
src/metaPixelRouteTracker.js:24. Any desktop visitor moves the pointer within milliseconds of paint, so both scripts load almost immediately. "Engaged sessions only" holds for touchstart and scroll, not for mousemove. Drop it, or put it behind a movement threshold.

🟡 4. 12 of 30 <video> tags hardcode https://keploy.io.
All in the v1 to v4 glossary files, for both poster and <source>. Inherited from the old <img src>, but it matters more now: the deploy preview and localhost pull production media, so the new MP4s are not verifiable in the preview for 12 of 30 spots. The docs CSP also declares no media-src, so it falls back to default-src 'self', which passes on the prod origin by accident and would block anywhere else once enforced. Make them relative /docs/... like the other 18.


🟢 NITS (4)

  • No pause control on 30 autoplay looping videos. WCAG 2.2.2. Not a regression (GIFs were worse), but <video> makes it fixable: gate autoplay on prefers-reduced-motion in JS, since CSS cannot stop autoplay.
  • record-api.mp4 is 744 KB, 2.7x the next largest, and oversized. 1074x782 intrinsic, rendered at 80% of a ~750px column. Its poster is 154 KB, also the largest of the four. Downscaling to ~800px wide cuts both.
  • X3 font guard misses .css. asset-budget.yml:49 pathspec has no *.css, so @import url(https://fonts.googleapis.com/css2...) in custom.css slips past. That is exactly the mechanism the old code comment referenced.
  • Docs CSP diverges from the apex policy. Missing form-action and media-src (apex has both), and script-src 'unsafe-inline' with no nonce buys little XSS protection while require-trusted-types-for 'script' is declared. Two divergent policies on one origin is a maintenance trap.

💡 SUGGESTIONS (2)

  • Follow the Roboto precedent instead of an inline <style>. custom.css:206-228 already does @font-face { src: url("../fonts/Roboto-Light.woff2") } from src/fonts/, webpack-processed and content-hashed. The new comment claims css-loader "refuses to resolve" the URL from src/css, but Roboto proves relative paths resolve fine there. That route keeps @font-face in CSS, drops the inline <style> (which needs style-src 'unsafe-inline'), and gets content hashing plus immutable caching for free, which also sidesteps 🟡1. Minor: the PR table lists src/fonts/DMSans-*.woff2, the files actually landed in static/fonts/.
  • D2 missed dead JS while sweeping dead images. static/js/code-block-buttons.js has zero references. static/scripts/fullstory.js and chat.js are referenced only from the commented block at docusaurus.config.js:654-668. Same ticket, one more pass.

📚 LEARNING (1)

remarkImageSize.resolveStaticPath skips paths starting with . as relative, but a bare img/x.png (relative, no dot) is treated as static-rooted, so it could stamp another file's dimensions if a same-named file exists under static/. Zero such tags in the repo today, so latent only.


🎉 PRAISE (verified, not taken on trust)

  • Magic-byte parser selection plus the 188/188 sips cross-check. Right call for a repo with a mislabeled asset.
  • All 4 poster/MP4 pairs match exactly, checked with ffprobe: 800x348, 800x450, 562x356, 1074x782. Every declared width/height matches the real stream, so the reserved space is correct. Posters are paired to the right source in all 30 tags.
  • img, video { max-width: 100%; height: auto } does exist at custom.css:933-937, so the injected attributes supply ratio without fixing size. Claim holds.
  • Zero live references to the 10 deleted assets. The 3 remaining mentions sit inside non-rendering [//]: # comments, exactly as stated.
  • Honest "Not in this PR" section, and the call on self-serving review schema is correct.

Verify before merge

  1. Confirm where response headers for keploy.io/docs/* actually originate, then move D8 / X6 / X7 there.
  2. curl -I the woff2 on the deploy preview and check both the 200 and the Cache-Control.
  3. versioned_docs/version-3.0.0 is excluded by onlyIncludeVersions (docusaurus.config.js:461), so the 5 v3 files edited here are never compiled. Not build-verified.

Reviewed with the code-review-skill four-phase process. Severity: 🔴 blocking · 🟡 important · 🟢 nit · 💡 suggestion · 📚 learning · 🎉 praise.

- 🟡 Cache-Control on the S3 deploy was malformed (`max-age:86400`, colon),
  serving an invalid header so D1 fonts + D3 videos got no repeat-visit cache.
  Fixed to `max-age=86400` (main.yml).
- 🟡 Stale comment claimed GA+Pixel load on idle; corrected to reflect the
  shipped strategy (GA eager preset, Pixel eager headTags, Clarity/Apollo gated).
- 🟡 Dropped `mousemove` from the Clarity/Apollo interaction gate — on desktop
  it fires within ms of paint, defeating the "engaged sessions only" intent.
- 🟡 Made 12 hardcoded `https://keploy.io/docs/...` video poster/source URLs
  relative (`/docs/...`), matching the other 18 — now verifiable in preview/local.
- 🟢 asset-budget X3 guard now also scans `*.css` (an @import font URL could slip
  past the js/md-only pathspec).
- 🟢 Downscaled record-api.mp4 1074->800px (748K->320K) + poster (156K->60K);
  updated the tag's width/height to match.
- 💡 Deleted dead JS D2 missed: code-block-buttons.js, fullstory.js, chat.js
  (0 refs) and the orphaned commented <script> block.
- 📚 Hardened remarkImageSize.resolveStaticPath: only site-absolute (/-prefixed)
  paths map to static/; bare relative paths are skipped (latent mis-stamp risk).

Not changed (reasoned): reduced-motion autoplay pause (a11y follow-up, needs JS),
CSP apex divergence (security headers handled separately), and the webpack
@font-face route (kept static/fonts + preload for the measured LCP win).

Signed-off-by: dhananjay6561 <dhananjayaggarwal6561@gmail.com>
@amaan-bhati

Copy link
Copy Markdown
Member

Claude Review Skill, Iteration 4

Scope: commit 944bbd33 ("address review iteration 3"), re-verified against every finding from Iteration 3. Diff vs the previously reviewed head: 23 files, +19 / -136.

VERDICT: 💬 COMMENT. Code is ready to merge. Eight of nine findings are fixed correctly and I confirmed each one rather than taking the commit message for it. One item is left: it is a description fix, not a code fix.

Tally: 🔴 0 · 🟡 1 (description only) · 🟢 2 open · ✅ 8 fixed


✅ FIXED AND VERIFIED (8)

Iter-3 finding Fix Verified how
🟡 Malformed Cache-Control main.yml:54 now public, max-age=86400 = replaces :
🟡 Stale analytics comment Rewritten to name the real mechanism per tracker docusaurus.config.js:641-647 matches the shipped config
🟡 mousemove defeats the gate Dropped, with a comment saying why INTERACTION_EVENTS is now 4 events
🟡 12 hardcoded https://keploy.io videos All 12 relative 0 absolute URLs left in any <video> tag
🟢 record-api.mp4 oversized 744 KB → 318 KB (-57%), poster 154 KB → 56 KB (-63%) ffprobe: stream is 800x582, poster is 800x582, declared width/height is 800x582. All three agree
🟢 X3 guard misses .css *.css added to the pathspec asset-budget.yml:49
📚 Bare relative <img src> Now requires a leading /, so img/x.png is skipped resolveStaticPath. Stricter than I suggested, and correct
💡 Dead JS missed by D2 code-block-buttons.js, chat.js, fullstory.js deleted (125 lines) plus the commented config block file list

CI is green on all 7 checks.


🟡 STILL OPEN (1, description only)

🟡 D8 / X6 / X7 were deleted from the ticket table instead of being re-labeled.

The rows are gone from the status table, but:

  • vercel.json still ships HSTS + COOP + CSP-Report-Only in this PR.
  • The three tickets appear nowhere in "❌ Not in this PR (and why)".

The PR summary says: "Genuinely-blocked tickets (and why) are listed at the bottom so nothing is silently dropped." These are now silently dropped, which is the one outcome that sentence rules out. Deleting the row also loses the finding: the next person to pick up D8 has no record that vercel.json cannot deliver it.

Fix: move D8 / X6 / X7 into the "Not in this PR" table with the reason (production /docs/* is served from S3 + CloudFront, so vercel.json only reaches the deploy preview), and note that the apex already sends the identical HSTS value. Keeping the vercel.json block is fine; it just needs to be described as preview-only.


🟢 NOT ADDRESSED, FINE TO DEFER (2)

  • CSP has no form-action or media-src, and script-src 'unsafe-inline' has no nonce. Unchanged. Only bites when the header is actually delivered, so it belongs with the CloudFront work above rather than here.
  • No prefers-reduced-motion handling for the 30 autoplay looping videos. Unchanged. Not a regression against the GIFs, and needs JS since CSS cannot stop autoplay. Reasonable as a follow-up.

The 💡 about moving the font to src/fonts/ with the Roboto relative-url pattern is worth much less now that the cache header is fixed. Dropping it is the right call.


🎉 PRAISE

  • Every fix was verified rather than asserted, and the two I expected to be fudged were not: the record-api re-encode updated the declared width/height and regenerated the poster at the new size, so the aspect ratio stays exact.
  • The resolveStaticPath fix is stricter than the finding asked for. Requiring a leading / closes the whole class instead of the one case I named.
  • The mousemove removal carries a comment explaining the reasoning, so it will not get re-added by someone adding "more interaction signals".

Reviewed with the code-review-skill four-phase process. Severity: 🔴 blocking · 🟡 important · 🟢 nit · 💡 suggestion · 📚 learning · 🎉 praise.

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.

3 participants