mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
chore(frontend): Cross-domain PostHog tracking (#12166)
This commit is contained in:
@@ -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("");
|
||||
});
|
||||
});
|
||||
61
frontend/src/components/providers/posthog-wrapper.tsx
Normal file
61
frontend/src/components/providers/posthog-wrapper.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user