---
name: posthog-venture-telemetry
description: Set up PostHog as the telemetry layer for HappyCouple and Consumer AI ventures using event taxonomy, activation dashboards, API-based recurring briefs, and Obsidian learning logs.
version: 1.0.0
author: Hermes Agent
---

# PostHog Venture Telemetry

Use this skill when Antoine asks to set up, inspect, or reason about PostHog analytics for HappyCouple or any future Consumer AI Company Factory venture.

## Core Principle

Do not treat PostHog as generic analytics. Treat it as venture telemetry: evidence about whether users reach first value, where they drop off, and what decision should change.

For person/email lookup across PostHog venture projects, use `references/person-email-lookup-across-projects.md`: query person identifiers/properties and raw event properties across the relevant project first, then all accessible venture projects if context is unclear; do not infer identity from an anonymous-looking email string.

For HappyCouple / Emma, first value means:

```text
A real user submits a real relationship moment and receives what may be happening underneath + what to say next + what to avoid.
```

If the product still uses report language internally, map that report view to `first_value_delivered`, but do not let "conversation count" or "report generated" replace the activation unit. The core question is not “how many visitors?” or “how many conversations?” It is:

```text
Are users submitting qualified relationship context and reaching first relationship value?
```

## Known HappyCouple Project

HappyCouple PostHog project:

```text
https://us.posthog.com/project/377678/
POSTHOG_HOST=https://us.posthog.com
POSTHOG_PROJECT_ID=377678
```

Do not confuse this with Antoine's AgentStore PostHog project. If the project ID in `~/.hermes/.env` is not `377678` while working on HappyCouple, correct it before interpreting telemetry.

## Recommended Flow

### 1. Check installation first

In PostHog UI:

- Project → Data Management → Events
- Confirm events are arriving.

Common default events:

```text
$pageview
$autocapture
$rageclick
$web_vitals
$exception
```

If only `$pageview` exists, PostHog is installed but the product is not instrumented enough.

### 2. Define the activation funnel

For HappyCouple, use this funnel spine:

```text
landing_viewed
→ start_voice_clicked
→ voice_session_started
→ first_user_message_sent
→ first_coach_response_received
→ end_and_summarize_clicked
→ report_generation_succeeded
→ report_viewed
→ save_report_clicked
→ signup_completed
```

Minimum custom events:

```text
landing_viewed
prompt_chip_clicked
start_voice_clicked
start_text_clicked
long_term_plan_clicked
voice_session_started
voice_session_failed
first_user_message_sent
first_coach_response_received
presence_answered
end_and_summarize_clicked
ended_by_natural_summary_request
report_generation_started
report_generation_succeeded
report_generation_failed
report_viewed
save_report_clicked
signup_started
signup_completed
saved_report_restored_after_signup
return_visit
new_moment_started
```

Useful properties:

```text
source
utm_source
utm_campaign
device
presence: solo | together | unknown
session_duration_seconds
message_count
report_generation_mode: llm | fallback
error_type
user_signed_in: true | false
```

### 3. Build the dashboard

Create dashboard:

```text
HappyCouple — Venture 001 Activation
```

Recommended widgets:

1. Core activation funnel

```text
landing_viewed
→ start_voice_clicked
→ voice_session_started
→ first_user_message_sent
→ report_generation_succeeded
→ report_viewed
→ save_report_clicked
→ signup_completed
```

2. Voice reliability

- `voice_session_started`
- `voice_session_failed`
- failure rate by browser/device

3. Report reliability

- `report_generation_succeeded`
- `report_generation_failed`
- fallback rate via `report_generation_mode`

4. First value metric

Preferred:

```text
report_viewed / voice_session_started
```

Also useful:

```text
report_generation_succeeded / landing_viewed
```

5. Save intent

```text
save_report_clicked / report_viewed
```

If this is low, the report or value proposition is weak.

6. Auth handoff

```text
signup_completed / save_report_clicked
```

If this is low, signup handoff is broken or too much friction.

7. Recordings playlists / filters

Prioritize recordings for:

- users who clicked voice but did not send first message
- users who viewed report but did not save
- report generation failures
- mobile users
- exceptions / rage clicks

