1616
1717import { WINDOW } from '../../../types' ;
1818import { getActivationStart } from './getActivationStart' ;
19+ import { addPageListener , removePageListener } from './globalListeners' ;
1920
2021let firstHiddenTime = - 1 ;
22+ const onHiddenFunctions : Set < ( ) => void > = new Set ( ) ;
2123
2224const initHiddenTime = ( ) => {
2325 // If the document is hidden when this code runs, assume it was always
@@ -29,35 +31,34 @@ const initHiddenTime = () => {
2931} ;
3032
3133const onVisibilityUpdate = ( event : Event ) => {
32- // If the document is 'hidden' and no previous hidden timestamp has been
33- // set, update it based on the current event data.
34- if ( WINDOW . document ! . visibilityState === 'hidden' && firstHiddenTime > - 1 ) {
35- // If the event is a 'visibilitychange' event, it means the page was
36- // visible prior to this change, so the event timestamp is the first
37- // hidden time.
38- // However, if the event is not a 'visibilitychange' event, then it must
39- // be a 'prerenderingchange' event, and the fact that the document is
40- // still 'hidden' from the above check means the tab was activated
41- // in a background state and so has always been hidden.
42- firstHiddenTime = event . type === 'visibilitychange' ? event . timeStamp : 0 ;
34+ // Handle changes to hidden state
35+ if ( isPageHidden ( event ) && firstHiddenTime > - 1 ) {
36+ // Sentry-specific change: Also call onHidden callbacks for pagehide events
37+ // to support older browsers (Safari <14.4) that don't properly fire visibilitychange
38+ if ( event . type === 'visibilitychange' || event . type === 'pagehide' ) {
39+ for ( const onHiddenFunction of onHiddenFunctions ) {
40+ onHiddenFunction ( ) ;
41+ }
42+ }
4343
44- // Remove all listeners now that a `firstHiddenTime` value has been set.
45- removeChangeListeners ( ) ;
46- }
47- } ;
48-
49- const addChangeListeners = ( ) => {
50- addEventListener ( 'visibilitychange' , onVisibilityUpdate , true ) ;
51- // IMPORTANT: when a page is prerendering, its `visibilityState` is
52- // 'hidden', so in order to account for cases where this module checks for
53- // visibility during prerendering, an additional check after prerendering
54- // completes is also required.
55- addEventListener ( 'prerenderingchange' , onVisibilityUpdate , true ) ;
56- } ;
44+ // If the document is 'hidden' and no previous hidden timestamp has been
45+ // set (so is infinity), update it based on the current event data.
46+ if ( ! isFinite ( firstHiddenTime ) ) {
47+ // If the event is a 'visibilitychange' event, it means the page was
48+ // visible prior to this change, so the event timestamp is the first
49+ // hidden time.
50+ // However, if the event is not a 'visibilitychange' event, then it must
51+ // be a 'prerenderingchange' or 'pagehide' event, and the fact that the document is
52+ // still 'hidden' from the above check means the tab was activated
53+ // in a background state and so has always been hidden.
54+ firstHiddenTime = event . type === 'visibilitychange' ? event . timeStamp : 0 ;
5755
58- const removeChangeListeners = ( ) => {
59- removeEventListener ( 'visibilitychange' , onVisibilityUpdate , true ) ;
60- removeEventListener ( 'prerenderingchange' , onVisibilityUpdate , true ) ;
56+ // We no longer need the `prerenderingchange` event listener now we've
57+ // set an initial init time so remove that
58+ // (we'll keep the visibilitychange and pagehide ones for onHiddenFunction above)
59+ removePageListener ( 'prerenderingchange' , onVisibilityUpdate , true ) ;
60+ }
61+ }
6162} ;
6263
6364export const getVisibilityWatcher = ( ) => {
@@ -75,14 +76,39 @@ export const getVisibilityWatcher = () => {
7576 // a perfect heuristic, but it's the best we can do until the
7677 // `visibility-state` performance entry becomes available in all browsers.
7778 firstHiddenTime = firstVisibilityStateHiddenTime ?? initHiddenTime ( ) ;
78- // We're still going to listen to for changes so we can handle things like
79- // bfcache restores and/or prerender without having to examine individual
80- // timestamps in detail.
81- addChangeListeners ( ) ;
79+ // Listen for visibility changes so we can handle things like bfcache
80+ // restores and/or prerender without having to examine individual
81+ // timestamps in detail and also for onHidden function calls.
82+ addPageListener ( 'visibilitychange' , onVisibilityUpdate , true ) ;
83+
84+ // Sentry-specific change: Some browsers have buggy implementations of visibilitychange,
85+ // so we use pagehide in addition, just to be safe. This is also required for older
86+ // Safari versions (<14.4) that we still support.
87+ addPageListener ( 'pagehide' , onVisibilityUpdate , true ) ;
88+
89+ // IMPORTANT: when a page is prerendering, its `visibilityState` is
90+ // 'hidden', so in order to account for cases where this module checks for
91+ // visibility during prerendering, an additional check after prerendering
92+ // completes is also required.
93+ addPageListener ( 'prerenderingchange' , onVisibilityUpdate , true ) ;
8294 }
95+
8396 return {
8497 get firstHiddenTime ( ) {
8598 return firstHiddenTime ;
8699 } ,
100+ onHidden ( cb : ( ) => void ) {
101+ onHiddenFunctions . add ( cb ) ;
102+ } ,
87103 } ;
88104} ;
105+
106+ /**
107+ * Check if the page is hidden, uses the `pagehide` event for older browsers support that we used to have in `onHidden` function.
108+ * Some browsers we still support (Safari <14.4) don't fully support `visibilitychange`
109+ * or have known bugs w.r.t the `visibilitychange` event.
110+ * // TODO (v11): If we decide to drop support for Safari 14.4, we can use the logic from web-vitals 4.2.4
111+ */
112+ function isPageHidden ( event : Event ) {
113+ return event . type === 'pagehide' || WINDOW . document ?. visibilityState === 'hidden' ;
114+ }
0 commit comments