Skip to content

[APPS-2961] Refactor: move local execution's side-effect-free helpers into their own module - #604

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 1 commit into
masterfrom
tiffany.trinh/apps-2961-shared-execution-helpers
Oct 28, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 1 commit into
masterfrom
tiffany.trinh/apps-2961-shared-execution-helpers

Conversation

@tyffical

@tyffical tyffical commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Changes

5 changes across local-execution-shared.ts, local-execution.ts and local-execution-shared.test.ts
What changed File
Moves the $.Actions validation, serializeActionInputs, assertJsonRoundTrippable and makeActionsProxy, with unchanged behavior, into a module with no import-time side effects. local-execution-shared.ts
makeActionsProxy takes a dispatch callback, so its caller decides where a call goes. local-execution-shared.ts
Splits the allowlist-free check out of validateActionCall as validateActionCallShape, and names the return-value error subject in returnValueSubject, for the next PR's child process. local-execution-shared.ts
Imports the moved helpers and passes the in-process dispatch to the proxy. local-execution.ts
Tests validateActionCallShape directly, including that it leaves the connection allowlist to its caller. local-execution-shared.test.ts

QA Instructions

  • All 8 local checks pass through the real Vite 6.4.3 dev server under Node 20.19.4, with this branch's built plugin and a mocked Datadog API, verified at 11b20006. Against a Datadog staging org, the same calls reach the real action runner: a $.Actions.http.request GET returns 200, and every refusal matches. Setup: build-plugins: Testing and QA Guide.
Check Result Evidence
Backend function call 200, in process {"hello":"qa","pid":18412}, dev server pid 18412
$.Actions call whose action fails 500 with the action's error Query execution failed: QA mock: the HTTP action returned 503 Service Unavailable
$.Actions inputs declaring allowedConnectionIds 500, refused must not declare its own allowedConnectionIds in inputs
$.Actions call on an unlisted connection 500, refused used connection "not-allowed", which is not in this function's allowed connections: []
$.Actions inputs holding a Map 500, refused Inputs to action $.Actions.http.request contains a Map (at "m")
$.Actions call with no arguments 500 No arguments provided to action $.Actions.http.request
Return value holding a Map 500, refused Local execution of "mapReturn"'s return value contains a Map
POST /__dd/debugBundle 200 626 characters of bundled script
yarn typecheck:all && yarn lint && yarn cli integrity
# Exits 0, with no file changes.
TMPDIR=/private/tmp yarn test:unit packages/plugins/apps
# All suites pass.
yarn workspace @datadog/vite-plugin build
# <qa>/app: the files in the details below, plus node_modules/vite linked to the checkout's own Vite.
cd <build-plugins checkout> && DD_API_KEY=qa-fake DD_APP_KEY=qa-fake node <qa>/dev-qa.mjs <qa>/app "$PWD"
# greet -> 200; callAction -> 500 with the mocked action error; the five refusals above -> 500; debugBundle -> 200.
  • On macOS, keep TMPDIR=/private/tmp: the manifest-tags and parameters suites fail under /var/folders, on master too.
Local QA app, helper script and output
// vite.config.mjs
import { datadogVitePlugin } from '<build-plugins checkout>/packages/published/vite-plugin/dist/src/index.mjs';

export default {
    plugins: [
        datadogVitePlugin({
            auth: { site: 'datadoghq.com', apiKey: 'qa-fake-api-key', appKey: 'qa-fake-app-key' },
            apps: { enable: true, identifier: 'qa-app604', name: 'qa-app604' },
        }),
    ],
};
// src/qa.backend.js
export async function greet(name) {
    return { hello: name, pid: process.pid };
}

export async function callAction() {
    return $.Actions.http.request({ inputs: { url: 'https://example.com' } });
}

export async function selfScope() {
    return $.Actions.http.request({ inputs: { url: 'https://example.com', allowedConnectionIds: ['x'] } });
}

export async function unlistedConnection() {
    return $.Actions.http.request({ inputs: { url: 'https://example.com' }, connectionId: 'not-allowed' });
}

export async function mapInput() {
    return $.Actions.http.request({ inputs: { m: new Map() } });
}

export async function noArgs() {
    return $.Actions.http.request();
}

export async function mapReturn() {
    return new Map([['a', 1]]);
}
// dev-qa.mjs
// Starts the real Vite dev server with the worktree's built plugin, Datadog API mocked, then calls
// backend functions through POST /__dd/executeAction and fetches one through POST /__dd/debugBundle.
// Usage: node dev-qa.mjs <appDir> <buildPluginsRepo>
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import path from 'node:path';

const [appDir, repoDir] = process.argv.slice(2);
const repoRequire = createRequire(path.join(repoDir, 'package.json'));
const { createServer } = await import(repoRequire.resolve('vite'));
const nock = repoRequire('nock');

const api = nock('https://api.datadoghq.com').persist();
api.post('/api/v2/app-builder/queries/preview-async').reply((_uri, body) => {
    const fqn = body?.data?.attributes?.query?.properties?.spec?.fqn;
    const id = fqn === 'com.datadoghq.datatransformation.jsFunctionWithActions' ? 'rc' : 'act';
    return [200, { data: { id } }];
});
api.get('/api/v2/app-builder/queries/execution-long-polling/rc').reply(200, {
    data: { attributes: { done: true, outputs: { data: { Source: {
        initiator: { id: 'qa-user', orgId: 'qa-org' },
        runAsUser: { id: 'qa-user', orgId: 'qa-org' },
    } } } } },
});
api.get('/api/v2/app-builder/queries/execution-long-polling/act').reply(200, {
    errors: [{ detail: 'QA mock: the HTTP action returned 503 Service Unavailable' }],
});
nock.enableNetConnect('127.0.0.1');

const server = await createServer({
    root: appDir,
    configFile: path.join(appDir, 'vite.config.mjs'),
    logLevel: 'warn',
    server: { host: '127.0.0.1', port: 5204 },
});
await server.listen();
const base = server.resolvedUrls.local[0].replace(/\/$/, '');
console.log(`vite dev server on ${base} (vite ${repoRequire('vite/package.json').version})`);

const stub = await fetch(`${base}/src/qa.backend.js`);
console.log(`GET /src/qa.backend.js -> ${stub.status}`);

const pathHash = createHash('sha256').update('src/qa').digest('hex');
const post = (route, name, args) =>
    fetch(`${base}${route}`, {
        method: 'POST',
        headers: { 'content-type': 'application/json', origin: base },
        body: JSON.stringify({ functionName: `${pathHash}.${name}`, args }),
    });

for (const [name, args] of [
    ['greet', ['qa']],
    ['callAction', []],
    ['selfScope', []],
    ['unlistedConnection', []],
    ['mapInput', []],
    ['noArgs', []],
    ['mapReturn', []],
]) {
    const response = await post('/__dd/executeAction', name, args);
    console.log(`POST /__dd/executeAction ${name} -> ${response.status} ${await response.text()}`);
}
const bundle = await post('/__dd/debugBundle', 'callAction', []);
const code = await bundle.text();
console.log(`POST /__dd/debugBundle callAction -> ${bundle.status} ${code.length} chars, contains "$.Actions.http.request": ${code.includes('$.Actions.http.request')}`);
console.log(`dev server pid ${process.pid}`);
await server.close();
vite dev server on http://127.0.0.1:5204 (vite 6.4.3)
GET /src/qa.backend.js -> 200
POST /__dd/executeAction greet -> 200 {"success":true,"result":{"data":{"hello":"qa","pid":18412}}}
[error|vite|apps] $.Actions call to "com.datadoghq.http.request" failed: Query execution failed: QA mock: the HTTP action returned 503 Service Unavailable
POST /__dd/executeAction callAction -> 500 {"success":false,"error":"Query execution failed: QA mock: the HTTP action returned 503 Service Unavailable"}
POST /__dd/executeAction selfScope -> 500 {"success":false,"error":"Action $.Actions.http.request must not declare its own allowedConnectionIds in inputs — this function's own allowlist already governs which connections it can use."}
POST /__dd/executeAction unlistedConnection -> 500 {"success":false,"error":"Action $.Actions.http.request used connection \"not-allowed\", which is not in this function's allowed connections: []"}
POST /__dd/executeAction mapInput -> 500 {"success":false,"error":"Inputs to action $.Actions.http.request contains a Map (at \"m\"), which JSON.stringify silently flattens to \"{}\" instead of serializing its entries — use a plain array or object instead."}
POST /__dd/executeAction noArgs -> 500 {"success":false,"error":"No arguments provided to action $.Actions.http.request"}
POST /__dd/executeAction mapReturn -> 500 {"success":false,"error":"Local execution of \"mapReturn\"'s return value contains a Map, which JSON.stringify silently flattens to \"{}\" instead of serializing its entries — use a plain array or object instead."}
POST /__dd/debugBundle callAction -> 200 626 chars, contains "$.Actions.http.request": true
dev server pid 18412
Staging QA against a Datadog staging org
  • The local app with auth: { site: process.env.DD_SITE, apiKey: process.env.DD_API_KEY, appKey: process.env.DD_APP_KEY }, this extra export in src/qa.backend.js, and dev-qa.mjs without the nock lines (port 5214, callActionGet added to its call list):
export async function callActionGet() {
    const response = await $.Actions.http.request({ inputs: { url: 'https://example.com', verb: 'GET' } });
    return { type: typeof response, keys: Object.keys(response ?? {}).sort() };
}
cd <build-plugins checkout> && DD_SITE=<staging site> DD_API_KEY=<staging API key> DD_APP_KEY=<staging app key> node <qa>/staging-qa.mjs <qa>/app-staging "$PWD"
vite dev server on http://127.0.0.1:5214 (vite 6.4.3)
GET /src/qa.backend.js -> 200
POST /__dd/executeAction greet -> 200 {"success":true,"result":{"data":{"hello":"qa","pid":<pid>}}}
POST /__dd/executeAction callAction -> 500 {"success":false,"error":"HTTP 400 Bad Request\nstatus: 400, id: <request id>, code: INVALID_INPUT_PARAMETER, title: error running query, detail: input parameter validation error: 1 error occurred:\n\t* missing properties: 'verb'\n\n"}
POST /__dd/executeAction callActionGet -> 200 {"success":true,"result":{"data":{"type":"object","keys":["body","headers","status"]}}}
POST /__dd/executeAction selfScope -> 500 {"success":false,"error":"Action $.Actions.http.request must not declare its own allowedConnectionIds in inputs — this function's own allowlist already governs which connections it can use."}
POST /__dd/executeAction unlistedConnection -> 500 {"success":false,"error":"Action $.Actions.http.request used connection \"not-allowed\", which is not in this function's allowed connections: []"}
POST /__dd/executeAction mapInput -> 500 {"success":false,"error":"Inputs to action $.Actions.http.request contains a Map (at \"m\"), which JSON.stringify silently flattens to \"{}\" instead of serializing its entries — use a plain array or object instead."}
POST /__dd/executeAction noArgs -> 500 {"success":false,"error":"No arguments provided to action $.Actions.http.request"}
POST /__dd/executeAction mapReturn -> 500 {"success":false,"error":"Local execution of \"mapReturn\"'s return value contains a Map, which JSON.stringify silently flattens to \"{}\" instead of serializing its entries — use a plain array or object instead."}
POST /__dd/debugBundle callAction -> 200 626 chars, contains "$.Actions.http.request": true

Blast Radius

  • No feature flag: a pure move, with no behavior change.
  • Low risk: only local-execution.ts imports the moved code, and its tests run unchanged.

Documentation

🤖 Generated with Claude Code

@datadog-official

datadog-official Bot commented Oct 21, 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: f1f4210 | Docs | View more details | Give us feedback!

@tyffical

Copy link
Copy Markdown
Contributor Author

@codex review

@tyffical
tyffical marked this pull request as ready for review October 21, 2026 20:07
@tyffical
tyffical requested a review from a team as a code owner October 21, 2026 20:07
@tyffical
tyffical requested review from Scott-Meyer and removed request for a team October 21, 2026 20:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused extraction preserves validation, serialization, and dispatch behavior, with no blocking issues identified.

No open issues 🎉

What changed in this PR

Extracts reusable local-execution helpers into a side-effect-free module, preparing for sandboxed backend calls without changing current behavior.

Changes:

  • Moves action validation, JSON checks, and proxy creation into a shared module.
  • Injects a dispatch callback while preserving existing in-process execution.
File Description
packages/​plugins/​apps/​src/​vite/​local-execution.ts Imports shared helpers and supplies guarded dispatch.
packages/​plugins/​apps/​src/​vite/​local-execution-shared.ts Exports reusable helpers without installing the global $ accessor.

Review effort: Balanced

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 144ff8af0e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T20:09:30.417651Z 144ff8a Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tyffical
tyffical added this pull request to stack #612 October 21, 2026 20:09
@tyffical
tyffical requested a review from sdkennedy2 October 21, 2026 20:16
@tyffical
tyffical removed this pull request from stack #612 October 23, 2026 13:10
@tyffical
tyffical added this pull request to stack #618 October 23, 2026 13:11
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-shared-execution-helpers branch from 144ff8a to 6bf6b18 Compare October 26, 2026 20:53
@tyffical
tyffical removed this pull request from stack #618 October 26, 2026 20:54
@tyffical
tyffical changed the base branch from master to tiffany.trinh/apps-2961-require-vite-7 October 26, 2026 20:54
@tyffical
tyffical added this pull request to stack #623 October 26, 2026 20:55
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-require-vite-7 branch from df0e1ca to a181de9 Compare October 26, 2026 21:29
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-shared-execution-helpers branch from 6bf6b18 to 9a36cab Compare October 26, 2026 21:32
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-require-vite-7 branch from a181de9 to 8cc274c Compare October 26, 2026 22:26
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-shared-execution-helpers branch from 9a36cab to b6220fb Compare October 26, 2026 22:30
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-require-vite-7 branch from 8cc274c to 9847ad6 Compare October 26, 2026 23:25
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-shared-execution-helpers branch from b6220fb to 5721b26 Compare October 26, 2026 23:28
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-require-vite-7 branch from 9847ad6 to ddbd01f Compare October 27, 2026 20:15
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-shared-execution-helpers branch from 5721b26 to 74f28bc Compare October 27, 2026 20:16
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-require-vite-7 branch from ddbd01f to ef2034f Compare October 28, 2026 05:39
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-shared-execution-helpers branch from 74f28bc to 11b2000 Compare October 28, 2026 05:39
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-require-vite-7 branch from ef2034f to 5b3a161 Compare October 28, 2026 16:12
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-shared-execution-helpers branch from 11b2000 to f08b751 Compare October 28, 2026 16:12
@github-actions

Copy link
Copy Markdown

Live Debugger Runtime Benchmark

SDK-loaded dormant-probe runtime overhead, measured against an uninstrumented bundle in the same browser session.

Browser Workload Quality Per-call overhead upper
chrome Hot clean <= 4.37 ns
chrome Tiny clean <= 0.03 ns
firefox Hot clean <= 9.75 ns
firefox Tiny clean <= 0.01 ns
safari Hot clean <= 4.47 ns
safari Tiny clean <= 2.93 ns

What do the Tiny and Hot workloads represent?

Browser Debugger SDK: 7.16.2 · built 2026-10-27 · S3 ETag 872de873

Full diagnostics
browser  workload  quality  per-call overhead upper  overhead upper          95% CI        A/A diag        block CI  acf(1)   baseline  instrumented                         samples
-------  --------  -------  -----------------------  --------------  --------------  --------------  --------------  ------  ---------  ------------  ------------------------------
chrome   Hot       clean                 <= 4.37 ns       <= 12.04%   4.31..4.36 ns  -0.03..0.00 ns   4.30..4.37 ns   -0.03  49.300 ms     55.163 ms   102 (trim 20%, outliers 3.9%)
chrome   Tiny      clean                 <= 0.03 ns        <= 0.48%  -0.01..0.03 ns  -0.01..0.03 ns  -0.01..0.03 ns    0.02  44.105 ms     44.147 ms  102 (trim 20%, outliers 12.7%)
firefox  Hot       clean                 <= 9.75 ns       <= 37.52%   9.73..9.75 ns   0.00..0.03 ns   9.73..9.75 ns   -0.11  40.220 ms     55.300 ms   102 (trim 20%, outliers 4.9%)
firefox  Tiny      clean                 <= 0.01 ns        <= 0.10%   0.00..0.01 ns  -0.01..0.00 ns   0.00..0.01 ns    0.06  57.900 ms     57.940 ms  102 (trim 20%, outliers 12.7%)
safari   Hot       clean                 <= 4.47 ns       <= 17.90%   4.43..4.47 ns  -0.02..0.01 ns   4.44..4.47 ns   -0.00  47.130 ms     55.520 ms   102 (trim 20%, outliers 8.8%)
safari   Tiny      clean                 <= 2.93 ns      <= 112.40%   2.93..2.93 ns  -0.00..0.00 ns   2.93..2.93 ns   -0.03  28.660 ms     60.860 ms  102 (trim 20%, outliers 10.8%)

Raw samples are in the live-debugger-runtime-bench-results artifact.

Base automatically changed from tiffany.trinh/apps-2961-require-vite-7 to master October 28, 2026 16:33
…their own module

The $.Actions validation, JSON round-trip checks and $.Actions proxy move to
local-execution-shared.ts, so a separate process can bundle them without
local-execution.ts's import-time globalThis.$ accessor. The proxy takes a
dispatch callback, so its caller decides where a call goes, and
validateActionCallShape and returnValueSubject give that process the
allowlist-free check and the return-value error subject, with
validateActionCallShape tested directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot force-pushed the tiffany.trinh/apps-2961-shared-execution-helpers branch from f08b751 to f1f4210 Compare October 28, 2026 16:34
@tyffical
tyffical removed this pull request from stack #623 October 28, 2026 16:38
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 0b42188 into master Oct 28, 2026
15 of 16 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the tiffany.trinh/apps-2961-shared-execution-helpers branch October 28, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants