Files
nexu-io__open-design/packages/contracts/tests/analytics-session-mode.test.ts
elihahah666 2aee23f11c feat(analytics): instrument composer, workspace tabs, HyperFrames kind + daemon $os (#3932)
* fix(analytics): stamp $os/$os_version on daemon-emitted events

posthog-node (unlike posthog-js) does not auto-enrich device props, so every
daemon-emitted event (run_created/run_finished/project_create_result/
file_upload_result/...) landed in the null/unknown bucket on any OS breakdown.
The daemon runs on the user's machine, so process.platform is authoritative;
stamp the canonical PostHog $os (Mac OS X|Windows|Linux) + $os_version on both
capture() and captureSafety() so backend events join the same OS segmentation
as the web client's posthog-js events.

* feat(analytics): track HyperFrames as a first-class project_kind

HyperFrames is a peer task type in the router (its own Home chip), but the
data model stores it as a 'video' project with videoModel='hyperframes-html',
so analytics recorded project_kind=video and HyperFrames was invisible in the
project-type dimension. Add 'hyperframes' to TrackingProjectKind, make
projectKindToTracking(kind, videoModel?) split it out of generic video, and
thread videoModel through every web/daemon emit site (project_create_result,
run_created/run_finished, recent projects, DS apply target).

* feat(analytics): instrument chat composer controls + per-send run context

Adds ui_click coverage across the home + in-project chat composer bottom bar,
which was almost entirely untracked:
- ask/design session-mode toggle (ComposerSessionModeClickProps, home + chat_panel)
- design toolbox open / action / resource (DesignToolboxClickProps)
- the rest of the bar (ComposerBarClickProps): + menu open/pick/add
  connector|plugin|mcp, design-system switch, working-dir switch, agent +
  model selection, context-chip removal
- home composer: working-dir picker, task-type chips
And enriches run_created/run_finished with the per-send context the spec asks
for: session_mode, plugin_id, mcp_ids[], skill_ids[] (model_id/agent_provider_id/
design_system_id already present; mcp_id no longer hardcoded null).

* feat(analytics): instrument the workspace tab launcher

The tab strip's '+' launcher (search/filter files, New Terminal, New Browser,
open file as tab, focus an open tab) had no tracking. Add TabLauncherClickProps
(page_name=file_manager, area=tab_launcher) and an onTrack hook on
TabLauncherMenu that FileWorkspace fills with page/area/project_id, covering
open / filter / create / open_file / open_tab.

* fix(analytics): cover home + menu and desktop working-dir switch

Review follow-up:
- HomeChatComposerClickProps gains plus_menu_open / plus_pick / plus_add
  (+ resource_kind/resource_id); HomeHero wraps the ComposerPlusMenu
  open/pick/add callbacks so the home '+' menu emits the same gestures the
  in-project composer already records.
- WorkingDirPill funnels both success paths (browser applyDir + desktop host
  pickAndReplaceHostProjectWorkingDir) through a shared trackWorkingDirSwitch()
  so packaged/desktop replacements are counted too.

* fix(analytics): fold persistent skillId into run skill_ids set

Review follow-up: skill_ids only carried the staged per-turn skillIds, so a
project run with a persisted skillId but no staged skills emitted skill_id=<id>
but skill_ids=[]. Build a de-duped skill_ids from [skillId, ...skillIds] so the
array field is the complete per-send skill context; keep singular skill_id.

* fix(analytics): emit agent_selector_open when the AvatarMenu opens

Review follow-up: the contract added ComposerBarClickProps.element=
'agent_selector_open' but nothing emitted it, so open-to-select dropoff was
unmeasurable. Add an onOpen prop to AvatarMenu fired on the closed->open
transition (not on close) and wire ProjectView to emit agent_selector_open with
the same project_id context as agent_select/agent_model_select.

* fix(analytics): gate session_mode to composer-originated runs

Review follow-up: session_mode defaulted a missing reqBody.sessionMode to 'ask',
so DS-generation runs (DesignSystemFlow sends no sessionMode) were classified as
Ask runs on run_created/run_finished, polluting composer-mode dashboards. Only
spread session_mode when !isDesignSystemRun && typeof reqBody.sessionMode ===
'string'; the field is optional in the contract for DS runs.

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-06-08 15:18:55 +00:00

21 lines
709 B
TypeScript

import { describe, expect, it } from 'vitest';
import { sessionModeToTracking } from '../src/analytics/events.js';
describe('sessionModeToTracking', () => {
it('maps the wire `chat` mode to the product term `ask`', () => {
expect(sessionModeToTracking('chat')).toBe('ask');
});
it('keeps `design` as `design`', () => {
expect(sessionModeToTracking('design')).toBe('design');
});
it('buckets unknown / null / undefined into the lighter `ask` default', () => {
expect(sessionModeToTracking(null)).toBe('ask');
expect(sessionModeToTracking(undefined)).toBe('ask');
expect(sessionModeToTracking('')).toBe('ask');
expect(sessionModeToTracking('bogus')).toBe('ask');
});
});