Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: correlate web terminal sessions by client_session_id - #27677

Merged
aqandrew merged 48 commits into
mainfrom
devex-663-web-terminal-session-id
Aug 27, 2026
Merged

feat: correlate web terminal sessions by client_session_id#27677
aqandrew merged 48 commits into
mainfrom
devex-663-web-terminal-session-id

Conversation

@aqandrew

@aqandrew aqandrew commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What

Implements the web terminal client half of the
Connection log collection and correlation RFC
(DEVEX-663). Generates a per-session correlation ID and attaches it to the
web terminal's requests and client logs so a single session can be traced end
to end.

Stacked on #27671 (DEVEX-659, the coderd tracing middleware that reads the
client_session_id baggage). Review/merge that first.

Changes

Session ID

  • 16-byte value encoded as a 32-character hex string, per RFC requirement 1.
    Generated with the generateConnectionSessionId function added in chore(site): add generateConnectionSessionId helper function #27935
  • Minted once per web terminal session: on TerminalPage load and on
    AgentsPage terminal panel mount. Unlike the reconnection token, it is not
    persisted in the URL, so a reload (or a new tab) is a new session, matching the
    RFC's session definition.

Propagation

  • HTTP API request: the reconnecting-pty signed-token request carries the ID
    via W3C baggage (baggage: client_session_id=<hex>), which the DEVEX-659 middleware
    reads.
  • PTY WebSocket: browsers cannot set the baggage header on a WebSocket
    handshake (the codebase already works around this for the session token), so
    the ID is sent as a client_session_id query parameter instead. The reconnecting-pty
    WebSocket handler reads and validates it and attaches it to the request and PTY
    logs.

Client logs

  • The terminal's connection-error console.error logs now include client_session_id.

Telemetry: the web terminal emits none today, so there is nothing to tag
(confirmed with the issue reporter).

Testing

  • site: unit tests for generateSessionId (format + uniqueness) and
    terminalWebsocketUrl (query param). Updated TerminalPage.test.tsx (mocks the
    generator to a fixed ID and asserts the WebSocket URL includes client_session_id).
    tsc, Biome, and the React Compiler check pass.
  • coderd: go test ./coderd/tracing/... and go vet ./coderd/workspaceapps/...
    pass; new ValidSessionID export reused by the PTY handler.
Design notes / decision log
  • Session vs reconnection token. The existing reconnect token is
    deliberately persisted in the URL to survive reloads. client_session_id is the
    opposite: a fresh value per page load, matching the RFC (a reload is a new
    session). They are separate identifiers.
  • Per-request baggage, not a global axios default. The frontend axios
    instance is a singleton shared by the whole app; a global baggage default
    would tag unrelated requests. The header is attached only to the terminal's
    signed-token request.
  • WebSocket uses a query param. Browser WebSocket cannot send custom
    headers, so baggage is impossible on the PTY handshake. The client_session_id query
    parameter is the counterpart, read server-side in workspaceAgentPTY.
  • Server-side scope. Reading the query param is localized to the web
    terminal's own WebSocket handler rather than broadening the shared tracing
    middleware to trust query params on every route.
  • Validation. Both the baggage and query-param paths validate the value as a
    32-char hex string (tracing.ValidSessionID) before logging, to avoid logging
    arbitrary client-controlled input.
  • AgentsPage compiler constraint. AgentsPage is React Compiler optimized
    (no useMemo/useCallback), so the panel mints its ID with useState lazy
    init instead.
  • Out of scope (other RFC tickets): agent-side middleware, connection_logs
    columns, Tailnet state-change logging, and the CLI CODER_TRACE_SESSION_ID
    env var.

Opened by Coder Agents on behalf of @aqandrew.

aqandrew added 3 commits July 30, 2026 00:28
Read the session_id baggage member on API requests and attach it to the
per-request log context and, when tracing is enabled, as a span attribute.
The value is added to the log context even when tracing is disabled so logs
can always be correlated by session_id, per the connection-log RFC.

