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
projectIdon the document you loaded or wrote (story video, project row, etc.). Do not add an extra DB round-trip just to resolveprojectId. - The change maps cleanly to a
UserLogVerb+UserLogEntity(seesharedTypes.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¶
projectIdis 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); uselogActivity/ 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, orlogAssetGenJobActivityStatusas appropriate. - Workbench flows that already use
WorkbenchService.createStoryActivitywith a typedProjectActivityAction— 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 notreq.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.
Related code¶
- Implementation and contracts:
projectActivities.service.ts(logUserActivity,LogUserActivityInput). - Verb/entity vocabulary:
src/shared/sharedTypes.ts(USER_LOG,UserLogVerb,UserLogEntity).