Skip to content

feat: user-facing activity logs with 19 curated events#66

Merged
wicky-zipstack merged 20 commits intomainfrom
feat/user-facing-activity-logs
Apr 17, 2026
Merged

feat: user-facing activity logs with 19 curated events#66
wicky-zipstack merged 20 commits intomainfrom
feat/user-facing-activity-logs

Conversation

@abhizipstack
Copy link
Copy Markdown
Contributor

What

Introduces a user-facing activity log system that replaces developer-internal log dumps with plain-language messages for all major user actions. The Logs panel now defaults to a "User activity" view showing curated, human-readable messages.

Backend — Event infrastructure:

  • New UserLevel base class in base_types.py with audience()="user" (existing events default to "developer")
  • LogHelper.log() extended with audience param, threaded through eventmgr.write_line to the socket payload
  • 19 proto types + event classes (U001–U019) across 5 priority tiers

19 events across all user-facing operations:

Code Event Trigger location
P1 — Core model operations
U001 ModelRunStarted visitran.py:execute_graph
U002 ModelRunSucceeded visitran.py:execute_graph
U003 ModelRunFailed visitran.py:execute_graph
U004 TransformationApplied transformation/views.py:set_model_transformation
U005 TransformationDeleted transformation/views.py:delete_model_transformation
U006 ModelConfigured transformation/views.py:set_model_config_and_reference
U007 SeedCompleted visitran.py:validate_and_run_seed
P2 — Job scheduler
U008 JobCreated scheduler/views.py:create_periodic_task
U009 JobUpdated scheduler/views.py:update_periodic_task
U010 JobDeleted scheduler/views.py:delete_periodic_task
U011 JobTriggered scheduler/views.py:_dispatch_task_run
P3 — Model/file CRUD
U012 ModelCreated explorer/views.py:create_model_explorer
U013 FileDeleted explorer/views.py:delete_a_file_or_folder
U014 FileRenamed explorer/views.py:rename_a_file_or_folder
P4 — Connections
U015 ConnectionCreated connection/views.py:create_connection
U016 ConnectionTested connection/views.py:test_connection
U017 ConnectionDeletedEvt connection/views.py:delete_connection
P5 — Environments
U018 EnvironmentCreated environment/views.py:create_environment
U019 EnvironmentDeleted environment/views.py:delete_environment

Frontend:

  • Socket handler reads data?.data?.audience alongside level/message
  • New "User activity" option (default) in the log-level dropdown — filters to audience === "user"
  • User-activity rows styled with primary-colored left border + subtle background + bolder font
  • Developer logs unchanged when viewing "All logs" / "Info & above" / etc.

Bug fix (bundled):

Why

The bottom Logs panel showed raw developer-internal messages like Executing Model Node: Database: globe_testing Database Type: postgres .... Users couldn't tell what happened after an action. Now the default view shows plain-language messages like Applied sort transformation on "mdoela" and Model "mdoela" built successfully in 0.42s.

How

  • UserLevel inherits from BaseEvent, overrides audience()"user".
  • BaseEvent gets a default audience()"developer" for backward compat.
  • eventmgr.py:write_line reads audience from the event via getattr and passes to LogHelper.log().
  • Socket payload shape becomes {level, message, audience} — additive, no breaking change.
  • Frontend filters: logsLevel === "user" → show only audience === "user" entries.

Can this PR break any existing features?

Low risk:

  • BaseEvent.audience() default is "developer" — all existing events behave identically.
  • LogHelper.log() audience param defaults to "developer" — existing callers unaffected.
  • Socket payload adds audience field — frontend falls back to "developer" if absent.
  • fire_event calls are added AFTER the operation succeeds — if the event itself fails, it's caught by the existing try/except in write_line and logged, never interrupts the operation.
  • CreateConnection TDZ fix is a pure declaration-order change — no logic change.

Database Migrations

None.

Env Config

None.

Related Issues or PRs

Dependencies Versions

No changes.

Notes on Testing

Backend verified:

  • Smoke test via Django shell: all 19 events create correctly, messages render in plain language, audience() returns "user", LogHelper.log() includes audience in payload.
  • execute_graph confirmed to run with 2 loggers active (file_log + stdout_log) during model execution.
  • Events fire through LogHelper → Celery queue → socket emission pipeline.

Frontend verified:

  • "User activity" is the default dropdown option.
  • User-audience rows render with left border + background styling.
  • Developer logs remain visible in "All logs" mode.
  • WebSocket delivery requires gunicorn (not runserver) or production nginx — tested pipeline end-to-end, socket proxy limitation in React dev server documented.

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

