Conversation exploration and QA read Rails-client retirement
ENG-3037 (child of ENG-2681)
removes the Rails client from the 14 conversation exploration and QA read
files in rulebase-ui/app/features/conversations. Owner: Chidi. The scoped
files now import only ~/lib/rulebase-api2-client/client and the generated
API2 schema; there is no Rails fallback path and no Rails base URL.
Explicitly out of scope and still Rails-backed: delete conversation, request
QA, transcription, transcript editing, contest-feedback submission, attachment
upload, import conversation (POST /conversations/import stays on
rulebaseApi), manual-evaluation submission, and Admin source retirement.
Routes
Existing parity-complete API2 routes wired as-is: GET /conversations
(cursor list), GET /conversation-filters, GET /tags, evaluation contest
reads, and QA agent evaluation reads.
New native reads in src/routes/conversation-qa-reads.ts with query glue in
src/lib/conversation-qa-reads/, plus one taxonomy route:
| Route | Permission / scope | Behavior reproduced from Rails |
|---|---|---|
GET /conversation_threads | web session, tenant | page[page]/page[limit], JSON filter (same snake_case keys as /conversations; team_ids → group_ids), JSON sort over contacts_count, initial_contact_at, last_contact_at; default initial_contact_at DESC, id DESC; plainto_tsquery('simple') search; Rails meta.page.{next,limit,total}; contact aggregation; root conversation prefixed IDs. |
GET /conversations/{conversation_id}/qa_evaluations | web session, tenant, conversation must belong to tenant | qa_evaluation_* prefixed IDs, ai/human source, created_at DESC, Active Storage files attachments served as signed absolute URLs. |
GET /conversations/{conversation_id}/conversation_cx_risk_snapshot_scorecard_results | web session, tenant | Scorecard → criterion (binary/scaled/options) → nested children results with score options, ordered as Rails. |
GET /conversation_cx_risk_snapshots/{id} | conversation_cx_risk_snapshot:all | Snapshot detail; excludes last_updated_at IS NULL; churn thresholds high ≥ 0.7, medium ≥ 0.4, low < 0.4, none when null. |
GET /conversations/search | web session, tenant | Ticket-ID/external-ID search for the ticket-IDs filter command and the import suggestion read shape (import itself remains Rails). Rails ConversationsController#search guard preserved: any same-organization conversation with the external_id (including rows the member’s policy scope hides) returns null before any provider or cache call. Rails SuggestedConversationImport.search cache preserved as Redis suggested-conversation-import:v1:{rails_org_id}:{sha256(external_id)}, TTL 1 day, positive and negative ("none") results; provider failures are not cached; Redis unavailability falls through to the providers (never treated as a hit or a miss). |
GET /conversation-filters/{id} | web session, tenant | JSON:API saved-filter detail; UI flattens data.attributes into { id, ...attributes }. |
GET /conversations list serialization was tightened (typed
review_status, legacy tags enum, customer_complaints with categories,
assignee timestamps) so the generated UI types replace the Rails
ConversationSummary alias without loosening the contract. Stored-view
preview totals use the API2 facet query instead of the Rails meta.page.total.
Behavior inventory
Preserved: authenticated membership and tenant isolation, Rails policy scope,
prefixed IDs, list/detail shapes consumed by the components, infinite
pagination (cursor for /conversations, page numbers for
/conversation_threads) via a Rails-shaped meta.page.next adapter, default
sort, every URL/filter key in stringifyFilters (ticket IDs, text, dates,
brands, channels, teams, employees, assignees, review status, risk/sentiment,
scorecards, QA source/status, contest outcome, conversation field values;
empty arrays stripped), group counts and list statistics, query keys and
structuralSharing, invalidations, thread contact aggregation, applicable
scorecards, QA and CX-risk ordering, tag option cap (100) and display-name
resolution, attachment URLs (neutral ~/lib/attachments#resolveAttachmentUrl
passes signed/blob/data URLs through and never rewrites onto
VITE_API_BASE_URL), and loading/empty/error/unauthorized behavior through
handleApi2Error.
Tests
rulebase-api2/src/routes/conversation-qa-reads.db.test.ts: real PGlite acceptance with anonymized production-shaped US org 25 and EU org 41 fixtures; cross-tenant negatives, prefixed IDs, mixed human/AI/contest/CX-risk records, conversation fields, realistic filter values, sort, pagination, attachments, permissions, search. No route query or policy scope is mocked; the search cases stub only the Zendesk HTTP client/credentials and assert the provider is never called when a hidden same-organization conversation already has theexternal_id.rulebase-api2/src/lib/conversation-qa-reads/conversation-import-suggestion.db.test.ts: PGlite coverage of the organization-wide suppression, cross-tenant non-suppression, one-day tenant/external-ID cache key shape, positive and negative result caching, tenant isolation of cached results, and provider-failure misses not being cached.rulebase-api2/src/routes/conversation-taxonomy-reads.db.test.ts: saved filter detail and tenant negatives.rulebase-ui/app/features/conversations/api/conversation-read-clients.test.ts: parameter serialization, cursor and page pagination, stable query keys, response-shape adapters, API2 error and 401 handling.rulebase-ui/app/lib/attachments.test.ts: attachment URL resolution.rulebase-ui/app/features/conversations/conversation-read-source-gate.test.ts: fails if any of the 14 files containsrulebase-api-client,VITE_API_BASE_URL,api.rulebase.co,buildApiURL,rulebaseApi, or a Rails fallback marker.
Required checks: pnpm typecheck and the focused vitest files in
rulebase-api2, pnpm openapi:export + pnpm rulebase-api2-openapi:generate,
and pnpm lint, pnpm typecheck, focused vitest in rulebase-ui.
Source delta
rulebase-ui Rails-client importers: 253 source/test (242 runtime) →
239 (228 runtime), a reduction of exactly the 14 scoped files.
rulebase-beam-web stays at 0/0. Type-only aliases in
~/lib/rulebase-api-client/types remain for components outside the scoped
files and are retired with the shared types module.
Known gaps
- Consumers outside the 14 files that still import the Rails client for the excluded mutations are unchanged by design.
meta.page.totalfor the cursor-based list is reported as0; the only consumer (stored-view preview) now reads the facet count.- Assignee
created_at/updated_atfall back to the epoch when the Rails row lacks timestamps; no UI consumer renders them.
Rollout, outcome query, and rollback
No feature flag: a default-off rollout is not practical without leaving a normal-path Rails fallback, which this package forbids. Deploy API2 in both regions before the UI bundle. Rollback is a code revert of the UI commit.
Merge gate: source zero in the scoped files (the source-gate test). Runtime
gate: after the UI deploy, the following Rails controllers tagged
first_party_client:rulebase-ui must stay absent for a 72-hour observation
window (checked at 24 h and 72 h) on the Sentry API2 Migration dashboard
(5423172): ConversationsController#index, #search,
ConversationThreadsController#index, QAEvaluationsController#index,
ConversationCxRiskSnapshotScorecardResultsController#index,
ConversationCxRiskSnapshotsController#show, ConversationFiltersController#show,
TagsController#index.
# Sentry Discover, per region
transaction.op:http.server first_party_client:rulebase-ui
transaction:[ConversationsController#index, ConversationsController#search,
ConversationThreadsController#index, QAEvaluationsController#index,
ConversationCxRiskSnapshotScorecardResultsController#index,
ConversationCxRiskSnapshotsController#show, ConversationFiltersController#show,
TagsController#index]State: implemented in PR #11908 ; review requested (Ronald); not merged, not deployed, no observation started. Current gate: review and merge.