Skip to content
Open
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
10 changes: 9 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,15 @@ export type BundlerReport = GlobalData['bundler'] & {
rawConfig?: any;
};

export type InjectedValue = string | (() => Promise<string>);
export type ChunkInfo = {
sourceOrHash: string;
fileName: string;
isEntry: boolean;
};

// Static string, lazy async loader (e.g. file fetch), or per-chunk code generator.
export type InjectedValue = string | (() => Promise<string>) | ((sourceOrHash?: string) => string);

export enum InjectPosition {
BEFORE,
MIDDLE,
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/apps/src/identifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe('Apps Plugin - identifier helpers', () => {
describe('buildIdentifier', () => {
test('Should hash the combination of repository and name when both exist', () => {
const result = buildIdentifier('https://github.com/org/repo', 'my-app');
// The identifier should be a 32-character MD5 hash
// The identifier should be a 32-character hex string (SHA-256 truncated to 128 bits)
expect(result).toMatch(/^[a-f0-9]{32}$/);
// Verify it's consistent
expect(buildIdentifier('https://github.com/org/repo', 'my-app')).toBe(result);
Expand Down
5 changes: 2 additions & 3 deletions packages/plugins/apps/src/identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,8 @@ export const buildIdentifier = (repository?: string, name?: string): string | un
}

const plainIdentifier = `${repository}:${name}`;
// Use MD5 hash (128 bits, 32 hex characters) for a compact identifier
// MD5 is sufficient for non-cryptographic purposes like creating unique identifiers
return createHash('md5').update(plainIdentifier).digest('hex');
// SHA-256 truncated to 128 bits (32 hex characters) for a compact identifier.
return createHash('sha256').update(plainIdentifier).digest('hex').slice(0, 32);
Comment on lines -79 to +80

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.

Was this change intentional?

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.

Yes, intentional. MD5 was only used for deterministic IDs, but it is not supported in FIPS environments, which can cause builds to fail (similar to getsentry/sentry-javascript-bundler-plugins#618). SHA-256 avoids that issue.

@yoannmoinet yoannmoinet Jun 15, 2026

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.

Please notify apps folks about this.
EDIT: Uh oh, it seems I AM part of the apps folks... I'll forward this.

};

export const resolveIdentifier = (
Expand Down
5 changes: 3 additions & 2 deletions packages/plugins/error-tracking/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { validateOptions } from './validate';
export { CONFIG_KEY, PLUGIN_NAME } from './constants';

export type types = {
// Add the types you'd like to expose here.
ErrorTrackingOptions: ErrorTrackingOptions;
};

Expand Down Expand Up @@ -54,7 +53,7 @@ export const getPlugins: GetPlugins = ({ options, context }) => {
totalTime.end();
};

return [
const plugins: ReturnType<GetPlugins> = [
{
name: PLUGIN_NAME,
enforce: 'post',
Expand Down Expand Up @@ -82,4 +81,6 @@ export const getPlugins: GetPlugins = ({ options, context }) => {
},
},
];

return plugins;
};
2 changes: 2 additions & 0 deletions packages/plugins/error-tracking/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ export type SourcemapsOptionsWithDefaults = Required<SourcemapsOptions>;

export type ErrorTrackingOptions = {
enable?: boolean;
debugId?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❓ question: ‏MFE was previously configured under rum. sourceCodeContext and we are now adding errorTracking.debugId.
Could we limit the number of options that the customers have to enable for MFE supports?

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 collocated the options like to

type SourceCodeContext = {
    service?: string;
    version?: string;
    ddDebugId?: string;
};

ddDebugId is new attribute within DD_SOURCE_CODE_CONTEXT entries

sourcemaps?: SourcemapsOptions;
};

export type ErrorTrackingOptionsWithDefaults = {
debugId?: boolean;
sourcemaps?: SourcemapsOptionsWithDefaults;
};

Expand Down
3 changes: 2 additions & 1 deletion packages/plugins/injection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"dependencies": {
"@dd/core": "workspace:*",
"@dd/internal-build-report-plugin": "workspace:*",
"chalk": "2.3.1"
"chalk": "2.3.1",
"magic-string": "0.30.21"
},
"devDependencies": {
"typescript": "5.4.3"
Expand Down
77 changes: 40 additions & 37 deletions packages/plugins/injection/src/esbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import path from 'path';
import { PLUGIN_NAME } from './constants';
import {
getContentToInject,
hasChunkInjection,
isNodeSystemError,
isFileSupported,
warnUnsupportedFile,
hasBeforeAfterInjection,
} from './helpers';
import type { ContentsToInject } from './types';

Expand Down Expand Up @@ -77,9 +79,7 @@ export const getEsbuildPlugin = (
namespace: PLUGIN_NAME,
},
async () => {
const content = getContentToInject(contentsToInject, {
position: InjectPosition.MIDDLE,
});
const content = getContentToInject(contentsToInject, InjectPosition.MIDDLE);

return {
// We can't use an empty string otherwise esbuild will crash.
Expand All @@ -98,28 +98,7 @@ export const getEsbuildPlugin = (
return;
}

const bannerForEntries = getContentToInject(contentsToInject, {
position: InjectPosition.BEFORE,
});
const footerForEntries = getContentToInject(contentsToInject, {
position: InjectPosition.AFTER,
});
const bannerForAllChunks = getContentToInject(contentsToInject, {
position: InjectPosition.BEFORE,
onAllChunks: true,
});
const footerForAllChunks = getContentToInject(contentsToInject, {
position: InjectPosition.AFTER,
onAllChunks: true,
});

if (
!bannerForEntries &&
!footerForEntries &&
!bannerForAllChunks &&
!footerForAllChunks
) {
// Nothing to inject.
if (!hasBeforeAfterInjection(contentsToInject)) {
return;
}

Expand All @@ -128,15 +107,11 @@ export const getEsbuildPlugin = (
// Process all output files
for (const [p, o] of Object.entries(result.metafile.outputs)) {
// Determine if this is an entry point
const isEntry =
o.entryPoint && entries.some((e) => e.resolved.endsWith(o.entryPoint!));

// Get the appropriate banner and footer
const banner = isEntry ? bannerForEntries : bannerForAllChunks;
const footer = isEntry ? footerForEntries : footerForAllChunks;
const isEntry = Boolean(
o.entryPoint && entries.some((e) => e.resolved.endsWith(o.entryPoint!)),
);

// Skip if nothing to inject for this chunk type
if (!banner && !footer) {
if (!isEntry && !hasChunkInjection(contentsToInject)) {
continue;
}

Expand All @@ -153,15 +128,43 @@ export const getEsbuildPlugin = (
proms.push(
(async () => {
try {
const source = await fsp.readFile(absolutePath, 'utf-8');
const data = await esbuild.transform(source, {
const mapPath = `${absolutePath}.map`;
const [sourceOrHash, hasSourcemap] = await Promise.all([
fsp.readFile(absolutePath, 'utf-8'),
fsp
.access(mapPath)
.then(() => true)
.catch(() => false),
]);
const fileName = path.basename(absolutePath);
// Resolve static and per-chunk content in one pass.
const banner = getContentToInject(
contentsToInject,
InjectPosition.BEFORE,
{ sourceOrHash, fileName, isEntry },
);
const footer = getContentToInject(
contentsToInject,
InjectPosition.AFTER,
{ sourceOrHash, fileName, isEntry },
);

if (!banner && !footer) {
return;
}

const data = await esbuild.transform(sourceOrHash, {
loader: 'default',
banner,
footer,
sourcemap: hasSourcemap ? 'external' : undefined,
sourcefile: fileName,
});

// FIXME: Handle sourcemaps.
await fsp.writeFile(absolutePath, data.code);
await Promise.all([
fsp.writeFile(absolutePath, data.code),
hasSourcemap && data.map ? fsp.writeFile(mapPath, data.map) : null,
]);
} catch (e) {
if (isNodeSystemError(e) && e.code === 'ENOENT') {
// When we are using sub-builds, the entry file of sub-builds may not exist
Expand Down
76 changes: 33 additions & 43 deletions packages/plugins/injection/src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@

import { InjectPosition, type ToInjectItem } from '@dd/core/types';
import {
processInjections,
processItem,
processLocalFile,
prepareInjections,
processDistantFile,
getInjectedValue,
processLocalFile,
resolveWithFallback,
} from '@dd/internal-injection-plugin/helpers';
import { addFixtureFiles, mockLogger } from '@dd/tests/_jest/helpers/mocks';
import nock from 'nock';
Expand All @@ -26,19 +25,26 @@ const localFileContent = 'local file content';
const distantFileContent = 'distant file content';
const codeContent = 'code content';

const code: ToInjectItem = { type: 'code', value: codeContent };
const existingFile: ToInjectItem = { type: 'file', value: 'fixtures/local-file.js' };
const code: ToInjectItem = { type: 'code', value: codeContent, position: InjectPosition.BEFORE };
const existingFile: ToInjectItem = {
type: 'file',
value: 'fixtures/local-file.js',
position: InjectPosition.BEFORE,
};
const nonExistingFile: ToInjectItem = {
type: 'file',
value: 'fixtures/non-existing-file.js',
position: InjectPosition.BEFORE,
};
const existingDistantFile: ToInjectItem = {
type: 'file',
value: 'https://example.com/distant-file.js',
position: InjectPosition.BEFORE,
};
const nonExistingDistantFile: ToInjectItem = {
type: 'file',
value: 'https://example.com/non-existing-distant-file.js',
position: InjectPosition.BEFORE,
};

describe('Injection Plugin Helpers', () => {
Expand All @@ -52,55 +58,37 @@ describe('Injection Plugin Helpers', () => {
// Add some fixtures.
addFixtureFiles(
{
[await getInjectedValue(existingFile)]: localFileContent,
[existingFile.value as string]: localFileContent,
},
process.cwd(),
);
});

describe('processInjections', () => {
describe('prepareInjections', () => {
test('Should process injections without throwing.', async () => {
const items: Map<string, ToInjectItem> = new Map([
['code', code],
['existingFile', existingFile],
['nonExistingFile', nonExistingFile],
['existingDistantFile', existingDistantFile],
['nonExistingDistantFile', nonExistingDistantFile],
]);
const items: ToInjectItem[] = [
code,
existingFile,
nonExistingFile,
existingDistantFile,
nonExistingDistantFile,
];

const results = await processInjections(items, mockLogger);
expect(Array.from(results.entries())).toEqual([
[
'code',
{
position: InjectPosition.BEFORE,
value: codeContent,
injectIntoAllChunks: false,
},
],
[
'existingFile',
{
position: InjectPosition.BEFORE,
value: localFileContent,
injectIntoAllChunks: false,
},
],
[
'existingDistantFile',
{
position: InjectPosition.BEFORE,
value: distantFileContent,
injectIntoAllChunks: false,
},
],
const contentsToInject: ReturnType<typeof Array<any>> = [];
await prepareInjections(mockLogger, items, contentsToInject);
expect(contentsToInject).toEqual([
{ type: 'code', position: InjectPosition.BEFORE, value: codeContent },
{ type: 'file', position: InjectPosition.BEFORE, value: localFileContent },
{ type: 'file', position: InjectPosition.BEFORE, value: '' },
{ type: 'file', position: InjectPosition.BEFORE, value: distantFileContent },
{ type: 'file', position: InjectPosition.BEFORE, value: '' },
]);

expect(nockScope.isDone()).toBe(true);
});
});

describe('processItem', () => {
describe('resolveWithFallback', () => {
test.each<{ description: string; item: ToInjectItem; expectation?: string }>([
{
description: 'basic code',
Expand Down Expand Up @@ -145,7 +133,9 @@ describe('Injection Plugin Helpers', () => {
},
])('Should process $description without throwing.', async ({ item, expectation }) => {
expect.assertions(1);
return expect(processItem(item, mockLogger)).resolves.toEqual(expectation);
return expect(resolveWithFallback(item, mockLogger)).resolves.toEqual(
expectation ?? '',
);
});
});

Expand Down
Loading
Loading