Skip to content

[APPS-2958] Add: list each backend function's action calls in the debugBundle response - #607

Draft
tyffical wants to merge 1 commit into
tiffany.trinh/apps-2947-connection-ids-fail-closedfrom
tiffany.trinh/apps-2958-debug-bundle-action-list
Draft

tyffical wants to merge 1 commit into
tiffany.trinh/apps-2947-connection-ids-fail-closedfrom
tiffany.trinh/apps-2958-debug-bundle-action-list

Conversation

@tyffical

@tyffical tyffical commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Architecture

flowchart LR
    REQ["POST /__dd/debugBundle, Accept: application/json"] --> NB["Nested Vite build of the function"]
    NB --> MG["Module-graph collector: app module records"]
    NB --> FQ["FQN collector: catalog export to fqn"]
    MG --> W["Action-catalog calls, one walk"]
    FQ --> W
    W --> IDS["allowedConnectionIds"]
    W --> ACT["actions, grouped by fqn"]
    IDS --> RES["code, allowedConnectionIds, actions?"]
    ACT --> RES
    RES --> DC["Dev client"] --> T["Transport"] --> MSG["run-query bundle, validated copy"]
Loading
interface DebugBundleResponse {
    code: string;
    allowedConnectionIds: string[];
    // UX hint, not a security boundary
    actions?: Array<{ fqn: string; connectionIds: string[] }>;
}
  • Scope: the same as allowedConnectionIds: direct @datadog/action-catalog calls in the function's .backend.ts file and the app modules it imports. One entry per FQN, ordered by fqn, with the distinct connection IDs bound at its call sites (empty when none).
  • FQN source: read from the catalog module the build loaded, not the import path, which 143 of 3,132 exports in @datadog/action-catalog@0.0.7 don't match (aws/agent exports com.datadoghq.aws.bedrock.agent.*).
  • Absent, not partial: actions is omitted when any call found has no known FQN, such as a default import or a nested namespace member.
  • Forwarding: the dev client and the frame transport copy a well-formed list into bundle by index; a malformed list drops only actions, never the bundle.

Changes

8 changes across protocol.ts, dev-server.ts, the AST helpers, the client and tests
What changed File
Add DebugBundleAction and optional actions on DebugBundleResponse packages/plugins/apps/src/backend/protocol.ts
Map each catalog module export to the one com.datadoghq.* FQN it calls packages/plugins/apps/src/backend/ast-parsing/action-catalog-fqns.ts
Expose the action-catalog call walk; connection IDs derive from it packages/plugins/apps/src/backend/ast-parsing/extract-connection-ids-from-module-graph.ts
Group calls by FQN, or return undefined on an unknown FQN packages/plugins/apps/src/backend/ast-parsing/list-action-calls.ts
Record catalog modules' export FQNs during the nested build packages/plugins/apps/src/vite/action-catalog-fqn-collector.ts
Walk calls once per bundle and return actions in the JSON response packages/plugins/apps/src/vite/dev-server.ts
Forward a validated, index-copied actions with the framed bundle packages/plugins/apps/src/backend/client/guards.ts, dev-client/dev-client.ts, post-message-transport.ts
Unit, client and real-dev-server tests; the fixture catalog gains http/http and a two-action backend *.test.ts, packages/tests/src/_jest/fixtures/

QA Instructions

yarn build:all
# Expected: exit 0 ✅ VERIFIED

TMPDIR=<a dir under /private/tmp> yarn test:unit packages/plugins/apps/src --testPathIgnorePatterns network-guard
# Expected: Test Suites: 44 passed; Tests: 979 passed ✅ VERIFIED (macOS needs a TMPDIR outside /var; CI on Linux doesn't)

yarn test:unit packages/plugins/apps/src/vite/network-guard.test.ts
# Expected: Tests: 129 passed, 129 total ✅ VERIFIED (run without TMPDIR set)

yarn lint && yarn typecheck:all && yarn cli integrity
# Expected: all exit 0, integrity leaves no extra changes ✅ VERIFIED
  • Live dev server against the published @datadog/action-catalog@0.0.7 (extracted into qa-app/node_modules/@datadog/action-catalog), with the vite plugin built from this branch and no auth.

  • qa-app/backend/notify.backend.ts:

import { createAgent } from '@datadog/action-catalog/aws/agent';
import * as chat from '@datadog/action-catalog/slack/chat';

const SLACK_CONNECTION_ID = 'conn-slack';

export async function notify(channel: string) {
    await chat.deleteChat({ inputs: { channel, ts: '1' }, connectionId: SLACK_CONNECTION_ID });
    await chat.deleteChat({ inputs: { channel, ts: '2' }, connectionId: 'conn-slack-backup' });
    return createAgent({ inputs: { agentName: 'qa' } as never, connectionId: 'conn-aws' });
}
  • qa-server.mjs:
// Starts a real Vite dev server for qa-app with the vite plugin built from the worktree.
import { createRequire } from 'module';
import path from 'path';
import { pathToFileURL } from 'url';

const [worktree, appRoot, port] = process.argv.slice(2);
const requireFromWorktree = createRequire(path.join(worktree, 'package.json'));
const { createServer } = await import(pathToFileURL(requireFromWorktree.resolve('vite')).href);
const { datadogVitePlugin } = await import(
    pathToFileURL(path.join(worktree, 'packages/published/vite-plugin/dist/src/index.mjs')).href
);

