@@ -250,6 +250,23 @@ export interface BrowserTracingOptions {
250250 */
251251 consistentTraceSampling : boolean ;
252252
253+ /**
254+ * If set to `true`, the pageload span will not end itself automatically, unless it
255+ * runs until the {@link BrowserTracingOptions.finalTimeout} (30 seconds by default) is reached.
256+ *
257+ * Set this option to `true`, if you want full control over the pageload span duration.
258+ * You can use `Sentry.reportPageLoaded()` to manually end the pageload span whenever convenient.
259+ * Be aware that you have to ensure that this is always called, regardless of the chosen route
260+ * or path in the application.
261+ *
262+ * @default `false`. By default, the pageload span will end itself automatically, based on
263+ * the {@link BrowserTracingOptions.finalTimeout}, {@link BrowserTracingOptions.idleTimeout}
264+ * and {@link BrowserTracingOptions.childSpanTimeout}. This is more convenient to use but means
265+ * that the pageload duration can be arbitrary and might not be fully representative of a perceived
266+ * page load time.
267+ */
268+ explicitPageloadEnd : boolean ;
269+
253270 /**
254271 * _experiments allows the user to send options to define how this integration works.
255272 *
@@ -297,6 +314,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
297314 detectRedirects : true ,
298315 linkPreviousTrace : 'in-memory' ,
299316 consistentTraceSampling : false ,
317+ explicitPageloadEnd : false ,
300318 _experiments : { } ,
301319 ...defaultRequestInstrumentationOptions ,
302320} ;
@@ -345,6 +363,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
345363 detectRedirects,
346364 linkPreviousTrace,
347365 consistentTraceSampling,
366+ explicitPageloadEnd,
348367 onRequestSpanStart,
349368 } = {
350369 ...DEFAULT_BROWSER_TRACING_OPTIONS ,
@@ -354,9 +373,11 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
354373 let _collectWebVitals : undefined | ( ( ) => void ) ;
355374 let lastInteractionTimestamp : number | undefined ;
356375
376+ let _pageloadSpan : Span | undefined ;
377+
357378 /** Create routing idle transaction. */
358379 function _createRouteSpan ( client : Client , startSpanOptions : StartSpanOptions , makeActive = true ) : void {
359- const isPageloadTransaction = startSpanOptions . op === 'pageload' ;
380+ const isPageloadSpan = startSpanOptions . op === 'pageload' ;
360381
361382 const initialSpanName = startSpanOptions . name ;
362383 const finalStartSpanOptions : StartSpanOptions = beforeStartSpan
@@ -390,7 +411,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
390411 finalTimeout,
391412 childSpanTimeout,
392413 // should wait for finish signal if it's a pageload transaction
393- disableAutoFinish : isPageloadTransaction ,
414+ disableAutoFinish : isPageloadSpan ,
394415 beforeSpanEnd : span => {
395416 // This will generally always be defined here, because it is set in `setup()` of the integration
396417 // but technically, it is optional, so we guard here to be extra safe
@@ -418,6 +439,10 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
418439 } ,
419440 } ) ;
420441
442+ if ( isPageloadSpan && explicitPageloadEnd ) {
443+ _pageloadSpan = idleSpan ;
444+ }
445+
421446 setActiveIdleSpan ( client , idleSpan ) ;
422447
423448 function emitFinish ( ) : void {
@@ -426,7 +451,8 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
426451 }
427452 }
428453
429- if ( isPageloadTransaction && optionalWindowDocument ) {
454+ // Enable auto finish of the pageload span if users are not explicitly ending it
455+ if ( isPageloadSpan && ! explicitPageloadEnd && optionalWindowDocument ) {
430456 optionalWindowDocument . addEventListener ( 'readystatechange' , ( ) => {
431457 emitFinish ( ) ;
432458 } ) ;
@@ -574,6 +600,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
574600 } ) ;
575601 } ) ;
576602 } ,
603+
577604 afterAllSetup ( client ) {
578605 let startingUrl : string | undefined = getLocationHref ( ) ;
579606
@@ -654,6 +681,13 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
654681 onRequestSpanStart,
655682 } ) ;
656683 } ,
684+
685+ endPageloadSpan ( ) {
686+ if ( _pageloadSpan && explicitPageloadEnd ) {
687+ _pageloadSpan . end ( ) ;
688+ _pageloadSpan = undefined ;
689+ }
690+ } ,
657691 } ;
658692} ) satisfies IntegrationFn ;
659693
@@ -723,6 +757,25 @@ export function getMetaContent(metaName: string): string | undefined {
723757 return metaTag ?. getAttribute ( 'content' ) || undefined ;
724758}
725759
760+ /**
761+ * Manually report the end of the page load, resulting in the SDK ending the pageload span.
762+ * This only works if {@link BrowserTracingOptions.explicitPageloadEnd} is set to `true`.
763+ * Otherwise, the pageload span will end itself based on the {@link BrowserTracingOptions.finalTimeout},
764+ * {@link BrowserTracingOptions.idleTimeout} and {@link BrowserTracingOptions.childSpanTimeout}.
765+ *
766+ * @param client - The client to use. If not provided, the global client will be used.
767+ */
768+ export function reportPageLoaded ( client ?: Client ) : void {
769+ const clientToUse = client ?? getClient ( ) ;
770+ if ( clientToUse ) {
771+ const browserTracing =
772+ clientToUse . getIntegrationByName < ReturnType < typeof browserTracingIntegration > > ( 'BrowserTracing' ) ;
773+ if ( browserTracing ) {
774+ browserTracing . endPageloadSpan ( ) ;
775+ }
776+ }
777+ }
778+
726779/** Start listener for interaction transactions */
727780function registerInteractionListener (
728781 client : Client ,
0 commit comments