From c0d3543573d5b9b5194e082a9ee3eae8815de697 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Sun, 9 Aug 2026 22:34:26 +0200 Subject: [PATCH] feat(metrics): compiles and runs remember which gallery example they came from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useProjectStore.currentExampleId — stamped by loadExample, dropped the moment a real project (or a blank workspace) takes over. The compile request and the run event carry it as example_id, analytics only, so 'which examples get compiled and run most' becomes answerable. --- backend/app/api/routes/compile.py | 9 +++++++-- .../src/components/editor/EditorToolbar.tsx | 4 +++- frontend/src/services/compilation.ts | 4 ++++ frontend/src/services/metricsService.ts | 2 ++ frontend/src/store/useProjectStore.ts | 18 ++++++++++++++++-- frontend/src/utils/loadExample.ts | 2 ++ 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/backend/app/api/routes/compile.py b/backend/app/api/routes/compile.py index c7265c50..7328478d 100644 --- a/backend/app/api/routes/compile.py +++ b/backend/app/api/routes/compile.py @@ -168,6 +168,9 @@ class CompileRequest(BaseModel): # RP2350 boards all compile as rpipico2), and only the kind can tell # them apart. Rides into the metric hook's `extra`. board_kind: str | None = None + # Optional: gallery example the workspace was loaded from. Analytics + # only ("which examples get compiled most"); rides in `extra` too. + example_id: str | None = None # Per-board ESP32 build options (Partition Scheme, CPU Freq, Flash Mode, # PSRAM, etc.). Loose dict so the frontend can add fields without a # backend deploy — espidf_compiler.compile validates known keys and @@ -520,6 +523,7 @@ async def _compile_job( "async": True, "initiated_by": request.initiated_by, "board_kind": request.board_kind, + "example_id": request.example_id, "partition_scheme": (request.board_options or {}).get("partitionScheme"), "spiffs_file_count": len(request.spiffs_files or []), }, @@ -541,7 +545,7 @@ async def _compile_job( success=False, duration_ms=int((time.monotonic() - started) * 1000), error_kind="exception", - extra={"file_count": len(files), "exception": str(exc)[:200], "async": True, "initiated_by": request.initiated_by, "board_kind": request.board_kind}, + extra={"file_count": len(files), "exception": str(exc)[:200], "async": True, "initiated_by": request.initiated_by, "board_kind": request.board_kind, "example_id": request.example_id}, ) @@ -574,7 +578,7 @@ async def compile_sketch( success=False, duration_ms=int((time.monotonic() - started) * 1000), error_kind="exception", - extra={"file_count": len(files), "exception": str(e)[:200], "initiated_by": request.initiated_by, "board_kind": request.board_kind}, + extra={"file_count": len(files), "exception": str(e)[:200], "initiated_by": request.initiated_by, "board_kind": request.board_kind, "example_id": request.example_id}, request=http_request, ) raise HTTPException(status_code=500, detail=str(e)) @@ -592,6 +596,7 @@ async def compile_sketch( "has_wifi": response.has_wifi, "initiated_by": request.initiated_by, "board_kind": request.board_kind, + "example_id": request.example_id, "partition_scheme": (request.board_options or {}).get("partitionScheme"), "spiffs_file_count": len(request.spiffs_files or []), }, diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index a27569d3..ff771bc4 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -235,6 +235,7 @@ export const EditorToolbar = ({ project_id: currentProject?.id ?? null, board_fqbn: fqbn ?? null, board_kind: boardKind ?? null, + example_id: useProjectStore.getState().currentExampleId, engine: engine ?? null, }); }, @@ -573,6 +574,7 @@ export const EditorToolbar = ({ boardOptions: activeBoard?.boardOptions, spiffsFiles: activeBoard?.spiffsFiles, boardKind: kind ?? undefined, + exampleId: useProjectStore.getState().currentExampleId, // P2.4 — THIS board's declared manifest (compile scope). Per-board so // two boards can use different libraries without clashing. libraries: activeBoard?.libraries?.length ? activeBoard.libraries : null, @@ -1079,7 +1081,7 @@ export const EditorToolbar = ({ })), ]); }, - { boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles, boardKind: board.boardKind, libraries: board.libraries?.length ? board.libraries : null, language: board.languageMode === 'espidf' ? 'espidf' : undefined }, + { boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles, boardKind: board.boardKind, exampleId: useProjectStore.getState().currentExampleId, libraries: board.libraries?.length ? board.libraries : null, language: board.languageMode === 'espidf' ? 'espidf' : undefined }, ); const resultLogs = parseCompileResult(result, label, boardTarget, lastStreamedLen > 0); diff --git a/frontend/src/services/compilation.ts b/frontend/src/services/compilation.ts index 27f9f032..e3768f81 100644 --- a/frontend/src/services/compilation.ts +++ b/frontend/src/services/compilation.ts @@ -31,6 +31,9 @@ export interface CompileExtras { // FQBN (Pimoroni RP2350 boards all compile as rpipico2); the kind is the // only identifier that tells them apart in the metrics. boardKind?: string; + // Gallery example the workspace was loaded from — analytics only + // ("which examples get compiled most"). Null/omitted outside examples. + exampleId?: string | null; } export interface CompileResult { @@ -127,6 +130,7 @@ export async function compileCode( board_options, spiffs_files, board_kind: extras?.boardKind ?? null, + example_id: extras?.exampleId ?? null, libraries, language: extras?.language ?? null, initiated_by: extras?.initiatedBy ?? null, diff --git a/frontend/src/services/metricsService.ts b/frontend/src/services/metricsService.ts index bb162387..90401836 100644 --- a/frontend/src/services/metricsService.ts +++ b/frontend/src/services/metricsService.ts @@ -14,6 +14,8 @@ export interface RunEventPayload { board_fqbn?: string | null; /** Editor BoardKind — disambiguates boards that share an FQBN (analytics). */ board_kind?: string | null; + /** Gallery example the workspace was loaded from (analytics). */ + example_id?: string | null; /** Which engine took this run — 'instant' (browser) or 'linux' (guest). * Absent for boards where the question doesn't apply. */ engine?: 'instant' | 'linux' | null; diff --git a/frontend/src/store/useProjectStore.ts b/frontend/src/store/useProjectStore.ts index 66fc9f7a..18b8f3e2 100644 --- a/frontend/src/store/useProjectStore.ts +++ b/frontend/src/store/useProjectStore.ts @@ -13,8 +13,16 @@ interface CurrentProject { interface ProjectState { currentProject: CurrentProject | null; + /** + * Gallery example currently loaded in the editor (null when the workspace + * came from a project, a file or scratch). Set by loadExample, cleared + * whenever a project takes over — analytics only: compiles/runs stamp it + * so "which examples get compiled most" is answerable. + */ + currentExampleId: string | null; setCurrentProject: (project: CurrentProject) => void; clearCurrentProject: () => void; + setCurrentExampleId: (id: string | null) => void; // Updated to accept either the legacy boolean OR the new enum so the // ShareModal callsite and any older callers keep working uniformly. setVisibility: (next: boolean | ProjectVisibility) => void; @@ -22,8 +30,14 @@ interface ProjectState { export const useProjectStore = create((set) => ({ currentProject: null, - setCurrentProject: (project) => set({ currentProject: project }), - clearCurrentProject: () => set({ currentProject: null }), + currentExampleId: null, + // Loading a real project supersedes the example context. + setCurrentProject: (project) => set({ currentProject: project, currentExampleId: null }), + // Also drops the example context: every caller (new project, blank + // workspace, logout cleanup) means "this workspace is no longer that". + // loadExample re-stamps its id right after calling this. + clearCurrentProject: () => set({ currentProject: null, currentExampleId: null }), + setCurrentExampleId: (id) => set({ currentExampleId: id }), setVisibility: (next) => set((s) => { if (!s.currentProject) return s; diff --git a/frontend/src/utils/loadExample.ts b/frontend/src/utils/loadExample.ts index e81eb209..d09564c0 100644 --- a/frontend/src/utils/loadExample.ts +++ b/frontend/src/utils/loadExample.ts @@ -114,6 +114,8 @@ export async function loadExample( // sees null as projectId during every subsequent simulator/editor change, // so no PUT goes out. useProjectStore.getState().clearCurrentProject(); + // Analytics context: compiles/runs from this workspace stamp the example id. + useProjectStore.getState().setCurrentExampleId(example.id); // P2.4 — this example's declared manifest (compile scope) is assigned to each // board it creates at the END of this function (the boards don't exist yet).