At low volume, recordings matter more than charts.

### 4. API access beats MCP initially

If Antoine asks whether PostHog has an MCP: there is an npm package `@posthog/mcp`, but as of the check during this workflow it looked like a “PostHog SDK for MCP servers” (`0.0.2`), not a mature analytics MCP server for querying a PostHog project. Do not rely on it as the primary path.

Use PostHog API/HogQL directly first. A custom MCP can be added later if the API script becomes useful enough.

Required env vars should be stored locally, not pasted into chat when avoidable:

```bash
POSTHOG_HOST=https://us.posthog.com   # or https://eu.posthog.com
POSTHOG_PROJECT_ID=...
POSTHOG_PERSONAL_API_KEY=...
```

On the Mr Han host, the shared Hermes env file is usually:

```text
/opt/mrhan/hermes-home/.env
```

When Antoine explicitly asks to save a PostHog org key for future projects, store it there with `chmod 600` and include non-secret org metadata if known:

```bash
POSTHOG_PERSONAL_API_KEY=...
POSTHOG_HOST=https://us.posthog.com
POSTHOG_ORG_SLUG=chronically-online
POSTHOG_ORG_ID=...
```

Never store PostHog secrets in memory, Obsidian, skill files, or chat summaries. If the key was pasted in Telegram/chat, remind Antoine it should be rotated later even if it was saved locally.

Preferred path:

```text
PostHog API
→ local telemetry script
→ Hermes cron brief
→ Obsidian metric/decision log
```

Optional later:

```text
Custom PostHog MCP server
→ mcp_posthog_query_funnel
→ mcp_posthog_query_hogql
→ mcp_posthog_get_recordings
```

### 5. Recurring telemetry brief

Create a script such as:

```text
~/.hermes/scripts/happycouple_posthog.py
```

It should read env vars, query the last 24h/7d, and output compact text/JSON with:

```text
Visitors:
Voice starts:
First messages:
Reports generated:
Report failures:
Report fallback rate:
Report views:
Save clicks:
Signups:
Biggest dropoff:
Most suspicious failure:
Recording cohort to watch:
Recommended next action:
Factory learning:
```

Then attach it to a cron job for a daily HappyCouple telemetry brief.

Cron prompt should be self-contained and should interpret metrics, not just report them.

### 6. Obsidian learning log

Create/update:

```text
/Users/antoinelevy/Documents/Obsidian Vault/Personal Development/Consumer AI Company Factory/Ventures/HappyCouple Metrics.md
```

Suggested structure:

```markdown
# HappyCouple Metrics

## Current activation definition

First value = user completes a moment and views a generated report.

## Funnel

Landing viewed
→ voice started
→ first user message
→ report generated
→ report viewed
→ save clicked
→ signup completed

## Weekly snapshots

### YYYY-MM-DD

- Visitors:
- Voice starts:
- First user messages:
- Reports generated:
- Report views:
- Save clicks:
- Signups:
- Biggest dropoff:
- Interpretation:
- Next action:
- Factory learning:
```

PostHog says what happened. Obsidian records what was decided and learned.

## Rough Validation Bars

For HappyCouple, before broad marketing:

- 10 real users tested manually
- 7/10 complete voice → report → save without help

Early traffic rough bars:

```text
voice start rate: >20–30% of landing visitors
first message rate: >60% of voice starts
report generated rate: >70% of first messages
save click rate: >20–30% of report views
signup completed after save: >40–60% of save clicks
```

These are not absolute truths; use them as practical alarm bells.

## Spinning up a new venture project

Detailed API reference: `references/posthog-org-project-creation-api.md`.

