Skip to content

[APPS-2792] Add: wire local execution into the real dev server - #481

Draft
tyffical wants to merge 3 commits into
tiffany.trinh/apps-2792-harden-local-execution-v2from
tiffany.trinh/apps-2792-wire-into-dev-server
Draft

[APPS-2792] Add: wire local execution into the real dev server#481
tyffical wants to merge 3 commits into
tiffany.trinh/apps-2792-harden-local-execution-v2from
tiffany.trinh/apps-2792-wire-into-dev-server

Conversation

@tyffical

@tyffical tyffical commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Motivation

Architecture

createDevServerMiddleware now routes the two execution endpoints down genuinely different paths — one bundle-free and in-process, one bundling and cloud-bound — that only reconverge at the shared submitQuery/pollQueryExecution helpers once an $.Actions call needs to reach the real Datadog API:

POST /__dd/executeAction                    POST /__dd/executeActionViaCloud
        │                                            │
        ▼                                            ▼
handleExecuteAction                       handleExecuteActionViaCloud
        │                                            │
        ▼                                            ▼
executeScriptLocally               bundleBackendFunction (vite build,
  (local-execution.ts)               in-memory, no bundling on the
        │                            executeAction path anymore)
        │ loadModule =                        │
        │ server.ssrLoadModule                ▼
        │ (direct import of the      executeScriptViaDatadog
        │  customer's *.backend.ts,            │
        │  no bundling)               wraps the whole bundled script as
        │                              a jsFunctionWithActions query
        ▼                                       │
runs in this process                            │
        │                                       │
        │ $.Actions call?                       │
        ▼                                       │
makeExecuteActionRemotely                       │
  (single-action preview-async                  │
   query: {fqn, inputs, connectionId})          │
        │                                       │
        └────────────────┬──────────────────────┘
                          ▼
              submitQuery + pollQueryExecution
           (POST + long-poll api.<site>/api/v2/
              app-builder/queries/preview-async)

The executeAction path never bundles at all — executeScriptLocally imports the customer's real file directly via loadModule (Vite's own ssrLoadModule, so it gets the same TS-transform/resolve rules and HMR-aware module cache a real request gets) and runs the exported function in this process. No auth check happens until the function actually calls $.Actions; that call becomes its own direct single-action preview-async query via makeExecuteActionRemotely, rather than being wrapped in a whole-script query. The executeActionViaCloud path is the unchanged production round trip: bundle the whole function with Rollup, wrap it as a jsFunctionWithActions query, and submit/poll it the same way. See the RFC's Proposed Solution for the design-level version of this split.

Changes

What changed File
/__dd/executeAction now looks up the requested function and runs it directly via executeScriptLocally — no bundling on this path at all. /__dd/debugBundle and the cloud round trip (/__dd/executeActionViaCloud) are unchanged and still bundle. dev-server.ts
makeExecuteActionRemotely now forwards connectionId into the single-action preview-async query spec ({fqn, inputs, connectionId}) instead of silently dropping it. dev-server.ts
createDevServerMiddleware takes a new loadModule: LoadModule parameter, threaded from vite/index.ts's configureServer(server) as server.ssrLoadModule.bind(server) — the real Vite dev server's own module loader, giving the local path the same TS-transform/resolve rules and HMR-aware module cache a real request gets. dev-server.ts, vite/index.ts
Added a config() hook returning ssr: { noExternal: [...] } for @datadog/apps-backend/@datadog/action-catalog. Found while testing: both ship ESM-only, and Vite's dev server externalizes node_modules by default (a plain require(), for speed) — which throws Cannot use import statement outside a module the first time a customer's function actually uses either SDK locally. noExternal forces Vite's SSR transform pipeline to handle them instead, matching how the production bundling path already inlines every dependency. vite/index.ts
Real end-to-end test: spins up an actual Vite dev server (createServer, middleware mode, no port bound) rooted at the same apps_backend_project fixture, and lets its real ssrLoadModule import a real .backend.ts file directly — no mocked bundler, no mocked loadModule. Confirms a real @datadog/apps-backend typed import resolves $.Source correctly through this exact path. dev-server.integration.test.ts (rewritten)
New/updated unit tests: 400/404 for the local path, running with no auth configured at all (a function that never calls $.Actions), a clear error when a function does call $.Actions with no auth configured, the single-action preview-async request-body shape now including connectionId, and the new config() hook's ssr.noExternal contract. Existing cloud-path tests unchanged aside from the new loadModule parameter threaded through every createDevServerMiddleware call. dev-server.test.ts, index.test.ts

QA Instructions

yarn install
yarn test:unit packages/plugins/apps
# Expected: Test Suites: 24 passed / Tests: 314 passed ✅ VERIFIED
yarn workspace @dd/apps-plugin run typecheck
# Expected: no output, clean exit ✅ VERIFIED
npx eslint packages/plugins/apps/src/vite/dev-server.ts packages/plugins/apps/src/vite/dev-server.test.ts packages/plugins/apps/src/vite/dev-server.integration.test.ts packages/plugins/apps/src/vite/index.ts packages/plugins/apps/src/vite/index.test.ts --quiet
# Expected: no output, clean exit ✅ VERIFIED

No manual local/staging QA beyond the automated real end-to-end test above (a real Vite dev server, real ssrLoadModule, real @datadog/apps-backend fixture): this branch isn't merged to main yet, so there's no released package to link into a real scaffolded app and click through. Manual QA against a real npm run dev session is planned once this stack is closer to landing.

Blast Radius

  • This is the first PR in the stack that changes customer-visible behavior: npm run dev's /__dd/executeAction now executes locally by direct import, with no bundling step, instead of round-tripping to the cloud. Still gated behind this whole stack not being released yet (no version bump, no bump.yaml trigger in this PR).
  • The existing cloud round trip is fully preserved, just moved to a new URL (/__dd/executeActionViaCloud) — nothing currently calling /__dd/executeAction in production exists yet (this endpoint isn't released), so there's no live caller to break.
  • The ssr.noExternal config change affects every Vite dev-server session this plugin runs in, not just the local-execution path — low risk in practice (it only forces two specific, already-known-to-this-plugin packages through the transform pipeline instead of externalizing them), but worth noting as a config-surface change.
  • Risk: medium — this is the PR that actually flips the execution model for any consumer of this endpoint once released, even though today there is none.

Out of Scope / Follow-ups

Item Status Next step
npm run dev:verify CLI (mode-aware routing to /__dd/executeActionViaCloud, web-ui template changes) Not started Milestone 3, separate PRs (build-plugins + web-ui)
Real manual QA against a scaffolded app Deferred Once this stack is closer to landing / released
A genuine local @datadog/action-catalog fixture package for a typed-import e2e test Deferred Reasonable, cheap follow-up — not required for this coverage to be meaningful, since both SDKs funnel through the identical $.Actions routing

Documentation

@datadog-official

datadog-official Bot commented Aug 7, 2026

Copy link
Copy Markdown

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

🚦 1 Pipeline job failed

Continuous Integration | Linting   View in Datadog   GitHub Actions

🔧 Fix in code. This looks caused by changes in this PR. Duplicate function implementation found in src/vite/local-execution.test.ts at lines 382 and 453. Linting errors: 'readOwnArgsAfterDelay' is already defined and test title is used multiple times in the same describe block.
📋 Copy prompt for your agent
CI on my pull request is failing. Help me find and fix the root cause of each failing job below — they were flagged as caused by changes in this PR, so focus on the diff. For each job, explain the failure and propose a fix.

Branch: tiffany.trinh/apps-2792-wire-into-dev-server

Continuous Integration | Linting
Commit: fbc2aae70078686df998b605d705f87e4c70886b
Error (code / build):
Duplicate function implementation found in src/vite/local-execution.test.ts at lines 382 and 453. Linting errors: 'readOwnArgsAfterDelay' is already defined and test title is used multiple times in the same describe block.
CI job: https://github.com/DataDog/build-plugins/actions/runs/31529585085/job/93906119351

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 1c7f1e0 | Docs | Datadog PR Page | Give us feedback!

Wires the new direct-import local-execution path (local-execution.ts)
into the real Vite dev server: threads server.ssrLoadModule through as
the loadModule dependency, drops the bundling step from
/__dd/executeAction entirely (debugBundle and executeActionViaCloud
still bundle, unchanged), and forwards connectionId end-to-end through
makeExecuteActionRemotely so a $.Actions call naming a specific
connection actually reaches it instead of being silently dropped.

Also forces @datadog/apps-backend and @datadog/action-catalog through
Vite's SSR transform pipeline (ssr.noExternal) rather than letting the
dev server's default node_modules externalization `require()` them
directly -- both ship ESM-only, so an externalized `require()` throws
"Cannot use import statement outside a module".
dev-server.test.ts and local-execution.test.ts (build-plugins#480/#484)
each defined their own near-identical LoadModule resolver double. Factor
the common resolve-or-throw logic into moduleResolverFor in the shared
mocks helper so both can build on it instead of duplicating it.
…C-proxy transform

server.ssrLoadModule(func.absolutePath) went through the same transform
hook that rewrites *.backend.ts into the client-side RPC-proxy stub
(globalThis.DD_APPS_RUNTIME.executeBackendFunction(...)) — so local
execution's "real" import was actually still the proxy stub, which
crashes immediately since that global doesn't exist server-side. Every
existing test mocked loadModule directly, so none of them exercised the
real transform pipeline and caught this.

Mark local execution's own load with a query suffix (matching Vite's own
?raw/?url convention) and have the transform hook skip proxy generation
for that specific marked request, deferring to Vite's normal TS/esbuild
transform instead. Checking the marker rather than the generic
Vite-supplied options.ssr flag keeps this from also affecting any other,
unrelated future SSR-context load of the same file.
tyffical added a commit that referenced this pull request Aug 11, 2026
…function body

server.ssrLoadModule(func.absolutePath) goes through the same transform
hook (vite/index.ts) that rewrites *.backend.ts into the client-side
RPC-proxy stub — so local execution's "real" import can actually still be
the proxy stub, which crashes since globalThis.DD_APPS_RUNTIME doesn't
exist server-side. Every existing test here mocks loadModule directly, so
none of them exercise the real transform pipeline and would catch this.

Append the same query-suffix marker introduced in #481 (matching Vite's
own ?raw/?url convention) so the shared transform hook can recognize this
specific request and skip proxy generation for it. The transform-hook
side of this fix lives in #481, since that's where local execution is
actually wired to a real, plugin-registered dev server — this PR only
needs its own call site and mocks to stay consistent with that contract
so the two branches reconcile cleanly whichever merges first.
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