Activity Logging¶
Tracks user-facing actions across Workbench 2 projects and surfaces them in the frontend activity panel (dropdown in the project header).
Architecture¶
Service layer (projects.service, workbench.service, *Guru classes)
│
│ logActivity() / logProjectLifecycleActivity() (fire-and-forget)
▼
ProjectActivitiesService (src/projectActivities/projectActivities.service.ts)
│
▼
projectActivities collection (MongoDB)
│
│ GET /api/projects/:projectId/activities (polled by FE when panel is open)
▼
ProjectActivityPanel (FE dropdown component)
Key Files¶
| File | Purpose |
|---|---|
BE src/shared/sharedTypes.ts |
ProjectActivityAction enum (source of truth) + ProjectActivityType interface |
BE src/projectActivities/projectActivities.service.ts |
logActivity(), logProjectLifecycleActivity() — fire-and-forget helpers |
BE src/models/projectActivities.model.ts |
MongoDB model, queries, indexes |
BE src/workbench/workbench.service.ts |
Logging for workbench operations (scenes, shots, images) |
BE src/projects/projects.service.ts |
Logging for project lifecycle (create, delete, rename, collaborators, settings) |
BE src/agenticGuru/workbenchStoryGuru.ts |
Logging for story guru tool executions |
BE src/agenticGuru/workbenchSceneGuru.ts |
Logging for scene guru tool executions |
BE src/agenticGuru/shotImageEditGuru.ts |
Logging for shot image edit guru |
BE src/projects/projects.service.ts |
Logging for project lifecycle + voice settings (assign/restore voice + variation) |
FE app/_shared/sharedTypes.ts |
Mirror of ProjectActivityAction enum + ProjectActivityType |
FE app/_components/v2/.../projectActivityPanel.tsx |
Dropdown UI with ACTION_LABELS map |
Tests tests/projectActivities/projectActivities.coverage.test.ts |
Integration tests for route-level activity coverage |
Tests tests/projectActivities/projectActivities.guruCoverage.test.ts |
Integration tests for guru tool activity coverage |
Adding a New Activity Action¶
All 5 steps are required. Skipping any step will cause TS compilation errors or missing UI labels.
1. Add enum value — BE src/shared/sharedTypes.ts¶
Add to ProjectActivityAction. Follow VERB_ENTITY naming (see conventions below).
// Example: adding dialogue generation tracking
GENERATE_SHOT_DIALOGUE = 'generate_shot_dialogue',
2. Wire logging in the service method¶
Call logActivity() or logProjectLifecycleActivity() in the relevant service. These are fire-and-forget — they do not block business logic.
For workbench/episode-scoped actions (scenes, shots, images, etc.) — use logActivity() or the helper createStoryActivity() in workbench.service.ts:
this.createStoryActivity(storyId, ProjectActivityAction.GENERATE_SHOT_DIALOGUE, userId, {
sceneId,
shotId,
status: JobStatus.PROCESSING,
job: { jobId: someJob.jobId },
});
For project-lifecycle actions (create, delete, rename, collaborators, settings) — use logProjectLifecycleActivity():
ProjectActivitiesService.logProjectLifecycleActivity({
action: ProjectActivityAction.ADD_PROJECT_COLLABORATOR,
projectId,
projectName: project.projectName,
actorUserId: authUser.userID,
actorEmail: authUser.email,
details: { collaborator: { userId, email, role } },
});
3. Mirror enum in FE — studio-frontend/app/_shared/sharedTypes.ts¶
Add the same enum value to the FE ProjectActivityAction enum. The key name, string value, and position should match the BE enum exactly.
4. Add label in FE — projectActivityPanel.tsx¶
Add a human-readable label to the ACTION_LABELS map:
[ProjectActivityAction.GENERATE_SHOT_DIALOGUE]: 'Shot dialogue generated',
If the action involves image output, also add it to IMAGE_PREVIEW_ACTIONS.
5. Add test case — tests/projectActivities/¶
- Route-triggered actions go in
projectActivities.coverage.test.ts - Guru tool-triggered actions go in
projectActivities.guruCoverage.test.ts
The test should trigger the action and assert the activity was written:
await expectActivity(C.TEST_PROJECT_ID, ProjectActivityAction.GENERATE_SHOT_DIALOGUE);
After all steps, verify:¶
# BE — must be zero errors
cd studio-backend && npx tsc --noEmit
# FE — must have no new errors (pre-existing errors are OK)
cd studio-frontend && npx tsc --noEmit
Conventions¶
Naming: VERB_ENTITY¶
Enum keys use VERB_ENTITY format. The verb comes first, the entity comes last.
- Examples:
GENERATE_SCENES,ADD_SHOT,EDIT_ACT,MERGE_SHOTS - String values are snake_case matching the key:
GENERATE_SCENES = 'generate_scenes' - Compound entities keep their natural order:
GENERATE_CHARACTERS_AND_ENVIRONMENTS,FIX_SHOTS_ASSETS_NAMES
Fire-and-forget logging¶
Activity logging must never block business logic. Use the log* methods which internally call .catch(() => {}). The core methods (createActivity, updateActivityByJobId, updateActivityByActivityId) log failures via ErrorLogger.logError(...) before re-throwing:
ProjectActivitiesService.logActivity(...)— general-purposeProjectActivitiesService.logProjectLifecycleActivity(...)— project-level shortcuts (create, delete, rename, collaborators, settings)WorkbenchService.createStoryActivity(...)— episode-scoped helper that resolves project context from storyId
Never await these in the main request path.
The details field¶
Optional and sparse. Currently used for:
- Collaborator actions:
{ collaborator: { userId, email, role, previousRole? } } - Settings changes:
{ settings: { changes: [{ field, oldValue, newValue }] } } - Voice actions:
{ voice: { voiceId, voiceName?, audioUrl?, source, assetGenJobId?, variation?: { voiceId, name?, audioUrl?, pitch, strength } } } voiceNameandaudioUrlare resolved at log time from Voices/AssetGenJob collections (fire-and-forget)variationis only present for variation assign/restore actions
If your new action needs extra context beyond the standard fields (actor, project, episode, scene, shot, asset), add it to details with a new optional key.
LogEvent vs ProjectActivityAction¶
These are completely separate enums. LogEvent is for analytics/cost tracking. ProjectActivityAction is for user-facing activity feeds. Do not conflate them.
Known Gaps¶
- Actor email passed as
'': MostlogActivity()call sites passemail: '', causing an extra DB lookup increateActivity()to resolve it. Optimization: passauthUser.emailwhen available.