Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions __tests__/components/providers/MixpanelSetup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const trackPageViewMock = jest.fn();
let connectedProfile: { id: number } | null = null;
let pathname = "/";
let performanceConsent: boolean | undefined = undefined;
let searchParams = new URLSearchParams();

jest.mock("@/components/auth/Auth", () => ({
useAuth: () => ({
Expand All @@ -26,6 +27,7 @@ jest.mock("@/components/cookies/CookieConsentContext", () => ({

jest.mock("next/navigation", () => ({
usePathname: () => pathname,
useSearchParams: () => searchParams,
}));

jest.mock("@/services/analytics/mixpanel", () => ({
Expand All @@ -41,6 +43,7 @@ describe("MixpanelSetup", () => {
connectedProfile = null;
pathname = "/";
performanceConsent = undefined;
searchParams = new URLSearchParams();
clearIdentityMock.mockReset();
disableAnalyticsMock.mockReset();
identifyMock.mockReset();
Expand Down Expand Up @@ -68,6 +71,9 @@ describe("MixpanelSetup", () => {
expect(identifyMock).toHaveBeenCalledWith("42");
expect(trackPageViewMock).toHaveBeenCalledWith("/waves", {
has_connected_profile: true,
logical_page: "waves_index",
page_group: "waves",
route_pattern: "/waves",
});
expect(identifyMock.mock.invocationCallOrder[0]).toBeLessThan(
trackPageViewMock.mock.invocationCallOrder[0]
Expand All @@ -83,6 +89,9 @@ describe("MixpanelSetup", () => {
expect(initAnalyticsMock).toHaveBeenCalledTimes(1);
expect(trackPageViewMock).toHaveBeenCalledWith("/waves", {
has_connected_profile: false,
logical_page: "waves_index",
page_group: "waves",
route_pattern: "/waves",
});

rerender(<MixpanelSetup />);
Expand All @@ -93,6 +102,35 @@ describe("MixpanelSetup", () => {
expect(trackPageViewMock).toHaveBeenCalledTimes(2);
expect(trackPageViewMock).toHaveBeenLastCalledWith("/notifications", {
has_connected_profile: false,
logical_page: "notifications",
page_group: "notifications",
route_pattern: "/notifications",
});
});

it("tracks drop detail views separately when the drop query changes", () => {
performanceConsent = true;
pathname = "/waves/wave-1";
searchParams = new URLSearchParams("drop=drop-1");

const { rerender } = render(<MixpanelSetup />);

expect(trackPageViewMock).toHaveBeenCalledWith("/waves/wave-1", {
has_connected_profile: false,
logical_page: "wave_drop_detail",
page_group: "waves",
route_pattern: "/waves/:waveId?drop=:dropId",
});

searchParams = new URLSearchParams("drop=drop-2");
rerender(<MixpanelSetup />);

expect(trackPageViewMock).toHaveBeenCalledTimes(2);
expect(trackPageViewMock).toHaveBeenLastCalledWith("/waves/wave-1", {
has_connected_profile: false,
logical_page: "wave_drop_detail",
page_group: "waves",
route_pattern: "/waves/:waveId?drop=:dropId",
});
});

Expand Down
75 changes: 75 additions & 0 deletions __tests__/services/analytics/pageClassification.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { classifyPageView } from "@/services/analytics/pageClassification";

describe("classifyPageView", () => {
it("classifies the waves index page", () => {
expect(classifyPageView({ pathname: "/waves" })).toEqual({
logicalPage: "waves_index",
pageGroup: "waves",
routePattern: "/waves",
trackingKey: "/waves",
});
});

it("classifies wave detail pages", () => {
expect(
classifyPageView({
pathname: "/waves/cbf4ca5f-06b3-4ea6-bf7d-b193a0699eed",
})
).toEqual({
logicalPage: "wave_page",
pageGroup: "waves",
routePattern: "/waves/:waveId",
trackingKey: "/waves/cbf4ca5f-06b3-4ea6-bf7d-b193a0699eed",
});
});

it("classifies wave drop detail pages from the drop query param", () => {
expect(
classifyPageView({
pathname: "/waves/cbf4ca5f-06b3-4ea6-bf7d-b193a0699eed",
searchParams: new URLSearchParams("drop=drop-123"),
})
).toEqual({
logicalPage: "wave_drop_detail",
pageGroup: "waves",
routePattern: "/waves/:waveId?drop=:dropId",
trackingKey: "/waves/cbf4ca5f-06b3-4ea6-bf7d-b193a0699eed?drop=drop-123",
});
});

it("classifies profile root pages", () => {
expect(classifyPageView({ pathname: "/alice" })).toEqual({
logicalPage: "profile_identity",
pageGroup: "profile",
routePattern: "/:handle",
trackingKey: "/alice",
});
});

it("classifies known profile tab routes", () => {
expect(classifyPageView({ pathname: "/alice/collected" })).toEqual({
logicalPage: "profile_collected",
pageGroup: "profile",
routePattern: "/:handle/collected",
trackingKey: "/alice/collected",
});
});

it("does not misclassify reserved roots as profiles", () => {
expect(classifyPageView({ pathname: "/the-memes" })).toEqual({
logicalPage: "the_memes",
pageGroup: "the_memes",
routePattern: "/the-memes",
trackingKey: "/the-memes",
});
});

it("falls back to a generic profile subpage for unknown user subroutes", () => {
expect(classifyPageView({ pathname: "/alice/followers" })).toEqual({
logicalPage: "profile_subpage",
pageGroup: "profile",
routePattern: "/:handle/:subpage",
trackingKey: "/alice/followers",
});
});
});
23 changes: 16 additions & 7 deletions components/providers/MixpanelSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,27 @@ import {
initAnalytics,
trackPageView,
} from "@/services/analytics/mixpanel";
import { usePathname } from "next/navigation";
import { classifyPageView } from "@/services/analytics/pageClassification";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect, useRef } from "react";

export default function MixpanelSetup() {
const pathname = usePathname();
const searchParams = useSearchParams();
const { connectedProfile } = useAuth();
const { performanceConsent } = useCookieConsent();
const lastTrackedPathRef = useRef<string | null>(null);
const lastTrackedPageKeyRef = useRef<string | null>(null);
const identifiedProfileIdRef = useRef<string | null>(null);
const hasConsent = performanceConsent === true;
const pageView = classifyPageView({
pathname,
searchParams,
});

useEffect(() => {
if (!hasConsent) {
disableAnalytics();
lastTrackedPathRef.current = null;
lastTrackedPageKeyRef.current = null;
identifiedProfileIdRef.current = null;
return;
}
Expand Down Expand Up @@ -58,20 +64,23 @@ export default function MixpanelSetup() {
}, [connectedProfile?.id, hasConsent]);

useEffect(() => {
if (!hasConsent || !pathname) {
if (!hasConsent) {
return;
}

if (lastTrackedPathRef.current === pathname) {
if (lastTrackedPageKeyRef.current === pageView.trackingKey) {
return;
}

lastTrackedPathRef.current = pathname;
lastTrackedPageKeyRef.current = pageView.trackingKey;
trackPageView(pathname, {
has_connected_profile:
connectedProfile?.id !== undefined && connectedProfile.id !== null,
logical_page: pageView.logicalPage,
page_group: pageView.pageGroup,
route_pattern: pageView.routePattern,
});
}, [connectedProfile?.id, hasConsent, pathname]);
}, [connectedProfile?.id, hasConsent, pageView, pathname]);

return null;
}
Loading