Skip to content

fix(plugin-detail): name the actor of an activity row that carries only actor_id (objectui#12067) - #12070

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-12067-history-actor-id
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-12067-history-actor-id

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12067

Clause-②: no

Implemented by the os-dev dispatched from seat domain:ui#3, session https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8.

What changed

  • record:history (record-history.tsx): its sys_activity self-fetch now passes $expand: ['actor_id'], and the entry's user comes from activityActorName(row) instead of actor_name alone.
  • recordActivityFeed.ts: a new module-level reader, activityActorName, applies the card's rule. It returns the actor_name snapshot when that is non-blank. Otherwise it returns the expanded actor_id record's name (sys_user's declared name field). Otherwise it returns null, so the caller keeps its existing fallback. activityRowToFeedItem now uses it in place of row.actor_name ?? systemActorLabel.
  • The two other sys_activity reads whose rows go through activityRowToFeedItem pass the same expand: the record:activity block's self-fetch (record-activity.tsx) and the console record page's merged feed (RecordDetailView.tsx, app-shell).
  • Docs: one paragraph in content/docs/plugins/plugin-detail.mdx (AGENTS.md Add automated testing infrastructure and CI/CD workflows #2).
  • Changeset .changeset/12067-history-actor-id.md: patch for @object-ui/plugin-detail and @object-ui/app-shell.

No export, prop, type or pack key changes. activityActorName is not re-exported from the package index. In the built packages/plugin-detail/dist/index.d.ts it has 0 hits, against 3 hits for the control activityRowToFeedItem. SysActivityRow is unchanged too: the reader reads actor_id through its existing index signature.

Measurements (the dispatch's three mechanism hypotheses)

  1. How History reads sys_activity, and whether the read can expand actor_id: yes, in the same request.
    • The renderer calls dataSource.find('sys_activity', { $filter: { object_name, record_id }, $orderby: { timestamp: 'desc' }, $top }). $expand is a declared QueryParams member (@object-ui/types data.ts).
    • ObjectStackAdapter.find sends any $expand to rawFindWithPopulate: one GET /api/v1/data/sys_activity?populate=actor_id&.... Its filter conversion is the same as the SDK route's: translateFilterToAST calls convertFiltersToAST.
    • On the server (objectstack 6befe19c, spec 17.7.0), sys_activity.actor_id is Field.lookup('sys_user'). It is the same at @objectstack/plugin-audit@17.0.0, so every 17.x server declares the field and the protocol's expand gate (assertExpandTargetsExist) admits it.
    • expandRelatedRecords loads the ids of the whole page with ONE find(sys_user, id in [...]) through the engine's own read path (CRUD gate, RLS, FLS). When that read is refused it keeps the bare id, and the parent read still succeeds.
    • This is the route objectui#11701's Audit Log page took for the same question: $expand=user_id, then read name.
  2. The sys_user read door: ordinary viewers are not refused, so the stop clause does not fire.
    • member_default grants sys_user allowRead: true, row-scoped by sys_user_self (id == current_user.id) and sys_user_org_members (id in current_user.org_user_ids). The read-only set carries the same two select policies.
    • So a viewer can read the name of every member of the same organization. An actor outside that scope, or a deleted user, keeps the bare id, and the entry shows the existing fallback. A raw id is never shown as a name.
  3. recordActivityFeed.ts: the same gap.
    • The same id-only rows reach activityRowToFeedItem through two reads: the record:activity self-fetch, and RecordDetailView's merged feed on the default console record page.
    • actor: row.actor_name ?? systemActorLabel showed those rows as made by "System", which credits the change to no person. Per the claim, the same rule now applies there.
    • The rule takes effect only where the read expands actor_id, so both of those reads now do.

Surface beyond the claim's file list (stated, not silent)

The claim names record-history.tsx, recordActivityFeed.ts (conditional), their tests and the changeset. This PR also touches:

  • record-activity.tsx and RecordDetailView.tsx: one $expand: ['actor_id'] line each. Without them, the rule the claim admits into recordActivityFeed.ts would read an expanded record that never arrives. Leaving RecordDetailView out would also make the console record page and the record:activity block disagree about the same row, a disagreement that file's own comment calls a bug.
  • RecordDetailView.activityActorId-12067.test.tsx (that read's pin) and the docs paragraph.

The seat may amend the claim's file surface.

Tests (all at 6b1a3ff64)

New pins:

  • record-history.actorId-12067.test.tsx, the claim's four:
    • a row with only actor_id shows the user's name;
    • a row with both shows actor_name;
    • a row with neither shows the existing fallback, and so does a bare id the viewer may not read;
    • a page of 6 id-only rows makes exactly one find, on sys_activity with $expand: ['actor_id'], and no sys_user read.
  • recordActivityFeed.actorId-12067.test.tsx: the same three rules on the mapper, plus the record:activity self-fetch (one read, expanded, both names shown, no "System").
  • RecordDetailView.activityActorId-12067.test.tsx: the console page's sys_activity read carries the expand, and the feed shows the user, not "System".

Runs:

  • pnpm exec vitest run packages/plugin-detail/ --maxWorkers=2: Test Files 250 passed | 1 skipped (251), Tests 2433 passed | 8 skipped (2441).
  • app-shell, narrowed and declared: the 111 app-shell test files that name RecordDetailView (git grep -l RecordDetailView over app-shell tests), run in two halves: 56 passed (56) / 495 passed, and 55 passed (55) / 394 passed. The full app-shell suite (1245 files) does not fit the foreground cap, so it is CI's.
  • apps/console/src/__tests__/record-block-record-reach.test.tsx: 13 passed (13).
  • pnpm --filter @object-ui/plugin-detail type-check and pnpm --filter @object-ui/app-shell type-check: both exit 0, after building each dependency closure. tsc -p tsconfig.test.json --listFilesOnly lists each new test file.
  • pnpm exec eslint on the 7 touched TS/TSX files: 0 errors and 140 warnings (no-explicit-any and the like; the repo has no warnings ratchet). This is a narrowed run, and three facts back it:
    • population: the repo's own eslint.config.js;
    • count: 7 files, read from --format json;
    • invariance: no parserOptions.project or projectService, and no custom rule in eslint-rules/ reads the filesystem, so this diff cannot move a verdict on an untouched file.
  • check:control-bytes, check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check:new-line-citations (0 new), check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:doc-types and check:doc-fences: all exit 0. So do check-changeset-presence.mjs (7 source files of 2 released packages, 1 changeset), check-changeset-no-major.mjs, check-changeset-fixed.mjs and check-changeset-overwrite.mjs. check-governed-queue-guard.mjs --test reports NOT GOVERNED for all 9 paths.
  • NOT MEASURED:
    • check:doc-snippets and check:doc-examples. Reason: prerequisite. The gate printed "THE GATE COULD NOT RUN" because the packages its examples import are not built. The docs diff adds 0 fenced blocks.
    • The console eager-closure budget. Reason: it needs a console build, which is CI's Bundle Analysis job.

Ablations (predictions written before each run)

Each run used ablation-replace.mjs in wrap mode. Every anchor hit 1 time and went to 0 on disk. Every restore matched the HEAD blob, and git diff HEAD was empty afterwards.

Mutation Predicted Observed
A1: drop $expand: ['actor_id'] from record-history.tsx history: 2 red (id-only, N rows), 2 green Tests 2 failed | 2 passed (4)
A2: user_name: activityActorName(r) back to r.actor_name ?? null the same 2 red / 2 green Tests 2 failed | 2 passed (4)
A3: mapper back to row.actor_name ?? systemActorLabel feed: 3 red, "both" green; app-shell pin red Tests 4 failed | 1 passed (5) across both files
A4: drop the expand from record-activity.tsx feed: self-fetch red, 3 mapper pins green Tests 1 failed | 3 passed (4)
A5: drop the expand from RecordDetailView.tsx app-shell pin red Tests 1 failed (1)

Acceptance notes

  • The $expand route skips the adapter's missing-resource memo. On a deployment with no sys_activity (no audit plugin), each mount now repeats a 404 read instead of remembering the first one. What the user sees is unchanged: a 404 is not a refusal, so the history and the feed stay empty. Observation only, nothing filed; carrier: none.
  • Avatar. actor_avatar_url is still the only avatar source. An id-only row shows initials from the resolved name. This is outside the card's acceptance.
  • Writer half. The writer half is plugin-audit: sys_activity.actor_name is declared but never written, so every record History entry reads "Unknown user" objectstack#22510, and this PR does not touch it. Once it lands, new rows carry actor_name, which still wins. The expand then names only the older rows, at the cost of one batched server-side sys_user read per page.

Generated by Claude Code

…ly actor_id (objectui#12067)

On @objectstack/* 17.7.0 the audit writer fills sys_activity.actor_id and
never actor_name, so record:history read every such entry as "Unknown user"
and the activity feed (record:activity, and the console record page's merged
feed) read it as "System".

The three sys_activity reads now pass $expand: ['actor_id'], so the engine
batch-loads the page's sys_user rows in the same request. One reader,
activityActorName, names the actor: the actor_name snapshot when present,
else the expanded actor_id's name, else the existing fallback. A bare id
(a user the viewer may not read) is never shown.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3167.9 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-DvFAgLii.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 587.83KB 141.44KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.42KB 68.20KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.09KB 65.63KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.11KB 45.88KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.62KB 69.16KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.10KB 30.28KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.93KB 23.24KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 20:10
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 20:10
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit e391f87 Oct 9, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12067-history-actor-id branch October 9, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: app-shell plugin tests

Projects

None yet

2 participants