chore(frontend): Cross-domain PostHog tracking (#12166)

This commit is contained in:
sp.wack
2026-01-13 18:07:56 +04:00
committed by GitHub
parent eabba5c160
commit 27c16d6691
3 changed files with 132 additions and 40 deletions

View File

@@ -0,0 +1,69 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { PostHogWrapper } from "#/components/providers/posthog-wrapper";
import OptionService from "#/api/option-service/option-service.api";
// Mock PostHogProvider to capture the options passed to it
const mockPostHogProvider = vi.fn();
vi.mock("posthog-js/react", () => ({
PostHogProvider: (props: Record<string, unknown>) => {
mockPostHogProvider(props);
return props.children;
},
}));
describe("PostHogWrapper", () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset URL hash
window.location.hash = "";
// Mock the config fetch
// @ts-expect-error - partial mock
vi.spyOn(OptionService, "getConfig").mockResolvedValue({
POSTHOG_CLIENT_KEY: "test-posthog-key",
});
});
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";
render(
<PostHogWrapper>
<div data-testid="child" />
</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({
bootstrap: {
distinctID: "user-123",
sessionID: "session-456",
},
}),
}),
);
});
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";
render(
<PostHogWrapper>
<div data-testid="child" />
</PostHogWrapper>,
);
// Wait for async config fetch and PostHog initialization
await screen.findByTestId("child");
// Verify URL hash was cleaned up
expect(window.location.hash).toBe("");
});
});

View File

@@ -0,0 +1,61 @@
import React from "react";
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 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");
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
window.history.replaceState(
null,
"",
window.location.pathname + window.location.search,
);
return { distinctID: distinctId, sessionID: sessionId };
}
return undefined;
}
export function PostHogWrapper({ children }: { children: React.ReactNode }) {
const [posthogClientKey, setPosthogClientKey] = React.useState<string | null>(
null,
);
const [isLoading, setIsLoading] = React.useState(true);
const bootstrapIds = React.useMemo(() => getBootstrapFromHash(), []);
React.useEffect(() => {
(async () => {
try {
const config = await OptionService.getConfig();
setPosthogClientKey(config.POSTHOG_CLIENT_KEY);
} catch {
displayErrorToast("Error fetching PostHog client key");
} finally {
setIsLoading(false);
}
})();
}, []);
if (isLoading || !posthogClientKey) {
return children;
}
return (
<PostHogProvider
apiKey={posthogClientKey}
options={{
api_host: "https://us.i.posthog.com",
person_profiles: "identified_only",
bootstrap: bootstrapIds,
}}
>
{children}
</PostHogProvider>
);
}

View File

@@ -6,50 +6,12 @@
*/
import { HydratedRouter } from "react-router/dom";
import React, { startTransition, StrictMode } from "react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { PostHogProvider } from "posthog-js/react";
import "./i18n";
import { QueryClientProvider } from "@tanstack/react-query";
import OptionService from "./api/option-service/option-service.api";
import { displayErrorToast } from "./utils/custom-toast-handlers";
import { queryClient } from "./query-client-config";
function PostHogWrapper({ children }: { children: React.ReactNode }) {
const [posthogClientKey, setPosthogClientKey] = React.useState<string | null>(
null,
);
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
(async () => {
try {
const config = await OptionService.getConfig();
setPosthogClientKey(config.POSTHOG_CLIENT_KEY);
} catch {
displayErrorToast("Error fetching PostHog client key");
} finally {
setIsLoading(false);
}
})();
}, []);
if (isLoading || !posthogClientKey) {
return children;
}
return (
<PostHogProvider
apiKey={posthogClientKey}
options={{
api_host: "https://us.i.posthog.com",
person_profiles: "identified_only",
}}
>
{children}
</PostHogProvider>
);
}
import { PostHogWrapper } from "./components/providers/posthog-wrapper";
async function prepareApp() {
if (