chore: fix test mocks, suppress cron pageview tracking, and minor docs

Add workspace mock to agent-runner tests, skip PostHog pageviews on cron paths to reduce noise, document web-chat storage path in CRM skill, and include cursor plans and build info.
This commit is contained in:
kumarabhirup 2026-03-05 21:20:35 -08:00
parent f279524e32
commit 38b062a71e
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
7 changed files with 1545 additions and 1 deletions

1186
.cursor/debug-23c6ec.log Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,91 @@
---
name: Root URL Refactor
overview: Replace the current `/workspace`-centric app shell with a root-route, URL-driven shell, then add deep regression coverage for all navigable/restorable UI state using both Vitest and browser E2E tests.
todos:
- id: define-url-model
content: Design the canonical root query schema and implement typed parse/serialize helpers plus legacy `/workspace` migration helpers.
status: completed
- id: promote-root-shell
content: Move the workspace app shell to the root route and leave `/workspace` as a compatibility redirect preserving query state.
status: completed
- id: make-shell-url-driven
content: Refactor page-level navigation, browse mode, chat/subagent selection, entry modal state, cron routes, and sidebar preview to derive from URL state.
status: completed
- id: lift-child-state
content: Extract child-owned navigation state into parent-controlled contracts before URL sync, especially table column visibility and subagent selection/identity.
status: completed
- id: url-drive-object-views
content: Lift object view state into URL-backed state and keep it synchronized with saved-view changes from `.object.yaml` updates.
status: completed
- id: update-link-producers
content: Replace all hard-coded `/workspace` link builders and imperative navigations with the new root-route helpers.
status: completed
- id: expand-regression-tests
content: Update affected Vitest suites and add browser E2E coverage for deep links, reloads, back/forward, legacy redirects, and all URL-persisted view states.
status: completed
isProject: false
---
# Root URL Refactor
## Target Architecture
- Make the real app live at `[apps/web/app/page.tsx](apps/web/app/page.tsx)` instead of the current landing page, and retire `[apps/web/app/workspace/page.tsx](apps/web/app/workspace/page.tsx)` as an app shell.
- Keep `/workspace` only as a compatibility entry that redirects to `/` while preserving search params, so existing chat/file/object links do not break.
- Establish a single canonical root query model, owned by a typed codec in `[apps/web/lib/workspace-links.ts](apps/web/lib/workspace-links.ts)` or a new sibling helper.
- Treat the URL as the source of truth for navigable/restorable state, not just an effect sink.
```mermaid
flowchart LR
UserAction --> UrlState
ServerPush[SSE or API refresh] --> RouteReducer
UrlState --> RouteReducer
RouteReducer --> WorkspaceShell
WorkspaceShell --> UrlWriters
UrlWriters --> UrlState
```
## Refactor Seams
- Extract a typed `WorkspaceUrlState` parser/serializer from the current `/workspace` helpers in `[apps/web/lib/workspace-links.ts](apps/web/lib/workspace-links.ts)`. It should cover at least:
- root app mode: chat vs file/object/document/report/database vs cron
- `path`, `entry`, `chat`, `subagent`, `send`
- browse-mode directory state and `showHidden`
- right-sidebar preview target
- object view state: active view, view type, filters, search, sort, page, pageSize, visible columns, view settings that affect rendering
- Add a contract-lifting phase before URL sync so child components stop owning route-relevant state:
- lift table column visibility and related callbacks out of the table layer so object views can serialize a single canonical column state
- change subagent navigation to use stable child session keys/metadata from the parent shell rather than task-label lookup in the message renderer
- identify any other child-local route-bearing state during implementation and convert it to controlled props before wiring it to the URL
- Refactor the page-level state machine out of `[apps/web/app/workspace/page.tsx](apps/web/app/workspace/page.tsx)` into a reusable root-shell component/hook so URL decoding happens on every navigation change, not only once through `initialPathHandled`.
- Convert imperative `/workspace` pushes/replaces throughout the app to root-route updates, including `[apps/web/app/components/sidebar.tsx](apps/web/app/components/sidebar.tsx)`, `[apps/web/app/components/workspace/database-viewer.tsx](apps/web/app/components/workspace/database-viewer.tsx)`, `[apps/web/app/components/workspace/document-view.tsx](apps/web/app/components/workspace/document-view.tsx)`, `[apps/web/app/components/workspace/markdown-editor.tsx](apps/web/app/components/workspace/markdown-editor.tsx)`, `[apps/web/app/components/workspace/slash-command.tsx](apps/web/app/components/workspace/slash-command.tsx)`, and `[apps/web/lib/workspace-cell-format.ts](apps/web/lib/workspace-cell-format.ts)`.
- Move browse/workspace traversal state in `[apps/web/app/hooks/use-workspace-watcher.ts](apps/web/app/hooks/use-workspace-watcher.ts)` behind URL-backed inputs so folder navigation, `..`, and workspace-root return are all reload-safe.
- Rework object view state in `[apps/web/app/workspace/page.tsx](apps/web/app/workspace/page.tsx)` and related components so the browser URL drives table/kanban/calendar/timeline/list/gallery rendering, filters, sort, search, pagination, and column visibility. The existing server query contract already supports much of this and should become the canonical browser contract too.
- Fix subagent deep-linking by routing with stable child session keys instead of the current task-label click path in `[apps/web/app/components/chat-message.tsx](apps/web/app/components/chat-message.tsx)`, then teach the main shell to restore subagent context from URL on refresh/back/forward.
- Preserve live `.object.yaml` edits by making SSE-driven saved-view updates recompute and rewrite the current URL state when the active view definition changes.
## Testing Strategy
- Update existing helper tests that currently hard-code `/workspace`, especially `[apps/web/lib/workspace-links.test.ts](apps/web/lib/workspace-links.test.ts)`, `[apps/web/lib/workspace-cell-format.test.ts](apps/web/lib/workspace-cell-format.test.ts)`, and `[apps/web/app/workspace/workspace-switch.test.ts](apps/web/app/workspace/workspace-switch.test.ts)`.
- Add focused Vitest coverage for the new URL codec and route reducer so the core contract is mutation-resistant:
- legacy `/workspace?...` migration into root URLs
- precedence rules when `path`, `chat`, `entry`, `subagent`, and preview state coexist
- contract tests proving lifted child state flows through controlled props/callbacks before URL serialization
- object filter/sort/search/page/pageSize/column/view serialization round-trips
- SSE/view-sync behavior when `.object.yaml` changes the active view
- Add browser E2E coverage for real navigation behavior because Vitest does not currently cover page-router behavior end to end:
- open from copied URL, refresh, and restore
- back/forward across chat, subagent, file, entry modal, cron, and browse-mode transitions
- object view deep links for table/kanban and filter/search/sort/pagination/column state
- legacy `/workspace` links redirecting to `/` without losing state
- Use the existing test-writer approach to prioritize invariants over snapshots: deep-link restoration, canonical URL updates after user actions, and state staying in sync when server-persisted view definitions change under the user.
## Main Risks To Manage
- `[apps/web/app/workspace/page.tsx](apps/web/app/workspace/page.tsx)` is both the current shell and the current state machine; splitting route logic from rendering will be the highest-risk step.
- Some state is still trapped inside child components, especially column visibility and subagent selection, so those contracts will need lifting before URL sync can be reliable.
- Mitigation for child-owned state risk: do the extraction first, add narrow contract tests around the lifted interfaces, then wire those parent-owned values into the URL layer instead of changing ownership and routing in one step.
- There is already a dirty worktree outside this refactor (`extensions/posthog-analytics/index.ts`, `src/cli/bootstrap-external.ts`), so the implementation should avoid touching unrelated changes while we migrate the web app.

