fix(analytics): fill missing DS event fields + submit_revision + studio apply (#2717)

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
This commit is contained in:
lefarcen
2026-05-22 19:04:12 +08:00
committed by GitHub
parent 64f077d366
commit 31700c046c
2 changed files with 103 additions and 8 deletions
+36 -6
View File
@@ -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 = {
+67 -2
View File
@@ -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 () => {