From 9d8e4c44cc8df8e6c3f405e34822d142a2f46e85 Mon Sep 17 00:00:00 2001
From: "sp.wack" <83104063+amanape@users.noreply.github.com>
Date: Wed, 4 Feb 2026 16:15:53 +0400
Subject: [PATCH] fix(frontend): fix cross-domain PostHog tracking param names
and persist bootstrap IDs across OAuth redirects (#12729)
---
.../providers/posthog-wrapper.test.tsx | 105 ++++++++++++++++--
.../components/providers/posthog-wrapper.tsx | 29 +++--
2 files changed, 118 insertions(+), 16 deletions(-)
diff --git a/frontend/__tests__/components/providers/posthog-wrapper.test.tsx b/frontend/__tests__/components/providers/posthog-wrapper.test.tsx
index 15ea5ec17a..bfcf00554f 100644
--- a/frontend/__tests__/components/providers/posthog-wrapper.test.tsx
+++ b/frontend/__tests__/components/providers/posthog-wrapper.test.tsx
@@ -17,6 +17,8 @@ describe("PostHogWrapper", () => {
vi.clearAllMocks();
// Reset URL hash
window.location.hash = "";
+ // Clear sessionStorage
+ sessionStorage.clear();
// Mock the config fetch
// @ts-expect-error - partial mock
vi.spyOn(OptionService, "getConfig").mockResolvedValue({
@@ -24,9 +26,9 @@ describe("PostHogWrapper", () => {
});
});
- it("should initialize PostHog with bootstrap IDs from URL hash", async () => {
- // Set up URL hash with cross-domain tracking params
- window.location.hash = "ph_distinct_id=user-123&ph_session_id=session-456";
+ it("should initialize PostHog with bootstrap IDs from URL hash (without ph_ prefix)", async () => {
+ // Webflow sends distinct_id and session_id without the ph_ prefix
+ window.location.hash = "distinct_id=user-123&session_id=session-456";
render(
@@ -34,10 +36,8 @@ describe("PostHogWrapper", () => {
,
);
- // Wait for async config fetch and PostHog initialization
await screen.findByTestId("child");
- // Verify PostHogProvider was called with bootstrap options
expect(mockPostHogProvider).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({
@@ -51,8 +51,7 @@ describe("PostHogWrapper", () => {
});
it("should clean up URL hash after extracting bootstrap IDs", async () => {
- // Set up URL hash with cross-domain tracking params
- window.location.hash = "ph_distinct_id=user-123&ph_session_id=session-456";
+ window.location.hash = "distinct_id=user-123&session_id=session-456";
render(
@@ -60,10 +59,98 @@ describe("PostHogWrapper", () => {
,
);
- // Wait for async config fetch and PostHog initialization
await screen.findByTestId("child");
- // Verify URL hash was cleaned up
expect(window.location.hash).toBe("");
});
+
+ it("should persist bootstrap IDs to sessionStorage for OAuth survival", async () => {
+ window.location.hash = "distinct_id=user-123&session_id=session-456";
+
+ render(
+
+
+ ,
+ );
+
+ await screen.findByTestId("child");
+
+ // After extracting from hash, IDs should NOT remain in sessionStorage
+ // because they were already consumed during this page load.
+ // But if a full-page redirect happened before PostHog init,
+ // sessionStorage would still have them for the next load.
+ // We verify the write happened by checking the provider received the IDs.
+ expect(mockPostHogProvider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ options: expect.objectContaining({
+ bootstrap: {
+ distinctID: "user-123",
+ sessionID: "session-456",
+ },
+ }),
+ }),
+ );
+ });
+
+ it("should read bootstrap IDs from sessionStorage when hash is absent (post-OAuth)", async () => {
+ // Simulate returning from OAuth: no hash, but sessionStorage has the IDs
+ sessionStorage.setItem(
+ "posthog_bootstrap",
+ JSON.stringify({ distinctID: "user-123", sessionID: "session-456" }),
+ );
+
+ render(
+
+
+ ,
+ );
+
+ await screen.findByTestId("child");
+
+ expect(mockPostHogProvider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ options: expect.objectContaining({
+ bootstrap: {
+ distinctID: "user-123",
+ sessionID: "session-456",
+ },
+ }),
+ }),
+ );
+ });
+
+ it("should clean up sessionStorage after consuming bootstrap IDs", async () => {
+ sessionStorage.setItem(
+ "posthog_bootstrap",
+ JSON.stringify({ distinctID: "user-123", sessionID: "session-456" }),
+ );
+
+ render(
+
+
+ ,
+ );
+
+ await screen.findByTestId("child");
+
+ expect(sessionStorage.getItem("posthog_bootstrap")).toBeNull();
+ });
+
+ it("should initialize without bootstrap when neither hash nor sessionStorage has IDs", async () => {
+ render(
+
+
+ ,
+ );
+
+ await screen.findByTestId("child");
+
+ expect(mockPostHogProvider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ options: expect.objectContaining({
+ bootstrap: undefined,
+ }),
+ }),
+ );
+ });
});
diff --git a/frontend/src/components/providers/posthog-wrapper.tsx b/frontend/src/components/providers/posthog-wrapper.tsx
index 7799fe1713..62f3a0e2ea 100644
--- a/frontend/src/components/providers/posthog-wrapper.tsx
+++ b/frontend/src/components/providers/posthog-wrapper.tsx
@@ -3,22 +3,37 @@ import { PostHogProvider } from "posthog-js/react";
import OptionService from "#/api/option-service/option-service.api";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
-function getBootstrapFromHash() {
+const POSTHOG_BOOTSTRAP_KEY = "posthog_bootstrap";
+
+function getBootstrapIds() {
+ // Try to extract from URL hash (e.g. #distinct_id=abc&session_id=xyz)
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
- const distinctId = params.get("ph_distinct_id");
- const sessionId = params.get("ph_session_id");
+ const distinctId = params.get("distinct_id");
+ const sessionId = params.get("session_id");
if (distinctId && sessionId) {
- // Remove the PostHog tracking params from URL hash to keep URL clean
- // replaceState(state, unused, url) - we pass null state, empty title (ignored by browsers), and the clean URL
+ const bootstrap = { distinctID: distinctId, sessionID: sessionId };
+
+ // Persist to sessionStorage so IDs survive full-page OAuth redirects
+ sessionStorage.setItem(POSTHOG_BOOTSTRAP_KEY, JSON.stringify(bootstrap));
+
+ // Clean the hash from the URL
window.history.replaceState(
null,
"",
window.location.pathname + window.location.search,
);
- return { distinctID: distinctId, sessionID: sessionId };
+ return bootstrap;
}
+
+ // Fallback: check sessionStorage (covers return from OAuth redirect)
+ const stored = sessionStorage.getItem(POSTHOG_BOOTSTRAP_KEY);
+ if (stored) {
+ sessionStorage.removeItem(POSTHOG_BOOTSTRAP_KEY);
+ return JSON.parse(stored) as { distinctID: string; sessionID: string };
+ }
+
return undefined;
}
@@ -27,7 +42,7 @@ export function PostHogWrapper({ children }: { children: React.ReactNode }) {
null,
);
const [isLoading, setIsLoading] = React.useState(true);
- const bootstrapIds = React.useMemo(() => getBootstrapFromHash(), []);
+ const bootstrapIds = React.useMemo(() => getBootstrapIds(), []);
React.useEffect(() => {
(async () => {