View File

@ -0,0 +1,257 @@
---
name: Unified telemetry identity
overview: Persist a single anonymous install ID in `~/.openclaw-dench/telemetry.json` and reuse it across CLI telemetry, web server telemetry, browser PostHog init, and the OpenClaw PostHog plugin so all product telemetry and AI traces share one stable distinct ID.
todos:
- id: persist-install-id
content: Add anonymousId to telemetry.json and implement getOrCreateAnonymousId() in src/telemetry/config.ts
status: completed
- id: cli-identity
content: Switch src/telemetry/telemetry.ts to use the persisted install ID instead of machine hashing
status: completed
- id: server-identity
content: Switch apps/web/lib/telemetry.ts to use the persisted install ID as fallback instead of randomUUID()
status: completed
- id: browser-bootstrap-id
content: Pass the persisted anonymous ID through apps/web/app/layout.tsx into posthog-provider.tsx and bootstrap posthog-js with it
status: completed
- id: plugin-identity
content: Switch extensions/posthog-analytics/lib/event-mappers.ts to use the persisted install ID instead of machine hashing
status: completed
- id: status-surface
content: Show anonymous install ID in denchclaw telemetry status for debugging
status: completed
- id: identity-tests
content: Add behavior-focused tests for persisted install ID generation, reuse, and adoption across CLI/server/browser/plugin layers
status: completed
- id: docs-update
content: Update TELEMETRY.md to document the shared anonymous install ID model
status: completed
isProject: false
---
# Unified Anonymous Install ID
## What the runtime evidence showed
We already have concrete evidence that identity is fragmented:
- `[src/telemetry/telemetry.ts](src/telemetry/telemetry.ts)` uses a **machine hash** via `getAnonymousId()`.
- `[extensions/posthog-analytics/lib/event-mappers.ts](extensions/posthog-analytics/lib/event-mappers.ts)` also uses a **machine hash** for all `$ai_*` events.
- `[apps/web/lib/telemetry.ts](apps/web/lib/telemetry.ts)` falls back to `**randomUUID()`** when `distinctId` is not explicitly passed.
- `[apps/web/app/components/posthog-provider.tsx](apps/web/app/components/posthog-provider.tsx)` initializes `posthog-js` without a server-provided bootstrap distinct ID, so the browser can mint its own in-memory identity.
- `[apps/web/app/api/web-sessions/shared.ts](apps/web/app/api/web-sessions/shared.ts)` stores session metadata but has no persisted telemetry identity.
That means a single user can show up as 3 separate “people” in PostHog:
- browser distinct ID
- server random UUID
- OpenClaw plugin machine hash
## Target architecture
The correct source of truth is the DenchClaw state dir, not the browser.
Store one install-scoped anonymous ID in `[src/telemetry/config.ts](src/telemetry/config.ts)`s `telemetry.json`, and use it everywhere.
```mermaid
flowchart TD
TelemetryJson["~/.openclaw-dench/telemetry.json"]
subgraph cli [CLI]
CliConfig[readTelemetryConfig]
CliTrack[track]
end
subgraph server [Web Server]
ServerConfig[readTelemetryConfig]
TrackServer[trackServer]
SessionRoutes[web sessions + chat routes]
end
subgraph browser [Browser]
Layout[layout.tsx]
PHProvider[posthog-provider.tsx]
ChatUI[chat-message / feedback]
end
subgraph plugin [OpenClaw Plugin]
PluginPrivacy[privacy.ts]
PluginEvents[event-mappers.ts]
end
TelemetryJson --> CliConfig --> CliTrack
TelemetryJson --> ServerConfig --> TrackServer
TelemetryJson --> Layout --> PHProvider
PHProvider --> ChatUI
TelemetryJson --> PluginPrivacy --> PluginEvents
SessionRoutes --> TrackServer
```
## Design
### 1. Persist one anonymous install ID in `telemetry.json`
Extend `[src/telemetry/config.ts](src/telemetry/config.ts)`:
- Add `anonymousId?: string` to `TelemetryConfig`
- Add `getOrCreateAnonymousId()`
- Generate via `randomUUID()` once, write it back to `telemetry.json`, and reuse forever unless the user deletes the state dir
Example target shape:
```json
{
"enabled": true,
"noticeShown": true,
"privacyMode": true,
"anonymousId": "7c3a8d3a-..."
}
```
Why this is correct:
- Stable across CLI, server, browser, and plugin
- Available before the browser exists
- Survives restarts, upgrades, and re-bootstrap
- Anonymous and install-scoped, not tied to machine fingerprinting
### 2. Replace all machine-hash / random UUID fallbacks
#### CLI
In `[src/telemetry/telemetry.ts](src/telemetry/telemetry.ts)`:
- Remove `getAnonymousId()` machine-hash logic
- Use `getOrCreateAnonymousId()` from `config.ts`
#### Web server
In `[apps/web/lib/telemetry.ts](apps/web/lib/telemetry.ts)`:
- Stop falling back to `randomUUID()`
- Read the same `anonymousId` from `telemetry.json`
- Keep allowing an explicit `distinctId` override when the browser passes one, but default to the persisted install ID
#### OpenClaw plugin
In `[extensions/posthog-analytics/lib/event-mappers.ts](extensions/posthog-analytics/lib/event-mappers.ts)`:
- Remove the plugin-local machine-hash `getAnonymousId()`
- Read the persisted install ID from `telemetry.json`
- Reuse it for all `$ai_generation`, `$ai_span`, `$ai_trace`, and custom events
This is the biggest correctness fix for PostHog identity.
### 3. Bootstrap the browser with the same distinct ID
In `[apps/web/app/layout.tsx](apps/web/app/layout.tsx)`:
- Read the persisted anonymous ID on the server side
- Pass it into `[apps/web/app/components/posthog-provider.tsx](apps/web/app/components/posthog-provider.tsx)`
In `[apps/web/app/components/posthog-provider.tsx](apps/web/app/components/posthog-provider.tsx)`:
- Accept `anonymousId` prop
- Initialize `posthog-js` with:
```typescript
bootstrap: {
distinctID: anonymousId,
isIdentifiedID: false,
}
```
That makes the browser use the same install identity instead of minting a separate in-memory one.
### 4. Keep feedback and chat APIs on the same identity
In `[apps/web/app/components/chat-message.tsx](apps/web/app/components/chat-message.tsx)`:
- Keep passing `posthog.get_distinct_id?.()` to `/api/feedback`
- After browser bootstrapping is fixed, this value will equal the persisted install ID
In `[apps/web/app/api/chat/route.ts](apps/web/app/api/chat/route.ts)`:
- Optionally accept `distinctId` from the client and pass it into `trackServer()`
- But even if omitted, the fallback should now be the persisted install ID rather than a random UUID
### 5. Expose the identity in a controlled way for debugging
Add a CLI status line in `[src/cli/program/register.telemetry.ts](src/cli/program/register.telemetry.ts)`:
- `Anonymous install ID: <uuid>`
This helps support/debugging without exposing anything sensitive.
## Files to change
- `[src/telemetry/config.ts](src/telemetry/config.ts)`
- add `anonymousId`
- add `getOrCreateAnonymousId()`
- `[src/telemetry/telemetry.ts](src/telemetry/telemetry.ts)`
- switch CLI telemetry to persisted install ID
- `[apps/web/lib/telemetry.ts](apps/web/lib/telemetry.ts)`
- switch server telemetry fallback from `randomUUID()` to persisted install ID
- `[apps/web/app/layout.tsx](apps/web/app/layout.tsx)`
- load anonymous ID and pass to provider
- `[apps/web/app/components/posthog-provider.tsx](apps/web/app/components/posthog-provider.tsx)`
- bootstrap browser PostHog with shared distinct ID
- `[extensions/posthog-analytics/lib/event-mappers.ts](extensions/posthog-analytics/lib/event-mappers.ts)`
- switch plugin telemetry identity to persisted install ID
- `[src/cli/program/register.telemetry.ts](src/cli/program/register.telemetry.ts)`
- show anonymous install ID in status output
- `[TELEMETRY.md](TELEMETRY.md)`
- document the new identity model
## Tests to add
Using the `test-writer` approach, focus on behavior and invariants, not defaults.
### `src/telemetry/config.test.ts`
Protect these invariants:
- generates an install ID once and persists it (prevents identity churn across restarts)
- reuses the same install ID on subsequent reads (keeps PostHog person stable)
- repairs missing/invalid config by generating a valid ID (self-heals corrupted state)
- preserves existing `enabled` / `privacyMode` flags when adding the ID
### `src/telemetry/telemetry.test.ts`
Protect:
- CLI telemetry uses persisted install ID instead of machine hash
- telemetry does not emit when disabled
### `apps/web/lib/telemetry.test.ts`
Protect:
- server telemetry falls back to persisted install ID instead of random UUID
- explicit `distinctId` override still wins when provided
### `apps/web/app/components/posthog-provider.test.tsx`
Protect:
- browser initializes with bootstrap distinct ID from server prop
- pageview capture still works with bootstrapped identity
### `extensions/posthog-analytics` behavior tests
Protect:
- plugin emits `$ai_*` events with the same distinct ID used by CLI/web
- privacy mode changes content redaction but does not change identity
## Expected outcome
After this change:
- one DenchClaw install maps to one PostHog distinct ID
- CLI events, web server events, browser events, feedback events, and OpenClaw plugin `$ai_*` events all roll up under the same person
- users no longer appear fragmented across browser/server/plugin telemetry
- the identity works for first-run `npx denchclaw` because it is created by the CLI/bootstrap path before the browser exists

View File

@ -34,6 +34,8 @@ function PageviewTracker() {
useEffect(() => {
if (!initialized) return;
const wsPath = searchParams?.get("path") ?? "";
if (wsPath.startsWith("~cron")) return;
const url = pathname + (searchParams?.toString() ? `?${searchParams.toString()}` : "");
posthog.capture("$pageview", { $current_url: url });
}, [pathname, searchParams]);

View File

@ -9,6 +9,12 @@ vi.mock("node:child_process", async (importOriginal) => {
spawn: vi.fn(),
};
});
vi.mock("./workspace", () => ({
resolveActiveAgentId: () => "main",
getEffectiveProfile: () => undefined,
resolveWorkspaceRoot: () => undefined,
resolveOpenClawStateDir: () => "/tmp/__agent_runner_test_state",
}));
const spawnMock = vi.mocked(spawn);
// Valid client IDs the Gateway accepts (from ui/src/ui/contracts/gateway-client-info.ts).

File diff suppressed because one or more lines are too long

View File

@ -11,6 +11,8 @@ All structured data lives in **DuckDB**. The primary database is `{{WORKSPACE_PA
All actions should look into / edit and work on `{{WORKSPACE_PATH}}/**` by default unless told otherwise. Exceptions to this are the `SOUL.md`, `skills/`, `memory/`, `USER.md`, `IDENTITY.md`, `TOOLS.md`, `AGENTS.md` and `MEMORY.md` and other such files.
All your workspace chats and past conversations are stored in `{{WORKSPACE_PATH}}/.openclaw/web-chat/`.
## Workspace Structure
```