-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(nextjs): Drop meta trace tags if rendered page is ISR #18192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
logaretm
merged 14 commits into
develop
from
awad/js-1152-nextjs-incremental-static-rendering-causes-cached-trace-id
Nov 17, 2025
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
88d8541
fix: Mark ISR pages for client side to ignore meta trace contents
logaretm 9a8b486
fix: just delete the tags
logaretm 81731ad
refactor: add a helper fn
logaretm ddef94c
tests: update tests
logaretm 7d1bdac
fix: lint
logaretm 0d0706a
test: added unit tests for isr route matching and detection
logaretm 4303908
test: added e2e tests
logaretm aaf244f
tests: test the actual transaction sending for page loads
logaretm f46d817
fix: added explicit array check
logaretm c72b29a
fix: route manifest is always a string
logaretm ed76ec8
fix: re-use types and lint issue
logaretm ec73021
fix: gate the meta removal with __SENTRY_TRACING__
logaretm a49edb2
feat(perf): re-use the existing parsed route manifest
logaretm 0854965
tests: added cache unit tests
logaretm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
17 changes: 17 additions & 0 deletions
17
dev-packages/e2e-tests/test-applications/nextjs-15/app/isr-test/[product]/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| export const revalidate = 60; // ISR: revalidate every 60 seconds | ||
| export const dynamicParams = true; // Allow dynamic params beyond generateStaticParams | ||
|
|
||
| export async function generateStaticParams(): Promise<Array<{ product: string }>> { | ||
| return [{ product: 'laptop' }, { product: 'phone' }, { product: 'tablet' }]; | ||
| } | ||
|
|
||
| export default async function ISRProductPage({ params }: { params: Promise<{ product: string }> }) { | ||
| const { product } = await params; | ||
|
|
||
| return ( | ||
| <div> | ||
| <h1>ISR Product: {product}</h1> | ||
| <div id="isr-product-id">{product}</div> | ||
| </div> | ||
| ); | ||
| } |
15 changes: 15 additions & 0 deletions
15
dev-packages/e2e-tests/test-applications/nextjs-15/app/isr-test/static/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| export const revalidate = 60; // ISR: revalidate every 60 seconds | ||
| export const dynamicParams = true; | ||
|
|
||
| export async function generateStaticParams(): Promise<never[]> { | ||
| return []; | ||
| } | ||
|
|
||
| export default function ISRStaticPage() { | ||
| return ( | ||
| <div> | ||
| <h1>ISR Static Page</h1> | ||
| <div id="isr-static-marker">static-isr</div> | ||
| </div> | ||
| ); | ||
| } |
11 changes: 11 additions & 0 deletions
11
dev-packages/e2e-tests/test-applications/nextjs-15/app/non-isr-test/[item]/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // No generateStaticParams - this is NOT an ISR page | ||
| export default async function NonISRPage({ params }: { params: Promise<{ item: string }> }) { | ||
| const { item } = await params; | ||
|
|
||
| return ( | ||
| <div> | ||
| <h1>Non-ISR Dynamic Page: {item}</h1> | ||
| <div id="non-isr-item-id">{item}</div> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
dev-packages/e2e-tests/test-applications/nextjs-15/tests/isr-routes.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { waitForTransaction } from '@sentry-internal/test-utils'; | ||
|
|
||
| test('should remove sentry-trace and baggage meta tags on ISR dynamic route page load', async ({ page }) => { | ||
| // Navigate to ISR page | ||
| await page.goto('/isr-test/laptop'); | ||
|
|
||
| // Wait for page to be fully loaded | ||
| await expect(page.locator('#isr-product-id')).toHaveText('laptop'); | ||
|
|
||
| // Check that sentry-trace and baggage meta tags are removed for ISR pages | ||
| await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0); | ||
| await expect(page.locator('meta[name="baggage"]')).toHaveCount(0); | ||
| }); | ||
|
|
||
| test('should remove sentry-trace and baggage meta tags on ISR static route', async ({ page }) => { | ||
| // Navigate to ISR static page | ||
| await page.goto('/isr-test/static'); | ||
|
|
||
| // Wait for page to be fully loaded | ||
| await expect(page.locator('#isr-static-marker')).toHaveText('static-isr'); | ||
|
|
||
| // Check that sentry-trace and baggage meta tags are removed for ISR pages | ||
| await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0); | ||
| await expect(page.locator('meta[name="baggage"]')).toHaveCount(0); | ||
| }); | ||
|
|
||
| test('should remove meta tags for different ISR dynamic route values', async ({ page }) => { | ||
| // Test with 'phone' (one of the pre-generated static params) | ||
| await page.goto('/isr-test/phone'); | ||
| await expect(page.locator('#isr-product-id')).toHaveText('phone'); | ||
|
|
||
| await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0); | ||
| await expect(page.locator('meta[name="baggage"]')).toHaveCount(0); | ||
|
|
||
| // Test with 'tablet' | ||
| await page.goto('/isr-test/tablet'); | ||
| await expect(page.locator('#isr-product-id')).toHaveText('tablet'); | ||
|
|
||
| await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0); | ||
| await expect(page.locator('meta[name="baggage"]')).toHaveCount(0); | ||
| }); | ||
|
|
||
| test('should create unique transactions for ISR pages on each visit', async ({ page }) => { | ||
| const traceIds: string[] = []; | ||
|
|
||
| // Load the same ISR page 5 times to ensure cached HTML meta tags are consistently removed | ||
| for (let i = 0; i < 5; i++) { | ||
| const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { | ||
| return !!( | ||
| transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload' | ||
| ); | ||
| }); | ||
|
|
||
| if (i === 0) { | ||
| await page.goto('/isr-test/laptop'); | ||
| } else { | ||
| await page.reload(); | ||
| } | ||
|
|
||
| const transaction = await transactionPromise; | ||
| const traceId = transaction.contexts?.trace?.trace_id; | ||
|
|
||
| expect(traceId).toBeDefined(); | ||
| expect(traceId).toMatch(/[a-f0-9]{32}/); | ||
| traceIds.push(traceId!); | ||
| } | ||
|
|
||
| // Verify all 5 page loads have unique trace IDs (no reuse of cached/stale meta tags) | ||
| const uniqueTraceIds = new Set(traceIds); | ||
| expect(uniqueTraceIds.size).toBe(5); | ||
| }); | ||
|
|
||
| test('ISR route should be identified correctly in the route manifest', async ({ page }) => { | ||
| const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { | ||
| return transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload'; | ||
| }); | ||
|
|
||
| await page.goto('/isr-test/laptop'); | ||
| const transaction = await transactionPromise; | ||
|
|
||
| // Verify the transaction is properly parameterized | ||
| expect(transaction).toMatchObject({ | ||
| transaction: '/isr-test/:product', | ||
| transaction_info: { source: 'route' }, | ||
| contexts: { | ||
| trace: { | ||
| data: { | ||
| 'sentry.source': 'route', | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| }); |
17 changes: 17 additions & 0 deletions
17
dev-packages/e2e-tests/test-applications/nextjs-16/app/isr-test/[product]/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| export const revalidate = 60; // ISR: revalidate every 60 seconds | ||
| export const dynamicParams = true; // Allow dynamic params beyond generateStaticParams | ||
|
|
||
| export async function generateStaticParams(): Promise<Array<{ product: string }>> { | ||
| return [{ product: 'laptop' }, { product: 'phone' }, { product: 'tablet' }]; | ||
| } | ||
|
|
||
| export default async function ISRProductPage({ params }: { params: Promise<{ product: string }> }) { | ||
| const { product } = await params; | ||
|
|
||
| return ( | ||
| <div> | ||
| <h1>ISR Product: {product}</h1> | ||
| <div id="isr-product-id">{product}</div> | ||
| </div> | ||
| ); | ||
| } |
15 changes: 15 additions & 0 deletions
15
dev-packages/e2e-tests/test-applications/nextjs-16/app/isr-test/static/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| export const revalidate = 60; // ISR: revalidate every 60 seconds | ||
| export const dynamicParams = true; | ||
|
|
||
| export async function generateStaticParams(): Promise<never[]> { | ||
| return []; | ||
| } | ||
|
|
||
| export default function ISRStaticPage() { | ||
| return ( | ||
| <div> | ||
| <h1>ISR Static Page</h1> | ||
| <div id="isr-static-marker">static-isr</div> | ||
| </div> | ||
| ); | ||
| } |
11 changes: 11 additions & 0 deletions
11
dev-packages/e2e-tests/test-applications/nextjs-16/app/non-isr-test/[item]/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // No generateStaticParams - this is NOT an ISR page | ||
| export default async function NonISRPage({ params }: { params: Promise<{ item: string }> }) { | ||
| const { item } = await params; | ||
|
|
||
| return ( | ||
| <div> | ||
| <h1>Non-ISR Dynamic Page: {item}</h1> | ||
| <div id="non-isr-item-id">{item}</div> | ||
| </div> | ||
| ); | ||
| } |
94 changes: 94 additions & 0 deletions
94
dev-packages/e2e-tests/test-applications/nextjs-16/tests/isr-routes.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { waitForTransaction } from '@sentry-internal/test-utils'; | ||
|
|
||
| test('should remove sentry-trace and baggage meta tags on ISR dynamic route page load', async ({ page }) => { | ||
| // Navigate to ISR page | ||
| await page.goto('/isr-test/laptop'); | ||
|
|
||
| // Wait for page to be fully loaded | ||
| await expect(page.locator('#isr-product-id')).toHaveText('laptop'); | ||
|
|
||
| // Check that sentry-trace and baggage meta tags are removed for ISR pages | ||
| await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0); | ||
| await expect(page.locator('meta[name="baggage"]')).toHaveCount(0); | ||
| }); | ||
|
|
||
| test('should remove sentry-trace and baggage meta tags on ISR static route', async ({ page }) => { | ||
| // Navigate to ISR static page | ||
| await page.goto('/isr-test/static'); | ||
|
|
||
| // Wait for page to be fully loaded | ||
| await expect(page.locator('#isr-static-marker')).toHaveText('static-isr'); | ||
|
|
||
| // Check that sentry-trace and baggage meta tags are removed for ISR pages | ||
| await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0); | ||
| await expect(page.locator('meta[name="baggage"]')).toHaveCount(0); | ||
| }); | ||
|
|
||
| test('should remove meta tags for different ISR dynamic route values', async ({ page }) => { | ||
| // Test with 'phone' (one of the pre-generated static params) | ||
| await page.goto('/isr-test/phone'); | ||
| await expect(page.locator('#isr-product-id')).toHaveText('phone'); | ||
|
|
||
| await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0); | ||
| await expect(page.locator('meta[name="baggage"]')).toHaveCount(0); | ||
|
|
||
| // Test with 'tablet' | ||
| await page.goto('/isr-test/tablet'); | ||
| await expect(page.locator('#isr-product-id')).toHaveText('tablet'); | ||
|
|
||
| await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0); | ||
| await expect(page.locator('meta[name="baggage"]')).toHaveCount(0); | ||
| }); | ||
|
|
||
| test('should create unique transactions for ISR pages on each visit', async ({ page }) => { | ||
| const traceIds: string[] = []; | ||
|
|
||
| // Load the same ISR page 5 times to ensure cached HTML meta tags are consistently removed | ||
| for (let i = 0; i < 5; i++) { | ||
| const transactionPromise = waitForTransaction('nextjs-16', async transactionEvent => { | ||
| return !!( | ||
| transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload' | ||
| ); | ||
| }); | ||
|
|
||
| if (i === 0) { | ||
| await page.goto('/isr-test/laptop'); | ||
| } else { | ||
| await page.reload(); | ||
| } | ||
|
|
||
| const transaction = await transactionPromise; | ||
| const traceId = transaction.contexts?.trace?.trace_id; | ||
|
|
||
| expect(traceId).toBeDefined(); | ||
| expect(traceId).toMatch(/[a-f0-9]{32}/); | ||
| traceIds.push(traceId!); | ||
| } | ||
|
|
||
| // Verify all 5 page loads have unique trace IDs (no reuse of cached/stale meta tags) | ||
| const uniqueTraceIds = new Set(traceIds); | ||
| expect(uniqueTraceIds.size).toBe(5); | ||
| }); | ||
|
|
||
| test('ISR route should be identified correctly in the route manifest', async ({ page }) => { | ||
| const transactionPromise = waitForTransaction('nextjs-16', async transactionEvent => { | ||
| return transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload'; | ||
| }); | ||
|
|
||
| await page.goto('/isr-test/laptop'); | ||
| const transaction = await transactionPromise; | ||
|
|
||
| // Verify the transaction is properly parameterized | ||
| expect(transaction).toMatchObject({ | ||
| transaction: '/isr-test/:product', | ||
| transaction_info: { source: 'route' }, | ||
| contexts: { | ||
| trace: { | ||
| data: { | ||
| 'sentry.source': 'route', | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { WINDOW } from '@sentry/react'; | ||
| import { getManifest, maybeParameterizeRoute } from './parameterization'; | ||
|
|
||
| /** | ||
| * Cache for ISR/SSG route checks. Exported for testing purposes. | ||
| * @internal | ||
| */ | ||
| export const IS_ISR_SSG_ROUTE_CACHE = new Map<string, boolean>(); | ||
|
|
||
| /** | ||
| * Check if the current page is an ISR/SSG route by checking the route manifest. | ||
| * @internal Exported for testing purposes. | ||
| */ | ||
| export function isIsrSsgRoute(pathname: string): boolean { | ||
| // Early parameterization to get the cache key | ||
| const parameterizedPath = maybeParameterizeRoute(pathname); | ||
| const pathToCheck = parameterizedPath || pathname; | ||
|
|
||
| // Check cache using the parameterized path as the key | ||
| if (IS_ISR_SSG_ROUTE_CACHE.has(pathToCheck)) { | ||
| return IS_ISR_SSG_ROUTE_CACHE.get(pathToCheck) as boolean; | ||
| } | ||
|
|
||
| // Cache miss get the manifest | ||
| const manifest = getManifest(); | ||
| if (!manifest?.isrRoutes || !Array.isArray(manifest.isrRoutes) || manifest.isrRoutes.length === 0) { | ||
| IS_ISR_SSG_ROUTE_CACHE.set(pathToCheck, false); | ||
| return false; | ||
| } | ||
|
|
||
| const isIsrSsgRoute = manifest.isrRoutes.includes(pathToCheck); | ||
| IS_ISR_SSG_ROUTE_CACHE.set(pathToCheck, isIsrSsgRoute); | ||
|
|
||
| return isIsrSsgRoute; | ||
| } | ||
|
|
||
| /** | ||
| * Remove sentry-trace and baggage meta tags from the DOM if this is an ISR/SSG page. | ||
| * This prevents the browser tracing integration from using stale/cached trace IDs. | ||
| */ | ||
| export function removeIsrSsgTraceMetaTags(): void { | ||
| if (!WINDOW.document || !isIsrSsgRoute(WINDOW.location.pathname)) { | ||
| return; | ||
| } | ||
|
|
||
| // Helper function to remove a meta tag | ||
| function removeMetaTag(metaName: string): void { | ||
| try { | ||
| const meta = WINDOW.document.querySelector(`meta[name="${metaName}"]`); | ||
| if (meta) { | ||
| meta.remove(); | ||
| } | ||
| } catch { | ||
| // ignore errors when removing the meta tag | ||
| } | ||
| } | ||
|
|
||
| // Remove the meta tags so browserTracingIntegration won't pick them up | ||
| removeMetaTag('sentry-trace'); | ||
| removeMetaTag('baggage'); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be an
LRUMapUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I didn't see that 🤦♂️ I will amend in a PR.