From f22d5397ee89862ebc978fc9370d91610f54de0c Mon Sep 17 00:00:00 2001 From: Tiffany Trinh Date: Wed, 9 Sep 2026 13:54:25 -0400 Subject: [PATCH 1/4] docs(apps): add v3 to v4 migration guide for local execution changes Documents the upcoming breaking change to how `npm run dev` runs backend functions: in-process execution instead of a cloud round trip, the new `npm run dev:verify` cloud-parity check, and the process.env allowlist during local execution. --- MIGRATIONS.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/MIGRATIONS.md b/MIGRATIONS.md index dd82adf17..90dee428c 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -3,6 +3,10 @@ Everything you need to know about breaking changes and major version bumps. +- [v3 to v4](#v3-to-v4) + - [`npm run dev` now executes backend functions in-process instead of via a cloud round trip](#npm-run-dev-now-executes-backend-functions-in-process-instead-of-via-a-cloud-round-trip) + - [Run `npm run dev:verify` to check cloud parity before publishing](#run-npm-run-devverify-to-check-cloud-parity-before-publishing) + - [`process.env` is now allowlisted during local execution](#processenv-is-now-allowlisted-during-local-execution) - [v2 to v3](#v2-to-v3) - [Renamed `disabled` to `enable`](#renamed-disabled-to-enable) - [Removed `options.errorTracking.sourcemaps.disableGit`](#removed-optionserrortrackingsourcemapsdisablegit) @@ -17,6 +21,43 @@ Everything you need to know about breaking changes and major version bumps. - [Log Level](#log-level) +## v3 to v4 + +This release changes how `npm run dev` runs an app's backend functions (`*.backend.ts`). Apps that don't define any backend functions are unaffected. + +### `npm run dev` now executes backend functions in-process instead of via a cloud round trip + +Previously, `npm run dev` bundled a backend function's file and sent it to Datadog's API on every call, executing it in the cloud and returning the result over the network. + +`npm run dev` now loads the function's file directly into the local Vite dev server and calls it there, with no network round trip. `$.Actions` calls, connection scoping, and input/output validation all behave the same as before — a function that only reads its arguments and calls `$.Actions` needs no changes. + +Static imports of Node built-ins (`fs`, `child_process`, `net`, etc.) and raw network globals (`fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`) in a backend file are rejected at build time. Backend functions have never had access to these in production, so this only surfaces earlier — at `npm run dev` time instead of only once the app is published — a case that previously appeared to work locally but would fail in production. + +### Run `npm run dev:verify` to check cloud parity before publishing + +Because local execution no longer talks to Datadog's API, it can no longer catch every difference between local and production behavior on its own (see the `process.env` allowlist below, for example). + +`npm run dev:verify` still executes backend functions through the full cloud round trip, the same way `npm run dev` used to. Run it before publishing an app to confirm a function's real dependencies (environment variables, connections) behave the same way in the cloud as they did locally. + +```bash +npm run dev:verify +``` + +### `process.env` is now allowlisted during local execution + +A backend function running under `npm run dev` no longer sees your real shell environment. `process.env` is scoped to a small, fixed set of safe variables during local execution: `PATH`, `HOME`, `NODE_ENV`, and `TMPDIR`. + +Reading any other variable — including one your shell has set, or one a secret-backed connection would resolve to in production — returns `undefined` locally, even though the equivalent read against the deployed function succeeds in production. + +```diff + export function myBackendFunction() { +- const region = process.env.AWS_REGION; // real value from your shell ++ const region = process.env.AWS_REGION; // undefined under `npm run dev` — not in the local allowlist + } +``` + +If a backend function depends on a variable like this, verify it with `npm run dev:verify` (see above) before publishing, since that path still runs against the real cloud environment. + ## v2 to v3 To sum up, here's the complete migration (to adapt for other bundlers) : From c29a6b2029840641f42ddff4c32fc8ee3719e7d1 Mon Sep 17 00:00:00 2001 From: Tiffany Trinh Date: Thu, 10 Sep 2026 00:57:40 -0400 Subject: [PATCH 2/4] docs: cover Custom Credentials and identity hydration in v3-to-v4 migration guide --- MIGRATIONS.md | 51 +++++++++++++++++++++++++++++++++++++++++++++------ README.md | 1 + 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/MIGRATIONS.md b/MIGRATIONS.md index 90dee428c..960fcc6fc 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -7,6 +7,8 @@ Everything you need to know about breaking changes and major version bumps. - [`npm run dev` now executes backend functions in-process instead of via a cloud round trip](#npm-run-dev-now-executes-backend-functions-in-process-instead-of-via-a-cloud-round-trip) - [Run `npm run dev:verify` to check cloud parity before publishing](#run-npm-run-devverify-to-check-cloud-parity-before-publishing) - [`process.env` is now allowlisted during local execution](#processenv-is-now-allowlisted-during-local-execution) + - [Custom Credentials resolve locally via `datadog-app.local.json`](#custom-credentials-resolve-locally-via-datadog-applocaljson) + - [`getInitiatingUser()` and `getExecutionUser()` continue to return your real identity locally](#getinitiatinguser-and-getexecutionuser-continue-to-return-your-real-identity-locally) - [v2 to v3](#v2-to-v3) - [Renamed `disabled` to `enable`](#renamed-disabled-to-enable) - [Removed `options.errorTracking.sourcemaps.disableGit`](#removed-optionserrortrackingsourcemapsdisablegit) @@ -29,13 +31,15 @@ This release changes how `npm run dev` runs an app's backend functions (`*.backe Previously, `npm run dev` bundled a backend function's file and sent it to Datadog's API on every call, executing it in the cloud and returning the result over the network. -`npm run dev` now loads the function's file directly into the local Vite dev server and calls it there, with no network round trip. `$.Actions` calls, connection scoping, and input/output validation all behave the same as before — a function that only reads its arguments and calls `$.Actions` needs no changes. +`npm run dev` now loads the function's file directly into the local Vite dev server and executes it there, instead of bundling it and sending it to the cloud on every call. `$.Actions` calls still reach Datadog's API exactly as they do in production, and connection scoping and input/output validation behave the same as before — a function that only reads its arguments and calls `$.Actions` needs no changes. -Static imports of Node built-ins (`fs`, `child_process`, `net`, etc.) and raw network globals (`fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`) in a backend file are rejected at build time. Backend functions have never had access to these in production, so this only surfaces earlier — at `npm run dev` time instead of only once the app is published — a case that previously appeared to work locally but would fail in production. +Static imports of Node built-ins (`fs`, `child_process`, `net`, etc.), dynamic imports of one using a literal string specifier (e.g. `import('fs')`), and raw network globals (`fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`) in a backend file are rejected at build time — this check covers every app-local module resolved into the backend bundle, not just the entry file itself, so an app-local helper module is checked too. A dynamic import using a runtime-computed specifier (e.g. `import(moduleName)`) isn't caught by this check. Backend functions have never had access to these in production, and `npm run dev` ran a function through that same restricted production environment before this release too (see above) — so code relying on one of these already failed under `npm run dev`, just later than it does now, and with a runtime error instead of this build-time one. + +A separate runtime guard also blocks network and subprocess access (`net`, `dns`, `child_process`, `worker_threads`, etc.) once a function body starts running. It exists for what the build-time check above can't see: a `node_modules` dependency that reaches the network or spawns a process directly from inside the function body — bypassing `$.Actions` — fails at call time under `npm run dev` instead of at build time. It doesn't cover a dependency's own top-level initialization code, which runs while the module loads, before the guard is in scope; because Vite then caches that evaluation, the same dependency can keep succeeding locally on every later call even though production rejects it. `npm run dev:verify` is what catches that gap before publishing. ### Run `npm run dev:verify` to check cloud parity before publishing -Because local execution no longer talks to Datadog's API, it can no longer catch every difference between local and production behavior on its own (see the `process.env` allowlist below, for example). +Because a function's own code now runs locally instead of in Datadog's cloud, local execution can no longer catch every difference between local and production behavior on its own (see the `process.env` allowlist below, for example). `npm run dev:verify` still executes backend functions through the full cloud round trip, the same way `npm run dev` used to. Run it before publishing an app to confirm a function's real dependencies (environment variables, connections) behave the same way in the cloud as they did locally. @@ -43,21 +47,56 @@ Because local execution no longer talks to Datadog's API, it can no longer catch npm run dev:verify ``` +If your `package.json` doesn't have a `dev:verify` script yet, `dev:verify` is just your existing dev command with Vite's `--mode dev-verify` flag appended (e.g. `vite --mode dev-verify`) — add the script, or run the equivalent command directly. + ### `process.env` is now allowlisted during local execution -A backend function running under `npm run dev` no longer sees your real shell environment. `process.env` is scoped to a small, fixed set of safe variables during local execution: `PATH`, `HOME`, `NODE_ENV`, and `TMPDIR`. +A backend function running under `npm run dev` no longer has unscoped access to `process.env`. It's scoped to a small, fixed set of safe variables during local execution: `PATH`, `HOME`, `NODE_ENV`, and `TMPDIR`. -Reading any other variable — including one your shell has set, or one a secret-backed connection would resolve to in production — returns `undefined` locally, even though the equivalent read against the deployed function succeeds in production. +Reading any other variable — including one a secret-backed connection would resolve to in production — returns `undefined` locally, even though the equivalent read against the deployed function succeeds in production. The one exception is a Custom Credentials-backed variable declared in `datadog-app.local.json` (see below). ```diff export function myBackendFunction() { -- const region = process.env.AWS_REGION; // real value from your shell +- const region = process.env.AWS_REGION; // resolved in production + const region = process.env.AWS_REGION; // undefined under `npm run dev` — not in the local allowlist } ``` If a backend function depends on a variable like this, verify it with `npm run dev:verify` (see above) before publishing, since that path still runs against the real cloud environment. +### Custom Credentials resolve locally via `datadog-app.local.json` + +Previously, a backend function's Custom Credentials-backed connection resolved to its real value under `npm run dev`, because the function ran through the cloud round trip described above. Now that the function runs locally, that same variable would otherwise fall under the allowlist above and read as `undefined`. + +`npm run dev` closes that gap by resolving Custom Credentials from a `datadog-app.local.json` file in your project root, if you create one. Add it to `.gitignore` and map each credential's env var name to its real value: + +```json +{ + "STRIPE_API_KEY": "sk_test_..." +} +``` + +A missing file resolves to no extra variables — most projects won't have one, and the variable then reads as `undefined` locally until you add it. A present-but-malformed file (invalid JSON, or a value that isn't a string) throws instead of silently resolving to `undefined`, so a typo doesn't look identical to an undeclared secret. + +A `datadog-app.local.json` entry is only resolved for code that runs inside the function body — code at the module's top level (e.g. a client constructed at import time) still sees the variable as `undefined`, since credentials aren't resolved until the function is actually invoked. Move the read, and anything constructed from it, inside the function body: + +```diff +-const client = new StripeClient(process.env.STRIPE_API_KEY); // undefined — evaluated at module load, before credentials resolve +- + export function myBackendFunction() { +- // ... ++ const client = new StripeClient(process.env.STRIPE_API_KEY); // real value, once declared in datadog-app.local.json + } +``` + +### `getInitiatingUser()` and `getExecutionUser()` continue to return your real identity locally + +Previously, `getInitiatingUser()` and `getExecutionUser()` (from `@datadog/apps-backend/user`) returned your real identity under `npm run dev`, because the function ran through the cloud round trip described above and `$.Source` came from that same authenticated session. + +Now that the function runs locally, `npm run dev` fetches your real, authenticated identity from a preview call before running any backend code, so `getInitiatingUser()` and `getExecutionUser()` keep returning that real identity (`id`, `orgId`, and optionally `email` and `name`) instead of falling back to a placeholder. + +Both calls return the identity of that preview invocation, i.e. your own account — not necessarily the identity a real deployed trigger would pass to `getExecutionUser()` (a scheduled run or a different end user's action, for example). `npm run dev:verify` doesn't help here either — it's also a manual preview under your own credentials, with no way to simulate another trigger's identity. Confirm behavior against an execution's real identity by publishing the app and running it through an actual deployed trigger instead. + ## v2 to v3 To sum up, here's the complete migration (to adapt for other bundlers) : diff --git a/README.md b/README.md index 2335aface..e0a6ba92b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ To interact with Datadog directly from your builds. > **Migrations**: > - [v1 to v2](/MIGRATIONS.md#v1-to-v2). > - [v2 to v3](/MIGRATIONS.md#v2-to-v3). +> - [v3 to v4](/MIGRATIONS.md#v3-to-v4). ## Table of content From a3e54879f46e8192e7bb45a307a188608911a965 Mon Sep 17 00:00:00 2001 From: Tiffany Trinh Date: Thu, 10 Sep 2026 16:07:07 -0400 Subject: [PATCH 3/4] docs: note dns.lookup exception, module-state persistence, and call serialization --- MIGRATIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATIONS.md b/MIGRATIONS.md index 960fcc6fc..53837fdf7 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -35,7 +35,7 @@ Previously, `npm run dev` bundled a backend function's file and sent it to Datad Static imports of Node built-ins (`fs`, `child_process`, `net`, etc.), dynamic imports of one using a literal string specifier (e.g. `import('fs')`), and raw network globals (`fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`) in a backend file are rejected at build time — this check covers every app-local module resolved into the backend bundle, not just the entry file itself, so an app-local helper module is checked too. A dynamic import using a runtime-computed specifier (e.g. `import(moduleName)`) isn't caught by this check. Backend functions have never had access to these in production, and `npm run dev` ran a function through that same restricted production environment before this release too (see above) — so code relying on one of these already failed under `npm run dev`, just later than it does now, and with a runtime error instead of this build-time one. -A separate runtime guard also blocks network and subprocess access (`net`, `dns`, `child_process`, `worker_threads`, etc.) once a function body starts running. It exists for what the build-time check above can't see: a `node_modules` dependency that reaches the network or spawns a process directly from inside the function body — bypassing `$.Actions` — fails at call time under `npm run dev` instead of at build time. It doesn't cover a dependency's own top-level initialization code, which runs while the module loads, before the guard is in scope; because Vite then caches that evaluation, the same dependency can keep succeeding locally on every later call even though production rejects it. `npm run dev:verify` is what catches that gap before publishing. +A separate runtime guard also blocks network and subprocess access (`net`, `dns`, `child_process`, `worker_threads`, etc.) once a function body starts running, except `dns.lookup()` — unlike `dns.resolve()` and its variants, it stays available locally, since blocking it risks breaking hostname validation used well outside any exfiltration path. The guard exists for what the build-time check above can't see: a `node_modules` dependency that reaches the network or spawns a process directly from inside the function body — bypassing `$.Actions` — fails at call time under `npm run dev` instead of at build time. It doesn't cover a dependency's own top-level initialization code, which runs while the module loads, before the guard is in scope; because Vite then caches that evaluation for the backend file and its app-local helpers too, not just a dependency, module-scoped state (a counter, a singleton, a cached result) persists across every later call instead of resetting the way the old cloud round trip did on each request. Calls to different backend functions are also serialized rather than run concurrently — every local execution goes through one queue to protect shared `$` access — so a slow call can visibly delay an unrelated fast one in a way production's independently-dispatched requests never would. `npm run dev:verify` is what catches all of this before publishing. ### Run `npm run dev:verify` to check cloud parity before publishing From 71d450e6abe9a6749837235cf3d7b9adc8356d05 Mon Sep 17 00:00:00 2001 From: Tiffany Trinh Date: Thu, 10 Sep 2026 16:56:25 -0400 Subject: [PATCH 4/4] docs: cover the CLI's inline dev:verify nudge before publish --- MIGRATIONS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MIGRATIONS.md b/MIGRATIONS.md index 53837fdf7..f682d47b4 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -49,6 +49,8 @@ npm run dev:verify If your `package.json` doesn't have a `dev:verify` script yet, `dev:verify` is just your existing dev command with Vite's `--mode dev-verify` flag appended (e.g. `vite --mode dev-verify`) — add the script, or run the equivalent command directly. +`datadog-apps draft`, `upload`, and `publish` also offer to run this check for you inline, right before they run. Accept, and the command starts `dev:verify`'s server, waits for you to exercise the app, then continues once you press Enter. Cancelling while it's running aborts the whole command, since checking the app was the point of accepting; declining the offer — or running non-interactively — just reminds you to run it yourself and continues normally, the same as before this offer existed. + ### `process.env` is now allowlisted during local execution A backend function running under `npm run dev` no longer has unscoped access to `process.env`. It's scoped to a small, fixed set of safe variables during local execution: `PATH`, `HOME`, `NODE_ENV`, and `TMPDIR`.