For text/SMS/WhatsApp-first assistants such as Alma, use the event taxonomy and privacy guidance in `references/messaging-assistant-telemetry.md`. For the concrete Alma landing/CTA dashboard pattern, including API insight creation and verification, see `references/alma-posthog-dashboard-2026-07-02.md`. For the concrete Alma session replay + Meta CTA verification pattern, including replacing direct `/capture/` with `posthog-js`, project replay settings, browser `/s/` verification, and interpreting a low Meta CTA rate, see `references/alma-session-replay-and-meta-cta-verification-2026-07-04.md`. For Alma's email-first landing change and the noisy-CTA attribution readout pattern, including how to separate Meta demand from QA/direct/legacy sparse clicks and switch the primary metric to `email_submitted / landing_viewed`, see `references/alma-email-first-landing-and-noisy-cta-attribution-2026-07-07.md`. For where Alma raw email registrations live and how they differ from PostHog `email_submitted` events, see `references/alma-email-capture-storage.md`; when Antoine asks “any more signups?”, query PostHog `email_submitted` first for fast counts, then attempt the raw export only if actual email addresses/export are needed. If the Alma admin export returns `401`, do not block the demand readout: report PostHog signup counts, say raw email export needs a refreshed admin token, and do not print or infer raw emails. When Alma shows more unique CTA clicks than Meta-attributed clicks, use `references/alma-cta-click-source-investigation.md` to inspect raw person/session properties and bucket the “mystery” clicks before making a demand/source conclusion.

When Antoine asks for paid-ad results, do not default to PostHog-only attribution. First load/use `paid-ads-campaign-ops` for the class-level paid ads workflow, then attempt the native ad platform readout (for Alma/Meta: Ads Manager or Meta Graph) to get spend, impressions, delivery/review status, CTR, CPC/CPM, placement/ad-set/ad breakdown, and creative context. If the active browser/API token is login-walled, missing, or scope-blocked, say that narrowly, then use `references/paid-ad-utm-readout-when-platform-access-blocked.md`: state the platform limitation, query PostHog by UTM campaign, compute unique-person conversion rates, and label any spend-derived costs as implied/unverified.

Use this flow when Antoine asks to create telemetry for a new Consumer AI Factory venture in the same PostHog organization as HappyCouple:

1. Identify the reference org from HappyCouple project `377678`, but do not assume the saved key can create projects.
2. Check whether the target project already exists before creating one; prior API attempts can create duplicates even if output was truncated.
3. Project creation requires an org-scoped PostHog personal API key with project write permissions. A project-scoped key may still query one project but will fail project/org discovery or creation with `403 Forbidden`.
4. For Vite/static apps, wire client telemetry behind env vars:

```text
VITE_POSTHOG_KEY=...
VITE_POSTHOG_HOST=https://us.i.posthog.com
```

Use the ingestion host (`us.i.posthog.com`) for the browser SDK and the API host (`us.posthog.com`) for management/HogQL calls.

For Vite apps deployed as static assets on Cloud Run/nginx, runtime `gcloud run services update --set-env-vars ...` is not enough because `VITE_*` values are baked at build time. Rebuild the container with Docker build args, then redeploy the image. Example pattern:

```yaml
steps:
- name: gcr.io/cloud-builders/docker
  args:
  - build
  - --build-arg
  - VITE_POSTHOG_KEY=${_VITE_POSTHOG_KEY}
  - --build-arg
  - VITE_POSTHOG_HOST=${_VITE_POSTHOG_HOST}
  - -t
  - ${_IMAGE}
  - .
images:
- ${_IMAGE}
```

Then deploy with:

```bash
gcloud builds submit <repo> --config <cloudbuild.yaml> --substitutions _IMAGE=<artifact-image>,_VITE_POSTHOG_KEY=<project-key>,_VITE_POSTHOG_HOST=https://us.i.posthog.com
gcloud run services update <service> --region <region> --image <artifact-image>
```

Verify the deployed JS bundle contains the ingestion host, project key, and expected event names before checking PostHog events.

For static Vite apps deployed from Docker/Cloud Run, remember that `VITE_*` values are **build-time** variables, not runtime-only server variables. Setting `--set-env-vars VITE_POSTHOG_KEY=...` on an already-built nginx/static image will not inject the key into the client bundle. Rebuild the image with Docker build args, then deploy that image:

```bash
# Dockerfile build stage
ARG VITE_POSTHOG_KEY
ARG VITE_POSTHOG_HOST=https://us.i.posthog.com
ENV VITE_POSTHOG_KEY=$VITE_POSTHOG_KEY
ENV VITE_POSTHOG_HOST=$VITE_POSTHOG_HOST
RUN npm run build
```

