mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
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:
@@ -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 = {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user