Track attribution, cta surfaces, and the email funnel in the viewer

This commit is contained in:
Jonathan Singer
2026-07-20 15:04:29 -04:00
parent 10d6136352
commit 2351bf9323
10 changed files with 212 additions and 178 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>Strix Results</title>
<script type="module" crossorigin src="./assets/index-D10QfT5L.js"></script>
<script type="module" crossorigin src="./assets/index-ApSY-bBm.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Cldwq1Im.css">
</head>
<body>
+13 -13
View File
@@ -38,7 +38,7 @@ import {
type LoadedRun,
type RunsPayload,
} from "@/data/serverSource";
import { SIGNUP_URL, trackCta } from "@/lib/cta";
import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta";
import Sidebar from "@/components/Sidebar";
import PastRunsView from "@/components/PastRunsView";
import EmailReportView from "@/components/EmailReportView";
@@ -183,20 +183,20 @@ export default function App() {
setView("overview");
}, []);
const goEmail = useCallback((skipDisclosure: boolean) => {
trackCta("email_report");
const goEmail = useCallback((skipDisclosure: boolean, surface: string) => {
trackCta("email_report", surface);
setEmailPurpose("report");
setEmailSkipDisclosure(skipDisclosure);
setView("email");
}, []);
// Sidebar entry keeps the disclosure (first place those users see it);
const openEmail = useCallback(() => goEmail(false), [goEmail]);
const openEmail = useCallback(() => goEmail(false, "sidebar"), [goEmail]);
// the Overview CTA already states the tradeoff, so it starts the flow directly.
const openEmailFromOverview = useCallback(() => goEmail(true), [goEmail]);
const openEmailFromOverview = useCallback(() => goEmail(true, "overview"), [goEmail]);
const openVerify = useCallback(() => {
trackCta("history_unlock");
trackCta("history_unlock", "past_runs");
setEmailPurpose("verify");
setEmailSkipDisclosure(false);
setView("email");
@@ -208,7 +208,7 @@ export default function App() {
}, [refreshRuns]);
const selectFeature = useCallback((slug: string) => {
trackCta(`nav_${slug}`);
trackCta(slug, "sidebar_nav");
setActiveFeature(slug);
setView("feature");
}, []);
@@ -244,10 +244,10 @@ export default function App() {
<div className="border-b border-[#222]">
<div className="max-w-[72rem] mx-auto px-6 py-4 flex items-center gap-1.5">
<a
href="https://app.strix.ai"
href={ctaUrl("https://app.strix.ai", "logo")}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta("logo")}
onClick={() => trackCta("logo", "topbar")}
className="flex items-center gap-1.5 opacity-90 transition-opacity hover:opacity-100 lg:hidden"
title="Open Strix Cloud"
>
@@ -266,10 +266,10 @@ export default function App() {
/>
)}
<a
href={SIGNUP_URL}
href={ctaUrl(SIGNUP_URL, "run_in_cloud")}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta("run_in_cloud")}
onClick={() => trackCta("run_in_cloud", "topbar")}
className="inline-flex items-center gap-1 rounded-lg bg-white px-3 py-1.5 text-xs font-semibold text-black transition-opacity hover:opacity-90"
>
Run in the cloud
@@ -657,7 +657,7 @@ function OverviewTab({
{/* Near Recommendations: act on the fixes. */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{RECOMMENDATION_CTAS.map((item) => (
<ProTile key={item.slug} item={item} />
<ProTile key={item.slug} item={item} surface="overview" />
))}
</div>
@@ -666,7 +666,7 @@ function OverviewTab({
<p className="mb-2 text-sm font-semibold text-white">Continuous coverage for your org</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{COVERAGE_CTAS.map((item) => (
<ProTile key={item.slug} item={item} />
<ProTile key={item.slug} item={item} surface="overview" />
))}
</div>
</div>
@@ -6,6 +6,7 @@ import {
sendReport,
type AuthStatus,
} from "@/data/serverSource";
import { track } from "@/lib/cta";
/**
* The email-report / email-verification flow rendered as its own page (not a
@@ -87,6 +88,7 @@ export default function EmailReportView({
setError(null);
const result = await sendReport(activeRun);
if (result.ok) {
track("report_sent");
setPassword(result.password);
setFilename(result.filename);
setStep("password");
@@ -126,6 +128,7 @@ export default function EmailReportView({
}
const domain = value.slice(value.lastIndexOf("@") + 1).toLowerCase();
if (COMMON_FREE_DOMAINS.has(domain)) {
track("work_email_required");
setError(OTP_START_ERRORS.work_email_required);
return;
}
@@ -134,9 +137,11 @@ export default function EmailReportView({
const result = await otpStart(value);
setBusy(false);
if (result.ok) {
track("email_submitted", { purpose });
setNotice(`We sent a 6-digit code to ${value}.`);
setStep("code");
} else {
if (result.error === "work_email_required") track("work_email_required");
setError(OTP_START_ERRORS[result.error] ?? "Could not send a code. Try again.");
}
};
@@ -155,6 +160,7 @@ export default function EmailReportView({
setError("That code did not match. Check it and try again.");
return;
}
track("email_verified", { purpose });
setSentTo(result.email);
onAuthChanged();
if (verifyOnly) onExit("history");
@@ -5,7 +5,7 @@ import {
Users,
ArrowUpRight,
} from "lucide-react";
import { SIGNUP_URL, PRICING_URL, trackCta } from "@/lib/cta";
import { SIGNUP_URL, PRICING_URL, ctaUrl, trackCta } from "@/lib/cta";
import type { ProFeature } from "@/lib/pro-features";
import { ProTag } from "@/components/ProCta";
@@ -70,20 +70,20 @@ export default function FeatureDetail({ feature }: { feature: ProFeature }) {
<div className="mt-6 flex flex-col items-center gap-3">
<a
href={SIGNUP_URL}
href={ctaUrl(SIGNUP_URL, feature.slug)}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(feature.slug)}
onClick={() => trackCta(feature.slug, "feature_page")}
className="inline-flex w-full items-center justify-center gap-1.5 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90"
>
Start free
<ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
</a>
<a
href={PRICING_URL}
href={ctaUrl(PRICING_URL, feature.slug)}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(`${feature.slug}_pricing`)}
onClick={() => trackCta(feature.slug, "feature_page_plans")}
className="inline-flex items-center gap-1 text-xs text-[#888] transition-colors hover:text-white"
>
View all plans
+8 -6
View File
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { ArrowUpRight } from "lucide-react";
import { SIGNUP_URL, trackCta } from "@/lib/cta";
import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta";
import type { ProFeature } from "@/lib/pro-features";
/**
@@ -69,14 +69,14 @@ export interface ProItem {
* Card-style Pro feature tile: icon + name + one-liner + Pro tag + arrow.
* Used in the top upsell row and inline CTA grids.
*/
export function ProTile({ item }: { item: ProItem }) {
export function ProTile({ item, surface }: { item: ProItem; surface?: string }) {
const Icon = item.icon;
return (
<a
href={SIGNUP_URL}
href={ctaUrl(SIGNUP_URL, item.slug)}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(item.slug)}
onClick={() => trackCta(item.slug, surface)}
title={item.desc}
className="group block cursor-pointer rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-4 text-left transition-colors hover:border-[#444]"
>
@@ -140,19 +140,21 @@ export function ProInlineCta({
desc,
slug,
icon: Icon,
surface,
}: {
label: string;
desc: string;
slug: string;
icon: React.ElementType;
surface?: string;
}) {
return (
<Tooltip text={desc}>
<a
href={SIGNUP_URL}
href={ctaUrl(SIGNUP_URL, slug)}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(slug)}
onClick={() => trackCta(slug, surface)}
className="group inline-flex items-center gap-2 rounded-lg border border-[#222] bg-[rgba(255,255,255,0.02)] px-3 py-2 text-sm text-[#aaa] transition-colors hover:border-[#444] hover:text-white"
>
<Icon className="h-4 w-4 text-[#888] transition-colors group-hover:text-white" aria-hidden="true" />
+5 -5
View File
@@ -9,7 +9,7 @@ import {
LogOut,
ShieldCheck,
} from "lucide-react";
import { SIGNUP_URL, trackCta } from "@/lib/cta";
import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta";
import { ProNavItem } from "@/components/ProCta";
import { FEATURES, PLATFORM_ORDER } from "@/lib/pro-features";
import type { View } from "@/App";
@@ -57,10 +57,10 @@ export default function Sidebar({
{/* Header: wordmark + Start free + signed-in chip */}
<div className="px-1.5">
<a
href="https://app.strix.ai"
href={ctaUrl("https://app.strix.ai", "logo")}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta("logo")}
onClick={() => trackCta("logo", "sidebar")}
className="flex items-center gap-1.5 opacity-90 transition-opacity hover:opacity-100"
title="Open Strix Cloud"
>
@@ -68,10 +68,10 @@ export default function Sidebar({
<span className="text-base font-medium tracking-tight text-white">Strix</span>
</a>
<a
href={SIGNUP_URL}
href={ctaUrl(SIGNUP_URL, "sidebar_start_free")}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta("sidebar_start_free")}
onClick={() => trackCta("sidebar_start_free", "sidebar")}
className="mt-3 flex w-full cursor-pointer items-center justify-center gap-1.5 rounded-lg bg-white px-3 py-2 text-sm font-semibold text-black transition-opacity hover:opacity-90"
>
Explore full platform
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef } from "react";
import { X } from "lucide-react";
import { AgentTranscript } from "./AgentTranscript";
import { SIGNUP_URL, trackCta } from "@/lib/cta";
import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta";
import type { TranscriptAgent, TranscriptEvent } from "@/data/serverSource";
/** Status -> the small leading dot color, matching the graph node styling. */
@@ -101,10 +101,10 @@ export function AgentDetailModal({
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-[#222] px-5 py-3">
<span className="text-xs text-[#888]">Steer agents live from your terminal.</span>
<a
href={SIGNUP_URL}
href={ctaUrl(SIGNUP_URL, "steer_agent")}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta("steer_agent")}
onClick={() => trackCta("steer_agent", "agents")}
className="text-xs font-medium text-[#60a5fa] hover:underline"
>
Upgrade to Pro to steer agents from the web &rarr;
@@ -242,18 +242,21 @@ export default function VulnerabilityDetail({ vulnerability }: VulnerabilityDeta
desc="Fix it for you and open a PR, retested."
slug="autofix"
icon={Wrench}
surface="finding_detail"
/>
<ProInlineCta
label="Sync to Jira / Linear"
desc="Two-way sync findings to Jira, Linear, and Slack."
slug="integrations"
icon={GitMerge}
surface="finding_detail"
/>
<ProInlineCta
label="Catch this in PR reviews"
desc="Pentest every pull request your team opens."
slug="pr_reviews"
icon={GitPullRequest}
surface="finding_detail"
/>
</div>
</div>
+29 -6
View File
@@ -4,17 +4,40 @@ export const SIGNUP_URL = "https://app.strix.ai/api/auth/signup";
export const DEMO_URL = "https://strix.ai/demo";
export const PRICING_URL = "https://strix.ai/pricing";
// Best-effort, anonymous conversion tracking. The local server forwards this to
// PostHog only if the user has telemetry enabled; it never blocks navigation.
export function trackCta(cta: string): void {
// Attribution params appended to every outbound CTA link so the destination
// analytics can see the local viewer drove the click, with utm_content carrying
// the CTA slug so we know which one.
const CTA_PARAMS =
"ref=oss_viewer&utm_source=oss_viewer&utm_medium=local_viewer&utm_campaign=oss_viewer";
export function ctaUrl(base: string, slug: string): string {
const sep = base.includes("?") ? "&" : "?";
return `${base}${sep}${CTA_PARAMS}&utm_content=${encodeURIComponent(slug)}`;
}
// Best-effort, anonymous beacon. The local server forwards this to PostHog only
// if the user has telemetry enabled; it never blocks navigation. Undefined
// props are dropped so we only send what is set. NEVER pass PII here (no email,
// code, or report content) - the props are limited to anonymous metadata.
export function track(event: string, props: Record<string, string | undefined> = {}): void {
try {
const body = JSON.stringify({ event: "cta_clicked", cta });
const body: Record<string, string> = { event };
for (const [key, value] of Object.entries(props)) {
if (value !== undefined) body[key] = value;
}
const payload = JSON.stringify(body);
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
navigator.sendBeacon("/api/event", body);
navigator.sendBeacon("/api/event", payload);
} else {
void fetch("/api/event", { method: "POST", body, keepalive: true });
void fetch("/api/event", { method: "POST", body: payload, keepalive: true });
}
} catch {
/* analytics is best-effort */
}
}
// Anonymous conversion tracking for a sign-up/upsell click. `surface` records
// where the click happened so one CTA slug can be reused across placements.
export function trackCta(cta: string, surface?: string): void {
track("cta_clicked", { cta, surface });
}