feat(analytics): track Workspace interactions - #6397
Conversation
mrcfps
left a comment
There was a problem hiding this comment.
Thanks @app/open-design-crew — this is a thorough Workspace analytics pass: privacy-safe dimensions, consent-gated group identify, daemon-side comment outcomes, and solid component coverage with focused tests. 🙏
I found a couple of non-blocking telemetry correctness gaps on the main Workspace switch / nav path (details inline). Nothing that should block merge on product safety, but fixing them will keep the new funnels trustworthy.
🔁 Powered by Looper · runner=reviewer · agent=grok-build · An autonomous AI dev team for your GitHub repos.
| const selectView = (next: EntryView) => { | ||
| trackEntryNavigationClick(analytics.track, { | ||
| page_name: analyticsPage, | ||
| area: 'entry_nav', | ||
| element: 'nav_item', | ||
| target: entryViewToTracking(next), | ||
| entry_from: 'sidebar', | ||
| ...workspaceDimensions, | ||
| }); | ||
| onViewChange(next); |
There was a problem hiding this comment.
Non-blocking: selectView attributes programmatic navigations as sidebar clicks
selectView always emits ui_click with element: 'nav_item' and entry_from: 'sidebar'. That is correct for the NavButton handlers, but the successful Workspace switch path still ends with the existing selectView('home') call after notifyTeamProjectsChanged().
Every successful switch therefore also records a phantom home nav click, even when the user never touched the Home item. That inflates sidebar navigation volume and makes switch → home look like a deliberate nav click in PostHog.
Suggested fix: split intent from side effects:
function changeView(next: EntryView) {
onViewChange(next);
}
function selectView(next: EntryView) {
trackEntryNavigationClick(analytics.track, {
page_name: analyticsPage,
area: 'entry_nav',
element: 'nav_item',
target: entryViewToTracking(next),
entry_from: 'sidebar',
...workspaceDimensions,
});
changeView(next);
}Use changeView('home') after a successful switch (and any other non-user navigation), and keep selectView only on real rail clicks. A small regression test that switches workspace and asserts no entry_nav / nav_item event was fired would lock this in.
🔁 Powered by Looper · runner=reviewer · agent=grok-build · An autonomous AI dev team for your GitHub repos.
There was a problem hiding this comment.
Follow-up (still present on 682f205)
Confirmed on the latest head: successful Workspace switch still ends in selectView('home'), so every successful switch also emits a synthetic ui_click with element: 'nav_item' / entry_from: 'sidebar' in addition to the switch funnel. The community page-view dedupe commit did not touch this path.
Suggested fix remains: give selectView an optional entry_from (or a separate non-tracking navigation helper) and call selectView('home', { entryFrom: 'workspace_switch' }) / skip tracking for programmatic navigation after switch.
🔁 Powered by Looper · runner=reviewer · agent=grok-build · An autonomous AI dev team for your GitHub repos.
| const requestId = analytics.newRequestId(); | ||
| trackWorkspaceSwitcherClick(analytics.track, { | ||
| page_name: analyticsPage, | ||
| area: 'workspace_switcher', | ||
| element: 'workspace_option', | ||
| target_workspace_type: selected.workspaceType, | ||
| is_current_workspace: false, | ||
| ...workspaceDimensions, | ||
| }); |
There was a problem hiding this comment.
Non-blocking: Workspace switch click/result funnel is not joined by request_id
This block allocates requestId and the success/failure workspace_switch_result events correctly pass { requestId }, but trackWorkspaceSwitcherClick(...) is called without it. Unlike invite (trackWorkspaceInviteClick + trackWorkspaceInviteResult) and shared project open, switch click and switch result cannot be joined in PostHog.
Two small gaps:
- Here: pass
{ requestId }into the switcher click track call. - In
apps/web/src/analytics/events.tstrackWorkspaceSwitcherClick: accept optionalTrackOptionsand forward them tosend, matchingtrackWorkspaceInviteClick/trackProjectCollectionClick.
export function trackWorkspaceSwitcherClick(
track: Track,
props: WorkspaceSwitcherClickProps,
options?: TrackOptions,
): void {
send(track, 'ui_click', props, options);
}
// call site
trackWorkspaceSwitcherClick(analytics.track, { ... }, { requestId });That keeps the same request-scoped funnel shape you already used for invites.
🔁 Powered by Looper · runner=reviewer · agent=grok-build · An autonomous AI dev team for your GitHub repos.
There was a problem hiding this comment.
Follow-up (still present on 682f205)
Still true on this head: requestId is allocated and attached to workspace_switch_result, but trackWorkspaceSwitcherClick(...) is called without { requestId }. trackWorkspaceSwitcherClick also does not accept TrackOptions yet (unlike invite/project collection helpers), so the click→result join cannot be formed.
Suggested fix remains: extend trackWorkspaceSwitcherClick with optional TrackOptions and pass { requestId } from switchWorkspace.
🔁 Powered by Looper · runner=reviewer · agent=grok-build · An autonomous AI dev team for your GitHub repos.
| onClick={() => { | ||
| trackAccountMenuClick(analytics.track, { | ||
| page_name: analyticsPage, | ||
| area: 'account_menu', | ||
| element: 'settings', | ||
| }); |
There was a problem hiding this comment.
Non-blocking: collapsed-rail Settings click drops Workspace dimensions
Other account-menu actions go through trackAccountAction, which spreads workspaceDimensions (workspace_key, role/plan/seat buckets, $groups.workspace). This direct trackAccountMenuClick for the collapsed-rail Settings button only sends page_name / area / element.
Those settings clicks will not segment by Workspace group or plan/seat state, so dashboards built on the new context contract will under-count or mis-attribute this entry point relative to the expanded account menu Settings row.
Suggested fix: reuse the shared helper (and keep parity with the expanded menu):
onClick={() => {
trackAccountAction('settings');
onOpenSettings?.();
}}Or at least spread ...workspaceDimensions into this payload.
🔁 Powered by Looper · runner=reviewer · agent=grok-build · An autonomous AI dev team for your GitHub repos.
There was a problem hiding this comment.
Follow-up (still present on 682f205)
Still true on this head: the collapsed-rail Settings path calls trackAccountMenuClick with only page_name / area / element, while the signed-in account-menu Settings path goes through trackAccountAction('settings') and spreads workspaceDimensions.
Suggested fix remains: route this click through trackAccountAction('settings'), or spread ...workspaceDimensions here so Settings segments match the rest of the account menu.
🔁 Powered by Looper · runner=reviewer · agent=grok-build · An autonomous AI dev team for your GitHub repos.
mrcfps
left a comment
There was a problem hiding this comment.
Thanks @app/open-design-crew — the StrictMode community page_view dedupe is a nice, well-tested follow-up. 🙏
I re-checked this head end-to-end. The three earlier Workspace switcher / nav telemetry gaps are still open on EntryNavRail (replied on those threads). I also found one new non-blocking page-view consistency gap for the drafts / all-projects surfaces added in this PR (inline).
None of these should block merge on product safety; fixing them will keep the new funnels trustworthy in PostHog.
🔁 Powered by Looper · runner=reviewer · agent=grok-build · An autonomous AI dev team for your GitHub repos.
| useEffect(() => { | ||
| if (view === 'drafts') trackPageView(analytics.track, { page_name: 'drafts' }); | ||
| else if (view === 'all-projects') trackPageView(analytics.track, { page_name: 'all_projects' }); | ||
| }, [analytics.track, view]); |
There was a problem hiding this comment.
Non-blocking: drafts / all-projects page_view can double-fire under StrictMode
This PR just taught Community to keep a one-view/one-event contract with a pageViewRecordedRef (plus a StrictMode fixture). The new collection page views in EntryShell fire unconditionally whenever view is drafts or all-projects:
useEffect(() => {
if (view === 'drafts') trackPageView(...);
else if (view === 'all-projects') trackPageView(...);
}, [analytics.track, view]);React StrictMode replays mount/update effects in development, so local validation and any StrictMode-enabled test harness will count two page_view events per visit while production counts one — the same contract break the community fix just closed. Sibling surfaces (PluginsView, DesignSystemsTab) already use a once-per-activation ref guard.
Why it matters: drafts / all-projects visit rates will disagree between local dashboards and production, and any future test that asserts “one exposure per visit” will flake under StrictMode.
Suggested change: mirror the community/plugins pattern with a last-tracked view ref that skips re-emit for the same view, and add a small StrictMode fixture next to the community page-view test:
const lastTrackedCollectionViewRef = useRef<'drafts' | 'all_projects' | null>(null);
useEffect(() => {
const page =
view === 'drafts' ? 'drafts'
: view === 'all-projects' ? 'all_projects'
: null;
if (!page) {
lastTrackedCollectionViewRef.current = null;
return;
}
if (lastTrackedCollectionViewRef.current === page) return;
lastTrackedCollectionViewRef.current = page;
trackPageView(analytics.track, { page_name: page });
}, [analytics.track, view]);🔁 Powered by Looper · runner=reviewer · agent=grok-build · An autonomous AI dev team for your GitHub repos.















Why
The Workspace redesign introduces new navigation, collaboration, and content-management paths that are not visible in the existing PostHog dataset. Product needs to understand whether people discover and complete these new flows without duplicating telemetry for unchanged interactions.
This PR adds analytics only for the modified and incremental Workspace surfaces in OpenDesign. It also establishes a privacy-safe, low-cardinality context contract so Workspace usage can be segmented consistently across PostHog without sending workspace names, member identities, prompts, comments, file paths, full URLs, or raw errors.
OpenDesign owns interaction intent and outcomes that happen inside this product. Cross-product mutations such as checkout completion remain owned by Vela; request and attribution identifiers let the two sides be analyzed together without double-counting.
What users will see
There is no visual or workflow change. Existing Workspace screens behave as before, while PostHog receives incremental measurements for:
$groups.workspace.Surface area
apps/weborapps/desktop(including Electron menu bar)odsubcommand or flag, newtools-dev/tools-packflag, or newOD_*env var/api/*endpoint, new SSE event, or changed shape inpackages/contractsskills/,design-systems/,design-templates/, orcraft/, or change to the skills protocolTRANSLATIONS.mdfor the locale workflow)package.json(dependenciesordevDependencies); workspace-packagepackage.jsonfiles are out of scope. Include a paragraph on what we get vs. what bytes we ship (seeCONTRIBUTING.md→ Code style)Screenshots
Not applicable: this PR adds telemetry to existing controls and does not change their rendered UI.
Bug fix verification
Not applicable: this is incremental product analytics coverage, not a bug fix.
Validation
pnpm guard— passed.pnpm --filter @open-design/contracts typecheck— passed.pnpm --filter @open-design/web typecheck— passed.pnpm --filter @open-design/daemon typecheck— passed.pnpm typecheck— all affected packages passed; the aggregate command remains red on the unchangedapps/desktop/src/main/index.tsbaseline errorTS2339: Property 'input' does not exist on type 'DesktopShowMessage'.