Skip to content

[APPS-2945] Update: switch the apps plugin's static checks to the Node backend runtime - #557

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 1 commit into
masterfrom
tiffany.trinh/apps-2945-follow-terrapin-flag
Oct 28, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 1 commit into
masterfrom
tiffany.trinh/apps-2945-follow-terrapin-flag

Conversation

@tyffical

@tyffical tyffical commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Architecture

flowchart TD
    T[".backend.* transform"] --> R["runBackendStaticChecks"]
    G["dev-server module-graph walk"] --> R
    B["production backend build (moduleParsed)"] --> R
    R --> M["rejectGlobalsMissingFromNode"]
    M --> Q{"XMLHttpRequest, EventSource or Deno used where undefined throws?"}
    Q -->|"bare use, or dereferenced, called, constructed, extended, destructured"| E["build error with a hint"]
    Q -->|"typeof Deno; if, ??, delete or case on globalThis.X; polyfill install; destructuring default"| A["allowed"]
Loading

Changes

9 changes across the static checks, vite/index.ts, constants.ts, README.md and tests
What changed File
Reject XMLHttpRequest, EventSource and Deno with hints. reject-globals-missing-from-node.ts
Remove the Deno-only checks and the crypto/Intl warning. run-backend-static-checks.ts
Report a watched global only where undefined throws. ambient-global-access.ts
Share the AST parent-map helper. walk-ast.ts
Warn at dev-server start on a local Node major other than 24. vite/index.ts
Add NODE_BACKEND_RUNTIME and its Node major. constants.ts
Document the rules and their gaps. README.md
Test must-reject and must-not-reject forms. reject-globals-missing-from-node.test.ts, index.test.ts
Have the opted-in library fixture's helper use Deno instead of importing fs, which the checks now allow. read-hosts.js, backend-function-packages.integration.test.ts

QA Instructions

  • Typecheck, lint, integrity, the apps unit tests and the local execution run in a Vite 8 scaffold pass at 2c6b5141; as does dev:verify against a Datadog staging org.
Check Result Evidence
Typecheck, lint, integrity Pass Command 1
Apps unit tests Pass, with a TMPDIR outside /var Command 2
Local execution Node built-ins, fetch, feature checks and a ?? parameter default pass; Deno and a globalThis.Deno parameter default fail development run
vite build Fails on each rejected fixture, packages the accepted ones build run
dev:verify Same verdicts and results, run in the cloud dev-verify run
yarn typecheck:all && yarn lint && yarn cli integrity
# Expected: all exit 0, integrity leaves no file changes
TMPDIR=/private/tmp yarn test:unit packages/plugins/apps
# Expected: every suite passes
QA helper scripts (also used by the stacked PRs)
  • install-plugin.sh:
#!/usr/bin/env bash
# Usage: QA_APP=<scaffold> install-plugin.sh <pr-number> <build-plugins-checkout>
# Builds the checkout's packages, packs @datadog/vite-plugin to vite-plugin-<pr>.tgz here, and installs it into QA_APP.
set -eu
QA_DIR="$(cd "$(dirname "$0")" && pwd)"
W="$2"
echo "building $(git -C "$W" log --oneline -1)"
(cd "$W" && yarn build:all > "$QA_DIR/logs/build-$1.log" 2>&1)
(cd "$W/packages/published/vite-plugin" && yarn pack --out "$QA_DIR/vite-plugin-$1.tgz" > /dev/null)
cd "${QA_APP:?set QA_APP to the scaffold}"
npm install "$QA_DIR/vite-plugin-$1.tgz" > "$QA_DIR/logs/install-$1.log" 2>&1
echo "installed @datadog/vite-plugin $(node -p "require('@datadog/vite-plugin/package.json').version") from vite-plugin-$1.tgz"
  • qa-run.sh:
#!/usr/bin/env bash
# Usage: QA_APP=<scaffold> qa-run.sh <development|dev-verify> <fixture>...   (needs DD_SITE, DD_API_KEY, DD_APP_KEY)
# Starts the scaffold's Vite dev server on ${QA_PORT:-5173} in the given mode with every fetch the process makes logged,
# transforms each src/<fixture>.backend.ts, runs each accepted fixture's exported functions once through
# /__dd/executeAction (dev-verify sends it to the cloud), and prints the plugin's warnings and cloud log lines,
# then every request the process made. QA_NODE picks the Node binary, QA_NODE_OPTIONS adds Node options and QA_PORT the port.
set -u
MODE="$1"; shift
# Fixtures live in src/; QA_URL_PREFIX is their path from Vite's root (empty for an app with root: 'src').
URL_PREFIX="${QA_URL_PREFIX-src/}"
QA_DIR="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$QA_DIR/logs"
# The request log lives in the temp dir, since a backend function's fetch appends to it inside the guarded run.
LOG="$QA_DIR/logs/vite-$MODE-$$.log"; NET_LOG="$(node -p "require('os').tmpdir()")/qa-net-$MODE-$$.log"; : > "$NET_LOG"
cd "${QA_APP:?set QA_APP to the scaffold}"
if [ -n "$(lsof -ti tcp:${QA_PORT:-5173} -sTCP:LISTEN)" ]; then echo "port ${QA_PORT:-5173} is busy"; exit 1; fi
env -u DD_OAUTH_ACCESS_TOKEN -u DATADOG_OAUTH_ACCESS_TOKEN QA_NET_LOG="$NET_LOG" \
    NODE_OPTIONS="--import $QA_DIR/log-requests.mjs${QA_EXTRA_IMPORT:+ --import $QA_EXTRA_IMPORT}${QA_NODE_OPTIONS:+ $QA_NODE_OPTIONS}" \
    "${QA_NODE:-node}" node_modules/vite/bin/vite.js --mode "$MODE" --port ${QA_PORT:-5173} --strictPort < /dev/null > "$LOG" 2>&1 &