abhizipstack and others added 8 commits April 16, 2026 19:31
Introduces a UserLevel event tier and 3 curated, plain-language
events that replace developer-internal log dumps for model execution:

  Building model "mmodela" → testing.mmodela as TABLE from "testing.country"
  Model "mmodela" built successfully in 0.42s
  Model "mmodela" failed: Table Not Found …

Backend
- base_types.py: UserLevel class with audience()="user"; BaseEvent
  gets a default audience()="developer" so all existing events are
  backward-compatible.
- proto_types.py: ModelRunStarted, ModelRunSucceeded, ModelRunFailed
  message types.
- types.py: 3 event classes (U001-U003) inheriting UserLevel.
- log_helper.py: LogHelper.log() accepts audience param, included in
  the socket payload dict.
- eventmgr.py: write_line reads audience from the event and passes
  through to LogHelper.
- visitran.py: fires the 3 events from execute_graph — started before
  run_model, succeeded after, failed in both exception handlers.

Frontend
- Socket handler reads data?.data?.audience alongside level/message.
- logsInfo entries now carry { level, message, audience }.
- New "User activity" option at the top of the log-level dropdown
  (default). Filters to audience==="user" only.
- Existing options (All logs, Info+, Warn+, Error) continue to show
  developer logs regardless of audience.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
useMemo for hasDetailsChanged was declared at line 411 but referenced
by the handleCreateOrUpdate useCallback at line 148. JavaScript's
temporal dead zone (TDZ) caused a ReferenceError on render. Moved
the useMemo above the useCallback that depends on it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User-audience log entries now render with:
- A primary-colored left border (3px, token.colorPrimary)
- Subtle background fill (token.colorFillQuaternary)
- Slightly bolder font (500 weight, 13px)
- Error-colored text for failed events
- No HTML parsing (user messages are plain text, no ANSI)

Developer logs continue rendering with the existing ANSI-parsed
style and severity-based coloring. The visual contrast makes it
immediately clear which entries are user-facing activity messages
vs developer-internal noise.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
msg_from_base_event builds the Msg class name as {ClassName}Msg.
Our events were named ModelRunStartedEvent → looked for
ModelRunStartedEventMsg which doesn't exist. Dropped the "Event"
suffix so ModelRunStarted → ModelRunStartedMsg matches proto_types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 new UserLevel events covering the core model-tab operations:

- TransformationApplied (U004): fired after set_model_transformation
  succeeds. Message: 'Applied sort transformation on "mdoela"'
- TransformationDeleted (U005): fired after delete_model_transformation.
  Message: 'Removed filter transformation from "mdoela"'
- ModelConfigured (U006): fired after set_model_config_and_reference.
  Message: 'Configured "mdoela" — source: raw.customers, destination: analytics.dim_customers'
- SeedCompleted (U007): fired after successful/failed seed execution.
  Message: 'Seed "raw_customers" loaded into "raw"' or 'Seed "raw_customers" failed in "raw"'

These fire through the existing UserLevel → LogHelper(audience="user")
→ Celery → socket pipeline. Frontend filters them via the "User
activity" dropdown option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 new UserLevel events for job lifecycle actions:

- JobCreated (U008): fired after create_periodic_task succeeds.
  Message: 'Job "Nightly refresh" created for environment "prod"'
- JobUpdated (U009): fired after update_periodic_task.
  Message: 'Job "Nightly refresh" updated'
- JobDeleted (U010): fired after delete_periodic_task.
  Message: 'Job "Nightly refresh" deleted'
- JobTriggered (U011): fired from _dispatch_task_run (covers both
  trigger_task_once and trigger_task_once_for_model).
  Message: 'Job "Nightly refresh" triggered manually — running
  all models' or '— running model mdoela'

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8 new UserLevel events covering model/file CRUD, connections, and
environments:

P3 — Model/project CRUD:
- ModelCreated (U012): model created in explorer
- FileDeleted (U013): files/models deleted
- FileRenamed (U014): file/model renamed

P4 — Connections:
- ConnectionCreated (U015): new connection created
- ConnectionTested (U016): connection test result
- ConnectionDeletedEvt (U017): connection deleted

P5 — Environments:
- EnvironmentCreated (U018): new environment created
- EnvironmentDeleted (U019): environment deleted

