From 02d4a9a8feebebde20d2775e86ff3cc19bf1d045 Mon Sep 17 00:00:00 2001
From: Fdy <73393700+MrFadiAi@users.noreply.github.com>
Date: Sun, 19 Jul 2026 02:18:01 +0200
Subject: [PATCH] fix(dashboard): strip browser-extension attrs before
hydration (#7073)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(dashboard): strip browser-extension attrs before hydration
Browser extensions (Bitdefender's bis_skin_checked, Grammarly's
data-gr-ext-installed, LanguageTool's data-lt-installed) inject
attributes into the DOM after SSR but before React hydrates, causing
"attributes didn't match" hydration errors in the dev console.
The and
tags already have suppressHydrationWarning, but
React only applies it one level deep — it doesn't propagate to Next.js
internal elements like the metadata boundary where the
mismatch actually surfaces.
Add a synchronous pre-hydration cleanup script in that:
1. Strips known extension attributes from document.documentElement
2. Observes for late injections via MutationObserver
3. Auto-disconnects after 5s (well past typical hydration)
Verified: curl /login confirms the script is present in the served HTML
with all target attributes (bis_skin_checked, data-google-query-id,
data-gr-ext-installed, data-lt-installed) and the MutationObserver.
Typecheck and lint clean.
* test(dashboard): guard the pre-hydration extension-attr strip script
Add a regression test mirroring the existing
tests/unit/dashboard/crypto-randomuuid-polyfill.test.ts pattern:
readFileSync src/app/layout.tsx and assert the full known
browser-extension attribute list (bis_skin_checked, data-google-query-id,
data-new-gr-c-s-check-loaded, data-gr-ext-installed, data-lt-installed,
data-lt-tmp-id), the MutationObserver wiring (attributeFilter + 5s
auto-disconnect), and the synchronous initial strip against
document.documentElement all stay present in layout.tsx.
Verified fail-then-pass: the assertions fail against the pre-fix tree
(no such script present) and pass once the pre-hydration script is
present, so a future layout.tsx refactor can no longer silently drop
this script and reintroduce the hydration-mismatch warnings extension
users were seeing.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---
src/app/layout.tsx | 32 +++++++++
.../hydration-extension-attrs.test.ts | 72 +++++++++++++++++++
2 files changed, 104 insertions(+)
create mode 100644 tests/unit/dashboard/hydration-extension-attrs.test.ts
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 8388309570..27886bc7d3 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -60,6 +60,38 @@ export default async function RootLayout({ children }) {
return (
+ {/* Pre-hydration cleanup: browser extensions (Bitdefender's
+ bis_skin_checked, Grammarly's data-gr-ext-installed, LanguageTool's
+ data-lt-installed, etc.) inject attributes into the DOM after SSR
+ but before React hydrates, causing hydration mismatch warnings in
+ dev. Strip them synchronously and observe for late injections; the
+ observer auto-disconnects after 5s (well past typical hydration). */}
+
{/* Material Symbols icon font is self-hosted via globals.css
(@import "material-symbols/outlined.css") so icons render even when
the Google Fonts CDN is unreachable (#3695). */}
diff --git a/tests/unit/dashboard/hydration-extension-attrs.test.ts b/tests/unit/dashboard/hydration-extension-attrs.test.ts
new file mode 100644
index 0000000000..4840761d95
--- /dev/null
+++ b/tests/unit/dashboard/hydration-extension-attrs.test.ts
@@ -0,0 +1,72 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { resolve, join } from "node:path";
+
+// Regression guard for the browser-extension hydration-mismatch fix: extensions
+// like Bitdefender (bis_skin_checked), Google Search (data-google-query-id),
+// Grammarly (data-new-gr-c-s-check-loaded / data-gr-ext-installed) and
+// LanguageTool (data-lt-installed / data-lt-tmp-id) inject attributes into the
+// DOM after SSR but before React hydrates. React's suppressHydrationWarning on
+// / only applies one level deep, so the mismatch still surfaces on
+// Next.js's internal elements. The fix adds a synchronous pre-hydration