fix(support): anchor support identity to the tenant id and send diagnostics - #220
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Comparing Console's handoff payload against Studio's and Pulse's turned up three real defects — Console was the outlier in every one.
externalInstallationIdworkspace._idworkspace.idtenantId✅context.organizationIdtenant.slug❌context.userNameuser.namebaseUrl && (onprem || crm && secret)baseUrlonly ❌The unique id itself was right:
tenantIdis the tenant_idfrom the JWT, same class as the other two products.What changed
organizationId: slug →nullThe CRM derives the support organization key as
context.organizationId ?? "${issuer}:${externalInstallationId}", and ticket visibility is organization-scoped (listExternalSupportTickets(userId, organizationId)). Sending the slug meant:organizationIdand merge two customers into one support organization.organizationId(organizationName ?? (organizationId ? null : workspaceName)).A Console tenant is both the customer and the workspace, so
nullis 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 nameThe 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 byassertTenantContext, and falls back to the email if the lookup fails — a display name must never block a handoff.Entry-point predicate
isSupportReachable()only checksSUPPORT_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. NewisSupportEntryPointEnabled()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./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 andbuild-deploy.yamlis entirely commented out. It ships as a self-hosted image, anddocker-compose.ymlpassed no support variables, so there was no way to enable Support from the path Console actually uses. It now passesSUPPORT_BASE_URL,SUPPORT_CRM_API_URL,SUPPORT_HANDOFF_SECRETandDEPLOYMENT_MODE=onpremthrough..env.examplecorrected to describe the real behaviour.Tests
20 new tests, and the two support endpoints are now off the
test:endpointsuncovered list:src/__tests__/unit/support-handoff.test.ts— identity contract (assertsorganizationIdis 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 --noEmitshows only pre-existing errors (stale.next/types,pdf-to-img,agentService) · 3175 tests pass · lint clean on all touched files.npm run buildfails onModule not found: 'pdf-to-img'— a declared dependency missing from localnode_modules, unrelated to this change.