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
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpans, getSpanOp, hidePage } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, hidePage, waitForSoftNavigation } from '@sentry-internal/test-utils';

// The correlation between a soft navigation and the SDK's navigation span hangs off the interaction
// that triggered it, so it only holds while the navigation span is started before the interaction's
Expand All @@ -15,6 +15,8 @@ test('attributes soft navigation web vitals to the navigation span they were mea
await page.goto('/');
await page.locator('#navLink').click();

await waitForSoftNavigation(page);

// A soft navigation's vitals are finalized at the next soft navigation or on pagehide, so nothing
// is reported for it until the page goes away.
await hidePage(page);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpans, getSpanOp, hidePage } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, hidePage, waitForSoftNavigation } from '@sentry-internal/test-utils';

// The correlation between a soft navigation and the SDK's navigation span hangs off the interaction
// that triggered it, so it only holds while the navigation span is started before the interaction's
Expand All @@ -17,6 +17,8 @@ test('attributes soft navigation web vitals to the navigation span they were mea
await page.goto('/');
await page.locator('#navigation').click();

await waitForSoftNavigation(page);

// A soft navigation's vitals are finalized at the next soft navigation or on pagehide, so nothing
// is reported for it until the page goes away.
await hidePage(page);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpans, getSpanOp, hidePage } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, hidePage, waitForSoftNavigation } from '@sentry-internal/test-utils';

// The correlation between a soft navigation and the SDK's navigation span hangs off the interaction
// that triggered it, so it only holds while the navigation span is started before the interaction's
Expand All @@ -16,6 +16,8 @@ test('attributes soft navigation web vitals to the navigation span they were mea
await page.goto('/');
await page.click('#navigation');

await waitForSoftNavigation(page);

// A soft navigation's vitals are finalized at the next soft navigation or on pagehide, so nothing
// is reported for it until the page goes away.
await hidePage(page);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpans, getSpanOp, hidePage } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, hidePage, waitForSoftNavigation } from '@sentry-internal/test-utils';

// The correlation between a soft navigation and the SDK's navigation span hangs off the interaction
// that triggered it, so it only holds while the navigation span is started before the interaction's
Expand All @@ -15,6 +15,8 @@ test('attributes soft navigation web vitals to the navigation span they were mea
await page.goto('/');
await page.locator('#navLink').click();

await waitForSoftNavigation(page);

// A soft navigation's vitals are finalized at the next soft navigation or on pagehide, so nothing
// is reported for it until the page goes away.
await hidePage(page);
Expand Down
2 changes: 1 addition & 1 deletion dev-packages/test-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type { OutputScanOptions } from './build-output';
export { assertBundlerInstrumentation } from './bundler-instrumentation';
export type { InstrumentationFixture } from './bundler-instrumentation';

export { hidePage } from './page';
export { hidePage, waitForSoftNavigation } from './page';
export { getPlaywrightConfig } from './playwright-config';
export { getRuntime } from './runtime';
export type { Runtime } from './runtime';
Expand Down
29 changes: 29 additions & 0 deletions dev-packages/test-utils/src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,32 @@ export async function hidePage(page: Page): Promise<void> {
});
/* oxlint-enable no-restricted-globals */
}

/**
* Waits until the browser has delivered the `soft-navigation` entry for a client-side navigation.
*
* The entry only arrives once the new route has painted, which can be after the click resolves.
* web-vitals starts the soft navigation's metrics when it sees the entry, so hiding the page before
* that reports the vitals for the previous navigation, and those of the soft navigation are never
* finalized.
*/
export async function waitForSoftNavigation(page: Page): Promise<void> {
/* oxlint-disable no-restricted-globals */
await page.evaluate(() => {
return new Promise<void>(resolve => {
if (!PerformanceObserver.supportedEntryTypes.includes('soft-navigation')) {
resolve();
return;
}

// The SDK's observer was registered first, so it is notified first. Resolving from a task after
// ours runs lets it handle the entry before the caller continues.
const observer = new PerformanceObserver(() => {
observer.disconnect();
setTimeout(resolve, 0);
});
observer.observe({ type: 'soft-navigation', buffered: true });
});
});
/* oxlint-enable no-restricted-globals */
}
Comment on lines +97 to +107

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The waitForSoftNavigation function lacks a timeout. If the browser doesn't emit a 'soft-navigation' entry, the test will hang until the global timeout, instead of failing quickly.
Severity: MEDIUM

Suggested Fix

Add a timeout to the waitForSoftNavigation function, similar to the pattern used in the hidePage function. A setTimeout can be used to reject the promise after a reasonable duration if no 'soft-navigation' entry is received, preventing the test from hanging.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: dev-packages/test-utils/src/page.ts#L88-L107

Potential issue: The `waitForSoftNavigation` function waits for a 'soft-navigation'
`PerformanceEntry` from the browser, but it lacks a timeout or fallback mechanism.
According to browser documentation, these entries are not guaranteed to be generated for
every navigation. If an entry is not created for any reason (e.g., browser heuristics
not met, a race condition, or a browser quirk), the promise will never resolve. This
will cause the test to hang for the full test runner timeout (e.g., 30 seconds) instead
of failing quickly. A similar function, `hidePage`, includes a `setTimeout` fallback to
prevent this exact issue.

Did we get this right? 👍 / 👎 to inform future reviews.

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.

That's the point

Loading