Skip to content

feat(apps): set @datadog/apps backend context - #457

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 1 commit into
masterfrom
scott.meyer/datadog-apps-backend-runtime-entry
Jul 23, 2026
Merged

feat(apps): set @datadog/apps backend context#457
gh-worker-dd-mergequeue-cf854d[bot] merged 1 commit into
masterfrom
scott.meyer/datadog-apps-backend-runtime-entry

Conversation

@Scott-Meyer

@Scott-Meyer Scott-Meyer commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What and why?

Backend functions using @datadog/apps-backend need the runtime $ context before SDK helpers run. Generated backend entries already receive that context, so the build plugin should initialize the SDK rather than requiring customer-authored handlers to wire it manually.

This follows the existing action-catalog integration pattern while keeping the import optional for projects that do not expose the new SDK backend-internal entry point.

How?

  • Resolve @datadog/apps-backend/backend/internal from the customer project using Node module resolution.
  • Conditionally import setBackendContext into generated backend entries.
  • Call setBackendContext($) after the runtime global is assigned and before action-catalog registration or the customer handler.
  • Share the same generated source between production and dev backend builds.
  • Leave projects using older SDK versions or no SDK unchanged.

The paired SDK change emits @datadog/apps-backend/backend/internal and public backend modules against one shared code-split module, so this setter updates the same context read by SDK helpers.

Related SDK work

QA

Prerequisites: ~/dd/web-ui and ~/dd/build-plugins checked out locally, logged into Datadog (OAuth is fine — no API/app keys needed unless yours lack "Actions API Access" scope).

1. Check out the branches

cd ~/dd/web-ui && git fetch origin && git checkout scott.meyer/datadog-apps-backend-user-simple
cd ~/dd/build-plugins && git fetch origin && git checkout scott.meyer/datadog-apps-backend-runtime-entry

2. Build the SDK packages and pack them as tarballs

cd ~/dd/web-ui
yarn install
TS_CUSTOM_WATCHER=0 yarn workspace @datadog/apps-backend build
yarn workspace @datadog/apps-backend pack --out /tmp/datadog-apps-backend.tgz

3. Build the vite plugin and prepare it for external linking

cd ~/dd/build-plugins
yarn install
yarn workspace @datadog/vite-plugin build
yarn cli prepare-link

4. Scaffold a new user app

cd /tmp
npm create @datadog/apps-backend@latest -- test-user-app --template vite-react -y
cd test-user-app

5. Point the app at your local builds — in package.json, add/change:

"dependencies": {
    "@datadog/apps-backend": "file:/tmp/datadog-apps-backend.tgz"
},
"devDependencies": { "@datadog/vite-plugin": "file:/Users/<you>/dd/build-plugins/packages/published/vite-plugin" }
npm install

6. Add a backend function that returns the invoking user — create src/getUser.backend.ts:

import { getExecutionUser, getInitiatingUser } from '@datadog/apps-backend/user';

export async function getUser() {
    return { executionUser: getExecutionUser(), initiatingUser: getInitiatingUser() };
}

Edit src/App.tsx to add a button that calls it and renders the result:

import { useState } from 'react';
import { getUser } from './getUser.backend';

function App() {
    const [user, setUser] = useState<Awaited<ReturnType<typeof getUser>>>();
    return (
        <div>
            <button onClick={() => getUser().then(setUser)}>Who am I?</button>
            {user && <pre>{JSON.stringify(user, null, 2)}</pre>}
        </div>
    );
}
export default App;

7. Run it locally and verify

npm run dev

Open the printed localhost URL in a browser, click Who am I?, confirm it renders your real Datadog user identity (email, id, name, organizationId).

8. Upload and verify the published app

npm run upload

Open the printed https://app.datadoghq.com/app-builder/apps-backend/... URL in a browser, click Who am I? again, confirm the same real user data renders.

Cleanup: yarn cli prepare-link --revert in ~/dd/build-plugins to restore the repo, and delete the test app from App Builder's app list.

Blast radius

Limited to generated Datadog Apps backend entries when the customer project exposes @datadog/apps-backend/backend/internal. Existing action-catalog initialization and projects without that SDK entry point retain their current behavior.

Automated test coverage (added)

Manual QA above is now backed by an automated integration test that exercises the real, non-mocked @datadog/apps-backend SDK end-to-end through an actual Vite build:

  • packages/tests/src/_jest/fixtures/apps_backend_project/ — a fixture project with @datadog/apps-backend@0.0.1 as a real dependency (not mocked), including a backend function that calls getExecutionUser/getInitiatingUser and one that doesn't use the SDK at all.
  • packages/plugins/apps/src/backend/integration.test.ts — runs a real (unmocked) Vite build of the fixture through the apps plugin, dynamically imports the emitted backend bundle, and asserts: real SDK user resolution matches the invoking context, arguments forward correctly, a backend not using the SDK still works, and an invalid context is rejected.

Building this test surfaced and fixed a real pre-existing bug: packages/plugins/apps/src/vite/index.ts destructured context.buildRoot once at plugin-setup time, before Vite's configResolved hook overwrites it with the resolved build root — so every downstream use (proxy codegen, backend function builds, dev-server middleware) could use a stale root whenever the bundler's resolved root differs from process.cwd() at setup time (e.g. monorepos, CI wrappers that cd before invoking the bundler). This was invisible in every prior test because they all mock vite.build. Fixed by reading context.buildRoot live at each use site, matching the existing pattern in injection/output plugins. No behavior change for the common case where the two roots already match; corrects behavior when they don't.

@Scott-Meyer
Scott-Meyer force-pushed the scott.meyer/datadog-apps-backend-runtime-entry branch 2 times, most recently from 86682ae to 145ec69 Compare July 17, 2026 01:18
@Scott-Meyer Scott-Meyer changed the title fix(apps): keep backend runtime context private chore(apps): deprecate legacy backend runtime global Jul 17, 2026
@Scott-Meyer Scott-Meyer changed the title chore(apps): deprecate legacy backend runtime global chore(apps): preserve backend function compatibility Jul 17, 2026
@Scott-Meyer
Scott-Meyer force-pushed the scott.meyer/datadog-apps-backend-runtime-entry branch from 145ec69 to d982beb Compare July 17, 2026 23:44
@Scott-Meyer Scott-Meyer changed the title chore(apps): preserve backend function compatibility feat(apps): inject @datadog/apps backend runtime provider Jul 17, 2026
@Scott-Meyer Scott-Meyer changed the title feat(apps): inject @datadog/apps backend runtime provider feat(apps): set @datadog/apps backend context Jul 18, 2026
@Scott-Meyer
Scott-Meyer requested a review from Copilot July 18, 2026 20:45

Copilot AI 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.

Pull request overview

Friend, this PR updates the Apps build plugin’s backend virtual-entry code generation to initialize the @datadog/apps backend runtime context ($) inside generated backend function bundles, aligning the generated entries with how the SDK expects helpers to run.

Changes:

  • Add conditional detection of @datadog/apps/backend/internal (via Node module resolution) and generate an optional import for setBackendContext.
  • Emit a generated snippet to call setBackendContext($) after globalThis.$ = $ and before action-catalog registration / handler execution.
  • Extend unit tests to cover presence/absence of the backend-internal import and validate call ordering.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/plugins/apps/src/backend/virtual-entry.ts Adds conditional SDK import and injects backend-context initialization into generated main($) entry.
packages/plugins/apps/src/backend/virtual-entry.test.ts Adds coverage for the new conditional import and ordering constraints.
packages/plugins/apps/src/backend/shared.ts Introduces reusable “is export installed” resolver + new import/snippet constants for Apps backend context.
packages/plugins/apps/src/backend/shared.test.ts Adds unit tests for the new backend-context snippet behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/plugins/apps/src/backend/virtual-entry.ts Outdated
@Scott-Meyer
Scott-Meyer requested a review from sdkennedy2 July 19, 2026 20:23
@Scott-Meyer
Scott-Meyer marked this pull request as ready for review July 19, 2026 21:00
@Scott-Meyer
Scott-Meyer requested review from a team as code owners July 19, 2026 21:00
@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Jul 20, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

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

sdkennedy2 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Suggestion: add a real SDK integration test for #457

I tested this end-to-end and the implementation works, but the manual test highlighted an important coverage gap: the current tests mock the SDK boundary and/or assert against generated source strings. They do not prove that a bundled backend importing the real @datadog/apps-backend/user module reads the singleton initialized by setBackend.

I think we should add an integration test that:

  1. Creates a backend fixture which imports getExecutionUser and getInitiatingUser from the real SDK:

    import {
      getExecutionUser,
      getInitiatingUser,
    } from '@datadog/apps-backend/user';
    
    export async function getRuntimeUsers(label: string) {
      const [executionUser, initiatingUser] = await Promise.all([
        getExecutionUser(),
        getInitiatingUser(),
      ]);
    
      return { label, executionUser, initiatingUser };
    }
    
    export async function plainEcho(value: string) {
      return { value };
    }
  2. Bundles that fixture through the plugin without mocking @datadog/apps-backend/runtime/jsFunctionWithActions or @datadog/apps-backend/user.

  3. Imports the emitted backend bundle and invokes it with a realistic $ context.

  4. Asserts that:

    • the execution and initiating users returned by the SDK match $.Source;
    • the argument is forwarded correctly;
    • a backend that does not use the runtime SDK still works;
    • an invalid context fails validation as expected.

That test would exercise the actual contract this PR introduces:

generated virtual entry
  -> buildRuntimeFromJsFunctionWithActions($)
  -> setBackend(runtime)
  -> backend handler
  -> @datadog/apps-backend/user
  -> initialized singleton

Manual integration-test results

For validation, I scaffolded a real Vite React High Code App using the local create-apps command, installed a tarball of @datadog/vite-plugin built from this PR's head (adb33852), and used a publish-shaped build of the real SDK.

  • Focused plugin unit tests: 31/31 passed
  • Scaffolded app: install, typecheck, and production build passed
  • Direct invocation of the bundles produced by /__dd/debugBundle passed for both the runtime-user and plain-handler paths
  • Local npm run dev test through dd-auth: both UI actions passed with no browser console errors or warnings
  • Published staging version 90aeba7ec6253faa: both actions passed through the App Builder iframe/postMessage path, with no app-scoped browser errors

Published test app: https://dd.datad0g.com/app-builder/apps/4549e2d0-228a-438a-a923-fbba155763b8

Two QA-instruction corrections found while running this:

  1. The scaffolding command is npm create @datadog/apps, not npm create @datadog/apps-backend.
  2. The example should await getExecutionUser() and getInitiatingUser() before returning them; returning the promises directly serializes them as {}.

Follows the @datadog/apps-backend runtime entry-point split: the apps
plugin now sets the JS-Function-with-Actions backend context and
gates it on SDK availability, using the new backend-internal SDK entry
and resolving published package exports to built dist output.

Adds an integration test that exercises the real (non-mocked)
@datadog/apps-backend SDK end-to-end through an actual Vite build via
a new fixture project. Building that test surfaced and fixed a
pre-existing bug: the Vite plugin destructured context.buildRoot once
at setup time, before Vite's configResolved hook overwrites it with
the resolved build root, so every downstream use (proxy codegen,
backend function builds, dev-server middleware) could use a stale
root when the bundler's resolved root differs from process.cwd() at
setup time. Fixed by reading context.buildRoot live at each use site.

That fix also exposed a second, previously dormant bug: the E2E
appsPlugin fixture's @datadog/action-catalog stub was missing a
package.json, so Node's require.resolve reported it as installed
while Vite/Rollup failed to resolve the bare-specifier subpath import
at build time. Fixed by adding the missing package.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Scott-Meyer
Scott-Meyer force-pushed the scott.meyer/datadog-apps-backend-runtime-entry branch from 4a040dc to 1b2142d Compare July 23, 2026 18:40
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 11f3a98 into master Jul 23, 2026
5 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the scott.meyer/datadog-apps-backend-runtime-entry branch July 23, 2026 18:54
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