test-driven-development
OBobra/superpowers298k近 7 天新增 2,631 个 star
Configure a Next.js app using posthog-js so session recording and dead-click capture survive browser ad blockers. Aliases the three blocker-flagged JS chunks (dead-clicks-autocapture.js, recorder.js, posthog-recorder.js) to bland same-origin paths via Next.js rewrites, and monkey-patches HTMLScriptElement so posthog-js requests the aliases. TRIGGER when posthog-js session replay or dead-click capture is failing in production with ERR_BLOCKED_BY_CLIENT or 404s on /static/dead-clicks-autocapture.js / /static/recorder.js / /static/posthog-recorder.js, or when the user wants to bypass ad blockers for these specific assets. Works alongside an existing PostHog reverse proxy (managed or otherwise) — only the three blocked files divert through Next.js, all other PostHog traffic stays on the original api_host. SKIP for unrelated PostHog setup, non-Next.js apps, or when capture/decide endpoints (/e/, /decide/, /array/*/config.js) are the ones being blocked — those need a different treatment.
The user has posthog-js working mostly — events, feature flags, decide, config all reach PostHog fine — but session recording and/or dead-click capture aren't working, and DevTools shows:
/static/dead-clicks-autocapture.js failing/static/recorder.js or /static/posthog-recorder.js failingERR_BLOCKED_BY_CLIENT or a synthesized 404 (the file actually returns 200 via curl)Root cause: ad blockers (uBlock Origin / Brave Shields / AdBlock Plus / EasyPrivacy) match on filename patterns (recorder, autocapture, dead-clicks) regardless of host. A reverse proxy alone doesn't help — renaming on a different host still trips the same filter. Confirmation step: ask the user to disable their ad blocker on the page; if the requests succeed, this skill applies.
This skill applies the surgical fix: only those three files divert through the Next.js app under bland filenames. All other PostHog traffic (events, decide, config, recording uploads) keeps using the existing api_host.
posthog-js from npm.curl -I https://<api_host>/static/dead-clicks-autocapture.js?v=1.x returns 200. If it returns 404, the upstream proxy is misconfigured — fix that first, this skill won't help.us-assets.i.posthog.com) or EU (eu-assets.i.posthog.com). Default to US if unclear, but ask.posthog.init(...) call (commonly src/lib/posthog.ts, a provider component, or app/layout.tsx). Note the file so you can hook the rewriter before init.next.config.{ts,js,mjs}Insert a rewrites() block. Use /_p/ as the alias prefix (or any path that doesn't contain words like recorder, analytics, tracking, posthog).
import type { NextConfig } from "next";
const PH_ASSETS = "https://us-assets.i.posthog.com"; // EU: "https://eu-assets.i.posthog.com"
const nextConfig: NextConfig = {
// ...existing config...
async rewrites() {
return [
{ source: "/_p/dc.js", destination: `${PH_ASSETS}/static/dead-clicks-autocapture.js` },
{ source: "/_p/r.js", destination: `${PH_ASSETS}/static/recorder.js` },
{ source: "/_p/r2.js", destination: `${PH_ASSETS}/static/posthog-recorder.js` },
];
},
};
export default nextConfig;
Why all three: posthog-js renamed the recorder chunk across versions. Covering both recorder.js and posthog-recorder.js makes the skill version-resilient. Rewrites preserve the ?v=1.x.y query string automatically and incur no Node overhead.
If the project already has a rewrites() or redirects() block, merge rather than replace.
New file at src/lib/posthog-rewrite-loader.ts (adjust path to match the repo's lib convention):
const REWRITES: Array<[RegExp, string]> = [
[/\/static\/dead-clicks-autocapture\.js/, "/_p/dc.js"],
[/\/static\/posthog-recorder\.js/, "/_p/r2.js"],
[/\/static\/recorder\.js/, "/_p/r.js"],
];
export function installPostHogScriptRewriter() {
if (typeof window === "undefined") return;
const w = window as Window & { __phRewriteInstalled?: boolean };
if (w.__phRewriteInstalled) return;
w.__phRewriteInstalled = true;
const desc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, "src");
if (!desc?.set || !desc?.get) return;
Object.defineProperty(HTMLScriptElement.prototype, "src", {
configurable: true,
enumerable: true,
get() {
return desc.get!.call(this);
},
set(value: string) {
if (typeof value === "string") {
for (const [pattern, replacement] of REWRITES) {
if (pattern.test(value)) {
const qIndex = value.indexOf("?");
const query = qIndex >= 0 ? value.slice(qIndex) : "";
value = replacement + query; // strip host -> same-origin
break;
}
}
}
desc.set!.call(this, value);
},
});
}
Notes:
__phRewriteInstalled flag is required because Next.js dev HMR can re-execute modules and Object.defineProperty would otherwise throw./_p/dc.js?v=...) makes the request same-origin, avoiding a CORS preflight on top of the blocker bypass.src property setter on the prototype — posthog-js loads chunks by setting script.src, so this catches them. It does not catch fetch() or dynamic import() (not used by posthog-js for these chunks today, but worth noting if a future SDK version changes).posthog.initThe patcher must be installed before any posthog.init call. Module-level code in the same file as posthog.init works because module evaluation precedes useEffect:
// src/lib/posthog.ts (or wherever posthog.init lives)
import posthog from "posthog-js";
import { installPostHogScriptRewriter } from "@/lib/posthog-rewrite-loader";
if (typeof window !== "undefined") {
installPostHogScriptRewriter();
}
export const initPostHog = () => {
if (typeof window !== "undefined") {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, // unchanged
// ...existing config...
});
}
};
export { posthog };
If posthog.init is called from <Script strategy="beforeInteractive"> in app/layout.tsx instead of from a client module, put the patcher in its own <Script strategy="beforeInteractive"> block placed above the PostHog one. Don't use afterInteractive — by then posthog may have already started loading chunks.
Server side — after deploy:
curl -I "https://<your-domain>/_p/dc.js?v=1.0"
curl -I "https://<your-domain>/_p/r2.js?v=1.0"
Both should return 200, content-type: text/javascript. If 404, the rewrite isn't picked up — Next.js needs a rebuild after next.config changes.
Browser with ad blocker enabled — in DevTools Network:
/_p/dc.js and /_p/r2.js succeed (200)./static/dead-clicks-autocapture.js or /static/posthog-recorder.js requests visible./_p/* entries should show -- (allowed), not red.PostHog dashboard:
next build. Local dev needs npm run dev restart. Production needs a redeploy./e/, /decide/, /array/*/config.js getting blocked is a different problem (host-level rather than filename-level). Solve with a PostHog reverse proxy on the user's own domain, not this skill. PostHog's docs cover that./static/*.js request leaking through and add a regex entry.| File | Change |
|---|---|
next.config.{ts,js} | Add rewrites() for /_p/dc.js, /_p/r.js, /_p/r2.js |
src/lib/posthog-rewrite-loader.ts | New — patches HTMLScriptElement.prototype.src |
src/lib/posthog.ts (or equivalent) | Call installPostHogScriptRewriter() at module load |
OBobra/superpowers298k近 7 天新增 2,631 个 star
MAmattpocock/skills288k近 7 天新增 10,178 个 star
AFaffaan-m/ECC278k近 7 天新增 4,046 个 star
AFaffaan-m/ECC278k近 7 天新增 4,046 个 star
FIfirecrawl/firecrawl191k近 7 天新增 2,010 个 star