diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/[id]/page.tsx new file mode 100644 index 000000000000..18cb7fcf532d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/[id]/page.tsx @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/nextjs'; + +// Only `test` is prerendered at build time. `exception` and `message` are generated +// on-demand at request time. +export function generateStaticParams() { + return [{ id: 'test' }]; +} + +export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + + if (id === 'exception') { + Sentry.captureException(new Error('Test error from cache components page')); + return
Error captured for id exception
; + } + + if (id === 'message') { + Sentry.captureMessage('Test message from cache components page'); + returnMessage captured for id message
; + } + + returnHello, {id}!
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheComponents.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheComponents.spec.ts index 9a60ac59cd8f..0be0e2e6e3a6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheComponents.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheComponents.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Should render cached component', async ({ page }) => { const serverTxPromise = waitForTransaction('nextjs-16-cacheComponents', async transactionEvent => { @@ -40,6 +40,34 @@ test('Should generate metadata', async ({ page }) => { await expect(page).toHaveTitle('Cache Components Metadata Test'); }); +// Capturing an event inside a Server Component that is (re)generated at request time must not +// trip Next.js Cache Components prerender guards (`new Date()` / `crypto`). +test('Should capture an exception from an on-demand generated Server Component', async ({ page }) => { + const errorPromise = waitForError('nextjs-16-cacheComponents', errorEvent => { + return errorEvent.exception?.values?.[0]?.value === 'Test error from cache components page'; + }); + + await page.goto('/exception'); + + await expect(page.locator('#result')).toHaveText('Error captured for id exception'); + + const error = await errorPromise; + expect(error.exception?.values?.[0]?.value).toBe('Test error from cache components page'); +}); + +test('Should capture a message from an on-demand generated Server Component', async ({ page }) => { + const messagePromise = waitForError('nextjs-16-cacheComponents', errorEvent => { + return errorEvent.message === 'Test message from cache components page'; + }); + + await page.goto('/message'); + + await expect(page.locator('#result')).toHaveText('Message captured for id message'); + + const message = await messagePromise; + expect(message.message).toBe('Test message from cache components page'); +}); + test('Should generate metadata async', async ({ page }) => { const serverTxPromise = waitForTransaction('nextjs-16-cacheComponents', async transactionEvent => { return transactionEvent.contexts?.trace?.op === 'http.server'; diff --git a/packages/core/src/checkin.ts b/packages/core/src/checkin.ts index e00c1045b7d0..a4d01e8c8087 100644 --- a/packages/core/src/checkin.ts +++ b/packages/core/src/checkin.ts @@ -4,6 +4,7 @@ import type { CheckInEnvelope, CheckInItem, DynamicSamplingContext } from './typ import type { SdkMetadata } from './types/sdkmetadata'; import { dsnToString } from './utils/dsn'; import { createEnvelope } from './utils/envelope'; +import { safeDateNow } from './utils/randomSafeContext'; /** * Create envelope from check in item. @@ -16,7 +17,7 @@ export function createCheckInEnvelope( dsn?: DsnComponents, ): CheckInEnvelope { const headers: CheckInEnvelope[0] = { - sent_at: new Date().toISOString(), + sent_at: new Date(safeDateNow()).toISOString(), }; if (metadata?.sdk) { diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 15b0d4ba576a..8a2faa114951 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -27,6 +27,7 @@ import { getSdkMetadataForEnvelopeHeader, } from './utils/envelope'; import { uuid4 } from './utils/misc'; +import { safeDateNow } from './utils/randomSafeContext'; import { shouldIgnoreSpan } from './utils/should-ignore-span'; import { showSpanDropWarning, spanToJSON } from './utils/spanUtils'; @@ -70,7 +71,7 @@ export function createSessionEnvelope( ): SessionEnvelope { const sdkInfo = getSdkMetadataForEnvelopeHeader(metadata); const envelopeHeaders = { - sent_at: new Date().toISOString(), + sent_at: new Date(safeDateNow()).toISOString(), ...(sdkInfo && { sdk: sdkInfo }), ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }), }; @@ -134,7 +135,7 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? const tunnel = client?.getOptions().tunnel; const headers: SpanEnvelope[0] = { - sent_at: new Date().toISOString(), + sent_at: new Date(safeDateNow()).toISOString(), ...(dscHasRequiredProps(dsc) && { trace: dsc }), ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }), }; diff --git a/packages/core/src/integrations/http/record-request-session.ts b/packages/core/src/integrations/http/record-request-session.ts index dfda6580cf16..cdef0538d3ee 100644 --- a/packages/core/src/integrations/http/record-request-session.ts +++ b/packages/core/src/integrations/http/record-request-session.ts @@ -4,6 +4,7 @@ import { DEBUG_BUILD } from '../../debug-build'; import type { Scope } from '../../scope'; import type { HttpServerResponse } from './types'; import type { AggregationCounts } from '../../types/session'; +import { safeDateNow } from '../../utils/randomSafeContext'; import { safeUnref } from '../../utils/timer'; const clientToRequestSessionAggregatesMap = new WeakMap< @@ -42,7 +43,7 @@ export function recordRequestSession( if (client && requestSession) { DEBUG_BUILD && debug.log(`Recorded request session with status: ${requestSession.status}`); - const roundedDate = new Date(); + const roundedDate = new Date(safeDateNow()); roundedDate.setSeconds(0, 0); const dateBucketKey = roundedDate.toISOString(); diff --git a/packages/core/src/tracing/spans/envelope.ts b/packages/core/src/tracing/spans/envelope.ts index 1bd99b03fd68..aeea71262b14 100644 --- a/packages/core/src/tracing/spans/envelope.ts +++ b/packages/core/src/tracing/spans/envelope.ts @@ -4,6 +4,7 @@ import type { SerializedStreamedSpan } from '../../types/span'; import { dsnToString } from '../../utils/dsn'; import { createEnvelope, getSdkMetadataForEnvelopeHeader } from '../../utils/envelope'; import { isBrowser } from '../../utils/isBrowser'; +import { safeDateNow } from '../../utils/randomSafeContext'; /** * Creates a span v2 span streaming envelope @@ -19,7 +20,7 @@ export function createStreamedSpanEnvelope( const sdk = getSdkMetadataForEnvelopeHeader(options._metadata); const headers: StreamedSpanEnvelope[0] = { - sent_at: new Date().toISOString(), + sent_at: new Date(safeDateNow()).toISOString(), ...(dscHasRequiredProps(dsc) && { trace: dsc }), ...(sdk && { sdk }), ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }), diff --git a/packages/core/src/transports/offline.ts b/packages/core/src/transports/offline.ts index c21d3dd4a7fb..8c58a1b0e00b 100644 --- a/packages/core/src/transports/offline.ts +++ b/packages/core/src/transports/offline.ts @@ -3,6 +3,7 @@ import type { Envelope } from '../types/envelope'; import type { InternalBaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types/transport'; import { debug } from '../utils/debug-logger'; import { envelopeContainsItemType } from '../utils/envelope'; +import { safeDateNow } from '../utils/randomSafeContext'; import { parseRetryAfterHeader } from '../utils/ratelimit'; import { safeUnref } from '../utils/timer'; @@ -108,7 +109,7 @@ export function makeOfflineTransport