Skip to content
Closed
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
4 changes: 2 additions & 2 deletions web/app/[locale]/(landing)/tracked-link.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import posthog from "posthog-js";
import { Link, usePathname } from "@/i18n/navigation";
import { captureAnalyticsClick } from "../../lib/analytics";

// Localized internal link that records a PostHog click event, so we can see how
// many clicks each guide / landing page gets and where they came from.
Expand All @@ -21,7 +21,7 @@ export function TrackedLink({
<Link
href={href}
className={className}
onClick={() => posthog.capture(event, { target: href, from: pathname })}
onClick={() => captureAnalyticsClick(event, { target: href, from: pathname })}
>
{children}
</Link>
Expand Down
8 changes: 4 additions & 4 deletions web/app/[locale]/components/download-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import { Menu } from "@base-ui-components/react/menu";
import { useTranslations } from "next-intl";
import posthog from "posthog-js";
import { useState } from "react";
import { Link, usePathname } from "../../../i18n/navigation";
import { captureAnalyticsClick } from "../../lib/analytics";
import {
DOWNLOAD_CONFIRMATION_HREF,
DOWNLOAD_CONFIRMATION_PATH,
Expand Down Expand Up @@ -91,7 +91,7 @@ export function DownloadButton({
"group flex items-center justify-center hover:bg-background/[0.04] dark:hover:bg-background/[0.03] data-[popup-open]:bg-background/[0.04] dark:data-[popup-open]:bg-background/[0.03]";

const captureMac = () =>
posthog.capture("cmuxterm_download_clicked", { location, platform: "mac" });
captureAnalyticsClick("cmuxterm_download_clicked", { location, platform: "mac" });

// The Apple mark artwork has an 814:1000 aspect ratio. Derive the box width
// from its height so the glyph fills the frame instead of letterboxing inside
Expand Down Expand Up @@ -194,7 +194,7 @@ export function DownloadButton({
<Menu.Item
render={<Link href="/ios" target="_blank" rel="noreferrer" />}
onClick={() =>
posthog.capture("cmuxterm_download_clicked", {
captureAnalyticsClick("cmuxterm_download_clicked", {
location,
platform: "ios",
})
Expand All @@ -214,7 +214,7 @@ export function DownloadButton({
<Menu.Item
key={platform}
onClick={() => {
posthog.capture("cmuxterm_waitlist_opened", {
captureAnalyticsClick("cmuxterm_waitlist_opened", {
location,
platform,
});
Expand Down
4 changes: 2 additions & 2 deletions web/app/[locale]/components/faq-platform-answer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { useTranslations } from "next-intl";
import posthog from "posthog-js";
import { useState } from "react";
import { captureAnalyticsClick } from "../../lib/analytics";
import { WaitlistDialog } from "./waitlist-dialog";

const LOCATION = "faq";
Expand All @@ -24,7 +24,7 @@ export function FaqPlatformAnswer({ linkClass }: { linkClass: string }) {
<button
type="button"
onClick={() => {
posthog.capture("cmuxterm_waitlist_opened", {
captureAnalyticsClick("cmuxterm_waitlist_opened", {
location: LOCATION,
platform: "any",
});
Expand Down
4 changes: 2 additions & 2 deletions web/app/[locale]/components/github-button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useTranslations } from "next-intl";
import posthog from "posthog-js";
import { captureAnalyticsClick } from "../../lib/analytics";

export function GitHubButton({ location = "hero" }: { location?: string }) {
const t = useTranslations("common");
Expand All @@ -10,7 +10,7 @@ export function GitHubButton({ location = "hero" }: { location?: string }) {
href="https://github.com/manaflow-ai/cmux"
target="_blank"
rel="noopener noreferrer"
onClick={() => posthog.capture("cmuxterm_github_clicked", { location })}
onClick={() => captureAnalyticsClick("cmuxterm_github_clicked", { location })}
className="inline-flex items-center whitespace-nowrap gap-2 rounded-full border border-border px-5 py-2.5 text-[15px] font-medium text-foreground hover:bg-code-bg transition-colors"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
Expand Down
4 changes: 2 additions & 2 deletions web/app/[locale]/components/github-stars.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useEffect, useState } from "react";
import posthog from "posthog-js";
import { captureAnalyticsClick } from "../../lib/analytics";

const STARS_CACHE_TTL_MS = 300_000;
let cachedStars: number | null = null;
Expand Down Expand Up @@ -105,7 +105,7 @@ export function GitHubStarsBadge({
target="_blank"
rel="noopener noreferrer"
onClick={() =>
posthog.capture("cmuxterm_github_clicked", { location })
captureAnalyticsClick("cmuxterm_github_clicked", { location })
}
className={classes}
>
Expand Down
6 changes: 3 additions & 3 deletions web/app/[locale]/components/nav-links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useTranslations } from "next-intl";
import { Link } from "../../../i18n/navigation";
import posthog from "posthog-js";
import { captureAnalyticsClick } from "../../lib/analytics";
import { ProUpgradeVisibility } from "./pro-upgrade-visibility";

export function NavLinks() {
Expand Down Expand Up @@ -37,7 +37,7 @@ export function NavLinks() {
<Link
href="/pricing"
onClick={() =>
posthog.capture("cmuxterm_pricing_nav_clicked", { location: "nav" })
captureAnalyticsClick("cmuxterm_pricing_nav_clicked", { location: "nav" })
}
className="hover:text-foreground transition-colors"
>
Expand All @@ -48,7 +48,7 @@ export function NavLinks() {
href="https://github.com/manaflow-ai/cmux"
target="_blank"
rel="noopener noreferrer"
onClick={() => posthog.capture("cmuxterm_github_clicked", { location: "navbar" })}
onClick={() => captureAnalyticsClick("cmuxterm_github_clicked", { location: "navbar" })}
className="hover:text-foreground transition-colors"
>
{t("github")}
Expand Down
4 changes: 2 additions & 2 deletions web/app/[locale]/components/pro-cta-link.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";

import posthog from "posthog-js";
import {
pricingActionClassName,
type PricingActionSize,
} from "../../components/pricing-shared";
import { captureAnalyticsClick } from "../../lib/analytics";
import {
isClientConfigFlagEnabled,
useClientConfigFlag,
Expand Down Expand Up @@ -44,7 +44,7 @@ export function ProCtaLink({
<a
href={checkout ? checkoutHref : fallbackHref}
onClick={() =>
posthog.capture("cmuxterm_pro_cta_clicked", {
captureAnalyticsClick("cmuxterm_pro_cta_clicked", {
location,
checkout,
})
Expand Down
4 changes: 2 additions & 2 deletions web/app/[locale]/components/waitlist-callout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { useTranslations } from "next-intl";
import posthog from "posthog-js";
import { useState } from "react";
import { captureAnalyticsClick } from "../../lib/analytics";
import { WaitlistDialog } from "./waitlist-dialog";

/**
Expand All @@ -27,7 +27,7 @@ export function WaitlistCallout({
<button
type="button"
onClick={() => {
posthog.capture("cmuxterm_waitlist_opened", {
captureAnalyticsClick("cmuxterm_waitlist_opened", {
location,
platform: "any",
});
Expand Down
53 changes: 17 additions & 36 deletions web/app/[locale]/components/waitlist-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import { Checkbox } from "@base-ui-components/react/checkbox";
import { Dialog } from "@base-ui-components/react/dialog";
import { useTranslations } from "next-intl";
import posthog from "posthog-js";
import { useRef, useState } from "react";
import { captureAnalyticsEvent } from "../../lib/analytics";
import {
WAITLIST_EARLY_ACCESS_FLAGS,
WAITLIST_PLATFORMS,
Expand All @@ -29,53 +29,34 @@ const SUBMIT_DELAY_MS = 600;
const VALIDATE_TIMEOUT_MS = 4000;

/**
* Records a waitlist signup by POSTing the event straight to PostHog's capture
* endpoint and awaiting delivery, so the UI shows success only when the record
* is actually accepted (posthog-js `capture` is fire-and-forget). The event
* `$set`s the email and Early Access enrollment, so the signup persists
* server-side even if the SDK's own requests are blocked. Returns whether the
* server accepted it.
* Records a waitlist signup through cmux's first-party analytics route and
* awaits delivery, so the UI shows success only when PostHog accepts the
* server-forwarded event. The event `$set`s the email and Early Access
* enrollment, so the signup persists server-side even when browser extensions
* block tracker-shaped PostHog ingest URLs.
*/
async function recordWaitlistSignup(
email: string,
platforms: WaitlistPlatform[],
location: string,
): Promise<boolean> {
const host = posthog.config?.api_host;
const token = posthog.config?.token;
if (!host || !token) return false;
const enrollment = Object.fromEntries(
platforms.map((p) => [
`$feature_enrollment/${WAITLIST_EARLY_ACCESS_FLAGS[p]}`,
true,
]),
);
try {
// `/e/` is the capture endpoint posthog-js itself posts events to (through
// the same first-party `api_host` proxy), so this matches the SDK's path
// and payload shape rather than inventing a new one.
const res = await fetch(`${host.replace(/\/+$/, "")}/e/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
keepalive: true,
body: JSON.stringify({
api_key: token,
event: "cmuxterm_waitlist_signup",
properties: {
distinct_id: email,
email,
platforms,
location,
$set: { email, ...enrollment },
$set_once: { waitlist_email: email },
},
timestamp: new Date().toISOString(),
}),
});
return res.ok;
} catch {
return false;
}
return await captureAnalyticsEvent(
"cmuxterm_waitlist_signup",
{
email,
platforms,
location,
$set: { email, ...enrollment },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Waitlist Identity Split

The signup event now uses the email address as the forwarded distinct_id, while pageviews and clicks still use the local anonymous PostHog ID from getAnalyticsDistinctId(). Because this path does not send an alias or $anon_distinct_id, a normal visitor who browses anonymously and then joins the waitlist can end up with the waitlist event on a separate email-keyed person, breaking attribution from the earlier pageviews and clicks.

$set_once: { waitlist_email: email },
},
{ distinctId: email, keepalive: true },
);
}

/**
Expand Down
13 changes: 10 additions & 3 deletions web/app/[locale]/posthog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect, Suspense } from "react";
import { captureAnalyticsClick } from "../lib/analytics";

if (typeof window !== "undefined") {
posthog.init("phc_opOVu7oFzR9wD3I6ZahFGOV2h3mqGpl5EHyQvmHciDP", {
api_host: "https://r.cmux.com",
api_host: "/api/analytics/posthog-sdk-disabled",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing PostHog SDK stub route

Medium Severity

posthog.init now sets api_host to /api/analytics/posthog-sdk-disabled, but the web app has no matching API handler under app/api/analytics. The SDK still issues requests to paths such as /e/ beneath that host, so they return 404 instead of a deliberate no-op sink.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9686abc. Configure here.

ui_host: "https://us.posthog.com",
autocapture: false,
person_profiles: "identified_only",
capture_pageview: false,
capture_pageleave: true,
capture_pageleave: false,
disable_session_recording: true,
disable_surveys: true,
disable_external_dependency_loading: true,
advanced_disable_flags: true,
advanced_disable_decide: true,
advanced_disable_feature_flags: true,
});
}
Expand All @@ -25,7 +32,7 @@ function PageviewTracker() {
let url = window.origin + pathname;
const search = searchParams.toString();
if (search) url += "?" + search;
posthog.capture("$pageview", { $current_url: url });
captureAnalyticsClick("$pageview", { $current_url: url });
}
}, [pathname, searchParams]);

Expand Down
Loading