chore(launch): reconcile README/store live-status + add demo-funnel analytics (#111)

Two scoped changes for the no-spend growth launch (Growth Launch Kit,
Notion page 3a1ac25eb49f81099cc9f3a4286c8ec4):

1. README.md and distribution/play-listing.md said Google Play was
   "coming soon" / internal-testing-only, while distribution/retention-analysis.md
   and the live play.google.com listing show it's actually public with 1K+
   installs. Fixed the contradiction, added Google Play as a third install
   channel, and added an accurate mention of the new offline demo mode
   ("Try a Demo" — reasoning, grep, diff, permission prompt, ~30s, no server)
   matching what app/demo.tsx + src/lib/demo-script.ts actually render.
   play-listing.md's stale pre-launch checklists are marked historical
   instead of rewritten, so #83's ASO copy/keyword work is untouched.

2. Added the demo funnel's key metric (demo-completion, per the launch
   kit) as four consent-gated PostHog events: demo_started,
   demo_step_advanced, demo_completed, demo_exited_to_connect. Pure
   property-derivation logic lives in src/lib/demo-analytics.ts (no
   RN/PostHog imports, unit-tested with node --test, same pattern as
   analytics-classify.ts) and is wired into app/demo.tsx's lifecycle.
   Updated docs/analytics.md's event table and the privacy policy's event
   list (distribution/privacy-policy.md + its two HTML mirrors) per the
   repo's "new event requires a policy update" convention.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Den
2026-07-17 19:49:47 -07:00
committed by GitHub
parent c9ec92d8b7
commit 1ea84f8236
10 changed files with 220 additions and 37 deletions

View File

@@ -85,6 +85,17 @@ export enum AnalyticsEvent {
/** An agent response finished streaming (session transitioned busy -> idle),
* excluding user-aborted runs. */
ResponseReceived = "response_received",
/** Fired once when the offline `/demo` screen mounts. */
DemoStarted = "demo_started",
/** User advanced a step in the scripted demo (currently: replied to the
* demo's permission prompt). Always paired with `step_index`/`step_name`. */
DemoStepAdvanced = "demo_step_advanced",
/** The scripted demo reached its end (completion or denial message shown
* after the permission reply). The key activation metric for the demo —
* see distribution/retention-analysis.md. Always paired with `outcome`. */
DemoCompleted = "demo_completed",
/** User tapped "Connect your own server" on the demo's CTA card. */
DemoExitedToConnect = "demo_exited_to_connect",
}
/** Where a connection test/failure was initiated from. The activation funnel

View File

@@ -0,0 +1,41 @@
import { test } from "node:test"
import assert from "node:assert/strict"
import {
DEMO_STEP_PERMISSION_REPLIED,
demoStepAdvancedProps,
demoCompletedOutcome,
demoExitedToConnectProps,
} from "./demo-analytics.ts"
test("demoStepAdvancedProps: includes the step index/name constant and the reply", () => {
const props = demoStepAdvancedProps("once")
assert.equal(props.step_index, DEMO_STEP_PERMISSION_REPLIED.index)
assert.equal(props.step_name, DEMO_STEP_PERMISSION_REPLIED.name)
assert.equal(props.reply, "once")
})
test("demoStepAdvancedProps: reply is passed through verbatim for each variant", () => {
assert.equal(demoStepAdvancedProps("always").reply, "always")
assert.equal(demoStepAdvancedProps("reject").reply, "reject")
})
test("demoStepAdvancedProps: properties are flat primitives only (no nested objects)", () => {
const props = demoStepAdvancedProps("once")
for (const value of Object.values(props)) {
assert.ok(["string", "number", "boolean"].includes(typeof value) || value === null)
}
})
test("demoCompletedOutcome: reject maps to denied", () => {
assert.equal(demoCompletedOutcome("reject"), "denied")
})
test("demoCompletedOutcome: once and always map to completed", () => {
assert.equal(demoCompletedOutcome("once"), "completed")
assert.equal(demoCompletedOutcome("always"), "completed")
})
test("demoExitedToConnectProps: reflects whether the demo reached completion first", () => {
assert.deepEqual(demoExitedToConnectProps(true), { reached_completion: true })
assert.deepEqual(demoExitedToConnectProps(false), { reached_completion: false })
})

38
src/lib/demo-analytics.ts Normal file
View File

@@ -0,0 +1,38 @@
// Pure derivation logic for the offline demo funnel's analytics properties
// (app/demo.tsx). Kept free of RN/PostHog imports — same pattern as
// analytics-classify.ts — so it is unit-testable with plain `node --test`
// and safe to import from the fully-offline demo screen.
import type { AnalyticsProps } from "./analytics"
/** Mirrors PermissionPrompt's `onReply` reply values (see
* src/components/chat/PermissionPrompt.tsx). */
export type DemoPermissionReply = "once" | "always" | "reject"
/** The demo funnel currently has exactly one step that "advances" past the
* initial script view: replying to the scripted permission prompt. Kept as
* a named constant (rather than inlined) so a future step can be added
* without hunting for magic numbers at each call site. */
export const DEMO_STEP_PERMISSION_REPLIED = { index: 1, name: "permission_replied" } as const
/** Properties for `demo_step_advanced` when the user replies to the
* scripted permission prompt. `reply` is a flat enum, not free text. */
export function demoStepAdvancedProps(reply: DemoPermissionReply): AnalyticsProps {
return {
step_index: DEMO_STEP_PERMISSION_REPLIED.index,
step_name: DEMO_STEP_PERMISSION_REPLIED.name,
reply,
}
}
/** Outcome bucket for `demo_completed` — never the raw completion/denial
* text, just whether the scripted permission was allowed or denied. */
export function demoCompletedOutcome(reply: DemoPermissionReply): "completed" | "denied" {
return reply === "reject" ? "denied" : "completed"
}
/** Properties for `demo_exited_to_connect`. The Connect CTA is reachable
* before the user has replied to the permission prompt, so this records
* whether they'd reached the end of the scripted flow first. */
export function demoExitedToConnectProps(reachedCompletion: boolean): AnalyticsProps {
return { reached_completion: reachedCompletion }
}