feat(metrics): compiles and runs remember which gallery example they came from

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.
This commit is contained in:
David Montero Crespo 2026-08-09 22:34:26 +02:00
parent 27f712444b
commit c0d3543573
6 changed files with 34 additions and 5 deletions

View File

@ -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 []),
},

View File

@ -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);

View File

@ -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,

View File

@ -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;

View File

@ -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<ProjectState>((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;

View File

@ -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).