feat(analytics): add project_id + project_kind to studio/artifact events (#1509)

Product tracking doc 260513 added project_id + project_kind to
studio_view (artifact), studio_click (share_option), and
artifact_export_result. The Studio funnel can now group by project
type without joining run_created on the back end.

- contracts: 3 props gain required project_id + project_kind
- ProjectView → FileWorkspace → FileViewer: thread projectKind down,
  converting metadata.kind via projectKindToTracking once at the top
- FileViewer + HtmlViewer: populate the three call sites
This commit is contained in:
lefarcen 2026-05-13 12:13:55 +08:00 committed by GitHub
parent c16297f10c
commit dc7791ef9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 65 additions and 14 deletions

View file

@ -4,6 +4,7 @@ import { APP_CHROME_FILE_ACTIONS_ID } from './AppChromeHeader';
import {
anonymizeArtifactId,
artifactKindToTracking,
type TrackingProjectKind,
} from '@open-design/contracts/analytics';
import { useAnalytics } from '../analytics/provider';
import {
@ -486,6 +487,7 @@ function setSlideStateCached(key: string, state: SlideState) {
interface Props {
projectId: string;
projectKind: TrackingProjectKind;
file: ProjectFile;
liveHtml?: string;
isDeck?: boolean;
@ -500,6 +502,7 @@ interface Props {
export function FileViewer({
projectId,
projectKind,
file,
liveHtml,
isDeck,
@ -536,13 +539,16 @@ export function FileViewer({
rendererId: rendererMatch?.renderer.id ?? null,
fileKind: file.kind ?? null,
}),
project_id: projectId,
project_kind: projectKind,
});
}, [projectId, file.name, file.kind, rendererMatch?.renderer.id, analytics.track]);
}, [projectId, projectKind, file.name, file.kind, rendererMatch?.renderer.id, analytics.track]);
if (rendererMatch?.renderer.id === 'html' || rendererMatch?.renderer.id === 'deck-html') {
return (
<HtmlViewer
projectId={projectId}
projectKind={projectKind}
file={file}
liveHtml={liveHtml}
isDeck={rendererMatch.renderer.id === 'deck-html'}
@ -3366,6 +3372,7 @@ function DocumentPreviewViewer({
function HtmlViewer({
projectId,
projectKind,
file,
liveHtml,
isDeck,
@ -3378,6 +3385,7 @@ function HtmlViewer({
onFileSaved,
}: {
projectId: string;
projectKind: TrackingProjectKind;
file: ProjectFile;
liveHtml?: string;
isDeck: boolean;
@ -3420,6 +3428,8 @@ function HtmlViewer({
action: 'select_share_option',
share_context: 'artifact',
export_format: format,
project_id: projectId,
project_kind: projectKind,
},
{ requestId },
);
@ -3432,6 +3442,7 @@ function HtmlViewer({
area: 'app_header',
artifact_id: artifactId,
project_id: projectId,
project_kind: projectKind,
export_format: format,
result,
...(errorCode ? { error_code: errorCode } : {}),

View file

@ -5,6 +5,7 @@ import {
useState,
type DragEvent as ReactDragEvent,
} from 'react';
import type { TrackingProjectKind } from '@open-design/contracts/analytics';
import { useT } from '../i18n';
import { isMacPlatform } from '../utils/platform';
import {
@ -42,6 +43,7 @@ import {
interface Props {
projectId: string;
projectKind: TrackingProjectKind;
files: ProjectFile[];
liveArtifacts: LiveArtifactSummary[];
onRefreshFiles: () => Promise<void> | void;
@ -78,6 +80,7 @@ type TabDropEdge = 'before' | 'after';
export function FileWorkspace({
projectId,
projectKind,
files,
liveArtifacts,
onRefreshFiles,
@ -784,6 +787,7 @@ export function FileWorkspace({
) : activeFile ? (
<FileViewer
projectId={projectId}
projectKind={projectKind}
file={activeFile}
isDeck={isDeck}
onExportAsPptx={onExportAsPptx}

View file

@ -37,6 +37,7 @@ import {
type MemorySystemPromptResponse,
type ResearchOptions,
} from '@open-design/contracts';
import { projectKindToTracking } from '@open-design/contracts/analytics';
import { navigate } from '../router';
import { agentDisplayName, agentModelDisplayName } from '../utils/agentLabels';
import { isMacPlatform } from '../utils/platform';
@ -2467,6 +2468,7 @@ export function ProjectView({
) : null}
<FileWorkspace
projectId={project.id}
projectKind={projectKindToTracking(project.metadata?.kind) ?? 'prototype'}
files={projectFiles}
liveArtifacts={liveArtifacts}
onRefreshFiles={() => {

View file

@ -76,6 +76,7 @@ describe('FileViewer Inspect/Picker empty-annotation hint (#890)', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlFile()}
liveHtml="<html><body><h1>Plain PRD with no data-od-id</h1></body></html>"
/>,
@ -107,6 +108,7 @@ describe('FileViewer Inspect/Picker empty-annotation hint (#890)', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlFile()}
liveHtml="<html><body><main data-od-id='hero'>Hero</main></body></html>"
/>,
@ -132,6 +134,7 @@ describe('FileViewer Inspect/Picker empty-annotation hint (#890)', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlFile()}
liveHtml="<html><body><h1>No annotations</h1></body></html>"
/>,

View file

@ -88,7 +88,7 @@ describe('FileViewer JSON artifacts', () => {
return new Response('', { status: 404 });
}));
const { container } = render(<FileViewer projectId="project-1" file={file} />);
const { container } = render(<FileViewer projectId="project-1" projectKind="prototype" file={file} />);
await waitFor(() => {
expect(container.querySelector('.lines')?.textContent).toBe(
@ -113,7 +113,7 @@ describe('FileViewer JSON artifacts', () => {
return new Response('', { status: 404 });
}));
const { container } = render(<FileViewer projectId="project-1" file={file} />);
const { container } = render(<FileViewer projectId="project-1" projectKind="prototype" file={file} />);
await waitFor(() => {
const displayedText = container.querySelector('.lines')?.textContent ?? '';
@ -139,7 +139,7 @@ describe('FileViewer JSON artifacts', () => {
return new Response('', { status: 404 });
}));
const { container } = render(<FileViewer projectId="project-1" file={file} />);
const { container } = render(<FileViewer projectId="project-1" projectKind="prototype" file={file} />);
await waitFor(() => {
const displayedText = container.querySelector('.lines')?.textContent ?? '';
@ -165,7 +165,7 @@ describe('FileViewer JSON artifacts', () => {
return new Response('', { status: 404 });
}));
const { container } = render(<FileViewer projectId="project-1" file={file} />);
const { container } = render(<FileViewer projectId="project-1" projectKind="prototype" file={file} />);
await waitFor(() => {
const displayedText = container.querySelector('.lines')?.textContent ?? '';
@ -191,7 +191,7 @@ describe('FileViewer JSON artifacts', () => {
return new Response('', { status: 404 });
}));
const { container } = render(<FileViewer projectId="project-1" file={file} />);
const { container } = render(<FileViewer projectId="project-1" projectKind="prototype" file={file} />);
await waitFor(() => {
const displayedText = container.querySelector('.lines')?.textContent ?? '';
@ -218,7 +218,7 @@ describe('FileViewer SVG artifacts', () => {
},
});
const markup = renderToStaticMarkup(<FileViewer projectId="project-1" file={file} />);
const markup = renderToStaticMarkup(<FileViewer projectId="project-1" projectKind="prototype" file={file} />);
expect(markup).toContain('class="viewer svg-viewer"');
expect(markup).not.toContain('class="viewer image-viewer"');
@ -230,7 +230,7 @@ describe('FileViewer SVG artifacts', () => {
it('keeps normal image artifacts on the existing image viewer path', () => {
const file = baseFile({ name: 'photo.png', path: 'photo.png' });
const markup = renderToStaticMarkup(<FileViewer projectId="project-1" file={file} />);
const markup = renderToStaticMarkup(<FileViewer projectId="project-1" projectKind="prototype" file={file} />);
expect(markup).toContain('class="viewer image-viewer"');
expect(markup).not.toContain('class="viewer svg-viewer"');
@ -263,7 +263,7 @@ describe('FileViewer SVG artifacts', () => {
}));
vi.stubGlobal('fetch', fetchMock);
const { container } = render(<FileViewer projectId="project-1" file={file} />);
const { container } = render(<FileViewer projectId="project-1" projectKind="prototype" file={file} />);
await waitFor(() => {
expect(container.querySelector('[data-testid="sketch-preview-svg"]')).toBeTruthy();
@ -298,7 +298,7 @@ describe('FileViewer SVG artifacts', () => {
}));
vi.stubGlobal('fetch', fetchMock);
const { container } = render(<FileViewer projectId="project-1" file={file} />);
const { container } = render(<FileViewer projectId="project-1" projectKind="prototype" file={file} />);
await waitFor(() => {
const svg = container.querySelector<SVGSVGElement>('[data-testid="sketch-preview-svg"] svg');
@ -349,7 +349,7 @@ describe('FileViewer SVG artifacts', () => {
});
const markup = renderToStaticMarkup(
<FileViewer projectId="project-1" file={file} liveHtml="<html><body>hi</body></html>" />,
<FileViewer projectId="project-1" projectKind="prototype" file={file} liveHtml="<html><body>hi</body></html>" />,
);
expect(markup).toContain('data-testid="artifact-preview-frame"');
@ -377,6 +377,7 @@ describe('FileViewer SVG artifacts', () => {
const markup = renderToStaticMarkup(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
isDeck
liveHtml={'<html><body><section class="slide">one</section></body></html>'}
@ -407,6 +408,7 @@ describe('FileViewer SVG artifacts', () => {
const markup = renderToStaticMarkup(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
liveHtml={'<html><body><section class="slide">one</section><section class="slide">two</section></body></html>'}
/>,
@ -435,6 +437,7 @@ describe('FileViewer SVG artifacts', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
liveHtml="<html><body><h1>Hello</h1></body></html>"
/>,
@ -517,6 +520,7 @@ describe('FileViewer SVG artifacts', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
liveHtml="<html><body><h1>Hello</h1></body></html>"
/>,
@ -581,6 +585,7 @@ describe('FileViewer SVG artifacts', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
liveHtml="<html><body><h1>Hello</h1></body></html>"
/>,
@ -709,6 +714,7 @@ describe('FileViewer SVG artifacts', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
liveHtml="<html><body><h1>Hello</h1></body></html>"
/>,
@ -802,6 +808,7 @@ describe('FileViewer SVG artifacts', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
liveHtml="<html><body><h1>Hello</h1></body></html>"
/>,
@ -851,6 +858,7 @@ describe('FileViewer SVG artifacts', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
liveHtml="<html><body><h1>Hello</h1></body></html>"
/>,
@ -915,6 +923,7 @@ describe('FileViewer SVG artifacts', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={file}
liveHtml="<html><body><h1>Hello</h1></body></html>"
/>,
@ -963,6 +972,7 @@ describe('FileViewer comment picker and tweaks mode', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlPreviewFile()}
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
/>,
@ -988,6 +998,7 @@ describe('FileViewer comment picker and tweaks mode', () => {
render(
<FileViewer
projectId="project-1"
projectKind="prototype"
file={htmlPreviewFile()}
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
/>,

View file

@ -138,6 +138,7 @@ describe('FileWorkspace upload input', () => {
const markup = renderToStaticMarkup(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[]}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}
@ -157,6 +158,7 @@ describe('FileWorkspace upload input', () => {
render(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[baseFile()]}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}
@ -214,6 +216,7 @@ describe('FileWorkspace upload input', () => {
render(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[baseFile({ name: 'uploaded.png', path: 'uploaded.png' })]}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}
@ -243,6 +246,7 @@ describe('FileWorkspace upload input', () => {
const markup = renderToStaticMarkup(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[]}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}
@ -263,6 +267,7 @@ describe('FileWorkspace upload input', () => {
const markup = renderToStaticMarkup(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[]}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}
@ -287,6 +292,7 @@ describe('FileWorkspace upload input', () => {
const markup = renderToStaticMarkup(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[]}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}
@ -325,6 +331,7 @@ describe('FileWorkspace design file rename', () => {
const container = renderWorkspace(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[workspaceFile('paste-1.txt'), workspaceFile('index.html')]}
liveArtifacts={[]}
onRefreshFiles={onRefreshFiles}
@ -387,6 +394,7 @@ describe('FileWorkspace design file rename', () => {
const container = renderWorkspace(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[workspaceFile('paste-1.txt')]}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}
@ -484,6 +492,7 @@ describe('FileWorkspace sketch round-trip', () => {
render(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[
baseFile({
name: 'diagram.sketch.json',
@ -577,6 +586,7 @@ describe('FileWorkspace sketch round-trip', () => {
render(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[
baseFile({
name: 'diagram.sketch.json',
@ -681,6 +691,7 @@ describe('FileWorkspace sketch round-trip', () => {
render(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[
baseFile({
name: 'diagram.sketch.json',
@ -749,6 +760,7 @@ describe('FileWorkspace tab reordering', () => {
const container = renderWorkspace(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[
workspaceFile('analysis.html'),
workspaceFile('notes.md'),
@ -788,6 +800,7 @@ describe('FileWorkspace tab reordering', () => {
const container = renderWorkspace(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[
workspaceFile('analysis.html'),
workspaceFile('notes.md'),
@ -826,6 +839,7 @@ describe('FileWorkspace tab reordering', () => {
const container = renderWorkspace(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[workspaceFile('analysis.html'), workspaceFile('notes.md')]}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}
@ -854,6 +868,7 @@ describe('FileWorkspace tab reordering', () => {
const container = renderWorkspace(
<FileWorkspace
projectId="project-1"
projectKind="prototype"
files={[workspaceFile('analysis.html'), workspaceFile('notes.md')]}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}

View file

@ -73,7 +73,7 @@ describe('FileViewer markdown code block copy', () => {
});
it('copies fenced code blocks from the markdown preview', async () => {
const { container } = render(<FileViewer projectId="project-1" file={baseFile()} />);
const { container } = render(<FileViewer projectId="project-1" projectKind="prototype" file={baseFile()} />);
await waitFor(() => {
expect(container.querySelector('.markdown-code-copy')).toBeTruthy();
@ -97,7 +97,7 @@ describe('FileViewer markdown code block copy', () => {
it('copies empty fenced code blocks instead of treating the button as broken', async () => {
mockedFetchProjectFileText.mockResolvedValue('```ts\n```');
const { container } = render(<FileViewer projectId="project-1" file={baseFile()} />);
const { container } = render(<FileViewer projectId="project-1" projectKind="prototype" file={baseFile()} />);
await waitFor(() => {
expect(container.querySelector('.markdown-code-copy')).toBeTruthy();
@ -117,7 +117,7 @@ describe('FileViewer markdown code block copy', () => {
value: vi.fn().mockReturnValue(true),
});
const execCommandSpy = vi.mocked(document.execCommand);
const { container } = render(<FileViewer projectId="project-1" file={baseFile()} />);
const { container } = render(<FileViewer projectId="project-1" projectKind="prototype" file={baseFile()} />);
await waitFor(() => {
expect(container.querySelector('.markdown-code-copy')).toBeTruthy();

View file

@ -340,6 +340,8 @@ export interface StudioViewArtifactProps {
// never the raw filename.
artifact_id: string;
artifact_kind: TrackingArtifactKind;
project_id: string;
project_kind: TrackingProjectKind;
}
export interface StudioClickShareOptionProps {
@ -350,6 +352,8 @@ export interface StudioClickShareOptionProps {
action: 'select_share_option';
share_context: 'artifact';
export_format: TrackingExportFormat;
project_id: string;
project_kind: TrackingProjectKind;
}
export interface ArtifactExportResultProps {
@ -357,6 +361,7 @@ export interface ArtifactExportResultProps {
area: 'app_header';
artifact_id: string;
project_id: string;
project_kind: TrackingProjectKind;
export_format: TrackingExportFormat;
result: TrackingExportResult;
error_code?: string;