mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
* docs(download): add redesign plan and copy positioning source of truth Captures motivation (Desktop is Multica's native form; CLI is a distinct scenario for servers/remote boxes, not a Desktop fallback), four-step execution plan, and every touchpoint's current-vs-new copy in EN + ZH. Subsequent UI steps read strings from the positioning doc instead of inventing them inline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(web): /download page with OS auto-detection New landing-group route that serves as the single canonical download destination. Auto-detects OS + arch via navigator.userAgentData (Chromium) with UA-string fallback, then surfaces the matching Desktop installer as the primary CTA. All platforms stay visible below, plus a CLI section (positioned for servers / remote boxes / headless setups, not as a lightweight Desktop) and a Cloud waitlist. Version + asset URLs come from api.github.com/repos/.../releases/latest with Vercel ISR (revalidate=300) so every release automatically propagates — no manual redeploy. Optional GITHUB_TOKEN env var lifts the 60/hr unauthenticated rate limit for local dev. Failure degrades cleanly to "Version unavailable" + a link to GitHub releases. Also points landing hero + footer Download links at /download (previously pointed at the GitHub releases page directly), and re-exports CloudWaitlistExpand from @multica/views/onboarding so the new Cloud section can reuse the existing form. Intel Mac has no binary today (electron-builder targets mac arm64 only); the page is honest about it and routes Intel users to CLI. i18n copy sourced verbatim from docs/download-positioning.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): rewrite Step 3 fork + web Welcome Desktop CTA Welcome screen now self-segments: on web (runtimeInstructions present), the primary CTA is "Download Desktop" with a benefit-led subtitle ("Desktop bundles the runtime — nothing to install. Continue on web to connect your own CLI.") that lets developers with their own CLI recognize their path while guiding everyone else toward the desktop app. Desktop branch drops the "3 minutes" estimate in favor of the aha promise. Download button is a real <a href> link so middle-click / copy-link / screen readers all behave correctly. Step 3 fork drops the stale isMac gate — Windows / Linux binaries now ship, the macOS-only muted card was a lie. The single Desktop card now routes to /download (not GitHub releases directly) so users land on the auto-detect page. CLI card is reframed around its real scenario (servers, remote dev boxes, headless) rather than posing as a lightweight Desktop, and the CLI dialog's stall tier redirects users to Desktop instead of Cloud waitlist when the daemon never registers — Desktop is the genuine retreat. cli-install-instructions gets a one-liner acknowledging the CLI's server use case, mirroring the card copy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(web,auth): desktop promotion on login + solid landing hero download LoginPage accepts a new `extra?: ReactNode` slot rendered below the Google button. The web shell injects a hardcoded-EN "Prefer the desktop app? Download →" nudge there — catching users at their lowest-investment moment, before they've typed an email. Desktop's login wrapper omits the slot (a download prompt inside the app would be absurd), so only the web surface renders it. Copy is English-only for now because the /login route sits outside the landing group's LocaleProvider. Lifting locale detection into the root layout would force every page dynamic and kill the Router Cache — a trade-off not worth two strings. The `auth.login.extra*` i18n keys added during Step 2 are removed for the same reason: they're dead code without a LocaleProvider wrapping login. Landing hero "Download Desktop" upgrades from ghost to solid and swaps its handwritten monitor SVG for lucide-react's Download icon. Both hero CTAs are now solid-weighted — the icon + distinct label differentiates them. href already points to /download from the Step 2 landing nav pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web/download): anchor dark LandingHeader with relative wrapper LandingHeader's dark variant uses `absolute top-0 inset-x-0`, which only reads correctly when wrapped by a positioned ancestor — see multica-landing.tsx:14 for the canonical pattern. Without the wrapper the header escaped to the initial containing block and appeared fixed as users scrolled the page. Also drops the <main> element around the body sections for consistency with the rest of the landing group (neither multica-landing nor about-page-client wraps in <main>). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(landing/hero): keep Download Desktop as ghost to preserve CTA hierarchy Upgrading to solid alongside the existing "Start free trial" CTA killed the primary / secondary distinction — both buttons were white on dark, competing for attention. Revert to ghost so the conversion CTA (trial) stays the visual primary. The lucide Download icon swap stays (cleaner than the handwritten monitor SVG). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(onboarding): update platform-fork assertions for /download route The Desktop card in Step 3 now opens the new /download page instead of GitHub releases, and the post-click feedback text changed to match ("Continuing on the download page…" in place of "Downloading Multica…"). Update the expectations and drop the isMac navigator stub that was only needed when the component had a macOS-only primary branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Merge origin/main into NevilleQingNY/download-redesign Main added onboarding funnel analytics (#1489) that captures `is_mac` as a dimension for each Step 3 path selection. This branch had removed the `isMac` state because the UI no longer branches on it (Windows / Linux desktop builds ship now). Git auto-merged the two diffs into a file that referenced a deleted variable. Reintroduce `isMac` as a lazy client-only computation scoped to analytics capture only — the UI stays platform-agnostic. Handlers fire client-side so SSR safety isn't needed; a plain const reads navigator on first render. typecheck passes across all 6 packages; all 166 views tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(analytics): instrument download funnel across 5 surfaces + /download Closes the gap left by PR #1489: onboarding analytics captured Step 3 path selection but missed the four surfaces that advertise the desktop app earlier in the funnel (landing hero, landing footer, login, Welcome), and the /download page itself had zero coverage — so we could see the last-mile path but not the top-of-funnel entry nor the page-to-installer conversion. Three new events, wired via `@multica/core/analytics`: 1. `download_intent_expressed` fires on any CTA pointing at /download. `source` splits the five surfaces cleanly; every authenticated emission also writes `platform_preference=desktop` on the person (same convention Step 3 already uses). 2. `download_page_viewed` fires once per /download mount after OS detect resolves. Carries `detected_os`, `detected_arch`, `detect_confident` (Chromium userAgentData vs UA fallback), and `version_available` so the Safari-on-Mac arm64-default cohort and GitHub-rate-limited degraded sessions are each isolable. Also $set_once's `first_detected_os/arch` on the person so every downstream event gains a platform dimension without re-emitting. 3. `download_initiated` fires on every installer click — Hero's primary CTA and each All Platforms matrix row. `primary_cta` splits hero-recommended from manual picks; `matched_detect` quantifies detect accuracy from the single event (no cross-join to download_page_viewed needed). Augments the existing `onboarding_runtime_path_selected` with a `source: "step3"` property — literal today, reserved for future surfaces reusing the same event name. `is_mac` kept for backward-compat with PR #1489's dashboards; the new events use `detected_os` + `detected_arch` instead. New `setPersonPropertiesOnce` wire helper in `packages/core/analytics/download.ts` for `$set_once` — mirrors the backend's `Event.SetOnce` semantics. docs/analytics.md update lands in the follow-up commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(analytics): document download_intent_expressed / page_viewed / initiated Adds the three new download-funnel events to the frontend-only section. Also notes the semantic shift on onboarding_runtime_path_selected: its `path: "download_desktop"` now signals Step 3 path choice, not actual download start — download_intent_expressed is the new canonical "user expressed intent to download desktop" signal across surfaces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
/**
|
|
* Download funnel instrumentation.
|
|
*
|
|
* Complements the onboarding events added in PR #1489 by covering
|
|
* every surface that advertises the desktop app — landing hero,
|
|
* landing footer, login, Welcome (web branch), Step 3 — and the
|
|
* /download page itself. Without this layer we can see Step 3
|
|
* path selection but not the touchpoint that got the user there,
|
|
* nor the /download → installer conversion.
|
|
*
|
|
* Event names and property shapes are governed by docs/analytics.md;
|
|
* keep the two in sync when adding a new source or field.
|
|
*/
|
|
|
|
import posthog from "posthog-js";
|
|
|
|
import { captureEvent, setPersonProperties } from "./index";
|
|
|
|
/**
|
|
* Where the user clicked a CTA that points at `/download`. Typed union
|
|
* prevents drift across the five touchpoints and lets PostHog funnels
|
|
* split cleanly by top-of-funnel entry.
|
|
*/
|
|
export type DownloadIntentSource =
|
|
| "landing_hero"
|
|
| "landing_footer"
|
|
| "login"
|
|
| "welcome"
|
|
| "step3";
|
|
|
|
/**
|
|
* OS + arch detect result for the /download page. Mirrors the shape of
|
|
* `@/features/landing/utils/os-detect.ts` without importing it (that
|
|
* module lives in the web app; core packages can't depend on it). Keep
|
|
* these enums in lockstep.
|
|
*/
|
|
export interface DownloadDetectPayload {
|
|
detected_os: "mac" | "windows" | "linux" | "unknown";
|
|
detected_arch: "arm64" | "x64" | "unknown";
|
|
detect_confident: boolean;
|
|
version_available: boolean;
|
|
}
|
|
|
|
/**
|
|
* Specific installer the user chose on /download. Version is the GitHub
|
|
* tag name (e.g. "v0.2.13") so we can correlate adoption-by-release.
|
|
*/
|
|
export interface DownloadInitiatedPayload {
|
|
platform: "mac" | "windows" | "linux";
|
|
arch: "arm64" | "x64";
|
|
format: "dmg" | "zip" | "exe" | "appimage" | "deb" | "rpm";
|
|
version: string;
|
|
primary_cta: boolean;
|
|
matched_detect: boolean;
|
|
}
|
|
|
|
/**
|
|
* Fires when a user clicks any CTA that navigates to `/download`. We
|
|
* also write `platform_preference` to person properties so the backend
|
|
* can segment subsequent events — same convention the Step 3 handler
|
|
* already uses (see `step-platform-fork.tsx`).
|
|
*/
|
|
export function captureDownloadIntent(source: DownloadIntentSource): void {
|
|
captureEvent("download_intent_expressed", {
|
|
source,
|
|
});
|
|
setPersonProperties({ platform_preference: "desktop" });
|
|
}
|
|
|
|
/**
|
|
* Fires once on /download page mount, after OS detection resolves. The
|
|
* first detection for a given person is mirrored into person properties
|
|
* via `$set_once` so every downstream event gains a platform dimension
|
|
* without re-emitting.
|
|
*/
|
|
export function captureDownloadPageViewed(
|
|
payload: DownloadDetectPayload,
|
|
): void {
|
|
captureEvent("download_page_viewed", {
|
|
detected_os: payload.detected_os,
|
|
detected_arch: payload.detected_arch,
|
|
detect_confident: payload.detect_confident,
|
|
version_available: payload.version_available,
|
|
});
|
|
setPersonPropertiesOnce({
|
|
first_detected_os: payload.detected_os,
|
|
first_detected_arch: payload.detected_arch,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fires when the user clicks a concrete installer link on `/download`.
|
|
* `primary_cta` marks the hero-level recommendation versus a manual
|
|
* pick from the All Platforms matrix; `matched_detect` captures
|
|
* whether the click matched what we detected (miss = detect got it
|
|
* wrong / user overrode).
|
|
*/
|
|
export function captureDownloadInitiated(
|
|
payload: DownloadInitiatedPayload,
|
|
): void {
|
|
captureEvent("download_initiated", { ...payload });
|
|
}
|
|
|
|
/**
|
|
* $set_once wire form. Mirrors the backend's `Event.SetOnce` path —
|
|
* first write wins, subsequent ones are no-ops on PostHog's side.
|
|
* Wrapping it here keeps call sites free of the no-op `$set_once`
|
|
* envelope quirk.
|
|
*/
|
|
function setPersonPropertiesOnce(props: Record<string, unknown>): void {
|
|
if (typeof window === "undefined") return;
|
|
posthog.capture("$set", { $set_once: props });
|
|
}
|