DEV Community

Cover image for Self-Healing Chrome Extension: Building a DOM Injector That Fights Back
Parvej Shah
Parvej Shah

Posted on Edited on Originally published at parvejshah.com

Self-Healing Chrome Extension: Building a DOM Injector That Fights Back

Originally published at parvejshah.com/blog/building-manifest-v3-ai-chrome-extensions by Parvej Shah.

LinkedIn's DOM Doesn't Just Drift

Most sites' markup drifts gradually over time. LinkedIn's is different: periodically, it ships builds where every semantic class name — feed-shared-update, social-actions-bar, all of it — is replaced with a short hashed token. Same page, same layout, but every selector you wrote against it stops matching overnight.

A content script that hardcodes selectors against a page you don't control is fragile by default. Against a page that occasionally scrambles its own class names on purpose, hardcoded selectors aren't just fragile — they're a losing strategy.

Building Something That Fixes Itself

Instead of chasing LinkedIn's markup by hand every time it changed, the extension eventually grew a self-healing layer: on load, it takes a snapshot of the feed DOM, sends it to an AI model, and asks for a fresh set of selectors.

The snapshot isn't a raw DOM dump. It's pruned aggressively  ads, nav, and sidebars stripped out, tag depth capped, children per node capped, long hashed class names filtered out, and any leaf text content run through PII stripping before being included. The whole thing is budgeted to stay under roughly 3,000 tokens serialized. That's a deliberate choice: sending less data isn't just cheaper, it also means the model isn't accidentally handed a stranger's post content to reason about.

The AI's job is narrow: look at this structural sketch of the page and return three CSS selectors — where posts live, where the post text is, where to inject a button — as JSON. That JSON becomes a "strategy," cached in `chrome.storage.local` and re-validated against the live DOM on every subsequent load before it's trusted again.

## The Fallback Chain

A single AI call is not something you want standing between a user and a working feature, so the actual flow on every page load is five steps deep:

Enter fullscreen mode Exit fullscreen mode


javascript
// 1. Load cached strategy, validate against live DOM
// 2. If invalid/stale, regenerate via AI (rate-limited to 1 call / 30 min)
// 3. If AI unavailable or fails, fall back to a bank of hardcoded selectors
// 4. If that fails too, fall back to content.js's original 5-tier
// heuristic parser (text-content walking, data-view-name anchors)
// 5. Log every outcome to a rolling debug buffer for the popup's debug panel




The rate limit on AI regeneration (once per 30 minutes) exists because a validation failure loop without one would mean an AI call on every page load for every user whenever LinkedIn's DOM was in a bad state — expensive and pointless if the underlying cause won't resolve by calling again immediately.

## When LinkedIn Obfuscates on Purpose

The specific failure mode this was built for shows up as a distinct code path: an `isCorruptedDOM()` check looks for a feed marked with `data-view-name="feed-full-update"` but with none of the expected semantic classes present — the signature of an obfuscated build. When that's detected, the extension stops trying to match classes entirely and instead walks the DOM by structural role: `span[dir="ltr"]` elements tend to survive obfuscation intact, and a `TreeWalker` heuristic can still locate the action bar by matching visible text content ("Like", "Comment", "Repost") rather than any class name at all.

This is the part of the project I'm most proud of technically, and also the part that made it clear where the ceiling was. Text-content heuristics work until LinkedIn changes wording, adds another localization variant, or restructures the markup enough that even structural role stops being a reliable anchor. Every layer we added bought time, not permanence.

## Where It Ended Up

The extension is published and live on the Chrome Web Store today. But active development stopped after the self-healing system's last iteration. LinkedIn's anti-automation posture — DOM obfuscation, and behavior that reads as active bot detection rather than incidental markup churn — kept escalating faster than a side project could track, and at some point the honest call was to stop rather than keep building a more elaborate response to a page actively working against being scraped.

The lesson isn't "don't build content-script extensions." It's that self-healing buys real resilience against ordinary drift, but it doesn't change the fundamental asymmetry: the host page can always change faster than you can adapt, and eventually that stops being a technical problem you can architect your way out of.

---

*Parvej Shah is a Lead Full-Stack Web Developer & Platform Architect based in Dhaka, Bangladesh. Explore full architecture case studies and production code at [parvejshah.com](https://parvejshah.com).*
Enter fullscreen mode Exit fullscreen mode

Top comments (0)