Skip to content

feat(error-tracking): add debug_id snippet injection for cross-MFE stack trace un-minification#401

Open
amortemousque wants to merge 8 commits into
masterfrom
aymeric/debug-id
Open

feat(error-tracking): add debug_id snippet injection for cross-MFE stack trace un-minification#401
amortemousque wants to merge 8 commits into
masterfrom
aymeric/debug-id

Conversation

@amortemousque

@amortemousque amortemousque commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

What and why?

Current unminification based on (service, version) breaks with MFEs.

When an error spans multiple micro frontends, its stack trace may contain frames from several bundles (for example, cart.js, header.js, and shell-app.js). Unminification today relies on a single (service, version) pair, typically derived from the originating MFE. The backend applies that pair to every frame in the stack trace. As a result, frames from other MFEs, which were uploaded under their own (service, version) pairs—cannot be matched to the correct source maps and remain minified.

debug_id solves this by attaching a UUID to each JS bundle. The backend can then resolves each frame independently.

How?

  • Add injection plugin capability to pass the chunk source or hash to hook callbacks.
    • Expose a new injection callback: (sourceOrHash) => {}
    • Move Rollup injection from banner/footer hooks to renderChunk so the plugin can access and modify the generated chunk source.
  • Implement debug_id injection in the RUM sourceCodeContext plugin.
    • Subscribe to the new injection callback and generate a deterministic UUID from the file content
    • Add ddDebugId: uuid to DD_SOURCE_CODE_CONTEXT entries.
  • Add end-to-end tests

@amortemousque amortemousque marked this pull request as draft June 10, 2026 08:18
@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Jun 11, 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: 88eab93 | Docs | Datadog PR Page | Give us feedback!

@amortemousque amortemousque marked this pull request as ready for review June 12, 2026 07:10
@amortemousque amortemousque requested review from a team as code owners June 12, 2026 07:10

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.

While implementing this, I took the opportunity to simplify the injection logic by:

  • processInjections + processItem becomes prepareInjections + resolveWithFallback where prepareInjections drives the loop and resolveWithFallback resolves async and file injections
  • processInjections iterated items one at a time with no-await-in-loop. Now prepareInjections resolves all injections in parallel.
  • Remove Map<string, ToInjectItem>: generated UUIDs were never used for lookup, replaced with ToInjectItem[] directly.

I think a good potential next step would be to replace the parameterized context.inject API with separate hooks per injection type.

const isPerChunk = (
item: ToInjectItem,
): item is ToInjectItem & { value: (chunk: ChunkInfo) => string } =>
typeof item.value === 'function' && item.value.length === 1;

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'm not a fan of this logic to detect (chunk) => ... per-chunk functions.
Improving this area would require a larger refactor, which I’d prefer to tackle in a dedicated PR. My plan is to replace the parameterized context.inject API with separate hooks for each injection type, making the implementation simpler and easier to reason about.

Comment on lines -79 to +80
// 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);

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.

Comment thread packages/plugins/error-tracking/package.json Outdated
Comment thread packages/core/src/types.ts Outdated
Comment on lines +142 to +150
| { position?: InjectPosition.BEFORE | InjectPosition.AFTER; injectIntoAllChunks?: boolean }
| { position: InjectPosition.BEFORE | InjectPosition.AFTER; allChunks?: boolean }

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 need a major bump, IIRC we expose this API through the customPlugins.
Is it really necessary?

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.

Hum, indeed. I can rollback this change to avoid breaking changes

Comment on lines -138 to -141
// Skip if nothing to inject for this chunk type
if (!banner && !footer) {
continue;
}

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 may have a pretty intense impact on performances, as we will read every single bundle now.
I guess it's necessary for the new injection features, but I would double check the impact on performances still.

If only esbuild would offer us a way to hook ourselves right before writing the bundles.

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 it's also one of my concern. I'll get some data on the overhead

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 re-added a check so we inject only when contentsToInject contains an entry with injectIntoAllChunks: true.
This limits the overhead to cases where the customer uses a plugin that requires it.

};

export const getPlugins: GetPlugins = ({ options, context }) => {
export const getPlugins: GetPlugins = ({ bundler, options, context }) => {

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.

bundler doesn't seem to be used.

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.

nit: This was fixed in rollup with MagicString, would it be a good occasion to do this here too?

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 indeed! It's not exactly the same, as be need to rewrite the already generated .map but I handled them.

Comment on lines -109 to -110
// Processing sequentially all the items.
for (const [id, item] of toInject.entries()) {

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 was required to keep the order of injection the same post processing.

Making it fully parallel may introduce a behavior change worth noting.
This could also lead to indeterministic errors if we have cross injection dependencies.

We could keep the processing parallel, but the final injection should keep the same order to prevent any indeterministic behavior.

wdyt?

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.

FMU, the order is already preserved. Promise.all returns results in input order regardless of resolution timing. Same guarantee as the sequential for...of. The only change is parallel execution. Am I missing something?

Comment on lines -54 to -55
// For Vite, we inject MIDDLE content by adding a script tag
// that references the virtual injected file

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.

Why remove the 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 don't know why but llms tend to remove them. I'll rewrite them

"glob": "11.1.0",
"json-stream-stringify": "3.1.6",
"jszip": "3.10.1",
"magic-string": "0.30.21",

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.

Now that it's not optional anymore (it seems) we could remove it from the peerDependencies.

@amortemousque amortemousque Jun 17, 2026

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.

Indeed :)

Comment thread packages/plugins/error-tracking/src/debugId.ts Outdated

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

@gh-worker-ownership-write-b05516 gh-worker-ownership-write-b05516 Bot removed the request for review from a team June 16, 2026 15:11
@amortemousque amortemousque force-pushed the aymeric/debug-id branch 2 times, most recently from b5e208b to 0d50a64 Compare June 17, 2026 12:52
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