From 31700c046c3bc21018ef12efe663ad096f0c5986 Mon Sep 17 00:00:00 2001 From: lefarcen <935902669@qq.com> Date: Fri, 22 May 2026 19:04:12 +0800 Subject: [PATCH] fix(analytics): fill missing DS event fields + submit_revision + studio apply (#2717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four follow-up gaps surfaced from comparing the v2 spec field lists against live PostHog data on a DS-create + DS-detail walkthrough: 1. **`design_system_apply_result` had no `page_name=studio` carrier.** PR #2706 wired the home variant (NewProjectPanel picker) but the in-project header picker (`ProjectDesignSystemPicker`, lives in ProjectView chrome) PATCHed `project.designSystemId` without emitting an apply row. The funnel only saw applies from the home-side, not from running projects switching their DS. Fix: `ProjectView.handleChangeDesignSystemId` emits `design_system_apply_result` with `page_name=studio`, `action=select_design_system` or `clear_selection`, `design_system_selection_mode=manual`, and the DS's origin/status mapped from the picker's `DesignSystemSummary`. 2. **`design_system_create_result.project_id` was always missing.** The contract had `project_id` optional but the emit site never populated it, even though a successful generate path opens a workspace project (`project.id` is in scope right after `ensureDesignSystemWorkspace`). `created_as_project: true` rows carried no project id at all. Fix: `emitCreateResult` signature gains a `projectId` parameter; the success branch passes `project.id`, failure branches pass `undefined`. 3. **`design_system_review_result` only emitted on Looks good / Needs work clicks (`review_action: looks_good|needs_work`).** The spec also defines `submit_revision` and `regenerate` actions. When a user clicks Needs work → types feedback → sends, the send IS the `submit_revision` lifecycle moment, but no row fired. Dashboards couldn't separate "picked Needs work but never sent" from "picked Needs work and dispatched a revision request". Fix: `sendProjectChatMessage` checks for `feedbackSection` (set by the Needs work onClick) and emits `review_action: submit_revision`, `result: submitted`, with the section slug and the feedback length bucket. `regenerate` stays out of scope — the current UI has no explicit regenerate button; the Generate path always creates a fresh DS. 4. **`page_view{page_name=design_system_project}` carried no `project_id`.** Both the generation-active and the design_system_preview emissions had `system.id` but not the workspace project id, so the funnel couldn't join the DS detail page_view to the project the user actually edits inside. Fix: both `trackPageView` calls pass `workspaceProjectId`; the useEffect deps add `workspaceProjectId` so a delayed ensure (workspace mounts after first render) re-emits with the now- known id. Validation: pnpm --filter @open-design/web typecheck clean pnpm --filter @open-design/web test 1844/1844 --- apps/web/src/components/DesignSystemFlow.tsx | 42 ++++++++++-- apps/web/src/components/ProjectView.tsx | 69 +++++++++++++++++++- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/DesignSystemFlow.tsx b/apps/web/src/components/DesignSystemFlow.tsx index 43c399f1a9..27f5cb0366 100644 --- a/apps/web/src/components/DesignSystemFlow.tsx +++ b/apps/web/src/components/DesignSystemFlow.tsx @@ -500,7 +500,8 @@ export function DesignSystemCreationFlow({ function emitCreateResult( result: 'success' | 'failed' | 'cancelled', designSystemId: string | undefined, - errorCode?: string, + errorCode: string | undefined, + projectId: string | undefined, ) { trackDesignSystemCreateResult(analytics.track, { page_name: 'design_systems', @@ -508,6 +509,7 @@ export function DesignSystemCreationFlow({ entry_from: createEntryFrom, result, design_system_id: designSystemId, + project_id: projectId, design_system_source: designSystemOrigin, source_count: snapshot.sourceCount, created_as_project: result === 'success', @@ -533,7 +535,7 @@ export function DesignSystemCreationFlow({ if (!created) { setError('Could not generate this design system.'); setStep('setup'); - emitCreateResult('failed', undefined, 'DS_DRAFT_CREATE_FAILED'); + emitCreateResult('failed', undefined, 'DS_DRAFT_CREATE_FAILED', undefined); onGenerateSettled?.(snapshot, { result: 'failed', errorCode: 'DS_DRAFT_CREATE_FAILED', @@ -544,7 +546,7 @@ export function DesignSystemCreationFlow({ if (!workspace) { setError('Could not open the design system workspace.'); setStep('setup'); - emitCreateResult('failed', created.id, 'DS_WORKSPACE_OPEN_FAILED'); + emitCreateResult('failed', created.id, 'DS_WORKSPACE_OPEN_FAILED', undefined); onGenerateSettled?.(snapshot, { result: 'failed', errorCode: 'DS_WORKSPACE_OPEN_FAILED', @@ -555,7 +557,7 @@ export function DesignSystemCreationFlow({ const setupState = state; const connector = githubConnector; onCreated(project.id, project); - emitCreateResult('success', created.id); + emitCreateResult('success', created.id, undefined, project.id); onGenerateSettled?.(snapshot, { result: 'success' }); scheduleAfterProjectHandoff(() => { void prepareCreatedDesignSystemProject({ @@ -576,7 +578,7 @@ export function DesignSystemCreationFlow({ const errorCode = err instanceof Error ? `DS_GENERATE_THREW:${err.message.slice(0, 80)}` : 'DS_GENERATE_THREW'; - emitCreateResult('failed', undefined, errorCode); + emitCreateResult('failed', undefined, errorCode, undefined); onGenerateSettled?.(snapshot, { result: 'failed', errorCode }); } finally { setGenerationStarting(false); @@ -1072,6 +1074,7 @@ export function DesignSystemDetailView({ view_type: 'page', entry_from: entryFrom, design_system_id: system.id, + project_id: workspaceProjectId ?? undefined, // Origin is the DS's provenance-style source. We don't yet // have a precise mapping from `system.source` / provenance // metadata to the v2 enum, so we report `unknown` rather @@ -1099,11 +1102,12 @@ export function DesignSystemDetailView({ view_type: 'page', entry_from: entryFrom, design_system_id: system.id, + project_id: workspaceProjectId ?? undefined, design_system_source: 'unknown', design_system_status: designSystemStatus, }); } - }, [analytics.track, system?.id, generationActive, designSystemStatus, system]); + }, [analytics.track, system?.id, generationActive, designSystemStatus, system, workspaceProjectId]); const introChatMessages = useMemo( () => buildDesignSystemChatMessages({ system, @@ -1303,6 +1307,32 @@ export function DesignSystemDetailView({ setChatError(null); setStatusLine(null); setChatSeed(null); + // `design_system_review_result` with `submit_revision` fires + // once per send that originates from a Needs-work section seed. + // The earlier Looks good / Needs work click emitted + // `result: submitted` with `review_action: looks_good|needs_work` + // — this is the second leg (`action=submit_revision`), recording + // the moment the user actually dispatched a fix request with + // text. Without it the funnel can't separate "user picked Needs + // work but never sent" from "user picked Needs work and sent a + // revision request". + if (feedbackSection && system) { + const slug = designSystemModuleSlug(feedbackSection); + trackDesignSystemReviewResult(analytics.track, { + page_name: 'design_system_project', + area: 'design_system_preview', + review_action: 'submit_revision', + result: 'submitted', + design_system_id: system.id, + project_id: projectId, + module_id: slug, + module_type: designSystemModuleType(slug), + module_index: 0, + feedback_length_bucket: designSystemLengthBucket(rawText), + has_custom_feedback: rawText.length > 0, + duration_ms: 0, + }); + } setFeedbackSection(null); const startedAt = Date.now(); const userMsg: ChatMessage = { diff --git a/apps/web/src/components/ProjectView.tsx b/apps/web/src/components/ProjectView.tsx index 0206fc4089..162207ddf9 100644 --- a/apps/web/src/components/ProjectView.tsx +++ b/apps/web/src/components/ProjectView.tsx @@ -46,8 +46,16 @@ import { type ResearchOptions, } from '@open-design/contracts'; import { projectKindToTracking } from '@open-design/contracts/analytics'; +import type { + TrackingDesignSystemApplyTargetKind, + TrackingDesignSystemOrigin, + TrackingDesignSystemStatusValue, +} from '@open-design/contracts/analytics'; import { useAnalytics } from '../analytics/provider'; -import { trackPageView } from '../analytics/events'; +import { + trackDesignSystemApplyResult, + trackPageView, +} from '../analytics/events'; import { clearOnboardingSessionId, peekOnboardingSessionId, @@ -3294,6 +3302,63 @@ export function ProjectView({ const handleChangeDesignSystemId = useCallback( (nextId: string | null) => { if ((project.designSystemId ?? null) === nextId) return; + // `design_system_apply_result` studio variant. The existing + // NewProjectPanel picker fires the same event under + // `page_name=home`; this in-project header picker fires under + // `page_name=studio` so the funnel sees applies from both + // surfaces. `target_project_kind` derives from + // `project.metadata.kind`. + const target = + (projectKindToTracking(project.metadata?.kind ?? null) ?? 'unknown') as TrackingDesignSystemApplyTargetKind; + const picked = nextId + ? designSystems.find((d) => d.id === nextId) + : null; + const origin: TrackingDesignSystemOrigin | undefined = picked + ? picked.source === 'user' + ? 'manual_create' + : picked.source === 'built-in' + ? 'official_preset' + : picked.source === 'installed' + ? 'template' + : 'unknown' + : undefined; + const status: TrackingDesignSystemStatusValue | undefined = picked + ? picked.status === 'draft' || picked.status === 'published' + ? picked.status + : 'unknown' + : undefined; + if (nextId === null) { + trackDesignSystemApplyResult(analytics.track, { + page_name: 'studio', + area: 'design_system_picker', + action: 'clear_selection', + result: 'success', + target_project_kind: target, + design_system_applied: false, + design_system_selection_mode: 'none', + is_default: false, + is_auto_selected: false, + available_design_system_count: designSystems.length, + duration_ms: 0, + }); + } else { + trackDesignSystemApplyResult(analytics.track, { + page_name: 'studio', + area: 'design_system_picker', + action: 'select_design_system', + result: 'success', + target_project_kind: target, + design_system_id: nextId, + design_system_source: origin, + design_system_status: status, + design_system_applied: true, + design_system_selection_mode: 'manual', + is_default: false, + is_auto_selected: false, + available_design_system_count: designSystems.length, + duration_ms: 0, + }); + } const updated: Project = { ...project, designSystemId: nextId, @@ -3302,7 +3367,7 @@ export function ProjectView({ onProjectChange(updated); void patchProject(project.id, { designSystemId: nextId }); }, - [project, onProjectChange], + [project, onProjectChange, designSystems, analytics.track], ); const handleSaveInstructions = useCallback(async () => {