for _ in $(seq 1 180); do curl -s -o /dev/null http://localhost:${QA_PORT:-5173}/ && break; sleep 1; done
for fixture in "$@"; do
  module="$(curl -s "http://localhost:${QA_PORT:-5173}/$URL_PREFIX$fixture.backend.ts")"
  if ! printf %s "$module" | grep -q executeBackendFunction; then
    message="$(printf %s "$module" | sed 's/\x1b\[[0-9;]*m//g' | grep -oE '"message":"([^"\\]|\\.)*' | head -1)"
    echo "$fixture: rejected: ${message:0:180}"
    continue
  fi
  echo "$fixture: accepted"
  for fn in $(sed -n 's/^export async function \([A-Za-z0-9_]*\).*/\1/p' "src/$fixture.backend.ts"); do
    FN="$(printf %s "$URL_PREFIX$fixture" | shasum -a 256 | cut -d' ' -f1).$fn"
    resp="$(curl -s -m 120 -w '\n%{http_code}' -X POST -H 'Content-Type: application/json' \
      -d "{\"functionName\":\"$FN\",\"args\":[\"QA\"]}" http://localhost:${QA_PORT:-5173}/__dd/executeAction)"
    echo "  $fn: $(printf %s "$resp" | sed '$d' | cut -c1-230) [HTTP $(printf %s "$resp" | tail -1)]"
  done
done
echo "plugin log:"
sed 's/\x1b\[[0-9;]*m//g' "$LOG" | grep -E "[|[](warn|error)\||via cloud as|script limit|in a child process|Debugger listening" | sed -E 's/(pid|temp dir|ws:\/\/127\.0\.0\.1:)[^,)]*/\1 <redacted>/g' | cut -c1-230
echo "requests made:"
sed -E "s#https://(api\.)?${DD_SITE}#<staging>#" "$NET_LOG" | sed -E 's#/execution-long-polling/[^ ]+#/execution-long-polling/<receipt>#' | sort | uniq -c
kill "$(lsof -ti tcp:${QA_PORT:-5173} -sTCP:LISTEN)" 2>/dev/null
for _ in $(seq 1 10); do [ -z "$(lsof -ti tcp:${QA_PORT:-5173} -sTCP:LISTEN)" ] && break; sleep 1; done
lsof -ti tcp:${QA_PORT:-5173} -sTCP:LISTEN || echo "server stopped"
  • log-requests.mjs:
// Preloaded with --import: appends the method and URL of every fetch the process makes to $QA_NET_LOG.
import { appendFileSync } from 'node:fs';

const realFetch = globalThis.fetch;
globalThis.fetch = (input, init) => {
    const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
    const method = init?.method ?? (typeof input === 'object' && 'method' in input ? input.method : 'GET');
    appendFileSync(process.env.QA_NET_LOG, `${method} ${url.split('?')[0]}\n`);
    return realFetch(input, init);
};
Fixtures, run commands and output
  • src/useFetch.backend.ts:
export async function getStatus() {
    const response = await fetch('https://example.com');
    return response.status;
}
  • src/useNode.backend.ts:
import os from 'os';

export async function getHostAndStatus() {
    const response = await fetch('https://example.com');
    return { host: os.hostname(), status: response.status };
}
  • src/useDeno.backend.ts:
export async function getCwd() {
    return Deno.cwd();
}
  • src/useFeatureCheck.backend.ts:
export async function detectRuntime() {
    const deno = typeof Deno !== 'undefined' || globalThis.Deno?.version !== undefined;
    return { deno, eventSource: 'EventSource' in globalThis };
}
  • src/useDefaults.backend.ts:
export async function fillMissing() {
    const { EventSource = class Polyfill {} } = globalThis;
    switch (undefined) {
        case globalThis.Deno:
            return { eventSource: EventSource.name };
        default:
            return null;
    }
}
  • src/useDenoChain.backend.ts:
export async function getEnv() {
    return (globalThis.Deno?.env).get('X');
}
  • src/useDenoDefault.backend.ts and src/useDenoDefaultFallback.backend.ts:
export async function getDenoEnv({ env } = globalThis.Deno) {
    return env;
}
export async function getEnvOrNone({ env } = globalThis.Deno ?? {}) {
    return env ?? null;
}
cd "$QA_DIR" && QA_APP=<vite 8 scaffold> ./install-plugin.sh 557 <build-plugins checkout of this branch>
FIXTURES="useFetch useNode useDeno useFeatureCheck useDefaults useDenoChain useDenoDefault useDenoDefaultFallback"
QA_APP=<vite 8 scaffold> ./qa-run.sh development $FIXTURES   # on Node 24
# Output: getStatus 200, getHostAndStatus {"host":"<host>","status":200}, 5 "in a child process" lines, no Node version warning;
# useDeno, useDenoChain and useDenoDefault: rejected: "\"Deno\" is not available in the Node backend runtime. Use a Node API or a node: built-in import instead";
# detectRuntime {"deno":false,"eventSource":false}, fillMissing {"eventSource":"Polyfill"}, getEnvOrNone null
QA_NODE=<node 20 binary> QA_APP=<vite 8 scaffold> ./qa-run.sh development useFetch
# Output: getStatus 200, and the warning "Node 20.19.4 has no global WebSocket, which backend functions get on the Node backend runtime; use Node 22 or later to run them locally.", plus the stack's `import.meta.resolve` warning
QA_NODE=<node 22 binary> QA_APP=<vite 8 scaffold> ./qa-run.sh development useFetch
# Output: getStatus 200, and the warning "Your local Node version "22.23.3" doesn't match the Node backend runtime's Node version "24", so backend functions may behave differently locally."
# Import each fixture's function from src/App.tsx, then in the scaffold:
node node_modules/vite/bin/vite.js build
# Output: with the accepted fixtures, exit 0 and datadog-app-assets.zip's backend/ holds getStatus, getHostAndStatus, detectRuntime, fillMissing and getEnvOrNone;
# adding useDeno, useDenoChain or useDenoDefault: exit 1 with the same "Deno" error
QA_APP=<vite 8 scaffold> ./qa-run.sh dev-verify $FIXTURES
# Output: the same verdicts and results as the development run, except getHostAndStatus {"host":"sandbox","status":200}, and no "in a child process" lines

Blast Radius

Out of Scope / Follow-ups

6 items deferred
Item Status Next step
Some throwing uses pass, such as plain assignments and globalThis aliases Accepted dev:verify catches them
A guarded use (if (globalThis.Deno) globalThis.Deno.cwd()) still fails Accepted Use an imported polyfill, as the hint says
child_process imports pass Stacked on this PR #563
The runtime doesn't pass process.env values Stacked on this PR #565
self, localStorage, sessionStorage, alert, confirm and prompt pass, though the runtime lacks them Follow-up A PR on top of the stack adds them to the missing-global checks
The jsSandboxWithActions and backendEnvironment: 'sandbox' paths are gone Deferred Restore only if #577 stays

Documentation

🤖 Generated with Claude Code

@tyffical

tyffical commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

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-07T04:35:03.872074Z 23556ed 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.

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.

Copilot review overview

🟢 Approval recommended

Runtime selection is consistently propagated, fails safely to v1, and has comprehensive coverage across affected execution paths.

Review effort: Balanced
Findings: None

What changed in this PR

Aligns backend static checks with each organization’s Terrapin feature-flag runtime while preserving v1 checks for cloud execution and fallback scenarios.

Changes:

  • Adds memoized, authenticated backend-runtime resolution with safe v1 fallback.
  • Threads runtime selection through build, transform, and local module-graph checks.
  • Adds runtime-specific tests and documentation.
File Description
packages/​plugins/​apps/​src/​backend-runtime.ts Resolves and caches the organization’s backend runtime.
packages/​plugins/​apps/​src/​backend-runtime.test.ts Tests authentication, fallback, timeout, caching, and logging.
packages/​plugins/​apps/​src/​backend/​ast-parsing/​run-backend-static-checks.ts Makes existing restrictions v1-only.
packages/​plugins/​apps/​src/​backend/​ast-parsing/​run-backend-static-checks.test.ts Verifies runtime-specific checks and warnings.
packages/​plugins/​apps/​src/​backend/​ast-parsing/​reject-restricted-globals.ts Updates runtime documentation.
packages/​plugins/​apps/​src/​vite/​index.ts Resolves and propagates runtime across Vite workflows.
packages/​plugins/​apps/​src/​vite/​index.test.ts Tests transforms, packaging, local execution, and dev:verify behavior.
packages/​plugins/​apps/​src/​vite/​backend-static-checks-plugin.ts Applies the selected runtime during nested builds.
packages/​plugins/​apps/​src/​vite/​backend-static-checks-plugin.test.ts Covers v1 and v2 plugin behavior.
packages/​plugins/​apps/​src/​vite/​build-backend-functions.ts Propagates runtime into production backend builds.
packages/​plugins/​apps/​src/​vite/​build-backend-functions.test.ts Tests runtime behavior in bundled dependency graphs.
packages/​plugins/​apps/​src/​vite/​dev-server-module-graph.ts Applies runtime-specific checks during local traversal.
packages/​plugins/​apps/​src/​vite/​dev-server-module-graph.test.ts Tests transitive module checks for both runtimes.
packages/​plugins/​apps/​src/​vite/​dev-server.ts Keeps cloud-generated bundles on v1.
packages/​plugins/​apps/​src/​vite/​dev-server.integration.test.ts Updates integration calls with explicit runtime.
packages/​plugins/​apps/​README.md Documents runtime resolution and resulting behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23556ed62a

ℹ️ 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".

Comment thread packages/plugins/apps/src/backend-runtime.ts Outdated
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch 2 times, most recently from 6164a77 to a160fb7 Compare October 7, 2026 18:17
@datadog-prod-us1-6

datadog-prod-us1-6 Bot commented Oct 7, 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: 2f4f385 | Docs | View more details | Give us feedback!

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch from 14c5415 to e898bea Compare October 7, 2026 18:33
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch 5 times, most recently from abf34ad to 892bda9 Compare October 15, 2026 01:04
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch 3 times, most recently from 2352fe9 to e7d5434 Compare October 15, 2026 19:07
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch 3 times, most recently from e44fe4a to 141d960 Compare October 16, 2026 00:47
@tyffical
tyffical marked this pull request as ready for review October 16, 2026 01:25
@tyffical
tyffical requested a review from a team as a code owner October 16, 2026 01:25
@tyffical
tyffical requested review from oliverli and removed request for a team October 16, 2026 01:25
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-parallel-local-execution branch from 314d9ca to 86c8430 Compare October 23, 2026 13:41
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch 2 times, most recently from eb7a6fb to 92afaf2 Compare October 23, 2026 14:30
@tyffical
tyffical requested a review from ksun154 October 23, 2026 14:56
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-parallel-local-execution branch from 86c8430 to b50260f Compare October 23, 2026 16:18
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch from 92afaf2 to b71e121 Compare October 23, 2026 16:19
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-parallel-local-execution branch 2 times, most recently from fbd073c to f0977cc Compare October 27, 2026 00:54
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch from b71e121 to 368c9ee Compare October 27, 2026 01:01
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-parallel-local-execution branch from f0977cc to a1c4904 Compare October 27, 2026 02:47
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch from 368c9ee to 67fb3ed Compare October 27, 2026 03:08
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-parallel-local-execution branch from a1c4904 to 9ac932c Compare October 27, 2026 20:16
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch from 67fb3ed to 539234b Compare October 27, 2026 20:16
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-parallel-local-execution branch from 9ac932c to 00ec61c Compare October 28, 2026 05:39
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch from 539234b to 2c6b514 Compare October 28, 2026 05:40
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-parallel-local-execution branch from 00ec61c to 1ed9fb5 Compare October 28, 2026 16:12
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch from 2c6b514 to acb744e Compare October 28, 2026 16:13
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot force-pushed the tiffany.trinh/apps-2961-parallel-local-execution branch from 1ed9fb5 to 69f856f Compare October 28, 2026 16:34
…d runtime

Backend functions run on the Node backend runtime, but the plugin's
static checks still enforced the Deno runtime's rules, so they rejected
code the Node backend runtime runs.

The checks now apply the Node backend runtime's rules: Node built-in
imports and Node's network globals, such as fetch and WebSocket, are
allowed, and XMLHttpRequest, EventSource and Deno, which Node lacks, are
rejected with a hint. They fail only where the missing global would
throw: a bare use other than typeof, or using it as an object, function
or class, including through a binding it was declared or destructured
into. Typeof checks, feature checks on globalThis, polyfill installs,
delete and destructuring defaults pass. The Deno-only checks are
removed: the Node built-in import check, the network-global check and
the crypto/Intl warning.

Local execution runs on the developer's own Node, so the dev server now
warns once at start when its major version isn't the Node backend
runtime's (24), and when it's before 22, which lacks the global
WebSocket.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2945-follow-terrapin-flag branch from acb744e to 2f4f385 Compare October 28, 2026 16:52
Base automatically changed from tiffany.trinh/apps-2961-parallel-local-execution to master October 28, 2026 17:01
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 88ebe25 into master Oct 28, 2026
9 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the tiffany.trinh/apps-2945-follow-terrapin-flag branch October 28, 2026 17:03
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.

4 participants