feat: user-facing activity logs with 19 curated events#66
feat: user-facing activity logs with 19 curated events#66wicky-zipstack merged 20 commits intomainfrom
Conversation
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>
|
| 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
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
da7c740 to
1c34c15
Compare
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>
1c34c15 to
f9c45eb
Compare
|
Additional fix pushed: Environment delete now shows a proper error when the environment is used by a job.
|
|
@abhizipstack PR description says "if the event itself fails, it's caught by the existing try/except in Fix in 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.
3. Bundled with merged PR #65 — CreateConnection.jsx The TDZ fix is already in main via #65. Please rebase so the diff doesn't show duplicate. 4. No collision with existing 5. Multi-model scope detection — scheduler/views.py:_dispatch_task_run
6. Extra DB roundtrip in delete_connection — connection/views.py:29-31 Fetches full connection (including masked credentials) just for the name. Either return name from 7.
8. localStorage migration — no-code-model.jsx:233 Existing users have 9. Bundled The new 10. No tests — at minimum add: |
… 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>
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>
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>
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>
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>
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:
UserLevelbase class inbase_types.pywithaudience()="user"(existing events default to"developer")LogHelper.log()extended withaudienceparam, threaded througheventmgr.write_lineto the socket payload19 events across all user-facing operations:
visitran.py:execute_graphvisitran.py:execute_graphvisitran.py:execute_graphtransformation/views.py:set_model_transformationtransformation/views.py:delete_model_transformationtransformation/views.py:set_model_config_and_referencevisitran.py:validate_and_run_seedscheduler/views.py:create_periodic_taskscheduler/views.py:update_periodic_taskscheduler/views.py:delete_periodic_taskscheduler/views.py:_dispatch_task_runexplorer/views.py:create_model_explorerexplorer/views.py:delete_a_file_or_folderexplorer/views.py:rename_a_file_or_folderconnection/views.py:create_connectionconnection/views.py:test_connectionconnection/views.py:delete_connectionenvironment/views.py:create_environmentenvironment/views.py:delete_environmentFrontend:
data?.data?.audiencealongside level/messageaudience === "user"Bug fix (bundled):
CreateConnection.jsx:hasDetailsChangeduseMemo was declared after the useCallback that depends on it — TDZ crash. Moved above. (Introduced by PR FIX: Connection Name and Description Losing Last Character When Editing #57)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 likeApplied sort transformation on "mdoela"andModel "mdoela" built successfully in 0.42s.How
UserLevelinherits fromBaseEvent, overridesaudience()→"user".BaseEventgets a defaultaudience()→"developer"for backward compat.eventmgr.py:write_linereadsaudiencefrom the event viagetattrand passes toLogHelper.log().{level, message, audience}— additive, no breaking change.logsLevel === "user"→ show onlyaudience === "user"entries.Can this PR break any existing features?
Low risk:
BaseEvent.audience()default is"developer"— all existing events behave identically.LogHelper.log()audienceparam defaults to"developer"— existing callers unaffected.audiencefield — frontend falls back to"developer"if absent.fire_eventcalls are added AFTER the operation succeeds — if the event itself fails, it's caught by the existing try/except inwrite_lineand logged, never interrupts the operation.Database Migrations
None.
Env Config
None.
Related Issues or PRs
Dependencies Versions
No changes.
Notes on Testing
Backend verified:
audience()returns"user",LogHelper.log()includes audience in payload.execute_graphconfirmed to run with 2 loggers active (file_log + stdout_log) during model execution.Frontend verified:
Checklist
I have read and understood the Contribution Guidelines.
🤖 Generated with Claude Code