diff --git a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/init.js b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/init.js new file mode 100644 index 000000000000..b8f186bc2896 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/init.js @@ -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); + } +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/mfe-header.js b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/mfe-header.js new file mode 100644 index 000000000000..16bfecfd15eb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/mfe-header.js @@ -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'); + }); +} diff --git a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/mfe-one.js b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/mfe-one.js new file mode 100644 index 000000000000..fdd091af4801 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/mfe-one.js @@ -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'); + }); +} diff --git a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/mfe-two.js b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/mfe-two.js new file mode 100644 index 000000000000..ff6c23d374e0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/mfe-two.js @@ -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'); + }); +} diff --git a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/subject.js b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/subject.js new file mode 100644 index 000000000000..b36ad956e913 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/subject.js @@ -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'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/template.html b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/template.html new file mode 100644 index 000000000000..acc42eb2480f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/template.html @@ -0,0 +1,9 @@ + + +
+ + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/test.ts b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/test.ts new file mode 100644 index 000000000000..e49b258b4ff2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/microfrontend-span-attribution/test.ts @@ -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: '{}' })); + + 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(); +});