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:
parent
f279524e32
commit
38b062a71e
1186
.cursor/debug-23c6ec.log
Normal file
1186
.cursor/debug-23c6ec.log
Normal file
File diff suppressed because it is too large
Load Diff
91
.cursor/plans/root_url_refactor_c8937d9e.plan.md
Normal file
91
.cursor/plans/root_url_refactor_c8937d9e.plan.md
Normal 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.
|
||||
|
||||
257
.cursor/plans/unified_telemetry_identity_2d5f5ce0.plan.md
Normal file
257
.cursor/plans/unified_telemetry_identity_2d5f5ce0.plan.md
Normal 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
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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
@ -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
|
||||
|
||||
```
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user