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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracePropagationTargets: ['http://sentry-test-site.example'],
tracesSampleRate: 1,
autoSessionTracking: false,
});

// Propagate MFE identity from current scope to span attributes.
// withScope() forks the current scope, so tags set on the fork are
// visible when fetch/XHR instrumentation creates spans synchronously.
const client = Sentry.getClient();
client.on('spanStart', span => {
const mfeName = Sentry.getCurrentScope().getScopeData().tags['mfe.name'];
if (mfeName) {
span.setAttribute('mfe.name', mfeName);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Sentry from '@sentry/browser';

export function mount() {
Sentry.withScope(scope => {
scope.setTag('mfe.name', 'mfe-header');
fetch('http://sentry-test-site.example/api/todos/1');
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Sentry from '@sentry/browser';

export function mount() {
Sentry.withScope(scope => {
scope.setTag('mfe.name', 'mfe-one');
fetch('http://sentry-test-site.example/api/todos/2');
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Sentry from '@sentry/browser';

export function mount() {
Sentry.withScope(scope => {
scope.setTag('mfe.name', 'mfe-two');
fetch('http://sentry-test-site.example/api/todos/3');
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Simulates a microfrontend architecture where MFEs are lazy-loaded

// Lazy-load each MFE (kinda like React.lazy + Module Federation)
import('./mfe-header').then(m => m.mount());
import('./mfe-one').then(m => m.mount());
import('./mfe-two').then(m => m.mount());

// Shell makes its own request, no MFE scope
fetch('http://sentry-test-site.example/api/shell-config');
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="root"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers';

sentryTest('should attribute spans to their originating microfrontend', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: '{}' }));
Copy link

Choose a reason for hiding this comment

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

Route glob pattern won't match multi-segment URL paths

Medium Severity

The page.route glob pattern 'http://sentry-test-site.example/*' uses a single * which only matches a single path segment in Playwright's URL glob matching. The fetched URLs (/api/todos/1, /api/todos/2, /api/todos/3, /api/shell-config) all have multiple path segments and won't be intercepted. The pattern needs **/* (as used in dsc-txn-name-update/test.ts) to match across path separators. Without this fix, the fetches hit the real (non-existent) host instead of being mocked, risking test flakiness or timeouts.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

nope, that works


const url = await getLocalTestUrl({ testDir: __dirname });

const reqPromise = waitForTransactionRequest(page, event => {
const spans = event.spans || [];
return (
spans.some(s => s.description?.includes('/api/todos/1')) &&
spans.some(s => s.description?.includes('/api/todos/2')) &&
spans.some(s => s.description?.includes('/api/todos/3')) &&
spans.some(s => s.description?.includes('/api/shell-config'))
);
});

await page.goto(url);

const req = await reqPromise;
const event = envelopeRequestParser(req);
const httpSpans = event.spans?.filter(({ op }) => op === 'http.client') || [];

// Each MFE's fetch is attributed via withScope + spanStart hook
expect(httpSpans.find(s => s.description?.includes('/api/todos/1'))?.data?.['mfe.name']).toBe('mfe-header');
expect(httpSpans.find(s => s.description?.includes('/api/todos/2'))?.data?.['mfe.name']).toBe('mfe-one');
expect(httpSpans.find(s => s.description?.includes('/api/todos/3'))?.data?.['mfe.name']).toBe('mfe-two');

// Shell span has no MFE tag
expect(httpSpans.find(s => s.description?.includes('/api/shell-config'))?.data?.['mfe.name']).toBeUndefined();
});
Loading