Skip to content

feat(telemetry): add an execution viewer to the VS Code and desktop UI - #1120

Closed
ParidelPooya wants to merge 5 commits into
feat/telemetry-mcp-execution-viewerfrom
feat/telemetry-ide-execution-viewer
Closed

ParidelPooya wants to merge 5 commits into
feat/telemetry-mcp-execution-viewerfrom
feat/telemetry-ide-execution-viewer

Conversation

@ParidelPooya

@ParidelPooya ParidelPooya commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The VS Code extension and the desktop app only show what the exporter wrote to a destination. Neither shows one execution's operations as they ran. To see a step's result, a retry, or which invocation failed, a user has to call the Lambda API by hand.

This is the second of two PRs. It is based on #1119, which adds getExecution to core. Merge that one first. This PR then shows only the UI changes.

Change

  • core: lambdaExecutions.ts gains three reads: listDurableFunctions, getFunctionInfo, and listExecutions (ListFunctions, GetFunctionConfiguration, ListDurableExecutionsByFunction). ExplorerSession exposes them and getExecution as messages. So the VS Code and desktop hosts share one code path.
  • VS Code and desktop: a new Executions area has three pages:
    1. The durable functions in the region.
    2. One function's configuration and its executions, newest first.
    3. One execution's summary, input, output, operation tree, invocations, and history. The history can be exported as CSV or JSON.
  • Live updates: a running execution is re-read every 4 seconds while it is on screen.
  • Links from results: query and SQS result rows that hold an execution ARN get an Open link.
  • Read only: the viewer cannot start or stop an execution. Core has no call that does either.
  • Renderer tests: refactor(telemetry): replace the write-access check with a notice #1115 removed the renderer test runner and its CI step, together with the only renderer test. This PR adds renderer tests again, so it restores both.

Decisions

  • The viewer reads the Lambda API, not the destination. So it works for any durable function, and for executions whose records were truncated or sampled out.
  • Each reply names what it answers. A reply names its function or execution ARN. A reply for a page the user has left is dropped. So a slow response cannot overwrite the page now showing.
  • A function keeps its region. A function reached from an execution in another region is listed in that region, not the configured one.

Testing

  • core 528, VS Code 64, desktop 48, MCP 577, and renderer 21 tests pass. typecheck:hosts, biome, the renderer typecheck, and both host builds pass.
  • The renderer's ARN check and ARN-column detection match core and the MCP server. A parity test fails if the rules disagree.
  • A minimal hook runtime runs the viewer's state hook without a DOM. Its tests cover polling, dropped stale replies, and the breadcrumb page loads.
  • The three viewer pages were rendered in headless Chrome with stub data.

Notes for reviewers

  • The viewer needs lambda:ListFunctions, lambda:GetFunctionConfiguration, lambda:ListDurableExecutionsByFunction, lambda:GetDurableExecution, and lambda:GetDurableExecutionHistory. Without them, each page shows the AWS error.

The UI gains a Lambda executions view: durable functions in the region, a
function's configuration and executions, and one execution's summary,
payloads, operation tree, invocations, and history. A running execution is
re-read every 4 seconds while it is on screen. Query result rows that hold
an execution ARN open it in the viewer.

The viewer only reads. It cannot start or stop executions.

The renderer tests run in CI again.
#1119 keeps it private because only its own file calls it there. The explorer session checks a renderer-supplied region with it.
ParidelPooya added a commit that referenced this pull request Oct 30, 2026
This is the first part of #1119, split out as the review there asked. It
adds only the core read. The MCP viewer and the VS Code and desktop
viewer (#1120) build on it in later pull requests.

## Problem

The telemetry tools read only what the exporter wrote to a destination.
They cannot show one execution's operations, retries and invocations as
Lambda recorded them.

## Cause

Core has no client for the durable-execution APIs that Lambda serves:
`GetDurableExecution` and `GetDurableExecutionHistory`.

## Who calls it

A tool that runs on the user's own machine: the VS Code extension, the
desktop app or the MCP server. The tool calls Lambda with the user's own
credentials, for an ARN that the user or the user's agent supplied. This
module accepts no request from another user, machine or app.

The user can already call the same APIs directly with the same
credentials. So the checks in this module are not a security boundary.
They catch mistakes: a value that is not an execution ARN, and a
response that would make the history read run without end. The file
header says the same.

## Options considered

1. **Read the destination's records instead.** Rejected. The exporter
writes one record per execution, and records can be truncated or sampled
out. A function without the telemetry plugin has none.
2. **Read history up to a cap.** Rejected. A cut history leaves
operations whose end was not read, and the fold cannot know their
status. The cap in #1119 (10,000 events) was also below what one
execution can hold.
3. **Chosen: read the whole history.** Lambda bounds its size (see
Decisions).

## Change

`packages/aws-durable-execution-sdk-js-telemetry-core` gains
`lambdaExecutions.ts`:

- `getExecution(ctx, arn)` reads the execution summary, then every
history page, 1,000 events per page. Both calls go to the region named
in the execution ARN. If the service returns the same marker twice, the
read stops with `historyError`.
- If a history page fails, the call still returns the summary.
`history`, `operations` and `invocations` are then empty, and
`historyError` gives the reason.
- `buildOperations(events)` folds the history into an operation tree.
Events that share an `Id` are one operation, and `ParentId` nests them.
- `buildInvocations(events)` returns one row per `InvocationCompleted`
event.
- `isDurableExecutionArn` and `regionFromExecutionArn` check the ARN's
shape and read its region. The partition must be `aws` or start with
`aws-`, and the account must be 12 digits.

The module only reads. It has no call that starts or stops an execution.
The package gains a dependency on `@aws-sdk/client-lambda`.

### How the fold reports status

1. A terminal event with `RetryDetails.NextAttemptDelaySeconds`
schedules another attempt. So the operation is PENDING, with
`nextAttemptTime`. This covers a step failure with a retry left, and a
`waitForCondition` check whose condition is not met yet.
2. A `*Started` event after a failure is a retry. It clears the earlier
attempt's error, the error's truncation flag and the next attempt time.
It keeps the result: a `waitForCondition` check's result is the state
the next check reads.
3. Each payload keeps its own truncation flag. `OperationNode` has
`resultTruncated` and `errorTruncated`. `InvocationRow` has
`errorTruncated`. `ExecutionDetail` has both when its result or error
comes from the history's end event; the `GetDurableExecution` summary
carries no flag.
4. The attempt count is the number of `*Started` events. It is set only
when it is above 1.
5. An operation still open in an ended execution is INCOMPLETE. The
history is always whole, so an open operation never ended.
6. The summary is read before the history. If the execution ends in
between, the history's terminal `Execution*` event gives the status, end
time, result and error. An operation's end does not count as the
execution's end.

Fixes #1118. That issue describes the fold in the unmerged viewer
branch. This fold has the fix from the start.

## Changes from the version in #1119

These address the review on #1119 and a first review round here:

- **The whole history is read.** #1119 stopped at 10,000 events. A cut
history then showed finished operations as INCOMPLETE, and an earlier
revision of this pull request showed them as RUNNING. Neither was true.
- **A failed history page returns no history.** A partial history has
the same problem as a cut one.
- **The attempt count uses Started events only.** The `CurrentAttempt`
branch handled "earlier events fell outside the history read". The
history is read oldest first, which is the API's default ("By default,
events are returned in chronological order",
`GetDurableExecutionHistoryRequest.ReverseOrder` in client-lambda). So
that case cannot happen (rule 4.4).
- **No character check on the ARN's execution part.** It existed to keep
`)` out of Markdown links. That belongs to the viewer, and the viewer
redesign uses an ID in each link instead of an ARN. The SDK
percent-encodes the whole ARN into the request path, so the path cannot
change. A consumer that puts an ARN into Markdown or HTML escapes it
itself.
- **No `maxHistoryEvents`, `maxHistoryBytes`, `deadlineMs` or
`requestTimeoutMs` options.** No caller in this pull request sets them
(rule 1.2). The viewer redesign states which limits it needs.
- **Removed branches for cases an oldest-first read cannot produce:** an
end event with no Started event before it, and two end events of one
operation with no Started between them. The 146 recorded histories have
0 of either in 3,096 events.

## Decisions (rule 7)

- **The history is read whole, with payloads.** Lambda allows at most
30,000 durable operations and 100 MB of persisted payload per execution,
and neither can be increased ([Lambda
quotas](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html)).
So a full read holds at most about 100 MB of payload.
- **Retries count toward the operation quota.** Each step attempt and
each `waitForCondition` poll is one durable operation ([announcement of
the 30,000
limit](https://aws.amazon.com/about-aws/whats-new/2026/10/aws-lambda-durable-gen-limit-increase/)).
So the 30,000 limit bounds the number of history events, and so the
number of calls.
- **A large execution takes many calls.** At most 1,000 events per page,
and `GetDurableExecutionHistory` allows 15 requests per second per
account and Region. An execution with 60,000 events takes about 60 calls
and at least 4 seconds. A caller that refreshes a running execution must
allow for that.
- **A history failure returns the summary with `historyError`.** The
summary is still correct. Failing the whole call would hide it.
- **A summary failure fails the call.** Without the summary there is
nothing to show.
- **A repeated marker stops the read.** Lambda is not expected to send
one. Without the stop, such a response would read the same page without
end, and the caller has no timeout to end it.
- **An event type the fold does not know is skipped.** It does not
create an operation or change one.
- **A malformed ARN is rejected before any AWS call.** The region comes
from the ARN, so a value without one cannot be routed.
- **`getExecution` has no timeout of its own.** It uses the SDK
defaults. A caller that needs a bound adds it with its own design.

## Assumptions about Lambda (rule 6)

I ran one execution on Lambda (us-east-1, `nodejs22.x`,
`@aws/durable-execution-sdk-js` 3.0.0) and read its history with
`GetDurableExecutionHistory`. The handler runs a step that fails once
and then succeeds, a step that fails both of its attempts, a
`waitForCondition` that checks three times, a map of two items, a wait,
and a chained invoke. The history is
`src/testdata/lambda-history-retries.json`, with the account id
replaced. A test folds it.

Verified against that history:

1. Each attempt of an operation writes one `*Started` event, before its
end event.
2. A failure with a retry left carries
`RetryDetails.NextAttemptDelaySeconds`, and the final failure does not.
3. A `waitForCondition` check whose condition is not met writes
`StepSucceeded` with `RetryDetails.NextAttemptDelaySeconds`. The last
check has none.
4. An end event carries the same `SubType` as its Started event.
5. `Execution*` events carry an `Id` that is not an operation id, and
`InvocationCompleted` events carry none.

Also observed:

- `RetryDetails.CurrentAttempt` equals the number of Started events on
Lambda. The example histories, which the testing library writes, have it
one lower. This pull request counts Started events, so it gives the same
number either way.
- Events arrive in time order, which is not always `EventId` order.
`WaitStarted` (id 30) came before the `ContextSucceeded` events of the
map (ids 27 to 29). The fold reads events in the order they arrive and
does not sort them.
- Read in pages of 10, the 37 events came in 4 pages. The last page had
no `NextMarker`, and no marker repeated.

Still unverified:

6. A history response holds about as much payload as the execution
persisted, so the 100 MB quota bounds it.
7. Once the summary reports an ended execution, the history already
holds the `Execution*` end event.
8. A malformed `NextAttemptDelaySeconds` (NaN or out of range) is read
as a final end. If a Started event then follows, the operation keeps
that end time while it runs. This needs a malformed value from Lambda.

## Testing

- core: 547 tests pass, 69 of them in `lambdaExecutions.test.ts`. MCP:
487 tests pass. Typecheck, `typecheck:hosts` and biome pass.
- One test uses a real `LambdaClient` against a local HTTP server. It
checks that both requests are signed for the ARN's region.
- With the code of the first revision of this pull request, 3 of the new
tests fail: the `awsfoo` partition, reading past 10,000 events, and the
failed later page.
- Rule 4 audit, round 2: an 82-mutation list that covers every condition
in the file. 74 of the 77 that still apply fail a test. The other 5
target code that this revision removed. The 3 survivors change no result
under the assumptions above: `executionEndOf` reading the first terminal
`Execution*` event instead of the last (assumption 7 allows only one);
not skipping events without an `Id` (assumption 5; the unknown-type skip
drops `Invocation*` events too); and taking `kind` from the first event
only (assumption 4).
- The real `LambdaClient` test clears
`AWS_IGNORE_CONFIGURED_ENDPOINT_URLS` and `AWS_PROFILE`, and points
`AWS_CONFIG_FILE` and `AWS_SHARED_CREDENTIALS_FILE` at a missing path.
With `AWS_IGNORE_CONFIGURED_ENDPOINT_URLS=true` set, and a preload that
refuses every non-local connection, it passes and connects nowhere else.
- Truncation: deleting any one of the 8 lines that set or clear a
truncation flag fails a test.
- `queryChokePoint.test.ts` classifies `getExecution` as a read that
takes an ARN, not a query.
@ParidelPooya

Copy link
Copy Markdown
Contributor Author

Closing. Its base, the #1119 branch, is closed, and it was written against the core read API before #1122. Building the viewer again on main is tracked in #1148, which lists what this pull request proposed and what to settle first. The branch feat/telemetry-ide-execution-viewer is kept, so its code can be reused.

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