For Google Cloud Build + Cloud Run, use substitutions/build args rather than only Cloud Run runtime env vars:

```yaml
steps:
- name: gcr.io/cloud-builders/docker
  args:
    - build
    - --build-arg
    - VITE_POSTHOG_KEY=${_VITE_POSTHOG_KEY}
    - --build-arg
    - VITE_POSTHOG_HOST=${_VITE_POSTHOG_HOST}
    - -t
    - ${_IMAGE}
    - .
images:
- ${_IMAGE}
```

Then deploy the resulting image with `gcloud run services update <service> --image <image> --region <region>`.

5. Keep the committed app safe before credentials exist: initialize PostHog only when `VITE_POSTHOG_KEY` is present, and make `track()` a no-op otherwise.
6. Document env vars in `.env.example` and telemetry semantics in `TELEMETRY.md`; never commit real keys.
7. Add the project key to Vercel env vars, redeploy production, then verify by triggering a real in-app event and checking PostHog ingestion.
8. For Vercel-hosted Vite/static landers, set `VITE_POSTHOG_KEY` and `VITE_POSTHOG_HOST` in Vercel env before the production redeploy, because `VITE_*` values are baked into the bundle at build time. Keep local code env-safe: initialize/track only when the key exists, add `.env.example`, ignore real `.env*`, and document the event contract in `TELEMETRY.md`. After deploy, verify by loading the public Vercel/custom-domain URL, triggering the exact event (usually `landing_viewed` plus CTA events), and checking recent events in the correct PostHog project before claiming telemetry is live.
9. If the SDK appears to load but custom events do not arrive, switch to a minimal direct `/capture/` helper before wasting time on dashboards. For tiny static/Vite probes, a direct capture wrapper is often more robust and smaller than `posthog-js`:

```js
const key = import.meta.env.VITE_POSTHOG_KEY
const host = import.meta.env.VITE_POSTHOG_HOST || 'https://us.i.posthog.com'
const enabled = Boolean(key) && typeof window !== 'undefined'

if (enabled) track('landing_viewed')

export function track(event, properties = {}) {
  if (!enabled || !event) return
  const payload = {
    api_key: key,
    event,
    distinct_id: getDistinctId(),
    properties: { ...pageProperties(), ...properties }
  }
  const body = JSON.stringify(payload)
  const endpoint = `${host.replace(/\/$/, '')}/capture/`
  try {
    if (navigator.sendBeacon) {
      const queued = navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' }))
      if (queued) return
    }
  } catch (_) {}
  fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, keepalive: true }).catch(() => {})
}
```

Keep a stable anonymous `distinct_id` in localStorage and include page properties (`hostname`, `path`, UTM fields) on every event.
- When Antoine says he cannot see a PostHog dashboard, do not answer from memory that it was created. Verify in API/UI terms: list projects, list dashboards, fetch the specific dashboard detail, inspect `tiles[*].insight.name`, and send the exact `https://us.posthog.com/project/{project_id}/dashboard/{dashboard_id}` URL. The dashboard list endpoint can show misleading/empty tile counts; the detail endpoint is the source of truth.
- Do not leave a venture with only PostHog's generic starter dashboard. If the project exists and events ingest, immediately create a named venture dashboard with decision-specific tiles and verify the detail response has tiles attached.
   - For lightweight paid-traffic landing probes, make Antoine's minimum dashboard first before adding heavier activation analysis: unique pageviews, unique pageviews by UTM campaign, and unique CTA clicks. Use the product's canonical landing/CTA events (e.g. Alma: `landing_viewed` and `text_alma_clicked`) with `math: "dau"` for distinct people and `breakdownFilter.breakdown: "utm_campaign"` for UTM reporting.
   - For an immediate-relief probe, default dashboard tiles should include activation funnel, core event volume, generation success/failure, friction signals, source/UTM breakdown, suggestion demand, and completion quality.
   - Use `FunnelsQuery` for `landing_viewed → plan_generated → step_started → step_completed`; use `TrendsQuery` with `breakdownFilter` for sources (`utm_source`/`utm_campaign`) and suggestion demand (`task_text`).
11. Create a local metric brief script for each venture, not just dashboards. It should query HogQL through `/api/projects/{project_id}/query/`, summarize last 24h/7d counts and conversion rates, and explicitly say when volume is too low for conclusions. Store the script under `~/.hermes/scripts/{venture}_posthog.py` and document it in the venture note.
12. Save numeric project IDs/dashboard URLs in memory or Obsidian if useful; never save personal API tokens or project API keys in memory, summaries, docs, or chat.

### Next.js + Vercel venture apps

Detailed setup/verification reference: `references/nextjs-vercel-posthog-setup.md`. For the specific post-merge production activation flow — upserting Vercel env vars, redeploying, checking the built `_next` bundle, browser resource timing, and PostHog `/events/` ingestion — use `references/nextjs-vercel-posthog-production-verification.md`. For the NextMove case where telemetry worked but Antoine could not see a useful dashboard, use `references/nextmove-posthog-dashboard-2026-07-08.md`.

When a Consumer AI venture is a Next.js app on Vercel, prefer a lightweight direct `/capture/` client helper for early demand probes unless the product already needs the full PostHog SDK. Use `NEXT_PUBLIC_POSTHOG_KEY` and `NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com`, set them in Vercel, redeploy production, then verify both the browser capture request and recent events via `/api/projects/{project_id}/events/` before claiming setup is live.

For upload/result probes like HairMatch, the activation question is not pageviews; it is whether strangers trust the product enough to upload the sensitive input and reach first value. For the concrete Next.js direct-capture PR pattern, including sanitizer tests and safe error typing, see `references/hairmatch-nextjs-direct-capture-pr.md`. A useful starter event spine is:

```text
landing_viewed
→ photo_uploaded
→ photos_ready
→ analysis_started
→ analysis_succeeded / analysis_failed
→ preview_edit_started
→ preview_edit_succeeded
```

For sensitive upload/result probes, harden the telemetry helper before opening the PR: sanitize capture properties to drop empty values and any `data:image/*` strings, and map raw analysis failures to a safe `error_type` enum instead of sending raw error messages.

### Lovable / hosted no-repo landing pages

Detailed verification/playbook reference: `references/lovable-waitlist-telemetry.md`.

When Antoine asks to “install PostHog” on a Lovable-hosted or other no-repo public landing page, separate what the agent can do from what requires editor access:

1. Create or reuse the venture PostHog project under the shared org.
2. Fetch the browser project API token (`api_token`) and use `https://us.i.posthog.com` as the client ingestion host.
3. Create the activation/waitlist dashboard immediately.
4. Verify the project by sending a harmless server-side event such as `mrhan_telemetry_verified` to `/capture/` using the project token, then confirm it appears via `event_definitions` and `/events/`.
5. Inspect the live public URL for existing PostHog/capture scripts if browser access is available.
6. If the app is only editable through Lovable, do not claim installation is complete. Provide a copy-paste Lovable implementation prompt with the exact project key, host, event taxonomy, privacy constraints, and verification steps.
7. After Lovable applies changes, verify the deployed page with browser/network checks for `https://us.i.posthog.com/capture/` and confirm events arrive in the new project.

For waitlist-only demand probes, minimum events:

```text
landing_viewed
hero_waitlist_clicked
waitlist_focus_selected
waitlist_submitted
sample_report_seen
progress_gallery_seen
```

Privacy rule: never send raw emails, body/photo data, or private free-text content to PostHog. Send boolean/properties only, e.g. `has_email: true`, `focus`, `utm_source`, `path`, `hostname`.

Recommended lightweight event taxonomy for immediate-relief / utility probes like Start Small:

```text
landing_viewed
suggestion_clicked
plan_generated
plan_generation_failed
plan_panel_opened
plan_panel_hidden
steps_made_smaller
step_edited
timer_started
timer_finished
step_completed
new_task_clicked
```

For relationship-moment products like HappyCouple / Emma, do not treat generic conversation count as the core validation metric. The activation unit is a qualified relationship moment: a user submits real context and receives a useful response with what is happening underneath + what to say next + what to avoid. Minimum Emma-style funnel:

```text
landing_viewed
→ mode_selected
→ first_context_submitted
→ emma_response_succeeded
→ first_value_delivered
→ suggested_text_copied / helpful_clicked / user_sent_followup
→ second_moment_submitted
```

Minimum Emma-style events and properties:

```text
start_whatsapp_clicked
whatsapp_opened
first_context_submitted
emma_response_started
emma_response_succeeded
emma_response_failed
first_value_delivered
suggested_text_copied
helpful_clicked
not_helpful_clicked
user_sent_followup
second_moment_submitted
```

Useful properties:

```text
mode: text | voice | screenshot | whatsapp
moment_type
has_image
text_length_bucket
source
utm_source
utm_campaign
device
latency_ms
response_format: underneath_next_words_avoid
```

If the product has only `first_user_message_sent` / `first_coach_response_received`, explicitly call out that it measures conversation mechanics, not whether users submitted a real relationship moment or reached first value.

For receipt-level demand probes, instrument the comparison itself, not just the final waitlist submit. Example Start Small Stuck Journal / memory-probe events:

```text
receipt_variant_assigned
receipt_shown
waitlist_cta_shown
memory_insight_shown
memory_cta_clicked
waitlist_joined
```

Useful properties:

```text
receipt_variant: control | memory
intent_type: generic_waitlist | memory_pattern_tracking
task_text
task_category
steps_completed
memory_pattern_label
```

Decision metric: compare `memory_cta_clicked / memory_insight_shown` and memory-variant `waitlist_joined / receipt_shown` against control `waitlist_joined / receipt_shown`. Do not build auth/history/backend memory until the memory CTA materially outperforms the generic waitlist baseline.

## Conversation / channel health checks

When Antoine asks whether HappyCouple “got any convos” across WhatsApp or web, do not answer from aggregate pageviews alone. Check the channel-specific evidence layers:

1. PostHog event counts for the last 24h and 7d:
   - WhatsApp: `mode_opened` with `mode=whatsapp`, `whatsapp_opened`, `start_whatsapp_clicked` if present.
   - Web text: `mode_opened` with `mode=text`, `text_chat_started`, `text_message_sent`, `text_response_received`.
   - Voice: `voice_session_started`, `voice_session_failed`, `first_user_message_sent`, `first_coach_response_received`.
   - Activation / first value: `first_context_submitted`, `emma_response_succeeded`, `first_value_delivered`, or the closest currently instrumented equivalent.
2. Recent raw event properties, not just counts: inspect `$current_url`, `$pathname`, `mode`, `source`, `error_type`, `has_image`, and `message_count` to distinguish a real user conversation from a repeated local/device failure.
3. Server/webhook logs where available:
   - Vercel logs for `/api/chat` POSTs indicate web text conversations.
   - Vercel logs for `/api/whatsapp` POSTs indicate Twilio WhatsApp webhook activity.
   - Absence of both, combined with zero PostHog text/WhatsApp events, is strong evidence of no conversations.
4. Traffic/source context: separately report pageviews, distinct persons, UTM/source breakdown, and content/SEO page paths. Do not imply traffic equals conversation.

For reports, use concise labels Antoine can act on:

```text
WhatsApp: no evidence of convos / X opens / Y webhook hits
Web chat: no evidence of convos / X starts / Y messages / Z responses
Voice: X starts, Y failures, top error = ...
Traffic: 24h and 7d people/pageviews + notable sources
Read: alive but not validating / validating / instrumentation uncertain
Next action: one concrete fix or traffic rep
```

If one user generates many repeated `voice_session_failed` events, call it out as a device/permission reliability issue rather than demand signal.

## Pitfalls

- Do not obsess over dashboards before enough traffic exists.
- Do not confuse pageviews with validation.
- Do not launch broadly if the core activation loop is unreliable.
- Do not build an MCP before the API script proves useful.
- Session recordings and exact dropoff paths are more valuable than statistical trends at tiny volume.
- Analytics must feed decisions; otherwise it is dashboard theater.
- When verifying dashboard creation or answering “I can’t see the dashboard,” fetch the individual dashboard endpoint (`GET /api/projects/{project_id}/dashboards/{dashboard_id}/`) and inspect `tiles`/insight names. Do not trust only the dashboard list endpoint: it can show `tiles_count: 0` even when the dashboard itself has populated tiles.
- For paid ad performance questions, do not answer from PostHog alone when native platform access may exist. Try Ads Manager/Graph/native reporting first; only fall back to PostHog UTM attribution after a specific auth/scope/login-wall failure, and label the fallback as partial.
- Do not use the ingestion host for API queries. Client apps often use `https://us.i.posthog.com`, but API/HogQL calls should use `https://us.posthog.com` or the matching EU API host.
- Do not assume a PostHog key is usable just because it was described as all-project access. Verify it against an API endpoint without printing the key.
- For creating a new project in an existing PostHog org, explicitly test scopes before attempting creation:
  - `/api/organizations/` should succeed for `organization:read`.
  - `/api/projects/{project_id}/` or `/api/organizations/{org_id}/projects/` should succeed for `project:read`.
  - `POST /api/organizations/{org_id}/projects/` requires `project:write`.
  - `/api/users/@me/` requires `user:read`; it is useful for validation but organization/project creation can still fail independently if `project:*` scopes are missing.
- A key can have `organization:read` and still be unable to create/list projects. If PostHog returns “API key missing required scope 'project:read'” or “'project:write'”, have Antoine create a new personal API key with `organization:read`, `project:read`, `project:write`, and preferably `user:read`, with project access set to all projects/organization-wide rather than one specific project.
- If Antoine pastes a PostHog personal API key in chat, treat it as compromised: use it only as instructed and recommend later rotation.
- If creating a project fails because the org has reached the PostHog plan project limit, stop retrying and list current projects. Ask Antoine which existing low-priority project to delete or repurpose, preserve core active ventures by default, and only delete after explicit approval. Then create the new project, create its dashboard, and verify ingestion with `mrhan_telemetry_verified`.
- If `/api/users/@me/`, `/api/organizations/`, or `/api/projects/` return `403 Forbidden`, the token may still work for project-scoped HogQL if you have the explicit project ID. Do not stop at discovery failure; try the HogQL script once `POSTHOG_PROJECT_ID` is known.
- `POSTHOG_PROJECT_ID` is required for HogQL/project event queries even when the token has broad project access. If project discovery is blocked, get it from PostHog UI: Project Settings → Project ID, or extract it from URLs like `https://us.posthog.com/project/379391` where `379391` is the project ID.
- Never store PostHog secrets in chat summaries or Obsidian. Store them in `~/.hermes/.env` with mode `600`, and refer to them as redacted in notes. Numeric project IDs are not secrets and can be logged in local notes if useful.
- If a repo defines tracking helpers but `track.` is unused in `src`, PostHog may only have `$pageview`/autocapture. Treat that as an instrumentation gap before interpreting activation funnels. For HappyCouple, the minimum reusable wiring pattern is:
  - `src/lib/track.ts`: central typed wrapper for `posthog.capture` using canonical event names (`landing_viewed`, not older `landing_page_viewed`).
  - `src/app/page.tsx`: `landing_viewed`, `prompt_chip_clicked`, `start_voice_clicked`, `voice_session_started/failed`, first user/coach message, summarize trigger, report generation, `report_viewed`, `save_report_clicked`, and long-term-plan clicks.
  - `src/app/onboarding/page.tsx`: `disclaimer_accepted`, `mode_selected`, `presence_answered`.
  - `src/app/session/page.tsx`: real ElevenLabs session start/fail, first user/coach message, end/summarize, session duration/message count.
  - `src/app/dashboard/page.tsx`: synthesis/report generation start/success/failure, report viewed, voice readback.
  - `src/app/auth/login/page.tsx` and `src/lib/auth-context.tsx`: signup started/completed, signin completed, saved report restored after signup.
- If `posthog-js` appears installed/configured but no app events arrive, verify at three layers before declaring success:
  1. Browser resource entries should show event uploads to `https://us.i.posthog.com/e/` or legacy/direct capture uploads to `https://us.i.posthog.com/capture/`, not only config/surveys scripts from `us-assets.i.posthog.com`.
  2. PostHog `event_definitions` should show the event names after a short delay.
  3. `/api/projects/{project_id}/events/` should show recent events with expected `hostname`/`path` properties.
  If only config scripts load and no event upload requests appear, the SDK is not actually sending the custom events. For small Vite/static probes where only events are needed, a direct `/capture/` wrapper can be more robust; but if session replay is required, use `posthog-js` because direct `/capture/` will not send replay snapshots.
- If custom PostHog events arrive but session replays are missing, check both project settings and client SDK. Project-level `session_recording_opt_in` must be true, and the browser must load `posthog-js`/recorder and send snapshot uploads to `/s/`. Direct `/capture/` helpers are event-only; they do not produce `$snapshot`/recordings. Verify with browser resource timing for `posthog-recorder.js` and `/s/`, then call `/api/projects/{project_id}/session_recordings/?limit=5` before claiming replay is live.
- For Vite static bundles on Cloud Run/nginx, absence of the project key or custom event names in the built JS is a build-time-env bug, not a PostHog API bug. Verify the deployed bundle directly without printing secrets: fetch the page, fetch its JS assets, and check booleans like `has_ingest_host`, `has_project_key`, `has_landing_viewed`, and `has_capture`. If runtime Cloud Run env vars were added after the image was built, rebuild with Docker build args and redeploy the image.
- Do not rely only on HogQL immediately after a brand-new project/event. In one Start Small setup, HogQL returned no rows while `/event_definitions/` and `/events/` showed the verification and product events. Use the project events endpoint as a pragmatic ingestion check.
- If only `$pageview`, `$web_vitals`, `$pageleave`, and `$autocapture` appear in the correct project, the direct interpretation is: PostHog is installed, but the product activation funnel is not instrumented yet.
- If `$pageview` is much higher than `landing_viewed`, do not use `landing_viewed` as the funnel denominator until verified. It may have been added recently, may not fire on all landing variants, or may be missing from the deployed path. Compare event definitions, HogQL counts, and recent raw event properties before making a product conclusion.
- When checking whether PostHog has “what we need,” inspect three layers: event definitions for what has ever arrived, 7d/24h HogQL counts for volume and recency, and recent raw event properties for UTM/source/device/prompt/error coverage. A present event name is not enough if it lacks the properties needed to answer the venture question.
- After modifying telemetry scripts, run them once and inspect output for duplicated event names or misleading funnel steps; small taxonomy bugs can make the brief look noisy or untrustworthy.
- When reviewing overall HappyCouple distribution, do not equate artifacts with execution. Separate the diagnosis into: strategy quality, infrastructure/artifacts, live distribution reps, activation evidence, learning-loop quality, and next concrete reps. Use telemetry for 24h and 7d windows, but call out if volume is too low for strong conclusions.
- When Antoine asks why engagement is low, run a broad channel health check instead of only the original voice/report funnel. For current Emma text-first surfaces, include `mode_opened`, `text_chat_started`, `text_message_sent`, `text_response_received`, and `share_clicked` alongside voice/report/signup events; break pageviews and activation events down by `utm_source`, `$referring_domain`, `$initial_referring_domain`, and path. If a failure count is high, group by error type and distinct persons before interpreting it — e.g. many `microphone_access_denied` events from one person/device are reliability noise, not broad market rejection.
- When CTA clicks exceed known campaign-attributed clicks, do not hand-wave “probably organic” or “probably QA.” Pull recent raw CTA events and bucket distinct people by event-time UTM, first-touch UTM/referrer, Meta click IDs/referrers, direct/returning, internal QA, organic referral, and unknown. Report the bucket counts plus one instrumentation/campaign-URL fix. For Alma, use `references/alma-cta-click-source-investigation.md`.
- If a telemetry brief reports an old event name such as `landing_page_viewed = 0` while the current taxonomy uses `landing_viewed`, or omits newer text-mode events, treat the dropoff as a taxonomy/reporting issue before making a product conclusion. Update scripts/dashboards to use canonical/current event names, and only then interpret activation dropoffs.

## Verification

When setting this up:

1. Confirm events appear in PostHog Data Management.
2. Confirm custom events fire for the full funnel.
3. Create the activation dashboard.
4. Run the API script locally and verify it returns real counts.
5. Create/update the Obsidian metrics note.
6. If scheduling, verify the cron job runs and delivers successfully.