The session_id is validated as a 32-character hexadecimal string to guard
against logging arbitrary client-controlled baggage values.

Part of DEVEX-659.
…ge key

Reference the exported SessionIDBaggageKey constant when reading the baggage
member and when constructing baggage headers in tests, instead of repeating
the string literal. The emitted slog field and span attribute names remain
snake_case string literals ("session_id"), as required by the slog field-name
lint rule.
Generate a 16-byte session ID (32-char hex) per web terminal session and
attach it to the terminal's requests and client logs, per the connection-log
correlation RFC. This is DEVEX-663 and stacks on the coderd tracing middleware
change (DEVEX-659).

Frontend:
- Add generateSessionId and mint one session ID per web terminal page load
  (TerminalPage) and per AgentsPage terminal panel mount. Unlike the
  reconnection token, it is not persisted in the URL: a reload is a new
  session.
- Send it on the PTY WebSocket as a session_id query parameter (browsers
  cannot set the baggage header on a WebSocket handshake).
- Send it via W3C baggage on the reconnecting-pty signed-token API request.
- Include it in the terminal's connection-error console logs.

Backend:
- The reconnecting-pty WebSocket handler reads the session_id query parameter,
  validates it, and attaches it to the request and PTY logs so the WebSocket
  session correlates. Export tracing.ValidSessionID for reuse.
@linear-code

linear-code Bot commented Jul 30, 2026

Copy link
Copy Markdown

DEVEX-663

aqandrew commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Comment thread site/src/pages/TerminalPage/TerminalPage.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/TerminalPanel.tsx Outdated
Comment thread site/src/api/api.ts Outdated
Comment thread coderd/workspaceapps/proxy.go Outdated
Comment thread site/src/utils/sessionId.ts Outdated
aqandrew and others added 2 commits July 30, 2026 08:41
The connection-log RFC was updated to mandate lowercase hexadecimal for
session IDs so that case-sensitive searches correlate reliably. Tighten
validSessionID to reject uppercase and update the tests accordingly.
aqandrew added a commit that referenced this pull request Aug 11, 2026
- adds a `generateConnectionSessionId` helper function which reuses our
existing `generateRandomString` logic
- adds tests for `generateConnectionSessionId` in utils/random.test.ts
(utils/random.ts was previously untested)
- renames our existing `generateRandomString` function to be
`generateRandomBase64String`

additional context in
#27671 (comment)

This PR doesn't change any of our UIs. The new
`generateConnectionSessionId` function is a piece of frontend plumbing
related to DEVEX-663. I originally implemented #27677 so that web
terminal connections to workspaces would be identified by uuids--but
according to the RFC for DEVEX-663, those connection session ids should
be lowercase hexadecimal strings (not uuids).
@aqandrew
aqandrew marked this pull request as ready for review August 11, 2026 16:50
Add a test asserting the baggage key, log field name, and span attribute
name all equal "session_id". slog field names must be snake_case string
literals, so the log field and span attribute cannot reference the
SessionIDBaggageKey const directly; this test guards against them drifting
apart and silently breaking log/trace correlation (PR review P2).
…utes

The middleware extracts session_id only on matched API/app routes. Add a
subtest that sends well-formed baggage to a non-matching path (/index.html)
and asserts session_id is absent from both the log fields and the span, so a
regression that logged client-controlled baggage on every request would be
caught (PR review P3).
The negative span assertions checked only that a specific value was not set,
so a bug setting session_id to a different derived value would pass. Scan the
span attributes for any key equal to session_id and assert its absence in the
malformed-baggage and non-matching-route cases (PR review P3).
Use hex.DecodeString to check the value is 16 bytes of hex, matching the
codebase convention, then require hex.EncodeToString(b) == s so only the
canonical lowercase encoding is accepted (hex.DecodeString also accepts
upper-case) (PR review nit).
Fold recordingTracer into fakeTracer via an optional recording span, and
add a tracing-enabled + no-baggage subtest that pins the branch which must
not set an empty session_id span attribute or log field.
…dleware' into devex-663-web-terminal-session-id

