Skip to content

fix(support): anchor support identity to the tenant id and send diagnostics - #220

Merged
cihanalagoz merged 1 commit into
mainfrom
fix/support-identity-and-diagnostics
Aug 17, 2026
Merged

fix(support): anchor support identity to the tenant id and send diagnostics#220
cihanalagoz merged 1 commit into
mainfrom
fix/support-identity-and-diagnostics

Conversation

@cihanalagoz

Copy link
Copy Markdown
Collaborator

Why

Comparing Console's handoff payload against Studio's and Pulse's turned up three real defects — Console was the outlier in every one.

Field Studio Pulse Console (before)
externalInstallationId workspace._id workspace.id tenantId
context.organizationId not sent org UUID tenant.slug
context.userName first + last name user.name email address
Entry-point predicate baseUrl && (onprem || crm && secret) same baseUrl only

The unique id itself was right: tenantId is the tenant _id from the JWT, same class as the other two products.

What changed

organizationId: slug → null

The CRM derives the support organization key as context.organizationId ?? "${issuer}:${externalInstallationId}", and ticket visibility is organization-scoped (listExternalSupportTickets(userId, organizationId)). Sending the slug meant:

  1. Renaming a slug would orphan the customer's entire ticket history — a new organization key produces a new organization, and the old tickets become invisible.
  2. Cross-product collision risk — the key is not namespaced by issuer, so a human-readable slug could collide with another product's organizationId and merge two customers into one support organization.
  3. The organization name never followed a company rename — the CRM skips the name refresh whenever a product supplies its own organizationId (organizationName ?? (organizationId ? null : workspaceName)).

A Console tenant is both the customer and the workspace, so null is the correct value: the CRM then derives an issuer-scoped key from the immutable tenant id, and all three problems go away.

userName: email → real name

The CRM writes this to externalUsers.fullName, so support staff and every notification greeted an email address. It now reads the name from the tenant database, guarded by assertTenantContext, and falls back to the email if the lookup fails — a display name must never block a handoff.

Entry-point predicate

isSupportReachable() only checks SUPPORT_BASE_URL, but issuing a handoff needs all three values. On SaaS with just the base URL set, the Help button appeared and every click ended in a 503. New isSupportEntryPointEnabled() matches the predicate Studio and Pulse already used, including the on-prem login fallback.

Diagnostics

openSupport() sent only { locale }, so the server-side diagnostic-draft path was unreachable from the UI — Studio has 3 contextual entry points, Pulse 2, Console had 0. Now:

  • openSupport(locale, diagnostic?) sends summary + category, error name/message/stack/digest, page, environment and timestamp.
  • The dashboard error boundary offers "Report to Support" with the error attached.
  • /support/status — a dead endpoint nothing called — is what tells it whether to offer that, and now returns the entry-point predicate.

Shipping path

Console is not deployed as a chart: there is no deploy/ directory and build-deploy.yaml is entirely commented out. It ships as a self-hosted image, and docker-compose.yml passed no support variables, so there was no way to enable Support from the path Console actually uses. It now passes SUPPORT_BASE_URL, SUPPORT_CRM_API_URL, SUPPORT_HANDOFF_SECRET and DEPLOYMENT_MODE=onprem through. .env.example corrected to describe the real behaviour.

Tests

20 new tests, and the two support endpoints are now off the test:endpoints uncovered list:

  • src/__tests__/unit/support-handoff.test.ts — identity contract (asserts organizationId is null and the tenant id is the anchor), name fallback, on-prem fallback, CRM reason never leaking to the caller.
  • src/__tests__/api/support.test.ts — route validation, auth, diagnostics forwarding, cache headers.

Validation

tsc --noEmit shows only pre-existing errors (stale .next/types, pdf-to-img, agentService) · 3175 tests pass · lint clean on all touched files.

npm run build fails on Module not found: 'pdf-to-img' — a declared dependency missing from local node_modules, unrelated to this change.

…ostics

Console sent its tenant slug as the CRM organization id. The CRM derives the
support organization key from that value, and ticket visibility is scoped to
the organization, so renaming a slug would have created a second organization
and hidden every earlier ticket from the customer. The key is also not
namespaced per issuer, so a human-readable slug risked colliding with another
product's organization id.

A Console tenant is both the customer and the workspace, so it now sends a null
organization id and lets the CRM derive an issuer-scoped key from the tenant id.
That also fixes the organization name never following a company rename, which
the CRM skips whenever a product supplies its own organization id.

Studio and Pulse both send a person's name; Console sent the email address, so
support staff and every notification greeted an address. It now reads the real
name from the tenant database and falls back to the email if that lookup fails,
which must never block a handoff.

The Help entry point used reachability rather than configurability, so on SaaS
with only SUPPORT_BASE_URL set the button appeared and every click ended in a
503. It now uses the same predicate Studio and Pulse already used.

Diagnostics never left the browser: openSupport sent only a locale, so the
diagnostic-draft path on the server was unreachable. The dashboard error
boundary can now report to Support with the error attached, and /support/status,
which nothing called, is what tells it whether to offer that.

docker-compose passes the support variables through. Console ships as a
self-hosted image rather than a chart, and there was no way to enable Support
from that path.
@cihanalagoz
cihanalagoz merged commit 3d23c12 into main Aug 17, 2026
1 check failed
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.

1 participant