[APPS-2792] Wire local Node execution into the real dev server - #473
Conversation
- npm run dev's /__dd/executeAction now runs backend functions via executeScriptLocally (a real forked child process) instead of the cloud preview-async round trip, per the design doc's Rollout section: no customer-facing toggle, local execution is the new unconditional default. - Preserves today's cloud round trip as a new, additive /__dd/executeActionViaCloud endpoint rather than deleting it -- no `npm run dev:verify` script exists yet to replace it (that requires create-apps scaffolding template changes, outside this package), and the existing dev-server.test.ts suite has extensive coverage of the cloud path that would otherwise be silently orphaned. - /__dd/executeAction no longer requires Datadog credentials to be configured, unlike /__dd/executeActionViaCloud -- local execution's $.Actions calls are stubbed today (see local-execution.ts), so there's nothing for it to authenticate yet. - Wires killAllLocalExecutionChildren() (from #471) into the dev server's own shutdown via server.httpServer's 'close' event, so killing the dev server mid-execution doesn't leave an orphaned child process. - Registers local-exec-child.js in the package's buildPlugin.toBuild config (same mechanism apps-runtime.ts already uses) so it's actually copied into the published dist/src output next to the compiled bundle. local-execution.ts resolves it at runtime via a __dirname-relative path, which only works if the file exists on disk next to the bundle -- found by running a real npm-linked build against a scaffolded app, since the existing jest tests resolve local-execution.ts's source directly and never exercise the published package layout. $.Actions calls remain stubbed until the single-action execution endpoint resolves (design doc's Open Dependency section) -- this is explicitly not customer-ready yet, same as the rest of this PR stack. Milestone 2 of APPS-2792's local Node execution kickoff plan.
@datadog/apps-backend's buildRuntimeFromJsFunctionWithActions is injected
into every backend function's bundle when that package is installed,
regardless of which specific function is called, and requires
$.Source.{initiator,runAsUser} to each be a User object with non-empty
id/orgId strings. Local execution's $ context never included Source,
so any app with @datadog/apps-backend installed (the create-apps scaffold
default) failed on its very first request -- not just functions that
call $.Actions, which was the only gap previously documented.
Confirmed by tracing the real, published package's bundled validation
logic and dd-source's production path: Source is populated upstream of
wf-actions-worker (the app-builder API layer, from the authenticated
caller's identity) before a bundled function ever runs -- it's plain
caller-supplied JSON by the time it reaches the script template, not a
server-signed value the runtime cross-checks. The validator only checks
shape (non-empty id/orgId strings), not a live session, so a synthetic
identity fully satisfies it.
Uses one placeholder identity for both initiator and runAsUser, since
on-behalf-of impersonation isn't meaningful when a single developer is
running their own code locally. No existing whoami-style endpoint exists
in this package's auth layer to fetch a real identity instead; real
identity resolution (if ever wanted) is a separate, non-blocking
enhancement, not a gap in this fix.
Verified against the real npm package (not a mock) via a new integration
test reusing the existing apps_backend_project fixture, and against a
real npm-linked scaffolded app calling getExecutionUser()/
getInitiatingUser() through /__dd/executeAction.
ff7de2b to
7c9ad51
Compare
|
Superseded: a team sync (2026-08-04, Terrapin) decided to drop The endpoint-split shape this PR introduced ( Closing, not deleting the branch, in case anything needs to be referenced later. |
Motivation
npm run devrequest path for the first time. Before this PR,local-execution.tswas fully built and tested but unreachable from any actual dev-server request.npm run devunconditionally uses local execution; the cloud round-trip becomes a separately-purposed command."Architecture
/__dd/executeActionis the customer-facing default and is now unconditionally local — no config field, no flag, per the design doc's rollout decision./__dd/executeActionViaCloudis new and additive: it preserves today's exact cloud round-trip (sameexecuteScriptViaDatadog/pollQueryExecutioncode, untouched) under a distinctly-purposed endpoint. Nonpm run dev:verifyCLI command exists yet to give customers an ergonomic way to reach it — that's now tracked as Milestone 3 in the kickoff doc, not deferred — and the existing test suite has deep coverage of this path that would otherwise be silently orphaned.Changes
/__dd/executeActionnow runs backend functions viaexecuteScriptLocally(a real forked child process) instead of the cloud round-trippackages/plugins/apps/src/vite/dev-server.ts/__dd/executeActionrequires no auth, since local execution's$.Actionscalls are stubbed today (see Follow-ups)packages/plugins/apps/src/vite/dev-server.ts/__dd/executeActionViaCloudendpoint preserves the originalexecuteScriptViaDatadogcloud round-trip byte-for-byte, including its auth gatepackages/plugins/apps/src/vite/dev-server.ts/__dd/executeActionViaCloud(the endpoint that actually needs credentials) instead of the old/__dd/executeActionnamepackages/plugins/apps/src/vite/dev-server.tskillAllLocalExecutionChildren()(from #471) into the dev server's own shutdown viaserver.httpServer'scloseeventpackages/plugins/apps/src/vite/index.ts/__dd/executeActionViaCloudinstead of/__dd/executeAction, preserving full existing coveragepackages/plugins/apps/src/vite/dev-server.test.ts/__dd/executeActionto assert against real local-execution bundle content instead of a stale nock mockpackages/plugins/apps/src/vite/dev-server.test.ts/__dd/executeActionViaCloudpackages/plugins/apps/src/vite/dev-server.test.tspackages/plugins/apps/src/vite/dev-server.test.tslocal-exec-child.jsinbuildPlugin.toBuildso it's copied into the publisheddist/srcoutput next to the compiled bundlepackages/plugins/apps/package.json$context now includes aSource: { initiator, runAsUser }object with a synthetic local-dev identity, so@datadog/apps-backend's injected runtime-context setup no longer throwspackages/plugins/apps/src/vite/local-exec-child.jsapps_backend_projectfixture (real@datadog/apps-backend) confirms a function callinggetExecutionUser()/getInitiatingUser()runs cleanly under local executionpackages/plugins/apps/src/vite/local-execution.integration.test.tsQA Instructions
yarn eslint packages/plugins/apps/src/vite/dev-server.ts packages/plugins/apps/src/vite/dev-server.test.ts packages/plugins/apps/src/vite/index.ts # Expected: no output, clean exit ✅ VERIFIEDManual QA — real dev server, both endpoints
Setup (one app covers every scenario below —
@datadog/apps-backendand@datadog/action-catalogare scaffold defaults). Three terminals are used in total: Terminal A builds this branch and is done after that; Terminal B runs the dev server and stays occupied by it; Terminal C is opened later, once the dev server is running, to send requests to it.Terminal A — folder: this repo (
~/dd/build-plugins):Terminal A isn't needed again after this — the built plugin is linked by absolute path below, not by anything left running in this terminal.
Terminal B — folder: a fresh scaffolded app outside this repo (
~/qa-app):Still Terminal B, still in
~/qa-app:Still Terminal B, still in
~/qa-app— this command blocks and keeps running, so leave it in the foreground here:# 4. Start local execution — no dd-auth, fixed port npm run dev -- --port 5180 --strictPortTerminal C — open a new terminal, folder:
~/qa-appagain (Terminal B is now occupied by the running dev server; everything below runs here instead):Local execution tests:
Timing check:
Shutdown cleanup — no orphaned child process (still Terminal C, until the Ctrl-C note below):
Switch to Terminal B (where
npm run devis still running in the foreground) and press Ctrl-C to stop it. Then back in Terminal C:pgrep -f local-exec-child.js # Expected: no output (child was killed with the dev server) ✅ VERIFIEDCloud round trip — the same
$QACHECK/$DOUBLENUMBER/$STREAMLOGSvariables from step 6 stay valid — the registered name only depends on the file's relative path, not the running server instance.Terminal B — folder: still
~/qa-app— restart with credentials, fixed port:Terminal C — folder: still
~/qa-app:Action-catalog build-time validation — no server needed, so Ctrl-C Terminal B's dev server first. Runs in either terminal, folder
~/qa-app:Blast Radius
npm run dev's/__dd/executeActionnow executes locally by default, unconditionally, per the design doc's rollout decision — no feature flag, matching the "no toggle" recommendation (Datadog's own internal canary/experiment gate, if wanted before this becomes the default for all customers, is out of scope for this PR; see design doc's Rollout section)./__dd/executeActionViaCloud) — no existing caller of the old/__dd/executeActionendpoint is silently broken without an equivalent path forward, though callers relying specifically on cloud execution at the old URL will need to switch to the new one.$.Actionscalls made during local execution remain stubbed (per [APPS-2792] Prototype: local Node execution for backend functions #461/[APPS-2792] Harden local Node execution: signal reporting, shutdown cleanup, edge-case coverage #471) — real action side effects are only available via/__dd/executeActionViaCloudtoday. This is a known, called-out gap tracked against the single-action-execution endpoint dependency, not something this PR silently papers over.@datadog/apps-backendSource-context gap found during this PR's manual QA (see above) is now fixed and re-verified against a real scaffolded app — local execution is safe as the default for apps using that package. The synthetic local-dev identity meansgetExecutionUser()/getInitiatingUser()won't reflect a real user locally (no live-session identity is available without the credentials local execution deliberately doesn't require) — acceptable for now since nothing in the design doc's scope depends on that identity being real, but worth knowing if a backend function's own logic ever branches on it.Out of Scope / Follow-ups
$.Sourceuses a synthetic local-dev identity, not a real user's —getExecutionUser()/getInitiatingUser()won't reflect real identity locallynpm run dev:verifyas a distinctly-named command for the cloud round-tripnpm run devdefaults to local execution is a real workflow regression, not just a missing nicety. Two PRs needed: mode-aware routing in this package (packages/plugins/apps/), plus acreate-appstemplate change inweb-ui(owned by@DataDog/app-builder-high-code) to add the script and an example backend function./__dd/executeActionViaCloudis reachable today via a manual curl call in the meantime.bump.yamlmajor bump +MIGRATIONS.mdentry)$.Actionsexecution during local runsDocumentation