Skip to content

feat(analytics): track Workspace interactions - #6397

Merged
lefarcen merged 2 commits into
feat/workspace-teamfrom
codex/workspace-team-analytics
Aug 4, 2026
Merged

feat(analytics): track Workspace interactions#6397
lefarcen merged 2 commits into
feat/workspace-teamfrom
codex/workspace-team-analytics

Conversation

@open-design-crew

Copy link
Copy Markdown
Contributor

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:

  • sidebar navigation, Workspace switching, invites, settings, and account-menu actions;
  • project rename, duplicate, team-space transfer, delete, bulk actions, filters, sorting, and view mode;
  • Community template detail, Copy Prompt, and Remix interactions;
  • Design System and plugin/skill actions, categorized by official, personal, or team scope;
  • comment creation on the user's own project versus another member's project;
  • Workspace-scoped grouping through PostHog $groups.workspace.

Surface area

  • UI — new page / dialog / panel / menu item / setting / empty state in apps/web or apps/desktop (including Electron menu bar)
  • Keyboard shortcut — new or changed
  • CLI / env var — new od subcommand or flag, new tools-dev / tools-pack flag, or new OD_* env var
  • API / contract — new /api/* endpoint, new SSE event, or changed shape in packages/contracts
  • Extension point — new entry under skills/, design-systems/, design-templates/, or craft/, or change to the skills protocol
  • i18n keys — added new translation keys (see TRANSLATIONS.md for the locale workflow)
  • New top-level dependency — adding any new entry to the root package.json (dependencies or devDependencies); workspace-package package.json files are out of scope. Include a paragraph on what we get vs. what bytes we ship (see CONTRIBUTING.md → Code style)
  • Default behavior change — changes what existing users experience without opting in (default model, default setting, file/SQLite schema, auto-network on startup, auto-install)
  • None — internal refactor, docs, tests, or translation update only

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.
  • Focused web analytics/component tests — 139 tests passed. The combined parallel run had one Marketplace loading timeout; its isolated rerun passed 9/9.
  • Focused daemon analytics, Workspace context, comments, and project tests — 96 tests passed across 7 files.
  • pnpm typecheck — all affected packages passed; the aggregate command remains red on the unchanged apps/desktop/src/main/index.ts baseline error TS2339: Property 'input' does not exist on type 'DesktopShowMessage'.

@lefarcen
lefarcen requested a review from mrcfps August 4, 2026 04:43
@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 Aug 4, 2026

@mrcfps mrcfps left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines 956 to 965
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +865 to +873
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,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

  1. Here: pass { requestId } into the switcher click track call.
  2. In apps/web/src/analytics/events.ts trackWorkspaceSwitcherClick: accept optional TrackOptions and forward them to send, matching trackWorkspaceInviteClick / 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +1574 to +1579
onClick={() => {
trackAccountMenuClick(analytics.track, {
page_name: analyticsPage,
area: 'account_menu',
element: 'settings',
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Visual regression review

Head: 12a64e7 · Base: 00840e9

Baseline unavailable; PR screenshots are new visual cases and need baseline review.

0 changed · 0 unchanged · 47 new without baseline · 0 failed

New cases without baselines

PR PR PR
visual-avatar-local-agent-list
pr
visual-avatar-local-agent-list-panel
pr
visual-avatar-menu
pr
visual-avatar-menu-panel
pr
visual-avatar-open-design-model-picker
pr
visual-critical-settings
pr
visual-critical-workspace
pr
visual-critical-workspace-preview
pr
visual-design-system-detail
pr
visual-design-systems
pr
visual-home
pr
visual-home-catalog
pr
visual-home-context-picker
pr
visual-home-context-picker-popover
pr
visual-home-plugin-filter
pr
visual-home-plugin-use-staged
pr
visual-home-plugin-use-with-query
pr
visual-home-staged-attachment
pr
visual-integrations-use-everywhere
pr
visual-new-project-modal
pr

27 additional new case(s) omitted from this comment.

Visual diff is advisory only and does not block merging.

@lefarcen
lefarcen requested a review from mrcfps August 4, 2026 06:08

@mrcfps mrcfps left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +1067 to +1070
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]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@lefarcen
lefarcen merged commit 9d3928c into feat/workspace-team Aug 4, 2026
20 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.

2 participants