Repository navigation
feat(telemetry): add an execution viewer to the desktop, VS Code, and MCP hosts - #1117
Closed
ParidelPooya wants to merge 19 commits into
Closed
ParidelPooya wants to merge 19 commits into
ParidelPooya wants to merge 19 commits into
Conversation
Adds lambdaExecutions.ts: list durable functions and their executions, fetch one execution with its paged history folded into an operation tree, and start or stop an execution. ExplorerSession exposes these as messages so every host gets the same execution viewer.
A new Executions area lists durable functions, a function's executions, and one execution's operation tree, payloads, invocations, and history, read from the Lambda API. A running execution refreshes every 4 seconds and can be stopped after a confirmation. Query and SQS result rows that carry an execution ARN get an Open link into the viewer.
Rows from query and list_executions that hold an execution ARN gain a viewerUrl column, and get_execution gains a viewerUrl field. The link opens a read-only page with the execution's operations, payloads, invocations, and history from the Lambda API, reloading while it runs. The page is served on first use from 127.0.0.1 only. It rejects foreign Host headers and requests a browser marks cross-site, accepts only GET and HEAD, and sends a no-script CSP. DURABLE_TELEMETRY_VIEWER_DISABLED turns it off and DURABLE_TELEMETRY_VIEWER_PORT fixes its port.
Clients that defer tool schemas show the agent only tool names, so a bare request such as "list the last 10 executions" had nothing pointing at this server. The instructions say what the server is for, which tool to start with, and to link executions to their viewerUrl.
Rows now carry viewerLink, a ready-made Markdown link titled with the execution name, or "show execution" when there is none, in place of the raw viewerUrl. list_executions returns the execution name on every destination so the title is available. The server instructions, tool descriptions, and skill tell the agent to show these links whenever it lists executions, without being asked, and never to print the raw URL.
#1115 removed the renderer test runner and its CI step along with its only test. The execution viewer adds renderer tests, so this restores both.
The prompt gains a "Don't ask again for these credentials" checkbox. It counts only with the confirm box, and saves a hash of the destination settings, region, and profile to ~/.durable-telemetry/accepted-credentials.json (mode 0600). Later sessions with the same settings skip the prompt; changing any of them asks again. An S3 + Athena configuration warning is always shown, because a saved answer covers the fixed notice only.
The viewer served any execution ARN a caller named, in any account or region the credentials reach, and had no bound on its work, so a few requests for a large execution ran the process out of memory. - Serve only executions this process linked, plus the child executions their pages link to; any other ARN gets 404 with no AWS call. - Run at most four fetches at once (503 beyond that), share one fetch per execution, stop history at 8 MB, and cap payload text at 64 KB each and 2 MB per page, with history rows capped at 2,000. - Reject a foreign Origin, any Sec-Fetch-Dest other than a page or its stylesheet, and absolute-form request lines. Answer HEAD with no AWS call. Send Cross-Origin-Opener-Policy. - Accept only letters, digits, '-', '_', '.' and '/' after the /durable-execution/ marker, encode ( ) ! * ' ~ in viewer URLs, and collapse control characters and escape < > and backticks in link text, so a link cannot end early. - Write the consent file through a random temp name opened with wx, refuse a symlinked directory, and tighten an existing loose one to 0700. - Rename the checkbox to "Don't ask again" and rewrite the README's exposure section to match.
Concurrent requests for one execution already shared the fetch, but each rendered and encoded its own copy of the page, which can be megabytes. They now share one encoded page.
Serializing each history page to measure it allocated a second copy of every payload. Summing the payload lengths plus a fixed per-event overhead measures the same thing without the copy.
A caller that knew one linked execution could hold unbounded memory: each request after a fetch finished built a new page, a client that stopped reading kept it alive with no timeout, and names and rows were rendered without a cap. A Lambda call that went silent also held a fetch slot forever. - Reuse a finished page for 5 s (16 pages at most), which also stops a looping caller from calling Lambda on every request. - Refuse with 503 when pages not yet read by browsers would pass 64 MB, counting a shared page once; close a response with no write progress for 60 s. - Cap each rendered field at 256 characters (ARNs and errors at 2,048) and the operations table at 2,000 rows. - Fail each Lambda request after 30 s (requestTimeout with throwOnRequestTimeout), and make the Busy page reload after 5 s. - Link get_execution only when the record is found, using the record's ARN, and let at most 1,000 children per page into the linked set. - Count Name, Id, SubType and ParentId in the history byte estimate. - Strip C1, bidi, zero-width and BOM characters from link text. - Log a failed "Don't ask again" save to stderr. - Correct stale comments, a test name, and the README's limits, and name the query-literal case the allowlist cannot stop.
Opening an execution from a query row while another function's list was loading left that loading state set, so the breadcrumb page never loaded. A start reply that landed after the user closed the modal moved them to the new execution.
The viewer's other checks detect how a browser behaves, and the allowlist still let any local process read an execution whose linked ARN it knew. Every execution page now needs this process's session token: 32 random bytes made at startup, kept in memory, and carried in each link (/s/<token>/execution?arn=...). The first request with it sets an HttpOnly, SameSite=Strict cookie named for the port, and a page load is redirected to the URL without the token, so it stays out of browser history. A request with neither the token nor the cookie gets 403 before any AWS call. Tokens are compared in constant time. The header checks, allowlist, and limits stay as a second layer. This is the scheme Jupyter and the VS Code server use for loopback servers opened from a terminal link.
… gaps Browsers send a cookie to every port on a host, so the session cookie reached any other server on 127.0.0.1 the browser loaded, and another server could shadow it to make every link fail. Chrome also kept the token URL in history despite the redirect. The token now lives only in the URL path: no cookie and no redirect. Child links are relative and the meta refresh reloads the same URL, so both keep the token. - Show callback IDs in full (up to 2,048 characters); they are about 364. - End a Lambda response body that stalls after its headers (socketTimeout) and abort a whole page fetch after 60 s across calls and retries, which then shows the 502 page. - Start the 60 s delivery deadline after the page is written, so a client waiting on a slow fetch is not cut off, and track pipelined responses through their connection. - Re-link the parent after its children, so viewed pages are not evicted. - Drop expired cache entries, and do not cache a page whose history failed. - Correct the README to match.
A reply for one function's start could close a newer start modal for another function and move the user. Replies are now dropped unless they name the function of the open start, closing the modal clears its in-progress state, and the ref is written in an effect.
…rectly Fixes #1118. - A failure with RetryDetails.NextAttemptDelaySeconds is a scheduled retry: the operation is PENDING, with the time of its next attempt and the error of the attempt that failed, not FAILED with an end time. - A Started event clears the previous attempt's error, so a step that succeeded on retry no longer shows an error next to its result. - attempt counts Started events; the service's CurrentAttempt runs one behind that count in every recorded history. - An operation still open when the execution ended is INCOMPLETE rather than shown as running, and an operation with no Started event has no duration rather than 0 ms. - When history reading fails part-way, both viewers say the history is incomplete instead of claiming operations are not shown, and the truncation notice says shown operations may look unfinished. - An empty execution status no longer makes the MCP page reload forever or the extension show Live. Against all 146 recorded histories: 0 attempt mismatches (was 30), 0 succeeded operations carrying an error (was 3), 0 operations left running in an ended execution (was 34).
…t saves - A function reached from an execution in another region is listed and started in that region, not the configured one. - Start new execution keeps the version or alias the execution ran on, instead of switching to $LATEST, and the modal names it. - Concurrent "Don't ask again" saves from several sessions take a lock file, so none is lost (was 11 of 30 keys lost across rounds).
- Add a minimal hook runtime (src/testing/fakeReact.ts) so the renderer's test runner can test useExecutionViewer without a DOM, and test its stale-reply, polling, region, version, and start-reply rules. - Run renderer tests in subfolders: outputs keep their path under src, and a missing bundle now fails the run instead of being skipped. - The renderer repeats core's execution-ARN rules, and detects the ARN column the same way the MCP server does; a parity test checks the ARN rules agree with core (0 disagreements, was 5).
… button A running execution's page reloaded itself every 5 seconds. Each reload read the execution and its whole history from Lambda again, and reset the reader's scroll position and opened sections. The page no longer reloads itself. A Refresh button, with an inline icon so the page still loads nothing but its stylesheet, loads it again on demand and skips the 5-second page cache, so a click always shows current data. The Busy and load-error pages get a Try again link instead of reloading. The VS Code and desktop views are unchanged: they update in place.
Contributor
Author
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.
Problem
The telemetry tools only read what the exporter wrote to a destination. None of them show one execution's operations as they ran. To see a step's result, a retry, or which invocation failed, a user has to leave the tools and call the Lambda API by hand. In the MCP server, an agent can return execution ARNs but has nothing to link them to.
Change
lambdaExecutions.tsreads durable functions and executions from the Lambda API (ListDurableExecutionsByFunction,GetDurableExecution,GetDurableExecutionHistory, plusInvokeandStopDurableExecution). It pages history up to 10,000 events and folds it into an operation tree, with retries, wait end times, callback IDs, and chained-invoke targets. Each call goes to the region in the execution ARN.ExplorerSessionexposes these as messages, so the VS Code and desktop hosts share one code path.queryandlist_executionsthat hold an execution ARN gain aviewerLinkcolumn, andget_executiongains aviewerLinkfield. Each is a Markdown link titled with the execution name, orshow executionwhen there is none.list_executionsnow returns the execution name on every destination. The link opens a read-only page served by the server, with a Refresh button to load the latest state. The server now sendsinstructionsat connect time, so clients that defer tool schemas route a bare "list the last 10 executions" here and show the links without being asked.~/.durable-telemetry/accepted-credentials.json(mode 0600). Changing any of them asks again, and an S3 + Athena configuration warning is always shown.The local viewer server (MCP)
The page shows execution payloads fetched with the user's credentials, so only the user's own browser should reach it.
127.0.0.1only. There is no setting to widen the bind./s/<token>/execution?arn=…). Child links and the Refresh button are relative, so both keep it. Anything without it gets 403 before any AWS call. There is no cookie: browsers send a cookie to every port on a host, so a cookie would hand the token to any other server on 127.0.0.1 the browser loads. This is the scheme Jupyter and the VS Code server use for loopback servers opened from a terminal link. The checks below stay as a second layer.get_executionlinks only when the record is found.querylinks every ARN its rows hold, so an agent can still link an ARN by selecting it as a literal; the README names this.Origin, anySec-Fetch-Destother than a page or its stylesheet, and absolute-form request lines.-,_,., and/, and viewer URLs encode( ) ! * ' ~, so a link cannot end early in Markdown. Link text drops C0/C1 controls, bidi overrides, and zero-width characters, and escapes[ ] \ < >and backticks.startExecutionorstopExecution.DURABLE_TELEMETRY_VIEWER_DISABLED=trueturns it off.DURABLE_TELEMETRY_VIEWER_PORTfixes the port.Decisions
webview-uibefore publishing.attemptcounts Started events, and operations still open in an ended execution show as INCOMPLETE.Fixes #1118.
Viewer correctness
Testing
typecheck:hosts, biome, and both host builds pass.aws-cn, andaws-us-govARNs get 404 with 0 Lambda calls (were 200). 200 parallel requests for 200 linked executions: 4 served, 196 got 503, peak 4 fetches in flight, 241 MB RSS (before: the process died of heap exhaustion at 100). 200 parallel requests for one execution: all 200 served from 1 fetch, 259 MB. One execution of 10,000 events x 256 KB: a 2.5 MB page at 160 MB RSS (before: a 525 MB page at 3.4 GB). A foreignOriginwithout fetch metadata gets 403 (was 200), HEAD makes no Lambda call, and absolute-form requests get 400.get_executionfor an ARN not in telemetry no longer makes it servable (404, 0 Lambda calls; was 200). A 64 KBNamein every history event stops paging after one page (was 10,000 events and a 502). A real Lambda client against a server that never answers fails within the timeout.lsofshowed the listener on127.0.0.1only.Notes for reviewers
lambda:GetDurableExecutionandlambda:GetDurableExecutionHistory. Without them the page shows the AWS error and the tools still work.