All fire through the UserLevel → audience="user" pipeline and
appear in the "User activity" log view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ConnectionDeletedEvt and EnvironmentDeleted were passing raw UUIDs.
Now fetch the name before deleting so the user-activity log shows
'Connection "my_postgres" deleted' instead of 'Connection "uuid" deleted'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@abhizipstack abhizipstack requested review from a team as code owners April 16, 2026 15:19
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 16, 2026

Greptile Summary

This PR introduces a user-facing activity log system with 19 curated events (U001–U019) across the backend and frontend. The event infrastructure is well-structured: UserLevel extends BaseEvent, developer logs are unchanged, and the additive socket payload change is backward-compatible. Several prior-round issues (name-lookup safety, seed schema, field naming, false-failure on connection delete, JobTriggered premature firing) have been addressed in follow-up commits.

Two minor inaccuracies remain in the new event messages worth noting before merge.

Confidence Score: 4/5

Safe to merge with one known pre-existing runtime bug in the seed path that prior review rounds flagged; all new view-function events are exception-safe.

All prior P0/P1 findings (false-failure on connection delete, premature JobTriggered, seed schema empty string, field naming) are addressed in the follow-up commits. The two new findings are both P2. Score stays at 4 rather than 5 because the SeedCompleted status() method-shadowing bug — flagged and acknowledged in the previous round — appears unresolved in HEAD, and would cause a successful seed run to be reported as a failure through the activity log path.

backend/visitran/events/types.py (SeedCompleted.status() still shadows proto field); backend/visitran/visitran.py (repr vs str); backend/backend/core/scheduler/views.py (multi-model scope label)

Important Files Changed

Filename Overview
backend/visitran/events/base_types.py Adds UserLevel base class with audience()="user", title(), subtitle(), status() defaults; BaseEvent.audience() defaults to "developer" for backward compat — clean design, but status() naming collides with proto field of the same name in SeedCompleted.
backend/visitran/events/eventmgr.py Branches write_line on audience: user events emit a rich structured payload (title/subtitle/status/timestamp); developer events unchanged. getattr(msg.data, "status", lambda: "info")() is safe for all new events except SeedCompleted (proto field shadows method).
backend/visitran/events/types.py Adds 20 UserLevel event classes (U001–U019 + ConnectionDeleteFailedEvt); messages and titles are well-written; SeedCompleted.status() method shadows its status: str proto field at runtime.
backend/visitran/visitran.py Fires ModelRunStarted/Succeeded/Failed per node and SeedCompleted per seed; start_time correctly scoped per iteration; generic except path uses repr(err) instead of str(err) for the user-facing error field.
backend/backend/core/scheduler/views.py JobCreated/Updated/Deleted/Triggered events fire after successful operations; scope falls back to "job" for multi-model models_override, producing misleading "running all models" log message for partial runs.
backend/backend/core/routers/connection/views.py ConnectionCreated/Tested/Deleted events added; VisitranBackendBaseException re-raised cleanly; ConnectionDeleteFailedEvt fires before re-raising the structured exception.
backend/backend/core/routers/environment/views.py EnvironmentCreated/Deleted events fire correctly; ProtectedError handler refactored to use EnvironmentInUse exception with a cleaner job-name list comprehension.
frontend/src/ide/editor/no-code-model/no-code-model.jsx Adds "User activity" default log filter, rich card rendering for user events (status icon, title, subtitle, timestamp), and guards against messages with only a title field; developer log rendering unchanged.
backend/backend/errors/validation_exceptions.py Adds EnvironmentInUse and ConnectionDeleteFailed structured exceptions with HTTP 400 and severity="Warning"; wired to the new error-code templates.
docker/docker-compose.yaml Adds celery_log_task_queue to the Celery worker's -Q list so log-event tasks are consumed in the docker-compose environment.

Sequence Diagram

sequenceDiagram
    participant View as Django View / visitran.py
    participant FE as fire_event()
    participant EM as EventManager write_line
    participant LH as LogHelper publish_log
    participant CQ as Celery log queue
    participant UI as Frontend jsx

    View->>FE: fire_event(UserLevelEvent)
    FE->>EM: write_line(EventMsg)
    EM->>EM: audience → "user"
    EM->>EM: build payload title/subtitle/status/timestamp
    EM->>LH: publish_log(session_id, payload)
    LH->>CQ: kombu producer publish
    Note over LH: exceptions caught internally
    CQ-->>UI: socket logs event
    UI->>UI: audience user → store rich entry
    UI->>UI: logsLevel user → render card
Loading

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: backend/backend/core/scheduler/views.py
Line: 639-641

Comment:
**"Running all models" shown for multi-model partial runs**

`scope` falls back to `"job"` whenever `models_override` has more than one element, so `JobTriggered.message()` renders `"running all models"` even when the user triggered only a specific subset. The one-model case is correct, but the multi-model case is misleading.

```suggestion
    if not models_override:
        scope = "job"
    elif len(models_override) == 1:
        scope = models_override[0]
    else:
        scope = f"{len(models_override)} models"
```

You'd also want to update `JobTriggered.message()` / `subtitle()` in `types.py` to handle the "N models" scope string alongside `"job"` and a single model name.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: backend/visitran/visitran.py
Line: 388-397

Comment:
**`repr(err)` leaks Python noise into the user-facing message**

The domain-exception catch (line 374) passes `str(visitran_err)` — clean, human-readable. The generic catch passes `repr(err)`, which produces output like `KeyError('column_name')` or `AttributeError("'NoneType' object has no attribute 'foo'")`. Since this string surfaces directly in the `ModelRunFailed` subtitle shown to users, it should use `str(err)` for consistency.

```suggestion
                fire_event(
                    ModelRunFailed(
                        model_name=dest_table or str(node_name),
                        error=str(err),
                    )
                )
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (9): Last reviewed commit: "fix: rename status() to event_status() t..." | Re-trigger Greptile

Comment thread backend/visitran/visitran.py
Comment thread backend/visitran/events/proto_types.py
@abhizipstack abhizipstack force-pushed the feat/user-facing-activity-logs branch from da7c740 to 1c34c15 Compare April 16, 2026 15:37
Backend returned {"message": "..."} on ProtectedError but the
frontend notification service reads "error_message". Changed key
to "error_message" for consistency with other endpoints.

Also improved the error message copy and added explicit error_message
extraction in the frontend catch block so the user sees:
"Cannot delete this environment because it is used by: Nightly from
'Deploy'. Remove it from the job first, then delete."

instead of the generic "Something went wrong".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@abhizipstack abhizipstack force-pushed the feat/user-facing-activity-logs branch from 1c34c15 to f9c45eb Compare April 16, 2026 15:42
@abhizipstack
Copy link
Copy Markdown
Contributor Author

Additional fix pushed: Environment delete now shows a proper error when the environment is used by a job.

  • Added EnvironmentInUse exception class + BackendErrorMessages.ENVIRONMENT_IN_USE error template following the same pattern as SourceTableDoesNotExist, ConnectionDependency, etc.
  • Raises through handle_http_request decorator → frontend gets {error_message, is_markdown: true, severity: 'Warning'} → renders markdown with bold job names.
  • Before: generic 'Something went wrong'. After: formatted warning showing which job(s) reference the environment.

Comment thread backend/backend/core/routers/connection/views.py
@wicky-zipstack
Copy link
Copy Markdown
Contributor

wicky-zipstack commented Apr 16, 2026

@abhizipstack
1. fire_event() is NOT wrapped in try/excepteventmgr.py:fire_event

PR description says "if the event itself fails, it's caught by the existing try/except in write_line" — I traced the code, no such guard exists. Most fire_event calls fire after the operation succeeds (e.g. delete_connection, _dispatch_task_run). If Redis hiccups or StateStore raises, the view returns 500 even though the connection/job was actually deleted.

Fix in fire_event (covers all 19 sites):

def fire_event(e: BaseEvent, level=None) -> None:
    try:
        event_manager.fire_event(e, level=level)
    except Exception:
        logger.exception("fire_event failed for %s", type(e).__name__)

2. JobTriggered fires BEFORE dispatchscheduler/views.py:_dispatch_task_run

fire_event(JobTriggered(...)) happens before the Celery dispatch + sync fallback. If both fail, log shows "Job triggered" but nothing ran. Move it AFTER successful dispatch.

3. Bundled with merged PR #65CreateConnection.jsx

The TDZ fix is already in main via #65. Please rebase so the diff doesn't show duplicate.

4. ConnectionDeletedEvt namingtypes.py:984

No collision with existing ConnectionDeleted — drop the Evt suffix to match EnvironmentDeleted/JobDeleted. Also: proto field is connection_id but you're passing the connection name (connection/views.py:32). Rename the field to connection_name.

5. Multi-model scope detectionscheduler/views.py:_dispatch_task_run

scope = models_override[0] if ... len(models_override) == 1 else "job" — if 2+ models in override, falls through to "job" which is wrong (it's a partial run). Use ", ".join(models_override) or a "selection" bucket.

6. Extra DB roundtrip in delete_connectionconnection/views.py:29-31

Fetches full connection (including masked credentials) just for the name. Either return name from delete_connection, or fetch only the name field.

7. FileDeleted for batchexplorer/views.py

", ".join(deleted_files) becomes a 1000-char string when wiping many models. Truncate: f"{first_3} and {N-3} more".

8. localStorage migrationno-code-model.jsx:233

Existing users have "info"/"debug" saved → new "user" default only applies to first-time users. Returning users never see the new view unless they manually pick it. Consider one-time migration from "info""user".

9. Bundled delete_environment refactor

The new EnvironmentInUse exception + ENVIRONMENT_IN_USE error code are good improvements but unrelated to user activity logs. Either split out or call out clearly in the description.

10. No tests — at minimum add: LogHelper.log(audience=...) payload shape + regression test that fire_event doesn't bubble exceptions when StateStore is unavailable (covers #1).

abhizipstack and others added 4 commits April 16, 2026 21:32
… names

P1: Connection name-lookup before delete could block deletion on
    transient errors. Wrapped in try/except so deletion proceeds
    regardless; event falls back to connection_id.

P2: SeedCompleted failure path had schema_name="". Now reads from
    self.context.schema_name with fallback to empty string.

P2: Renamed misleading proto fields — connection_id → connection_name
    and environment_id → environment_name since they carry display
    names, not UUIDs. Updated event classes and all callers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If fetching the connection fails (doesn't exist, DB down), no point
attempting deletion. Both operations now live in one try block.
fire_event fires from both success and exception paths so the
activity log captures the attempt either way. Exception re-raises
so handle_http_request returns the proper error to the frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces bare re-raise with a dedicated ConnectionDeleteFailed
exception following the same pattern as EnvironmentInUse —
BackendErrorMessages template with markdown formatting, caught by
handle_http_request decorator for uniform error response.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Success fires ConnectionDeletedEvt (U017, UserLevel/info):
  'Connection "my_postgres" deleted'

Failure fires ConnectionDeleteFailedEvt (U020, UserLevel/error):
  'Failed to delete connection "my_postgres": reason...'

The failure event uses level_tag=ERROR so it renders in red in the
activity log, clearly distinguishing it from a successful delete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread backend/backend/core/routers/connection/views.py
fire_event inside try could throw (e.g., logger error) after
delete_connection succeeded, triggering the except block and raising
ConnectionDeleteFailed for a successful deletion. Moved success
event after the try block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread backend/backend/core/scheduler/views.py Outdated
@abhizipstack abhizipstack self-assigned this Apr 17, 2026
JobTriggered was fired optimistically before send_task/sync execution.
If both dispatch paths failed, the activity log showed "triggered"
for a job that never ran. Moved fire_event to after each successful
dispatch, consistent with all other events in this PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use materialization .name instead of .value so logs show "TABLE"/"VIEW"
  instead of integer values like "1"/"2"
- Extract transformation type from step_config (where frontend sends it)
  instead of top-level request data, fixing "Applied unknown transformation"
- Add celery_log_task_queue to docker-compose celery worker so activity
  log events are actually consumed and delivered via WebSocket

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@abhizipstack abhizipstack requested a review from a team as a code owner April 17, 2026 09:23
Comment thread backend/backend/core/routers/connection/views.py
abhizipstack and others added 2 commits April 17, 2026 16:54
Replace raw text logs with structured data for user-level events:
- Send title, subtitle, status, timestamp as separate fields
- Render as activity cards with status icons and color-coded borders
- Remove [ThreadPool] prefix from developer logs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Re-raise VisitranBackendBaseException subclasses (ConnectionNotExists
404, ConnectionDependencyError 409) directly instead of wrapping all
errors as ConnectionDeleteFailed 400.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explicitly pass page 1 when switching jobs to prevent fetching a stale
page number from the previous job selection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread backend/visitran/events/types.py
SeedCompleted has a proto field named 'status' which shadows the
method. Renamed to event_status() across all UserLevel events and
the base class to prevent runtime errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@tahierhussain tahierhussain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@wicky-zipstack wicky-zipstack merged commit 24d477c into main Apr 17, 2026
7 checks passed
@wicky-zipstack wicky-zipstack deleted the feat/user-facing-activity-logs branch April 17, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants