Skip to content

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-purpose
  • ProjectActivitiesService.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 } } }
  • voiceName and audioUrl are resolved at log time from Voices/AssetGenJob collections (fire-and-forget)
  • variation is 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 '': Most logActivity() call sites pass email: '', causing an extra DB lookup in createActivity() to resolve it. Optimization: pass authUser.email when available.