diff --git a/packages/core/src/configuration.ts b/packages/core/src/configuration.ts index 9ae8dec166..835110a20a 100644 --- a/packages/core/src/configuration.ts +++ b/packages/core/src/configuration.ts @@ -2,7 +2,7 @@ import { BuildEnv, Datacenter, Environment } from './init' import { ONE_KILO_BYTE, ONE_SECOND } from './utils' export const DEFAULT_CONFIGURATION = { - enableExperimentalFeatures: [], + enableExperimentalFeatures: [] as string[], isCollectingError: true, maxErrorsByMinute: 3000, maxInternalMonitoringMessagesPerPage: 15, @@ -42,7 +42,7 @@ export interface UserConfiguration { sampleRate?: number resourceSampleRate?: number datacenter?: Datacenter - enableExperimentalFeatures?: [] + enableExperimentalFeatures?: string[] silentMultipleInit?: boolean // Below is only taken into account for e2e-test bundle. diff --git a/packages/core/src/internalMonitoring.ts b/packages/core/src/internalMonitoring.ts index b19045faec..f244c63ee2 100644 --- a/packages/core/src/internalMonitoring.ts +++ b/packages/core/src/internalMonitoring.ts @@ -98,6 +98,7 @@ export function monitor(fn: T): T { } export function addMonitoringMessage(message: string) { + logMessageIfDebug(message) addToMonitoringBatch({ message, status: StatusType.info, @@ -151,3 +152,9 @@ function logErrorIfDebug(e: any) { console.warn('[INTERNAL ERROR]', e) } } + +function logMessageIfDebug(message: any) { + if (monitoringConfiguration.debugMode) { + console.log('[MONITORING MESSAGE]', message) + } +} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index e74271b960..29937f0519 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,7 +1,9 @@ export type Omit = Pick> export const ONE_SECOND = 1000 -export const ONE_MINUTE = 60 * 1000 +export const ONE_MINUTE = 60 * ONE_SECOND +export const ONE_HOUR = 60 * ONE_MINUTE +export const ONE_DAY = 24 * ONE_HOUR export const ONE_KILO_BYTE = 1024 export enum ResourceKind { diff --git a/packages/rum/src/newSessionChecks.ts b/packages/rum/src/newSessionChecks.ts new file mode 100644 index 0000000000..a2e988e68a --- /dev/null +++ b/packages/rum/src/newSessionChecks.ts @@ -0,0 +1,145 @@ +import { + addMonitoringMessage, + getCookie, + monitor, + ONE_DAY, + ONE_HOUR, + ONE_MINUTE, + SESSION_COOKIE_NAME, + setCookie, +} from '@datadog/browser-core' +import { LifeCycle, LifeCycleEventType } from './lifeCycle' + +const SESSION_START_COOKIE_NAME = '_dd_exp_s' +const SESSION_TIMEOUT_DURATION = 4 * ONE_HOUR +const CHECK_INTERVAL = ONE_MINUTE +const onStillVisibleCallbacks: Array<() => void> = [] + +let expandedReported = false +let visiblePageAfterTimeoutReported = false +let visiblePageAfterSessionExpirationReported = false + +export function startNewSessionChecks(lifeCycle: LifeCycle) { + lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => { + setCookie(SESSION_START_COOKIE_NAME, Date.now().toString(), ONE_DAY) + expandedReported = false + visiblePageAfterTimeoutReported = false + visiblePageAfterSessionExpirationReported = false + }) + + expandedSessionCheck() + timeoutSessionCheck() + renewedSessionCheck(lifeCycle) + + startVisibilityTracker() +} + +function getSessionDuration() { + return Date.now() - parseInt(getCookie(SESSION_START_COOKIE_NAME)!, 10) +} + +/** + * Send a log when a page with an expired session is visible + */ +function expandedSessionCheck() { + onPageStillVisible(() => { + if (!expandedReported && !hasSession() && hasSessionStart() && getSessionDuration() < SESSION_TIMEOUT_DURATION) { + addMonitoringMessage('[session check][expanded] session should have been expanded') + expandedReported = true + } + }) +} + +/** + * Send a log when a session is above 4h + * Send a log when a session is expired and the page is still visible after a 4h session + */ +function timeoutSessionCheck() { + setInterval( + monitor(() => { + if (hasSession() && hasSessionStart() && getSessionDuration() > SESSION_TIMEOUT_DURATION) { + addMonitoringMessage('[session check][timeout] session duration above timeout') + // avoid to trigger this log on following pages + // other check on session duration should have been triggered at this point + setCookie(SESSION_START_COOKIE_NAME, '', 1) + } + }), + CHECK_INTERVAL + ) + + onPageStillVisible(() => { + if ( + !visiblePageAfterTimeoutReported && + !hasSession() && + hasSessionStart() && + getSessionDuration() > SESSION_TIMEOUT_DURATION + ) { + addMonitoringMessage('[session check][timeout] page still visible after session timeout') + visiblePageAfterTimeoutReported = true + } + }) +} + +/** + * Send a log when a page with an expired session become visible + * Send a log when a page is renewed by an user interaction + */ +function renewedSessionCheck(lifeCycle: LifeCycle) { + const hasPageStartedWithASession = hasSession() + let isFirstRenewal = true + + onPageStillVisible(() => { + if (!visiblePageAfterSessionExpirationReported && !hasSession()) { + addMonitoringMessage('[session check][renewed] page visible after session expiration') + visiblePageAfterSessionExpirationReported = true + } + }) + + lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => { + const isRenewedByUserInteraction = hasPageStartedWithASession || !isFirstRenewal + + if (isRenewedByUserInteraction) { + addMonitoringMessage('[session check][renewed] session renewed by user interaction') + } + + isFirstRenewal = false + }) +} + +function onPageStillVisible(callback: () => void) { + onStillVisibleCallbacks.push(callback) +} + +function startVisibilityTracker() { + let visibilityInterval: number + + document.addEventListener( + 'visibilitychange', + monitor(() => { + document.visibilityState === 'visible' ? setVisible() : setHidden() + }) + ) + document.visibilityState === 'visible' ? setVisible() : setHidden() + + function setVisible() { + onStillVisibleCallbacks.forEach((callback) => callback()) + visibilityInterval = window.setInterval( + monitor(() => { + onStillVisibleCallbacks.forEach((callback) => callback()) + }), + CHECK_INTERVAL + ) + } + + function setHidden() { + clearInterval(visibilityInterval) + } +} + +function hasSession() { + return !!getCookie(SESSION_COOKIE_NAME) +} + +function hasSessionStart() { + return !!getCookie(SESSION_START_COOKIE_NAME) +} diff --git a/packages/rum/src/rum.entry.ts b/packages/rum/src/rum.entry.ts index 34e05acaac..5a7b3aacc2 100644 --- a/packages/rum/src/rum.entry.ts +++ b/packages/rum/src/rum.entry.ts @@ -16,6 +16,7 @@ import lodashAssign from 'lodash.assign' import { buildEnv } from './buildEnv' import { LifeCycle, LifeCycleEventType } from './lifeCycle' +import { startNewSessionChecks } from './newSessionChecks' import { startPerformanceCollection } from './performanceCollection' import { startRum } from './rum' import { startRumSession } from './rumSession' @@ -66,6 +67,9 @@ datadogRum.init = monitor((userConfiguration: RumUserConfiguration) => { const lifeCycle = new LifeCycle() const { errorObservable, configuration, internalMonitoring } = commonInit(rumUserConfiguration, buildEnv) + if (configuration.enableExperimentalFeatures.includes('new-session-checks')) { + startNewSessionChecks(lifeCycle) + } const session = startRumSession(configuration, lifeCycle) const globalApi = startRum(rumUserConfiguration.applicationId, lifeCycle, configuration, session, internalMonitoring)