# Conflicts:
#	coderd/tracing/httpmw.go
Comment thread coderd/workspaceapps/proxy.go Outdated
The baggage is well-formed; it is the session ID value that is malformed.
Addresses review feedback on #27671.
The tracer provider is never nil in production (coderd defaults it to a
no-op provider when tracing is disabled), so drop the tracer == nil
special case. Default a nil provider to no-op and always start the span
and set the client_session_id attribute; the no-op tracer discards the
span. Addresses review feedback on #27671.
Browser WebSocket clients such as the web terminal PTY cannot set
arbitrary baggage headers. Extend the tracing middleware to read the
client_session_id from baggage first, then fall back to the
client_session_id query parameter. The query value passes the same
lowercase 32-hex validation, and baggage takes precedence when both are
present. This lands the session ID on both the log context and the
trace span for PTY requests.
@aqandrew
aqandrew requested a review from code-asher August 17, 2026 22:17
Each subtest builds its own fakeTracer and recordingSpan, and the
middleware handles each request synchronously on a single goroutine.
The span's attributes are read only after ServeHTTP returns on the same
goroutine, so there is a strict happens-before ordering and no shared
access. The sync.Mutex and defensive slices.Clone are therefore
unnecessary. Verified with go test -race.
…tracing-middleware

# Conflicts:
#	coderd/tracing/httpmw_test.go
Extract a sessionIDFromQueryString helper that mirrors
sessionIDFromHeaders: extract the client_session_id from its source,
validate it, and return an empty string when absent or malformed.
sessionIDFromRequest now composes the two extractors, keeping baggage
precedence. Each extractor stays self-validating so sessionIDFromHeaders
keeps its validated contract, which the agent SessionIDMiddleware relies
on. Adds a symmetric TestSessionIDFromQueryString internal test.
Base automatically changed from devex-659-session-id-tracing-middleware to main August 19, 2026 00:28
…l-session-id

# Conflicts:
#	site/src/pages/TerminalPage/TerminalPage.test.tsx
#	site/src/pages/TerminalPage/TerminalPage.tsx
Comment thread site/src/pages/AgentsPage/components/TerminalPanel.tsx Outdated
const { proxy } = useProxy();
const { metadata } = useEmbeddedMetadata();
const terminalRef = useRef<WorkspaceTerminalHandle>(null);
const [sessionId] = useState(() => generateConnectionSessionId());

@code-asher code-asher Aug 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried testing this panel but honestly I am not sure how to get one opened. Maybe I need an agent that has a workspace associated or something?

Do you know if this session ID would get regenerated if you close and re-opened the tab or anything like that? Not entirely sure how it would work with React, I think it might unmount it then we would reconnect and also have a new session ID even if the page has not been reloaded.

@aqandrew aqandrew Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I need an agent that has a workspace associated or something?

Yep that's correct:

return workspace && workspaceAgent ? (
<TerminalPanel

It's in the panel on the right, in the Terminal tab:

Screenshot 2026-08-20 at 6 02 46 PM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if this session ID would get regenerated if you close and re-opened the tab or anything like that? ... I think it might unmount it then we would reconnect and also have a new session ID even if the page has not been reloaded.

That's also correct, a new session ID would be generated whenever TerminalPanel remounts.

So in the situation of closing+reopening the terminal tab, reconnectionToken would persist. However, currently client_session_id does not persist since a new one would be generated. This is not

explicitly terminating the workspace connection

as described in the RFC. Do you think we should consider it the same session (i.e., persist the session ID) since we're reattaching to a live PTY? Then I think we'd have to derive/persist the session ID with reconnectionToken, instead of storing it in React state in TerminalPanel

@code-asher code-asher Aug 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think it should probably be the same session ID as long as we remain on that same page. The umount/destroy is more an implementation detail, from the user's perspective I would say it is still the same session, and it is more like just temporarily hiding the terminal than disconnecting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and that's how I implemented it. The client session ID now stays the same as long as you're on the page (no reload). I moved it off TerminalPanel's local useState into a module-level registry keyed by reconnectionToken, so a remount (switching chats, or hiding/showing the terminal) reuses the same ID and only a reload starts a new one.

One nuance worth calling out: "close/reopen" behaves differently depending on the tab, because of what handleCloseTab does:

Action reconnectionToken client_session_id
Built-in terminal close/reopen persists (it's the chat id; the tab is only hidden, not removed) same (reattaching to the live PTY)
User terminal tab close/reopen new (the tab is removed, so reopening mints a new token) new (a brand-new PTY)

So closing the built-in Terminal tab keeps the session (matches your "temporarily hiding" framing), while closing a user-created terminal tab and opening a new one is genuinely a new session.

Done in 86801ed: a reconnectionToken -> client_session_id registry that survives remounts and is cleared on reload.

This reply was generated by Coder Agents.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah cool yeah that makes sense, closing the terminal tab is like an explicit disconnect, so new ID. That makes sense to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on our Slack conversation for posterity, since it isn't captured in this PR: you pointed out that keying the client session ID off reconnectionToken couples it to the PTY identity. If someone later changes how the reconnection token is derived (so it's no longer the chat/agent id), that would quietly regenerate the session ID mid-session even without a page reload. You suggested generating the session ID at the page component instead, so it's tied to page load / navigating to and from the page, regardless of what the reconnection token does.

Agreed, that's a cleaner separation of concerns. I'm moving generation up to AgentChatPageView (keyed by agentId) and providing it to every TerminalPanel via context, and dropping the reconnectionToken-keyed registry. reconnectionToken goes back to meaning only "which PTY to reattach to," and the session ID means "this client's visit to the page."

Resulting behavior:

Event client_session_id
Switch tabs / terminal remount / websocket reconnect same
Built-in or user terminal close then reopen same
Switch to a different chat (agentId changes) new
Navigate away and back, or reload new

Note this supersedes the earlier table I posted: now both the built-in terminal and a user-created terminal keep the same ID across close/reopen, because it's one session per page visit rather than per PTY.

This reply was generated by Coder Agents.

…cross remounts

TerminalPanel generated the client_session_id with a local useState, so it
regenerated on every remount (switching chats, or hiding/showing the
built-in terminal tab) even though the terminal reattaches to the same live
PTY. Per PR review, the id should stay the same while the user remains on the
page and only reset on reload.

Move ownership into a module-level registry keyed by reconnectionToken. It
survives in-page remounts and resets on reload. The built-in terminal keeps
its token (the chat id) across hide/show, so it keeps its session id; a
user-created terminal tab is removed on close and mints a new token on
reopen, so it correctly gets a new session id.

Generated by Coder Agents.
@aqandrew
aqandrew requested a review from code-asher August 25, 2026 19:10
Comment thread site/src/pages/AgentsPage/utils/terminalClientSessionId.ts Outdated
… to the page

The client_session_id was keyed by reconnectionToken in a module-level
registry. That coupled the session identity to the PTY reconnection token, so
a future change to how the token is derived could silently regenerate the
session ID mid-session without a page reload.

Generate the ID once in AgentChatPageView (keyed by agentId) and share it with
every terminal through TerminalClientSessionContext. reconnectionToken now only
identifies which PTY to reattach to. The session ID stays stable across tab
switches, terminal remounts, and terminal close/reopen, and regenerates when
switching chats or reloading. Drop the reconnectionToken-keyed registry.

Generated by Coder Agents.
…l-session-id

# Conflicts:
#	site/src/pages/AgentsPage/AgentChatPageView.tsx
@aqandrew
aqandrew requested a review from code-asher August 26, 2026 05:52
@aqandrew
aqandrew merged commit 197c814 into main Aug 27, 2026
27 checks passed
@aqandrew
aqandrew deleted the devex-663-web-terminal-session-id branch August 27, 2026 19:55
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants