feat(conversations): log in as customer directly from support ticket - #66053
Conversation
|
Reviews (1): Last reviewed commit: "feat(conversations): log in as customer ..." | Re-trigger Greptile |
|
Size Change: -5.24 kB (-0.01%) Total Size: 64.9 MB 📦 View Changed
ℹ️ View Unchanged
|
af2d0e2 to
ce47321
Compare
7ff302a to
1982a19
Compare
6d842b4 to
ae3e1a9
Compare
1982a19 to
481ec72
Compare
ae3e1a9 to
cb934a4
Compare
🤖 CI report
|
| Root | Eager (shipped) | Δ vs base | Budget |
|---|---|---|---|
entry (logged-out pages, app bootstrap)src/index.tsx |
1.24 MiB · 22 files | no change | ███░░░░░░░ 27.5% of 4.51 MiB |
authenticated shell (every logged-in page)src/scenes/AuthenticatedShell.tsx |
8.20 MiB · 3,001 files | 🔺 +2.3 KiB (+0.0%) | ████████░░ 84.4% of 9.71 MiB |
🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
Largest files eagerly shipped from src/index.tsx
| Size | File |
|---|---|
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 24.6 KiB | ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js |
| 6.3 KiB | ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js |
| 4.5 KiB | ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js |
| 3.9 KiB | ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js |
| 1.4 KiB | ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js |
| 1.3 KiB | src/RootErrorBoundary.tsx |
| 912 B | ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js |
| 789 B | src/scenes/ChunkLoadErrorBoundary.tsx |
| 762 B | src/index.tsx |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 281.3 KiB | ../node_modules/.pnpm/posthog-js@1.405.2/node_modules/posthog-js/dist/rrweb.js |
| 267.7 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 236.0 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 224.4 KiB | ../node_modules/.pnpm/posthog-js@1.405.2/node_modules/posthog-js/dist/module.js |
| 167.1 KiB | src/queries/validators.js |
| 154.3 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 106.2 KiB | src/lib/api.ts |
| 94.0 KiB | ../packages/quill/packages/quill/dist/index.js |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479
✅ Toolbar bundle — eager 2.18 MiB within budget
What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.
| Metric | Size | Δ vs base | Budget |
|---|---|---|---|
| Eager (shipped) entry + static imports |
2.18 MiB · 17 files | no change | ████░░░░░░ 38.1% of 5.72 MiB |
| Deferred (lazy) | 2.07 MiB · 33 files | no change | n/a — loads on demand |
Loader dist/toolbar.js |
1.1 KiB | no change | █░░░░░░░░░ 5.8% of 19.5 KiB |
Largest eagerly-shipped chunks
| Size | File |
|---|---|
| 713.2 KiB | dist/toolbar/toolbar-app-DW4OA6K5.css |
| 543.1 KiB | dist/toolbar/chunk-chunk-DZIY6ZHS.js |
| 484.2 KiB | dist/toolbar/chunk-chunk-OA2ZAIIW.js |
| 133.7 KiB | dist/toolbar/chunk-chunk-LCXEX372.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-T5KY5WYR.js |
| 71.0 KiB | dist/toolbar/toolbar-app-X5CCAKF6.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-27JL52RE.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-FWYAUBW5.js |
| 20.9 KiB | dist/toolbar/chunk-chunk-KJSY6JVG.js |
| 12.2 KiB | dist/toolbar/chunk-chunk-PIK3PADE.js |
Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile
✅ Dist folder size — 🔺 +16.3 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1356.66 MiB · 🔺 +16.3 KiB (+0.0%)
⚠️ Backend coverage — 96.0% of changed backend lines covered — 8 uncovered
🧪 Backend test coverage
Patch coverage — changed backend lines (products + core): ███████████████████░ 96.0% (231 / 239)
| File | Patch | Uncovered changed lines |
|---|---|---|
products/conversations/backend/api/widget.py |
85.7% | 65–66 |
ee/admin/loginas_views.py |
92.9% | 45, 63, 175–176, 189, 229 |
🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29923624437 -n patch-coverage), or the coverage-data block at the end of this comment.
Per-product line coverage (touched products)
| Product | Coverage | Lines |
|---|---|---|
platform_features |
██░░░░░░░░░░░░░░░░░░ 12.1% |
7 / 58 |
batch_exports |
████████░░░░░░░░░░░░ 39.6% |
8,414 / 21,250 |
demo |
███████████░░░░░░░░░ 56.2% |
1,497 / 2,663 |
warehouse_sources_queue |
████████████░░░░░░░░ 59.2% |
148 / 250 |
tasks |
██████████████░░░░░░ 68.2% |
29,946 / 43,927 |
data_tools |
██████████████░░░░░░ 70.0% |
63 / 90 |
ai_gateway |
███████████████░░░░░ 75.0% |
9 / 12 |
signals |
████████████████░░░░ 78.9% |
20,159 / 25,545 |
cdp |
████████████████░░░░ 80.7% |
3,117 / 3,863 |
data_modeling |
█████████████████░░░ 82.8% |
5,529 / 6,675 |
wizard |
█████████████████░░░ 84.8% |
935 / 1,102 |
notebooks |
█████████████████░░░ 85.3% |
7,266 / 8,522 |
agent_platform |
█████████████████░░░ 86.4% |
3,807 / 4,405 |
actions |
█████████████████░░░ 86.6% |
717 / 828 |
cohorts |
█████████████████░░░ 87.4% |
4,426 / 5,063 |
product_tours |
██████████████████░░ 87.9% |
1,303 / 1,482 |
exports |
██████████████████░░ 88.4% |
6,949 / 7,861 |
engineering_analytics |
██████████████████░░ 88.9% |
5,507 / 6,196 |
business_knowledge |
██████████████████░░ 89.0% |
4,391 / 4,936 |
conversations |
██████████████████░░ 89.3% |
16,630 / 18,630 |
dashboards |
██████████████████░░ 89.4% |
5,983 / 6,693 |
visual_review |
██████████████████░░ 89.4% |
5,818 / 6,505 |
mcp_analytics |
██████████████████░░ 89.7% |
2,663 / 2,969 |
error_tracking |
██████████████████░░ 89.7% |
10,174 / 11,342 |
alerts |
██████████████████░░ 89.9% |
4,054 / 4,508 |
early_access_features |
██████████████████░░ 90.1% |
1,031 / 1,144 |
streamlit_apps |
██████████████████░░ 90.4% |
2,501 / 2,767 |
links |
██████████████████░░ 90.6% |
183 / 202 |
slack_app |
██████████████████░░ 90.7% |
9,025 / 9,948 |
marketing_analytics |
██████████████████░░ 91.0% |
11,792 / 12,964 |
stamphog |
██████████████████░░ 91.1% |
4,056 / 4,450 |
product_analytics |
██████████████████░░ 91.3% |
5,800 / 6,351 |
mcp_store |
██████████████████░░ 91.9% |
4,257 / 4,634 |
notifications |
███████████████████░ 92.6% |
1,011 / 1,092 |
ai_observability |
███████████████████░ 92.7% |
14,845 / 16,018 |
data_warehouse |
███████████████████░ 92.7% |
20,300 / 21,896 |
surveys |
███████████████████░ 93.0% |
5,730 / 6,163 |
web_analytics |
███████████████████░ 93.0% |
14,099 / 15,159 |
posthog_ai |
███████████████████░ 93.2% |
1,325 / 1,421 |
approvals |
███████████████████░ 93.3% |
3,426 / 3,671 |
reminders |
███████████████████░ 93.4% |
468 / 501 |
workflows |
███████████████████░ 93.5% |
6,337 / 6,775 |
managed_migrations |
███████████████████░ 93.8% |
1,220 / 1,300 |
legal_documents |
███████████████████░ 94.1% |
1,568 / 1,667 |
endpoints |
███████████████████░ 94.1% |
8,640 / 9,177 |
messaging |
███████████████████░ 94.3% |
2,682 / 2,845 |
revenue_analytics |
███████████████████░ 94.5% |
3,598 / 3,809 |
tracing |
███████████████████░ 94.5% |
2,670 / 2,826 |
skills |
███████████████████░ 94.5% |
2,893 / 3,061 |
review_hog |
███████████████████░ 94.6% |
6,808 / 7,193 |
logs |
███████████████████░ 95.4% |
9,935 / 10,416 |
experiments |
███████████████████░ 95.7% |
24,882 / 25,996 |
replay_vision |
███████████████████░ 96.1% |
14,857 / 15,461 |
growth |
███████████████████░ 96.1% |
3,233 / 3,364 |
annotations |
███████████████████░ 96.2% |
732 / 761 |
feature_flags |
███████████████████░ 96.5% |
17,110 / 17,734 |
user_interviews |
███████████████████░ 96.5% |
2,638 / 2,734 |
access_control |
███████████████████░ 96.8% |
851 / 879 |
warehouse_sources |
███████████████████░ 96.9% |
316,978 / 327,229 |
customer_analytics |
███████████████████░ 97.3% |
7,779 / 7,998 |
data_catalog |
███████████████████░ 97.4% |
2,367 / 2,429 |
analytics_platform |
████████████████████ 98.0% |
2,112 / 2,155 |
metrics |
████████████████████ 98.2% |
2,491 / 2,536 |
pulse |
████████████████████ 98.4% |
2,017 / 2,049 |
live_debugger |
████████████████████ 99.2% |
613 / 618 |
field_notes |
████████████████████ 99.4% |
158 / 159 |
Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.
cb934a4 to
473fec8
Compare
|
Reviews (2): Last reviewed commit: "add identity_verified checks before impe..." | Re-trigger Greptile |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, please remove the |
b9051e0 to
64125a9
Compare
🦔 Hogbox preview · ❌ build failedThe preview didn't come up for commit Previews are optional and never block merging. A failure here is often a hogland or tailnet hiccup rather than anything in your PR, so the check stays green and this comment is the status. |
🦔 ReviewHog reviewed this pull requestFound 2 must fix, 1 should fix, 1 consider. Published 4 findings (view the review). |
|
ReviewHog Alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏 |
There was a problem hiding this comment.
ReviewHog Report
Business logic
Issues: 2 issues
Files (6)
ee/admin/loginas_views.pyee/urls.pyposthog/middleware.pyposthog/api/authentication.pyposthog/settings/base_variables.pyproducts/conversations/backend/api/widget.py
What were the main changes
- New POST /admin/impersonation/from-ticket/ endpoint resolves a support ticket's customer to a User and starts a read-only impersonation session
- identity_verified gate: explicit False blocks login-as (400), null/true allowed; on verified widget tickets the target is resolved from the HMAC-attested distinct_id rather than the mutable email trait
- Cross-region handling: if the ticket's inferred region differs from the current deployment region, returns a redirect_url instead of impersonating locally (REGION_DOMAINS)
- New IMPERSONATION_TICKET_ID_SESSION_KEY session key persists the originating ticket id; /logout now redirects staff back to that ticket instead of the admin user page
- _configure_impersonation_session refactor consolidates session flag setting + analytics capture shared between loginas_user and the new ticket flow
- New POSTHOG_INTERNAL_TEAM_ID setting scopes ticket-based impersonation to PostHog's internal support project (with DEBUG-mode fallback to the staff user's current team)
- Widget API infers a region trait from the reporting app's URL for PostHog's internal support project, feeding the cross-region redirect logic
Frontend
Issues: 2 issues
Files (7)
frontend/src/layout/navigation/ImpersonationNotice/AdminLoginButtons.tsxfrontend/src/layout/navigation/ImpersonationNotice/adminLoginAs.tsfrontend/src/layout/navigation/ImpersonationNotice/impersonationNoticeLogic.tsfrontend/src/layout/navigation/ImpersonationNotice/ImpersonationNotice.tsxfrontend/src/layout/navigation/ImpersonationNotice/ImpersonationReasonModal.tsxproducts/conversations/frontend/scenes/ticket/StaffActionsPanel.tsxproducts/conversations/frontend/scenes/ticket/supportTicketSceneLogic.ts
What were the main changes
- AdminLoginButtons collapsed into a single direct-login button wired to a new initiateImpersonation flow, replacing the old multi-region admin-redirect buttons
- loginAsFromTicket (adminLoginAs.ts) calls the new backend endpoint via the existing admin OAuth2 handshake, handling cross-region redirect responses
- impersonationNoticeLogic: initiateImpersonation action, ReturnToTicketContext persisted across the post-login reload (keyed by email), canReturnToTicket/expiredSessionFromTicket selectors, and clearing of stale ticket context on staff load or mismatched customer
- ImpersonationNotice: 'Return to ticket #X' button replaces the logout dropdown for ticket-started sessions, same action offered on the session-expired overlay, and reason modals prefilled with 'Investigating ticket #X'
- ImpersonationReasonModal gains a loading state on the cancel/return button
- StaffActionsPanel shows a warning banner when the ticket's identity was never assessed and disables the login button with an explanation when verification failed
- supportTicketSceneLogic now passes ticketNumber and identityVerified into the ticket context consumed by the impersonation logic
49ec14d to
01c5565
Compare
Adds a direct in-app "log in as customer" flow from a support ticket: a backend endpoint (loginas_user_from_ticket) resolves the ticket's customer, enforces staff + identity_verified checks, handles cross-region redirects, and starts a read-only impersonation session that can return to the ticket. Rebased onto master and squashed. Direct in-app login supersedes the per-region admin-link approach shipped separately in #64903; master's other impersonation-tray reworks (change user, ordered members, session expiry) are preserved. Squashed commits: - feat(conversations): log in as customer directly from support ticket - chore(conversations): address review feedback on ticket impersonation - return to ticket on ImpersonationNotice - drop stored return-to-ticket on mismatched impersonation - add identity_verified checks before impersonation - fix(conversations): make ticket impersonation testable in local dev - fix(conversations): guard cross-region redirect against unknown region values - fix(conversations): resolve verified ticket login-as from attested identity - fix(conversations): type _get_ticket return so mypy sees Ticket attributes Generated-By: PostHog Code Task-Id: c8c004e0-8c63-4a33-84e5-ff34688b8d18
Resolve every identity-verified ticket from the attested distinct_id — widget verification attests a PostHog distinct_id, while Slack/Teams/email attest an email into the same read-only field — so the mutable email trait can never repoint a verified ticket at another account. Gate the from-ticket session mutations on the login actually rebinding request.user, so a rejected attempt during an active impersonation can't report success or stamp the ticket onto an unrelated session. Return the resolved account's email so the return-to-ticket context doesn't rely on the mutable ticket trait, and handle a blocked cross-region popup with a manual open action. Generated-By: PostHog Code Task-Id: 4df0bc15-bab3-4238-a27e-9e2ac7399042
01c5565 to
238174b
Compare

Problem
When working a support ticket as PostHog staff, logging in as the customer meant leaving the ticket, opening the Django admin user search, finding the user, and impersonating from there. Slow, easy to lose the ticket's context, and there was no way back to the ticket afterwards.
This continues the work started in #48334, rebuilt on the current support layout, and builds on the
identity_verifiedtrust signal from #54628 (now merged).Changes
The ticket "Staff actions" tile's Login as customer button now starts a read-only impersonation session directly, and the impersonation notice knows how to get you back to the ticket when you're done.
Direct login from the ticket
/admin/impersonation/from-ticket/endpoint (ee/admin/loginas_views.py) resolves the ticket's customer email to a user, auto-fills the reason (Support ticket #<n>), and starts a read-only session. If the ticket originated in another region it returns a redirect to that region's admin instead of failing.regiontrait from the app URL (products/conversations/backend/api/widget.py), which drives the cross-region routing above.AdminLoginButtonsis a single direct-login button wired to a newinitiateImpersonationflow (loginAsFromTicketreuses the existing admin OAuth2 handshake fromadminLoginAs.ts). The old multi-region admin-redirect machinery was removed.Verified identity, US region:
CleanShot.2026-07-03.at.11.36.11.mp4
Identity unknown:
Unverified identity:
Return to ticket
/logoutfrom a ticket-initiated impersonation lands staff back on the ticket rather than the admin user page.{ticketNumber, email}context is persisted across the post-login reload and dropped whenever the stored ticket no longer matches the active impersonation (staff load, or a different customer impersonated via Django admin).Investigating ticket #X.Identity verification gate
Uses the nullable
identity_verifiedtrust signal on tickets (#54628) to close the confused-deputy gap where a ticket's claimed email is attacker-controlled:identity_verified = false(assessed, not attested) – login as is blocked: the button is disabled with an explanation, and the backend independently rejects with 400.identity_verified = null(never assessed) – allowed, with a warning banner telling staff to confirm they have the right customer.identity_verified = true– allowed, and the impersonation target is resolved from the ticket's HMAC-attesteddistinct_id(matchingUser.distinct_idor email against the attested value), never from the email trait – which stays customer-mutable on every widget message even after verification.How did you test this code?
I'm an agent (PostHog Code), human-driven by @luke-belton. Automated tests only – I did not manually click through the UI (a screen recording is coming separately).
impersonationNoticeLogic.test.ts, 43 pass):initiateImpersonationcases (cross-region opens the other region's admin, endpoint failure surfaces a toast, no-context guard is a no-op, success persists the return context),returnToTicketnavigation + loading state, parameterizedcanReturnToTicket/expiredSessionFromTicketgating (catches the stale-context bug where an old ticket would attach to an unrelated impersonation), and stored-ticket clearing on staff load / mismatched customer.TestLoginAsFromTicketintest_middleware.py, 13 pass locally): success path, logout returns to the ticket, non-staff → 404, unknown ticket → 404, no email → 400, no matching user → 404, cross-region redirect without impersonating, invalidticket_id, and a parameterizedidentity_verifiedgate (true → 200, null → 200, false → 400 with no session started – guards against a naive falsy check that would wrongly block null), attested-identity resolution (a verified ticket claiming someone else's email impersonates the attested identity, and an unresolvable attested identity 404s with no email fallback), and an unknown customer-supplied region falling back to local login instead of a 500. Plus parameterized widget region-inference tests intest_widget.py.typescript:check, oxlint, ruff, andty check(via lint-staged) pass.Note for reviewers: the red "Frontend Tests Pass" check is a draft-PR CI artifact, not a test failure – draft-PR jest selection packs the near-full reachable suite into a single 15-minute job that times out (reported to devex). Marking the PR ready runs the normal sharded fanout.
Automatic notifications
🤖 Agent context
Autonomy: Human-driven (agent-assisted) – DRI: @luke-belton
Built with PostHog Code (Claude). The starting point was #48334, ~6,700 commits behind with a closed conflicting PR; we branched fresh off master and re-implemented the intent onto the current
StaffActionsPanel+AdminLoginButtonslayout rather than replay six WIP commits.Decisions along the way:
AdminImpersonationMiddleware(feat(admin): keep admin panel usable while impersonating #56594) keeps/admin/*running as the staff user during impersonation, so staff can keep the ticket open in another tab.identity_verifiedgate deliberately lives on the ticket: the identity claim is only attested per submission (widget HMAC, SPF email, signed platform webhook), and a person-level flag would let a spoofed anonymous ticket inherit a real user's verification.is_impersonated_ticket_idon/api/users/@me/would be strictly better – candidate follow-up. (The backend target resolution itself no longer trusts the email trait on verified tickets – see the gate section.)identity_verifiedboolean toTicket#54628; after that merged it was rebased onto master, dropping the duplicated commits.Skills invoked:
/improving-drf-endpoints,/writing-tests,/writing-kea-logicsconventions,/debugging-ci-failures.Created with PostHog Code