Skip to content

When to call logUserActivity

ProjectActivitiesService.logUserActivity writes a lightweight audit line for the project activity feed (stored as action: USER_LOG with actor.log: verb + entity + optional metadata). The UI renders it like: “Alex updated shot · …”.

Use it when

  • A user-facing mutation should appear in the project / episode activity timeline (who did what, on which project asset).
  • You already have projectId on the document you loaded or wrote (story video, project row, etc.). Do not add an extra DB round-trip just to resolve projectId.
  • The change maps cleanly to a UserLogVerb + UserLogEntity (see sharedTypes.ts: UserLogVerb, UserLogEntity).
  • You want fire-and-forget logging: the method returns immediately; persistence runs on the next tick.

Typical call sites: model or service code that persists episodes, scenes, shots, assets, scripts, etc., where the product expects a human-readable audit trail per project.

Do not use it when

  • projectId is missing — the call is a no-op by design (avoids hidden lookups).
  • There is no authenticated actor and you did not pass actor — another no-op (e.g. unauthenticated cron); use logActivity / explicit flows if you need system attribution instead.
  • You need the rich activity row (job status, asset gen, lifecycle banners, Spine push by default) — use logActivity, logProjectLifecycleActivity, or logAssetGenJobActivityStatus as appropriate.
  • Workbench flows that already use WorkbenchService.createStoryActivity with a typed ProjectActivityAction — keep that path unless you are intentionally migrating a surface to the user-log shape.

Optional fields

  • storyId / sceneId / shotId — scope the log for deep links / filtering.
  • metadata — small structured hints (e.g. { field: 'description' } or { fields: ['description', 'actingInstructions'] }); keep it minimal.
  • actor — only when the acting user is not req.auth (rare).
  • publishToSpine: true — only if realtime fan-out is required; default is off because these logs can be high volume.

Grouping

Rapid verb: 'updated' logs from the same actor on the same entity scope are grouped into one row (fields unioned into metadata.fields). Uploads / adds always create a new row.

The window is ~5 minutes measured from the existing row's createdAt — a fixed window, not a sliding one. So a long editing session produces a row roughly every 5 minutes rather than one ever-growing row, and a row's displayed time never drifts far from the createdAt it is sorted by.

Grouping is a single atomic findOneAndUpdate (ProjectActivitiesModel.mergeRecentUserLog), so concurrent saves from multiple tabs or collaborators can't lose fields or split into duplicate rows. It is backed by a compound index on project.projectId + action + actor.userId + actor.log.verb + actor.log.entity + shot.shotId + createdAt.

  • Implementation and contracts: projectActivities.service.ts (logUserActivity, LogUserActivityInput).
  • Verb/entity vocabulary: src/shared/sharedTypes.ts (USER_LOG, UserLogVerb, UserLogEntity).