Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/plugins/apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ Local execution doesn't support Yarn Plug'n'Play, whose packages load through ho

Backend functions run on the Node backend runtime, and the plugin's build-time and dev-server checks follow its rules: backend functions may import Node built-ins and use Node's network globals such as `fetch` and `WebSocket`, but not `XMLHttpRequest`, `EventSource` or `Deno`, which Node doesn't expose by default. The checks fail only where the missing global would throw: any bare use but `typeof Deno`, or using `globalThis.Deno` as an object, function or class, even after a feature check or a polyfill install, so use an imported polyfill by its name. Feature checks such as `if (globalThis.EventSource)` and destructuring defaults pass. The checks follow a binding the global is declared or destructured into, but not a plain assignment such as `d = globalThis.Deno`, a `let` or parameter alias of `globalThis`, a bulk copy such as `{ ...globalThis }`, or a value that is stored in an object, array or class field, exported, returned or passed on. Local execution runs on your own Node, while the Node backend runtime runs Node 24, so the dev server warns once at start on another major version, and on one before Node 22, which lacks the global `WebSocket`.

The checks also warn, without failing the build, about `process.env` reads: in production, the Node backend runtime doesn't pass your own environment variables or Custom Credentials to backend functions, so those reads don't see your values there. They catch direct reads: `process.env.X`, `process.env[key]`, a destructure of `process.env`, an `env`, default or namespace import from `process` or `node:process`, and a `const` bound to `process.env` or to an `env` destructured from `process`. Like the other checks, they cover only the app's own code and opted-in packages, so an SDK that reads `process.env` (for example, for a default API key) doesn't warn.

## Package output

A production `vite build` writes `datadog-app-assets.zip` beside the Vite output. The ZIP contains `frontend/`, `backend/`, and `manifest.json`. The app's identity is resolved by `@datadog/apps-cli` at deploy time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function resolvesToAmbientGlobal(
return false;
}

function staticMemberName(
export function staticMemberName(
node: MemberExpression,
scopeAnalysis: ModuleScopeAnalysis,
): string | undefined {
Expand All @@ -122,11 +122,11 @@ function staticMemberName(
}

// A defaulted property value (`{ x = fallback }`) is an AssignmentPattern wrapping the real binding — unwrap it so a defaulted alias is treated the same as a bare one.
function unwrapDefaultValue(value: Property['value']): Property['value'] {
export function unwrapDefaultValue(value: Property['value']): Property['value'] {
return value.type === 'AssignmentPattern' ? value.left : value;
}

function staticPropertyName(
export function staticPropertyName(
property: Property,
scopeAnalysis: ModuleScopeAnalysis,
): string | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { analyzeModuleScope } from '@dd/apps-plugin/backend/ast-parsing/module-scope';
import { NODE_REPLACEMENT_HINTS } from '@dd/apps-plugin/backend/ast-parsing/reject-globals-missing-from-node';
import { runBackendStaticChecks } from '@dd/apps-plugin/backend/ast-parsing/run-backend-static-checks';
import { createBackendStaticChecks } from '@dd/apps-plugin/backend/ast-parsing/run-backend-static-checks';
import { ensureProgram } from '@dd/apps-plugin/backend/ast-parsing/type-guards';
import { NODE_BACKEND_RUNTIME } from '@dd/apps-plugin/constants';
import { getMockLogger, mockLogFn } from '@dd/tests/_jest/helpers/mocks';
Expand All @@ -16,11 +16,11 @@ function check(code: string): void {
const ast = parseAst(code);
const program = ensureProgram(ast, FILE_PATH);
const scopeAnalysis = analyzeModuleScope(program);
const log = getMockLogger();
runBackendStaticChecks(ast, FILE_PATH, log, scopeAnalysis);
const runStaticChecks = createBackendStaticChecks(getMockLogger());
runStaticChecks(ast, FILE_PATH, scopeAnalysis);
}

describe('Backend Functions - runBackendStaticChecks', () => {
describe('Backend Functions - createBackendStaticChecks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
Expand Down Expand Up @@ -82,4 +82,12 @@ describe('Backend Functions - runBackendStaticChecks', () => {

expect(mockLogFn).not.toHaveBeenCalled();
});

test('should warn about a process.env read', () => {
check('export function run() { return process.env.STRIPE_KEY; }');

const logged = mockLogFn.mock.calls.filter(([, level]) => level === 'warn');
const messages = logged.map(([message]) => message);
expect(messages).toEqual([expect.stringContaining('process.env.STRIPE_KEY')]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import type { BaseNode } from 'estree';

import type { ModuleScopeAnalysis } from './module-scope';
import { rejectGlobalsMissingFromNode } from './reject-globals-missing-from-node';
import { type WarnedEnvReads, warnAboutEnvReads } from './warn-env-reads';

/** Runs every static check against a single module; shared by every call site so the checks can't drift. */
export function runBackendStaticChecks(
export type RunBackendStaticChecks = (
ast: BaseNode,
filePath: string,
log: Logger,
scopeAnalysis: ModuleScopeAnalysis,
): void {
rejectGlobalsMissingFromNode(ast, filePath, scopeAnalysis);
) => void;

/** Create one per plugin instance and share it across call sites, so the checks can't drift and each warning shows once. */
export function createBackendStaticChecks(log: Logger): RunBackendStaticChecks {
const warnedEnvReads: WarnedEnvReads = new Map();
return (ast, filePath, scopeAnalysis) => {
rejectGlobalsMissingFromNode(ast, filePath, scopeAnalysis);
warnAboutEnvReads(ast, filePath, log, scopeAnalysis, warnedEnvReads);
};
}
Loading
Loading