Skip to content

feat(analytics): ship PostHog v2 event schema#2285

Merged
lefarcen merged 12 commits into
nexu-io:mainfrom
lefarcen:feat/analytics-2.0
May 20, 2026
Merged

feat(analytics): ship PostHog v2 event schema#2285
lefarcen merged 12 commits into
nexu-io:mainfrom
lefarcen:feat/analytics-2.0

Conversation

@lefarcen
Copy link
Copy Markdown
Contributor

Why

Use case — product team handed off a refreshed埋点文档 2.0 last week (CSV in Drive) covering the v0.8 redesigned home, the new sidebar layout, the artifact toolbar overhaul, and a handful of brand-new pages (Automations / Plugins / Design Systems / Integrations). The previous v1 catalogue was built around the v0.7 layout and was missing roughly two-thirds of the new surfaces; the product team also asked for run_finished result=failed to carry an error_code classifier so dashboards can bucket agent failures, plus a guaranteed-fire page_view on every home render.

Pain being addressed — without v2 we have no way to attribute funnel drop-off to the new home, the new project modal, the four new top-level pages, or to slice agent failures by error_code. We also can't keep adding a per-page event name for every new product surface; the v1 model didn't scale past ~13 events.

What users will see

Nothing visible. This is wire-format only — no UI changes, no new copy. The PostHog dashboard team will see the new v2 events flowing on the next deploy with event_schema_version: 2; existing v1 dashboards continue to work against historical data via the schema-version filter.

Surface area

  • API / contractpackages/contracts/src/analytics collapses 13+ v1 event names down to 4 core events (`page_view` / `ui_click` / `surface_view` / `*_result`), renames `anonymous_id` → `device_id`, and bumps `EVENT_SCHEMA_VERSION` to 2. New event interfaces cover every page / area / element combo in the v2 CSV.
  • Default behavior change — the daemon now reads the new `x-od-analytics-device-id` request header instead of `x-od-analytics-anonymous-id`. Same value (the installationId), different header name; old web builds talking to a new daemon stop carrying analytics context until the matching web build ships.
  • UI / Keyboard shortcut / CLI / Extension point / i18n / New dependency

Highlights

  • Schema collapse. `page_view{page_name}` / `ui_click{page_name,area,element}` / `surface_view{page_name,area}` / discrete `*_result` events. Each surface owns a typed `*ClickProps` interface so call sites stay safe; the central `UiClickProps` union picks them up for `track()`.
  • device_id rename + configure-state globals. The wire field for the distinct id is now `device_id` (value still installationId). The configure-state triplet (`has_available_configure_cli` / `configure_type` / `configure_availability`) is promoted to a PostHog `register()` call so every event inherits it; no more boilerplate at each helper.
  • run_finished error_code. `run_created` / `run_finished` are now wired through the canonical `server.ts` `/api/runs` handler (a previous duplicate in `chat-routes.ts` was shadowed by Express's earlier registration and never executed — surfacing a separate latent bug where `run.clientType` was also never set, see "Adjacent fix" below). `run_finished result=failed` now carries a discrete `error_code` classifier: `run.errorCode` (explicit emit) → `AGENT_SIGNAL_` → `AGENT_EXIT_` → `AGENT_TERMINATED_UNKNOWN` fallback. Token totals and duration unchanged.
  • Coverage. P0/P1/P2 call sites wired across every page: home nav / toolbar / chat composer / recent projects, projects list, automations, plugins (installed / available / sources tabs), design systems (modal + share popover), integrations (mcp / connectors / skills / use_everywhere), file manager, artifact toolbar / header / present popover / tweaks variants / share & export, feedback panel (with the v1 `clear_feedback_rating` rating-value bug fixed), settings sidebar / language / appearance / notifications / pets / privacy / connectors.
  • PostHog UA filter opt-out. `opt_out_useragent_filter: true` so embedded webviews, fingerprinted browsers, and the Playwright-based e2e runs ingest captures. The Privacy → "Share usage data" toggle remains the single consent gate.

Adjacent fix — `run.clientType` now reaches Langfuse

`chat-routes.ts` declared a full `app.post('/api/runs', …)` handler that Express never used (the canonical handler in `server.ts` was registered first). Among the unreachable code: `run.clientType` was being set from `x-od-client` / Electron UA. Result: `langfuse-bridge.ts:320` has been shipping `run.clientType: undefined` for traces. The clientType inference moved to the canonical handler in this PR (single-line behavior change).

Verification

  • `pnpm guard` / `pnpm typecheck` clean for daemon / web / contracts.
  • `pnpm --filter @open-design/web test` — 1645 / 1645 passing.
  • End-to-end through Playwright + the real PostHog ingest project 420348:
    • 28 unique page+area+element combos hit on a single home-to-projects-to-settings walk; every event landed with `event_schema_version: 2` and the expected `device_id` / `page_name` / `area` / `element` props.
    • `page_view` fired on all 8 page names: home / projects / automations / design_systems / plugins / integrations / chat_panel / file_manager.
    • `surface_view` fired on help_resources_popover / new_project_modal / plugin_replacement_modal.
    • `settings_view` fired across 9 areas including configure_execution_mode / language / appearance / notifications / pets / privacy.
    • `project_create_result` fired with `result=success`.
    • `settings_cli_test_result` fired with `result=success` from a real codex CLI test.
    • `run_created` + `run_finished` fired through a codex CLI run; result=failed because codex was misconfigured locally, and the new `error_code` field surfaced `AGENT_EXIT_1` — confirming the fallback chain works end-to-end.

Adjacent issues (out of scope)

  • The remaining `*_result` events (`artifact_export_result` / `feedback_submit_result` / `file_upload_result` / `plugin_replacement_result` / `settings_byok_test_result` / `settings_connector_auth_result`) are wired in code but their UI paths ("upload a real file", "export a real artifact", "OAuth a real connector") were not automated in this PR's Playwright run. Production traffic will exercise them.
  • The duplicate `reconcileAssistantMessageOnRunEnd` helper (chat-routes.ts had a local copy shadowed by the canonical one in server.ts) has been removed as part of this change.

Aligns the PostHog wire format with the product team's v2 tracking
spec (Open Design 埋点文档 2.0). The previous v1 catalogue defined a
flat per-page event name (home_view / studio_click / settings_view…);
v2 collapses everything to four core events identified through the
page_name + area + element triplet so dashboards can group by surface
without owning a separate event per page.

Key changes
- packages/contracts/src/analytics: collapse to page_view / ui_click /
  surface_view / *_result event names; bump EVENT_SCHEMA_VERSION to 2;
  rename the wire field anonymous_id → device_id (value unchanged);
  promote the configure-state triplet (has_available_configure_cli /
  configure_type / configure_availability) to a global PostHog register
  so every event inherits it without per-helper boilerplate.
- apps/web/src/analytics: rewrite the 43 trackXxx helpers behind the
  new typed catalogue; opt out of PostHog's built-in UA bot filter so
  legitimate embedded webviews, fingerprinted browsers, and the
  Playwright-based e2e runs ingest captures (the Privacy → "Share
  usage data" toggle remains the single consent gate).
- apps/web components: wire P0/P1/P2 click + view + result surfaces
  end-to-end — left nav, toolbar, home chat composer, recent projects,
  new project modal, plugins / design systems / integrations /
  automations pages, file manager, artifact toolbar/header/share popup,
  feedback panel, settings sidebar / language / appearance /
  notifications / pets / privacy / connectors. Fixes the v1 feedback
  bug where action=clear_feedback_rating shipped rating=null instead of
  the rating being cleared.
- apps/daemon: extend run_created / run_finished with the v2 context
  (entry_from / project_kind / target_platforms / fidelity /
  connectors / etc.), add explicit error_code classification on
  result=failed (run.errorCode → AGENT_SIGNAL_* → AGENT_EXIT_* →
  AGENT_TERMINATED_UNKNOWN), and read device_id from the new
  x-od-analytics-device-id header. Also moves the run_created /
  run_finished emission to the canonical /api/runs handler in
  server.ts; the chat-routes copy was shadowed by Express's earlier
  registration and never executed, which also meant run.clientType
  never made it to Langfuse — fixed in the same move.

Verification
- pnpm guard / pnpm typecheck clean for daemon, web, and contracts.
- pnpm --filter @open-design/web test: 1645/1645 passing.
- End-to-end smoke through Playwright + local PostHog ingest project
  420348: every page_view (home/projects/automations/design_systems/
  plugins/integrations/chat_panel/file_manager), every nav element,
  the new_project_modal surface_view + tab + create flow, the
  plugin_replacement_modal surface_view, settings_view across nine
  sections, settings_cli_test_result (codex CLI), the
  project_create_result success path, and run_created + run_finished
  (result=failed, error_code=AGENT_EXIT_1) all reached PostHog with
  the v2 schema and the expected device_id / page_name / area /
  element / fidelity / target_platforms props. The remaining
  *_result events (artifact_export / feedback_submit / file_upload /
  plugin_replacement / settings_byok_test / settings_connector_auth)
  are wired in code; production traffic will trigger them.
@lefarcen lefarcen requested a review from mrcfps May 19, 2026 16:10
@lefarcen lefarcen added size/XXL PR changes 1500+ lines risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps type/feature New feature labels May 19, 2026
@mrcfps
Copy link
Copy Markdown
Contributor

mrcfps commented May 19, 2026

@lefarcen I'm holding off on generating review comments for #2285 because this pull request has merge conflicts right now.

Please resolve the conflicts with main and push the updated branch. Once that's done, request or wait for the review to run again and I'll take another look.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

# Conflicts:
#	apps/daemon/src/chat-routes.ts
#	apps/web/src/components/DesignSystemsTab.tsx
#	apps/web/src/components/EntryShell.tsx
#	apps/web/src/components/NewProjectPanel.tsx
#	apps/web/src/components/TasksView.tsx
#	packages/contracts/src/analytics/events.ts
Copy link
Copy Markdown
Contributor

@mrcfps mrcfps left a comment

Choose a reason for hiding this comment

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

@lefarcen Thanks for pushing the v2 analytics migration through. I walked the changed ranges and found two follow-up fixes worth landing before the new dashboards depend on this payload shape.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

Comment thread packages/contracts/src/analytics/events.ts
Comment thread apps/daemon/src/server.ts
…p switch

The merge resolution in DesignSystemsTab incorrectly re-introduced a
`setCategory('All')` call alongside the new `trackDesignSystemsTopClick`
emit. main intentionally keeps the active style category when the surface
filter refines within it; the regression was caught by the existing
"keeps the style category when a surface chip refines within it" test
in tests/components/DesignSystemsTab.test.tsx.
@lefarcen lefarcen requested a review from mrcfps May 19, 2026 17:37
… configure-state

Two follow-ups from the v2 schema review on nexu-io#2285:

1. `byokProtocolToTracking()` was still falling through to `null` for
   `senseaudio` even though the v2 BYOK provider enum now lists it. Every
   `SettingsDialog` BYOK call site guards on `if (byokProviderId)`, so a
   user on SenseAudio was silently dropping the provider-option,
   field-focus, and test-result captures. Added the missing case so
   SenseAudio gets the same analytics coverage as the other providers.

2. The daemon-authoritative `run_created` / `run_finished` events were
   missing the configure-state triplet (`has_available_configure_cli` /
   `configure_type` / `configure_availability`) that v2 promotes to a
   global register on the web side. Daemon captures don't go through the
   PostHog global register, so dashboards couldn't segment run lifecycle
   by execution setup after the migration.

   The fix derives the triplet server-side from `detectAgents()` and the
   request's `agentId` before `design.analytics.capture(...)`:
     - has_available_configure_cli: any CLI on PATH reports installed
     - configure_type: 'local_cli' when the run targets an installed CLI,
       otherwise 'unknown' (daemon can't see BYOK keys, which live in
       web-client storage)
     - configure_availability: 'available' / 'unavailable' / 'unknown'
       based on the requested agent's install status, with a fallback to
       'available' when any CLI is installed

   This keeps the v2 schema consistent across both daemon-side and
   web-side captures.
@lefarcen
Copy link
Copy Markdown
Contributor Author

Thanks @mrcfps — both addressed in a7cd7928:

  1. byokProtocolToTracking('senseaudio') now returns 'senseaudio' instead of falling through to null, so the SettingsDialog BYOK guards (if (byokProviderId)) no longer suppress SenseAudio's provider_option / field_focus / test_result captures.

  2. Daemon-side run_created / run_finished now derive the configure-state triplet at capture time from detectAgents() + reqBody.agentId:

    • has_available_configure_cli: any CLI on PATH installed
    • configure_type: local_cli when the run targets an installed CLI, otherwise unknown (daemon can't see BYOK keys at this layer)
    • configure_availability: available / unavailable / unknown based on the requested agent's install status

    The BYOK arm staying unknown from the daemon is a deliberate compromise — the keys live in web-client storage and are not on the daemon's filesystem. If you'd rather the daemon mirror an authoritative web-side value, I can wire the web client to attach configureGlobals to the /api/runs body and have the daemon transit them through, but that's a larger change.

Copy link
Copy Markdown
Contributor

@mrcfps mrcfps left a comment

Choose a reason for hiding this comment

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

@lefarcen Thanks for addressing the earlier follow-ups. I walked the refreshed changed ranges on a7cd7928 and found one more analytics follow-up worth landing so the v2 payload matches the schema comments across the browser-side events.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

Comment thread apps/web/src/analytics/client.ts
…h state

Third follow-up from the v2 schema review on nexu-io#2285. The previous fix
addressed senseaudio + daemon-side configure-state, but reviewer flagged
that `setConfigureGlobals` was still defined-only — no caller — so every
browser-side capture inherited the boot defaults
(`has_available_configure_cli=false`, `configure_type='unknown'`,
`configure_availability='unknown'`). PostHog dashboards therefore could
not segment the new `page_view` / `ui_click` / `surface_view` events by
execution setup after a user configured their environment.

Changes:

- `packages/contracts/src/analytics/events.ts` — add a pure
  `deriveConfigureGlobals(mode, agentId, agents, byokConfigured)` helper
  so the web client and the daemon can derive the triplet from the same
  source of truth. The helper covers all 5 `configure_type` buckets
  (`local_cli` / `byok` / `both` / `none` / `unknown`) and the 3
  `configure_availability` buckets (`available` / `unavailable` /
  `unknown`).
- `apps/web/src/App.tsx` — add a useEffect that re-derives the triplet
  whenever the user changes execution mode, selects a new CLI, saves a
  BYOK key, or the detected-agent list refreshes, then pushes it to
  PostHog via `analytics.setConfigureGlobals(...)`. The setter goes
  through the provider so the analytics module stays the single source
  of truth.
- `apps/web/src/analytics/provider.tsx` — expose
  `setConfigureGlobals` on the analytics context and the test stub so
  consumers route through the provider boundary.
- `apps/daemon/src/server.ts` — switch the daemon-side derive in
  `/api/runs` to the shared `deriveConfigureGlobals` helper so the
  authoritative run_created/run_finished captures match the web-side
  payload. BYOK credentials live in the web client and stay invisible
  to the daemon, so the daemon arm passes `byokConfigured: undefined`
  and falls back to the installed-CLI signal.
- `apps/web/tests/analytics-configure-globals.test.ts` — new regression
  test that pins the derive behavior across all branches and confirms
  the setter actually mutates the client-side store. Locks the wire-up
  so a future refactor can't silently turn the setter back into a
  no-op.

Verification: pnpm guard clean; daemon / web typecheck clean; web tests
1703/1703 passing (up from 1696 — 7 new tests in the configure-globals
suite).
@lefarcen
Copy link
Copy Markdown
Contributor Author

Thanks for the follow-up @mrcfps — addressed in e0cfc5f9:

  • Added a pure deriveConfigureGlobals() helper in `packages/contracts/src/analytics/events.ts` so the web client and the daemon derive the triplet from the same source of truth (covers all five `configure_type` buckets and three `configure_availability` buckets).
  • `apps/web/src/App.tsx` now drives the triplet from a useEffect watching mode / agentId / apiKey / apiProtocolConfigs / agents, and re-registers it via `analytics.setConfigureGlobals(...)` whenever any of those change. The next capture inherits the fresh values.
  • Exposed `setConfigureGlobals` through the `useAnalytics()` context (plus the test-stub) so consumers stay behind the provider boundary.
  • Migrated the daemon-side derive in `server.ts` to the same helper so daemon-authoritative run_created / run_finished payloads match the web-side ones. The daemon arm passes `byokConfigured: undefined` because BYOK keys live in web-client storage; it falls back to the installed-CLI signal.
  • New regression test at `apps/web/tests/analytics-configure-globals.test.ts` pins the derive matrix across all branches and confirms the setter actually mutates the client-side store. Locks the wiring so a future refactor can't silently regress the setter back into a no-op.

Verification: `pnpm guard` clean; daemon / web typecheck clean; web tests 1703/1703 passing (up from 1696 — 7 new tests in the configure-globals suite).

@lefarcen lefarcen requested a review from mrcfps May 20, 2026 01:35
Copy link
Copy Markdown
Contributor

@mrcfps mrcfps left a comment

Choose a reason for hiding this comment

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

@lefarcen Thanks for iterating on the analytics follow-ups. I walked the latest changed ranges on e0cfc5f9 and found two remaining telemetry mismatches worth tightening before the v2 dashboards start depending on these fields.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

Comment thread apps/web/src/components/DesignsTab.tsx
Comment thread apps/web/src/components/ChatComposer.tsx Outdated
…el source

Fourth review pass on PR nexu-io#2285. Two follow-ups from mrcfps:

1. DesignsTab (projects landing) was emitting click events but no
   matching page_view. Opening /projects without clicking anything left
   the surface invisible in PostHog. Added a once-per-mount
   trackPageView({ page_name: 'projects' }) with the same ref-keyed
   pattern HomeView / PluginsView use.

2. ChatComposer was hard-coding source: 'recent_project' on every
   chat_panel page_view. The web router currently only carries
   projectId / conversationId / fileName, so we cannot distinguish a
   New-project launch from a template-pick or a Recent-projects click
   from this layer. A false constant would over-attribute every chat
   launch to 'recent_project' and break the funnel slice this schema
   was meant to unlock. Dropped the field for now — better no source
   than the wrong source — until the router grows a launch-source
   channel; the field is still defined as optional on PageViewProps so
   the channel can land in a follow-up PR.

Verification: web typecheck clean; web tests 1703/1703 passing.
@lefarcen
Copy link
Copy Markdown
Contributor Author

Thanks @mrcfps — both addressed in c670570f:

  1. DesignsTab projects page_view — added a once-per-mount trackPageView({ page_name: 'projects' }) with the same ref-keyed pattern HomeView / PluginsView use. Opening /projects now produces the v2 projects page-view event even when the user does not click anything.

  2. ChatComposer hard-coded source — dropped the source: 'recent_project' constant entirely. You are right that this layer can't distinguish New-project / template / Recent-projects launches given the current router shape; over-attributing every chat to recent_project would corrupt the funnel slice this schema was meant to unlock. The field stays optional on PageViewProps, so when the router grows a launch-source channel we can populate it correctly in a follow-up PR. Better-no-source-than-wrong-source for now.

Verification: web typecheck clean; web tests 1703/1703 passing.

@lefarcen lefarcen requested a review from mrcfps May 20, 2026 01:52
Copy link
Copy Markdown
Contributor

@mrcfps mrcfps left a comment

Choose a reason for hiding this comment

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

@lefarcen Thanks for tightening the analytics follow-ups on this head. I walked the refreshed changed ranges and found three non-blocking telemetry mismatches that are still worth fixing before the v2 dashboards rely on these result events.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

Comment thread apps/web/src/components/HomeView.tsx Outdated
Comment thread apps/web/src/components/FileWorkspace.tsx Outdated
Comment thread apps/web/src/components/NewProjectPanel.tsx Outdated
…us upload + missing requestId

Three follow-ups from the fifth review pass on PR nexu-io#2285:

1. **plugin_replacement_result emitted before the apply settled**
   (`apps/web/src/components/HomeView.tsx`). The modal's confirm action
   was a synchronous wrapper around an async `usePlugin(...)` call, so
   the surrounding try/catch never observed real failures and every
   attempt was reported as `result=success`. Changed `PendingReplacement.
   confirm` to return `Promise<void>`, made the wrapper return the
   underlying promise, and moved the analytics emit into an async
   IIFE in the click handler so the success/failure branches reflect
   the actual outcome.

2. **file_upload_result mis-typed heterogeneous batches**
   (`apps/web/src/components/FileWorkspace.tsx`). The earlier
   implementation only inspected `picked[0]`, so a mixed batch like
   `image.png + demo.mp4` reported `file_type=image`. Per the comment
   above the block ("mixed batches collapse to other"), the
   implementation now maps every file to a tracking type, collapses to
   `other` when more than one distinct type is present, and falls
   back to the single type otherwise.

3. **project_create_result lost the click→result correlation id**
   (`apps/web/src/components/NewProjectPanel.tsx`). The click event
   no longer carried the locally-generated `requestId` that
   `project_create_result` keeps, so the two could not be joined.
   `trackNewProjectModalElementClick()` now accepts an optional
   `{ requestId }`, mirroring the other helpers, and the create-button
   click threads the same id used for the result.

Verification: web typecheck clean; web tests 1703/1703 passing.
@lefarcen
Copy link
Copy Markdown
Contributor Author

Thanks @mrcfps — all three addressed in d4fb5a00:

  1. plugin_replacement_result async outcome (HomeView.tsx): changed PendingReplacement.confirm to return Promise<void>, made the wrapper return the underlying usePlugin(...) promise, and moved the analytics emit into an async IIFE in the click handler. The success/failure branches now fire off the real settle so any plugin-apply rejection lands in the failed bucket.

  2. file_upload_result heterogeneous batches (FileWorkspace.tsx): the implementation now maps every file in the batch to a tracking type, collapses to 'other' when more than one distinct type is present, and falls back to the single type otherwise. Matches the comment above the block.

  3. project_create_result click→result correlation (NewProjectPanel.tsx): trackNewProjectModalElementClick() now accepts an optional { requestId }, and the create-button click threads the same id that project_create_result already carries.

Verification: web typecheck clean; web tests 1703/1703 passing.

@lefarcen lefarcen requested a review from mrcfps May 20, 2026 02:58
Copy link
Copy Markdown
Contributor

@mrcfps mrcfps left a comment

Choose a reason for hiding this comment

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

@lefarcen Thanks for tightening the analytics follow-ups on this head. I walked the refreshed changed ranges and found two telemetry gaps that are still worth landing before the v2 dashboards start depending on these payloads.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

Comment thread apps/web/src/App.tsx
Comment thread apps/daemon/src/server.ts Outdated
…n_created fields

Two follow-ups from the sixth review pass on PR nexu-io#2285:

1. **Cold-start configure-state was stamped before fetchAgents() landed**
   (`apps/web/src/App.tsx`). The useEffect that pushes the v2 triplet
   into the PostHog global register fired on first paint with
   `agents=[]`, so the first home/projects/plugins page_view reported
   `has_available_configure_cli=false` / `configure_availability=
   unavailable` even on machines that did have an installed CLI. The
   effect now waits on `agentsLoading === false` and leaves the boot
   defaults ('unknown'/'unknown') in place until the probe resolves.

2. **Daemon read run-context fields the web never sends**
   (`apps/daemon/src/server.ts`). The daemon-side run_created /
   run_finished baseProps read `projectKind`, `entryFrom`,
   `projectSource`, `targetPlatforms`, `companionSurfaces`, `fidelity`,
   `connectors`, `useSpeakerNotes`, `includeAnimations`,
   `referenceTemplate`, and `aspect` from `req.body`, but
   `packages/contracts/src/api/chat.ts` and
   `apps/web/src/providers/daemon.ts` don't carry those keys on the
   wire. Reading them therefore always produced null/undefined.
   Dropped the unsent fields from the daemon capture; a follow-up can
   extend the create payload to thread the real context through. The
   `design_system_id` field stays because the chat contract does send
   it.

Tests: added 3 regression tests in `tests/analytics-configure-globals.
test.ts` covering the boot-time gating contract (empty agents +
daemon mode → unavailable / local_cli; installed agent → available;
undefined agents list → unavailable). Verification: web typecheck
clean; daemon typecheck clean; web tests 1706/1706 passing (up from
1703 — 3 new cold-start tests).
@lefarcen
Copy link
Copy Markdown
Contributor Author

Thanks @mrcfps — both addressed in d8e79eb3:

  1. Cold-start configure-state gating (apps/web/src/App.tsx): the useEffect now waits on `agentsLoading === false` before pushing the triplet, so the first home/projects/plugins page_view inherits the boot defaults (`unknown`/`unknown`) rather than stamping `unavailable` on installed-CLI machines. Added 3 regression tests in `tests/analytics-configure-globals.test.ts` covering the empty-agents-with-daemon-mode and undefined-agents cases.

  2. Daemon stopped reading unsent run_context fields (apps/daemon/src/server.ts): removed the `projectKind` / `entryFrom` / `projectSource` / `targetPlatforms` / `companionSurfaces` / `fidelity` / `connectors` / `useSpeakerNotes` / `includeAnimations` / `referenceTemplate` / `aspect` reads. The chat contract and `providers/daemon.ts` only thread `designSystemId` (which I kept), so the dropped fields were stamping null/undefined. A follow-up can extend the create-run payload to carry the full context properly; until then the daemon emits exactly what it can observe.

Verification: web typecheck clean; daemon typecheck clean; web tests 1706/1706 passing (up from 1703 — 3 new cold-start tests).

Copy link
Copy Markdown
Contributor

@mrcfps mrcfps left a comment

Choose a reason for hiding this comment

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

@lefarcen Thanks for tightening the analytics follow-ups on this head. I walked the current changed ranges and found one remaining telemetry classification mismatch worth fixing before the v2 run lifecycle dashboards depend on these fields.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

Comment thread apps/daemon/src/server.ts
…lable

Eleventh review pass on PR nexu-io#2285. mrcfps flagged that
`apps/daemon/src/server.ts` was calling `deriveConfigureGlobals(...)`
without `mode`, so the helper fell through to the generic branch.
Result: a run for an uninstalled agent was tagged
`configure_availability: 'available'` whenever any OTHER CLI was on
PATH, because the generic branch only looks at the cohort-wide
"any installed?" signal. That precisely undermines the slice the
daemon emit is trying to power.

The daemon's /api/runs handler is always a daemon-mode capture
(daemon is the local CLI runner — BYOK lives in the web layer), so we
now pin `mode: 'daemon'` on the call site. The helper then judges
`configure_availability` from the REQUESTED agent's install status and
reports `unavailable` when the user picked an agent that is not
installed, even if peers are.

Added a regression case in `tests/analytics-configure-globals.test.ts`:
`{ mode: 'daemon', agentId: 'codex', agents: [{claude,true},{codex,false}] }`
→ `{ has_available_configure_cli: true, configure_type: 'local_cli',
configure_availability: 'unavailable' }`.

Verification: daemon typecheck clean; web tests 1707/1707 passing
(up from 1706 — 1 new regression test).
@lefarcen
Copy link
Copy Markdown
Contributor Author

Thanks @mrcfps — addressed in ad180c7a:

Pinned `mode: 'daemon'` on the `deriveConfigureGlobals(...)` call in `apps/daemon/src/server.ts`. The daemon's `/api/runs` handler is always a daemon-mode capture (BYOK lives in the web client), so the explicit mode tells the helper to judge `configure_availability` from the requested agent's install status rather than the cohort-wide fallback. A run aimed at an uninstalled agent now reports `unavailable` even when peers are on PATH.

Added a regression case in `tests/analytics-configure-globals.test.ts`:
```
{ mode: 'daemon', agentId: 'codex', agents: [{claude,true},{codex,false}] }
→ { has_available_configure_cli: true, configure_type: 'local_cli',
configure_availability: 'unavailable' }
```

Verification: daemon typecheck clean; web tests 1707/1707 passing (up from 1706 — 1 new regression test).

Copy link
Copy Markdown
Contributor

@mrcfps mrcfps left a comment

Choose a reason for hiding this comment

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

@lefarcen Thanks for pushing the analytics v2 follow-ups through. I walked the latest changed ranges on ad180c7a and found two remaining telemetry correctness issues worth tightening before the new dashboards depend on these page-view and feedback-correlation signals.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

Comment thread apps/web/src/components/ChatComposer.tsx Outdated
Comment thread apps/web/src/components/AssistantMessage.tsx
- Move chat_panel page_view emit from ChatComposer to ProjectView so
  it survives activeConversationId-driven ChatPane remounts. ProjectView
  keys the dedupe ref by project.id; the composer drops its duplicate.
- Thread { requestId } into trackAssistantFeedbackReasonSubmitClick so
  the click pairs with the existing feedback_submit_result on the same
  request id (mirrors the trackNewProjectModalElementClick pattern).
Copy link
Copy Markdown
Contributor

@mrcfps mrcfps left a comment

Choose a reason for hiding this comment

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

@lefarcen Thanks for pushing the analytics v2 follow-ups through. I walked the latest changed ranges and found two merge-safe analytics correctness gaps worth tightening before dashboards depend on this schema.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

Comment thread apps/web/src/analytics/client.ts
Comment thread apps/daemon/src/server.ts
@lefarcen lefarcen requested a review from mrcfps May 20, 2026 04:38
…gn_system_source

- Snapshot the register payload in client.ts on PostHog init and
  re-register it from applyConsent(true) and applyIdentity() so a
  privacy-toggle or Delete-my-data rotation does not resume capture
  without event_schema_version / device_id / session_id / locale /
  configure-state globals. setConfigureGlobals() also patches the
  cache so a later restore picks up the current configure state.
- Stamp design_system_source on daemon-side run_created / run_finished
  (it is required by RunCreatedProps / RunFinishedProps). Daemon
  can't tell default vs user_selected vs inherited from the wire, so
  it derives 'unknown' when designSystemId is present, 'not_applicable'
  otherwise — a follow-up that threads designSystemSource through
  CreateRunRequest can replace this with the precise source.
@mrcfps
Copy link
Copy Markdown
Contributor

mrcfps commented May 20, 2026

@lefarcen I'm holding off on generating review comments for #2285 because this pull request has merge conflicts right now.

Please resolve the conflicts with main and push the updated branch. Once that's done, request or wait for the review to run again and I'll take another look.

🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.

# Conflicts:
#	apps/web/src/components/EntryShell.tsx
@lefarcen lefarcen merged commit 204599a into nexu-io:main May 20, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps size/XXL PR changes 1500+ lines type/feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants