Skip to content

[APPS] Add backend function connection support#331

Open
sdkennedy2 wants to merge 11 commits intomasterfrom
sdkennedy2/apps-emit-connections-manifest
Open

[APPS] Add backend function connection support#331
sdkennedy2 wants to merge 11 commits intomasterfrom
sdkennedy2/apps-emit-connections-manifest

Conversation

@sdkennedy2
Copy link
Copy Markdown
Collaborator

@sdkennedy2 sdkennedy2 commented Apr 27, 2026

Motivation

High Code App backend functions today fail at runtime when they call an action that requires a connectionId (e.g. @datadog/action-catalog/openai/openai#chatCompletion). The Action Platform server rejects the call with Access denied for connection {uuid}. Add it to the allowed list in the step configuration. because the build plugin never tells the server which connections each function is allowed to use.

This PR implements the Backend Function Connections RFC, which chose a single-file approach: customers maintain a centralized connections.ts at the project root listing every connection their app uses, and the build plugin reads it once at build time to feed both the production upload manifest and the dev-preview request body.

CLI tooling and the ESLint rule from the RFC's Future Enhancements section are intentionally out of scope.

Changes

Customer-facing surface

Customers add a connections.ts (or .tsx / .js / .jsx) at the project root. Either CONNECTIONS (uppercase) or connections (lowercase) is accepted as the export name:

// connections.ts
export const CONNECTIONS = {
    OPEN_AI: '9e51df63-7cba-4067-a1fc-a156195074e8',
    SLACK:   '321d6080-5c01-4793-be58-475f0f20e801',
} as const;

Backend functions reference connection IDs as before (literal UUIDs or via CONNECTIONS.OPEN_AI once they import it). On npm run build, the plugin emits a manifest.json at the root of the upload ZIP with the per-function allowed connection IDs nested under backend.functions, keyed by encoded query name. Every backend function key gets the same allowedConnectionIds array (the union from connections.ts). The server-side actions runtime supports per-function distinct lists, but the RFC explicitly chose the flat union.

{
  "backend": {
    "functions": {
      "631339bbf278cb23b10f1118572d0e3d21166de9e4363c6941d055c9f1b19ea1.helloBackend": {
        "allowedConnectionIds": [
          "321d6080-5c01-4793-be58-475f0f20e801",
          "9e51df63-7cba-4067-a1fc-a156195074e8"
        ]
      }
    }
  }
}

For dev preview, the same list is forwarded inside inputs.allowedConnectionIds of the /api/v2/app-builder/queries/preview-async request body, matching the action shape the server expects.

QA Instructions

End-to-end against a scaffolded high-code-app (e.g. ~/dd/web-ui/test-action-catalog-app):

  1. No connections file, build path — remove any connections.ts, run npm run build with logLevel: 'debug'. Expect manifest.json at the root of the upload ZIP with backend.functions[<key>].allowedConnectionIds: [] for every function.
  2. With connections file, build path — add connections.ts exporting two UUIDs, npm run build, inspect the assembled ZIP — root manifest.json should contain every backend-function key under backend.functions, each with both UUIDs in allowedConnectionIds.
  3. Dev preview — with DD_API_KEY / DD_APP_KEY set and a connections.ts containing the OpenAI UUID, add a backend function calling chatCompletionAction({ ..., connectionId: '...' }). npm run dev, click the action — request should succeed (no Access denied for connection ...). Edit connections.ts to remove the UUID, re-trigger — should fail with the expected denial. Verifies handleHotUpdate-driven re-extraction.
  4. Lowercase variant — rename the export to export const connections = { ... }, repeat steps 2–3. Behavior should be identical.
  5. Strict failure modesOPEN_AI: process.env.OPEN_AI_IDnpm run build fails with a framed error pointing at the offending node. default export form → fails with connections file must define "export const CONNECTIONS" (or "connections") = { ... }.
  6. Unit testsyarn test:unit packages/plugins/apps (171 tests pass, including 26 extractor tests covering both export-name variants, buildStart/handleHotUpdate test groups, and a dev-server allowedConnectionIds assertion).
  7. E2E testsyarn test:e2e packages/tests/src/e2e/appsPlugin (60 tests pass, including the new root manifest.json shape assertion).

Blast Radius

  • Only affects customers building high-code apps (@datadog/vite-plugin with apps.enable). Other bundlers and other plugin features are untouched.
  • Strict failure mode: unresolvable values inside connections.ts now fail the build where they previously didn't. Customers without a connections.ts see no change — the manifest is emitted with empty allowedConnectionIds arrays, matching pre-existing behavior (no connection-using actions allowed).
  • The dev preview now sends allowedConnectionIds inside the inputs field of the preview-async request body. This is additive — empty list means same behavior as today.
  • Shipping path: this is the first PR implementing the RFC. CLI helper and ESLint rule are deferred to follow-ups per RFC Future Enhancements.

Documentation

Copy link
Copy Markdown
Collaborator Author

sdkennedy2 commented Apr 27, 2026

@sdkennedy2 sdkennedy2 changed the title [APPS] Add connections.ts support: emit backend/manifest.json and forward allowedConnectionIds to dev preview [APPS] Add connections.ts support — emit backend/manifest.json and forward allowedConnectionIds to dev preview Apr 27, 2026
@sdkennedy2
Copy link
Copy Markdown
Collaborator Author

@codex review
@cursor review

Copy link
Copy Markdown

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

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: 4e3230b4ae

ℹ️ 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/vite/dev-server.ts
@sdkennedy2
Copy link
Copy Markdown
Collaborator Author

@codex review
@cursor review

Copy link
Copy Markdown

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

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: 4c0359d4fc

ℹ️ 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/vite/index.ts Outdated
log.debug(`Refreshed connection IDs from ${connectionsPath} (${connectionIds.length})`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log.error(`Failed to refresh connection IDs: ${message}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear stale connection IDs after failed HMR refresh

When connections.ts has already loaded IDs and a later edit makes server.transformRequest/extraction fail, this catch only logs the error while connectionRegistry keeps the previous array. The dev middleware calls getConnectionIds() on each /__dd/executeAction, so previews keep sending removed connection IDs until another successful refresh or a server restart; because these IDs are an allowlist, a failed refresh should clear or block the registry instead of reusing stale permissions.

Useful? React with 👍 / 👎.

@sdkennedy2
Copy link
Copy Markdown
Collaborator Author

@codex review
@cursor review

Copy link
Copy Markdown

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

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: 1c34e58376

ℹ️ 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/vite/index.ts Outdated
@sdkennedy2 sdkennedy2 changed the title [APPS] Add connections.ts support — emit backend/manifest.json and forward allowedConnectionIds to dev preview [APPS] Add backend function connection support Apr 28, 2026
@sdkennedy2 sdkennedy2 changed the title [APPS] Add backend function connection support [APPS] Add connections.ts support — emit manifest.json and forward allowedConnectionIds to dev preview Apr 28, 2026
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-emit-connections-manifest branch from 1c34e58 to ce1106b Compare April 28, 2026 16:42
@sdkennedy2
Copy link
Copy Markdown
Collaborator Author

@codex review

…s validation

- buildStart now uses server.transformRequest in dev (vite serve), where
  this.load returns a ModuleInfo proxy whose `code` getter throws. server
  is captured via configureServer, which fires before buildStart in dev.
- buildStart logs the framed [connections] error before re-throwing so it
  survives any masking by downstream closeBundle errors (e.g. error-tracking
  sourcemaps' "No output files found").
- extractConnectionIds now derives line:col from node.start byte offsets
  (which SWC produces) against the source text. Previously the loc?.start
  branch was dead — rollup's parser doesn't emit ESTree loc info.
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-emit-connections-manifest branch from 8ba24f1 to e7ee68b Compare April 28, 2026 17:11
@sdkennedy2 sdkennedy2 changed the title [APPS] Add connections.ts support — emit manifest.json and forward allowedConnectionIds to dev preview [APPS] Add backend function connection support Apr 28, 2026
Copy link
Copy Markdown

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

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: 8ba24f1659

ℹ️ 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/index.ts Outdated
Comment thread packages/plugins/apps/src/vite/index.ts
@sdkennedy2 sdkennedy2 marked this pull request as ready for review April 28, 2026 17:39
@sdkennedy2 sdkennedy2 requested a review from yoannmoinet as a code owner April 28, 2026 17:39
Copy link
Copy Markdown
Contributor

@sarenji sarenji left a comment

Choose a reason for hiding this comment

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

Some thoughts

Comment on lines +216 to +224
test('Should throw when both "connections" and "CONNECTIONS" are exported', () => {
const ast = program([
exportConnections([stringProperty('A', 'uuid-1')], 'connections'),
exportConnections([stringProperty('B', 'uuid-2')], 'CONNECTIONS'),
]);
expect(() => extractConnectionIds(ast, filePath, '')).toThrow(
'multiple top-level "export const CONNECTIONS" (or "connections") declarations are not allowed',
);
});
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.

Out of curIosity, why should we support both cases? Do we need to, or can we pick one?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thats fair pushback. I just thought it was an easy foot gun to make. I plan on adding a couple eslint rules perhaps we standardize on the uppercase version and we can avoid the footgun with linting.

{
type: 'SpreadElement',
argument: { type: 'Identifier', name: 'other' },
} as SpreadElement,
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.

Does satisfies work here in place of as?

// surfaces immediately.
describe('framed source location from parseAst', () => {
test('Should include line:col when value is not a string literal', async () => {
const { parseAst } = await import('rollup/parseAst');
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.

Why is this dynamically imported? Also why rollup's AST parser specifically? I feel like something should expose the parser somewhere that we can use instead of doing this.

Comment on lines +97 to +104
const middleware = createDevServerMiddleware({
viteBuild: mockViteBuild,
getBackendFunctions: () => mockFunctions,
getConnectionIds: () => [],
auth: mockAuth,
projectRoot: '/project',
log: mockLog,
});
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.

Nice, this is a much better interface

Comment on lines +417 to +425
const inputs = (
capturedBody as {
data: {
attributes: { query: { properties: { spec: { inputs: unknown } } } };
};
}
).data.attributes.query.properties.spec.inputs as {
allowedConnectionIds: string[];
};
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.

This is an awkward construction especially when capturedBody is set to unknown and inputs is also set to unknown

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That is wild. Claude wrote that. I glossed over the tests, I usually don't invest too much resources in unit test coverage during the design partner phase since nothing is really set in stone and things are likely to change.

This line is pretty egregious though haha. Happy to fix it.

Comment on lines -23 to -31
/**
* Returns the Vite-specific plugin hooks for the apps plugin.
*
* Production (closeBundle): builds backend functions (if any) then uploads
* all assets sequentially.
*
* Dev (configureServer): registers middleware for local backend function
* testing when auth credentials are available.
*/
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.

Please restore this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I actually intentionally removed it because I thought it was weird describing all of the inner functions at the top (claude wanted to add even more to it as the complexity of the plugin grew), but it could be useful to differentiate the production versus dev. I'm not particularly opinionated. I can add it back.

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.

There's a lot of mocking and extracting in the new helper functions at the top... is there a way to simplify without faking everything? The more mocks/fakes there are, the less confidence I have in the code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I wonder if we should just exclude index.ts from unit tests and rely on the e2e test. This file mostly just glues everything together and it might just be better to verify everything works end to end.

We could in a later PR just refactor index.ts and pull out distinct pieces and create unit tests for them directly for example:

  • Upload function
  • Registry functions
  • etc

Comment thread packages/plugins/apps/src/vite/index.ts
);
expect(Object.keys(manifest.backend.functions).sort()).toEqual(expectedKeys.sort());
for (const entry of Object.values(manifest.backend.functions)) {
expect(Array.isArray(entry.allowedConnectionIds)).toBe(true);
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.

Are we able to test the values of the array as well, or are we satisfied with an Array.isArray check?

@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-emit-connections-manifest branch from 6f12a4e to b64302d Compare April 30, 2026 13:53
Copy link
Copy Markdown
Member

@yoannmoinet yoannmoinet left a comment

Choose a reason for hiding this comment

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

Overall looks good to me.
Just a nit on some error handling that can be misleading.

Comment on lines +25 to +27
} catch {
// not found at this extension — try the next.
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will swallow any errors, not just the "not found" errors.
Maybe a specific gate of ENOENT errors only would be necessary.
Unless we're ok with this.

@sdkennedy2
Copy link
Copy Markdown
Collaborator Author

@codex review
@cursor review

Copy link
Copy Markdown

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

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: c605f1893d

ℹ️ 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 on lines +76 to +78
if (filePath) {
this.addWatchFile(filePath);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Watch for newly added connection files

In vite build --watch this only registers the selected file when one already exists, so a watch session that starts without connections.* will never rerun when the developer later adds connections.ts/.js, and a session that starts with a lower-priority file will not notice a newly added higher-priority one. Because buildStart is the only production path that refreshes connectionRegistry, the uploaded manifest keeps an empty or stale allowedConnectionIds list until the process is restarted.

Useful? React with 👍 / 👎.

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.

3 participants