const server = await createServer({
    configFile: false,
    root: appRoot,
    logLevel: 'info',
    server: { port: Number(port), strictPort: true },
    plugins: [datadogVitePlugin({ apps: {} })],
});
await server.listen();
console.log(`qa-server listening on ${port}`);
node qa-server.mjs "$WORKTREE" "$PWD/qa-app" 5281 &
QN=$(curl -s http://localhost:5281/backend/notify.backend.ts | grep -o '[0-9a-f]\{64\}\.notify' | head -1)

curl -s -o qa-json.txt -w "HTTP %{http_code} %{content_type}\n" -X POST http://localhost:5281/__dd/debugBundle \
  -H 'Content-Type: application/json' -H 'Accept: application/json' -d "{\"functionName\":\"$QN\"}"
# Expected: HTTP 200 application/json; keys [code, allowedConnectionIds, actions] ✅ VERIFIED

curl -s -o qa-plain.txt -w "HTTP %{http_code} %{content_type}\n" -X POST http://localhost:5281/__dd/debugBundle \
  -H 'Content-Type: application/json' -d "{\"functionName\":\"$QN\"}"
grep -o 'com\.datadoghq\.[A-Za-z.]*' qa-plain.txt | sort -u
# Expected: HTTP 200 text/plain; code === qa-json.txt's code; exactly the two FQNs listed in actions ✅ VERIFIED
  • Captured qa-json.txt (code elided):
{
  "code": "<2212 chars, identical to the text/plain response>",
  "allowedConnectionIds": ["conn-aws", "conn-slack", "conn-slack-backup"],
  "actions": [
    { "fqn": "com.datadoghq.aws.bedrock.agent.createAgent", "connectionIds": ["conn-aws"] },
    { "fqn": "com.datadoghq.slack.chat.deleteChat", "connectionIds": ["conn-slack", "conn-slack-backup"] }
  ]
}

Blast Radius

  • Low: dev server only. actions is an optional field on the JSON debugBundle response and the framed bundle; the plain-text response, the production build and allowedConnectionIds are unchanged.
  • No feature flag: the field is additive and dev-only.
  • Each dev bundle also parses the loaded modules that contain a com.datadoghq. string.

Out of Scope / Follow-ups

10 items deferred
Item Status Next step
Raw and computed $.Actions calls Not listed: only direct @datadog/action-catalog import calls are recognized, as for connection IDs Recognize static $.Actions.a.b(...) chains if the host page needs them
Catalog functions re-exported through a local module Not listed, as for connection IDs Follow re-export bindings in the shared call walk
Per-function lists for a file exporting several functions The list covers the whole file, like allowedConnectionIds Per-export reachability, if needed
Which actions are writes Left to the host page Host page classifies by FQN
A call with no connectionId beside bound calls to the same FQN Its entry lists only the bound IDs Revisit if the consent UX needs an "unbound" marker
Parse only the action-catalog modules the calls import Deferred: every bundle parses each loaded package module naming a com.datadoghq. action, executeActionViaCloud bundles included Run the FQN parse after the call walk, on the imported modules only
hasPackageExportFqns can fire or stay quiet wrongly Deferred: it checks for any package FQN table, so it misses one changed module beside working ones, and fires when the catalog resolves inside the app root Warn per call whose imported module has no table
Narrow isDebugBundle to omit the unvalidated actions Deferred: the guard types actions without checking it; both callers re-validate it through copyDebugBundleActions Narrow the guard to Omit<DebugBundleResponse, 'actions'>
One source for allowed connection IDs Deferred: the dev server derives them with getConnectionIds(collectActionCatalogCalls(...)) while the production build calls BackendConnectionIdCollector.getAllowedConnectionIds; identical today, but a filter added to one wouldn't reach the other Add a collector method both paths call
Reassigned top-level function declarations Deferred: they count as stable exports, unlike let/var, so a reassigned one could map to the wrong FQN; affects only the action-list hint Treat reassigned function declarations like let

Documentation

None: the response shape is documented on DebugBundleResponse.

🤖 Generated with Claude Code

@datadog-official

datadog-official Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: e881537 | Docs | View more details | Give us feedback!

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2947-debug-bundle-connection-ids branch from 403cc08 to 2e8c7d4 Compare October 23, 2026 05:20
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2958-debug-bundle-action-list branch from c8122aa to 19ec355 Compare October 23, 2026 05:20
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2947-debug-bundle-connection-ids branch from 2e8c7d4 to cac186d Compare October 23, 2026 14:52
@tyffical
tyffical changed the base branch from tiffany.trinh/apps-2947-debug-bundle-connection-ids to tiffany.trinh/apps-2947-connection-ids-fail-closed October 23, 2026 14:53
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2958-debug-bundle-action-list branch from 19ec355 to 447c145 Compare October 23, 2026 14:53
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2947-connection-ids-fail-closed branch from e64de90 to 23be3cd Compare October 23, 2026 15:59
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2958-debug-bundle-action-list branch from 447c145 to bc68d52 Compare October 23, 2026 15:59
…dle response

POST /__dd/debugBundle's JSON response gains an optional actions list:
one { fqn, connectionIds } entry per action statically called through
@datadog/action-catalog imports in the function's file and the modules
it imports, the same calls allowedConnectionIds comes from, ordered by
fqn. The dev server builds the list only for the JSON response, from the
connection-ID collector's module records and an FQN collector that reads
each action's FQN from the catalog module the bundle loaded, since the
import path alone doesn't name the action. Each bundle walks the calls
once, for both allowedConnectionIds and the list. The FQN collector
reuses an app module's parsed record and logs a module it can't read at
debug level.

The list is omitted, with a debug log, when any call's FQN is unknown.
When a function calls actions but no bundled package module maps an
export to an action FQN, the dev server warns once per middleware, since
the action-catalog build output may have changed shape.

The dev client and the frame transport forward a well-formed list with
the bundle, copied by index; a malformed list is dropped and the bundle
is still sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2958-debug-bundle-action-list branch from bc68d52 to e881537 Compare October 23, 2026 16:22
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