Skip to content
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

✨[RUMF-430] new session strategy #343

Merged
merged 15 commits into from
Apr 8, 2020
Merged
98 changes: 80 additions & 18 deletions packages/core/src/sessionManagement.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { cacheCookieAccess, COOKIE_ACCESS_DELAY, CookieCache } from './cookie'
import { monitor } from './internalMonitoring'
import { Observable } from './observable'
import { tryOldCookiesMigration } from './oldCookiesMigration'
import * as utils from './utils'

export const SESSION_COOKIE_NAME = '_dd_s'
export const EXPIRATION_DELAY = 15 * utils.ONE_MINUTE
export const SESSION_EXPIRATION_DELAY = 15 * utils.ONE_MINUTE
glorieux marked this conversation as resolved.
Show resolved Hide resolved
export const SESSION_TIME_OUT_DELAY = 4 * utils.ONE_HOUR
export const VISIBILITY_CHECK_DELAY = utils.ONE_MINUTE

export interface Session<T> {
renewObservable: Observable<void>

getId(): string | undefined

getType(): T | undefined
}

export interface SessionState {
id?: string

created?: string
expire?: string
[key: string]: string | undefined
}

Expand All @@ -25,22 +27,26 @@ export interface SessionState {
*/
export function startSessionManagement<Type extends string>(
sessionTypeKey: string,
computeSessionState: (rawType?: string) => { type: Type; isTracked: boolean }
computeSessionState: (rawType?: string) => { type: Type; isTracked: boolean },
withNewSessionStrategy = false
): Session<Type> {
const sessionCookie = cacheCookieAccess(SESSION_COOKIE_NAME)
tryOldCookiesMigration(sessionCookie)
const renewObservable = new Observable<void>()
let currentSessionId = retrieveSession(sessionCookie).id
let currentSessionId = retrieveActiveSession(sessionCookie, withNewSessionStrategy).id

const expandOrRenewSession = utils.throttle(() => {
const session = retrieveSession(sessionCookie)
const session = retrieveActiveSession(sessionCookie, withNewSessionStrategy)
const { type, isTracked } = computeSessionState(session[sessionTypeKey])
session[sessionTypeKey] = type
if (isTracked && !session.id) {
session.id = utils.generateUUID()
if (withNewSessionStrategy) {
session.created = String(Date.now())
}
}
// save changes and expand session duration
persistSession(session, sessionCookie)
persistSession(session, sessionCookie, withNewSessionStrategy)

// If the session id has changed, notify that the session has been renewed
if (isTracked && currentSessionId !== session.id) {
Expand All @@ -49,15 +55,23 @@ export function startSessionManagement<Type extends string>(
}
}, COOKIE_ACCESS_DELAY)

const expandSession = () => {
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
const session = retrieveActiveSession(sessionCookie, withNewSessionStrategy)
persistSession(session, sessionCookie, withNewSessionStrategy)
}

expandOrRenewSession()
trackActivity(expandOrRenewSession)
if (withNewSessionStrategy) {
trackVisibility(expandSession)
}

return {
getId() {
return retrieveSession(sessionCookie).id
return retrieveActiveSession(sessionCookie, withNewSessionStrategy).id
},
getType() {
return retrieveSession(sessionCookie)[sessionTypeKey] as Type | undefined
return retrieveActiveSession(sessionCookie, withNewSessionStrategy)[sessionTypeKey] as Type | undefined
},
renewObservable,
}
Expand All @@ -74,6 +88,24 @@ export function isValidSessionString(sessionString: string | undefined): session
)
}

function retrieveActiveSession(sessionCookie: CookieCache, withNewSessionStrategy: boolean): SessionState {
const session = retrieveSession(sessionCookie)
if (!withNewSessionStrategy || isActiveSession(session)) {
return session
}
clearSession(sessionCookie)
return {}
}

function isActiveSession(session: SessionState) {
// created and expire can be undefined for versions which was not storing them
// these checks could be removed when older versions will not be available/live anymore
return (
(session.created === undefined || Date.now() - Number(session.created) < SESSION_TIME_OUT_DELAY) &&
(session.expire === undefined || Date.now() < Number(session.expire))
)
}

function retrieveSession(sessionCookie: CookieCache): SessionState {
const sessionString = sessionCookie.get()
const session: SessionState = {}
Expand All @@ -89,25 +121,55 @@ function retrieveSession(sessionCookie: CookieCache): SessionState {
return session
}

export function persistSession(session: SessionState, cookie: CookieCache) {
export function persistSession(session: SessionState, cookie: CookieCache, withNewSessionStrategy = false) {
if (utils.isEmptyObject(session)) {
clearSession(cookie)
return
}
if (withNewSessionStrategy) {
session.expire = String(Date.now() + SESSION_EXPIRATION_DELAY)
}
const cookieString = utils
.objectEntries(session)
.map(([key, value]) => `${key}=${value}`)
.join(SESSION_ENTRY_SEPARATOR)
cookie.set(cookieString, EXPIRATION_DELAY)
cookie.set(cookieString, SESSION_EXPIRATION_DELAY)
}

function clearSession(cookie: CookieCache) {
cookie.set('', 0)
}

export function stopSessionManagement() {
registeredActivityListeners.forEach((e) => e())
registeredActivityListeners = []
stopCallbacks.forEach((e) => e())
stopCallbacks = []
}

let registeredActivityListeners: Array<() => void> = []
let stopCallbacks: Array<() => void> = []

export function trackActivity(expandOrRenewSession: () => void) {
const doExpandOrRenewSession = monitor(expandOrRenewSession)
const options = { capture: true, passive: true }
;['click', 'touchstart', 'keydown', 'scroll'].forEach((event: string) => {
document.addEventListener(event, expandOrRenewSession, options)
registeredActivityListeners.push(() => document.removeEventListener(event, expandOrRenewSession, options))
;[utils.DOM_EVENT.CLICK, utils.DOM_EVENT.TOUCH_START, utils.DOM_EVENT.KEY_DOWN, utils.DOM_EVENT.SCROLL].forEach(
(event: string) => {
document.addEventListener(event, doExpandOrRenewSession, options)
stopCallbacks.push(() => document.removeEventListener(event, doExpandOrRenewSession, options))
}
)
}

function trackVisibility(expandSession: () => void) {
const expandSessionWhenVisible = monitor(() => {
if (document.visibilityState === 'visible') {
expandSession()
}
})

const visibilityCheckInterval = window.setInterval(expandSessionWhenVisible, VISIBILITY_CHECK_DELAY)
document.addEventListener(utils.DOM_EVENT.VISIBILITY_CHANGE, expandSessionWhenVisible)

stopCallbacks.push(() => {
clearInterval(visibilityCheckInterval)
document.removeEventListener(utils.DOM_EVENT.VISIBILITY_CHANGE, expandSessionWhenVisible)
})
}
8 changes: 4 additions & 4 deletions packages/core/src/transport.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import lodashMerge from 'lodash.merge'

import { monitor } from './internalMonitoring'
import { Context, jsonStringify, objectValues } from './utils'
import { Context, DOM_EVENT, jsonStringify, objectValues } from './utils'

/**
* Use POST request without content type to:
Expand Down Expand Up @@ -161,7 +161,7 @@ export class Batch<T> {
* caveat: unload can still be canceled by another listener
*/
window.addEventListener(
'beforeunload',
DOM_EVENT.BEFORE_UNLOAD,
monitor(() => {
this.beforeFlushOnUnloadHandlers.forEach((handler) => handler())
})
Expand All @@ -172,7 +172,7 @@ export class Batch<T> {
* (e.g. when user switches to a different application, goes to homescreen, etc), or is being unloaded.
*/
document.addEventListener(
'visibilitychange',
DOM_EVENT.VISIBILITY_CHANGE,
monitor(() => {
if (document.visibilityState === 'hidden') {
this.flush()
Expand All @@ -184,7 +184,7 @@ export class Batch<T> {
* - a visibility change during doc unload (cf: https://bugs.webkit.org/show_bug.cgi?id=194897)
* - a page hide transition (cf: https://bugs.webkit.org/show_bug.cgi?id=188329)
*/
window.addEventListener('beforeunload', monitor(() => this.flush()))
window.addEventListener(DOM_EVENT.BEFORE_UNLOAD, monitor(() => this.flush()))
}
}
}
16 changes: 15 additions & 1 deletion packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@ export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
export const ONE_SECOND = 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 DOM_EVENT {
BEFORE_UNLOAD = 'beforeunload',
CLICK = 'click',
KEY_DOWN = 'keydown',
LOAD = 'load',
POP_STATE = 'popstate',
SCROLL = 'scroll',
TOUCH_START = 'touchstart',
VISIBILITY_CHANGE = 'visibilitychange',
}

export enum ResourceKind {
DOCUMENT = 'document',
XHR = 'xhr',
Expand Down Expand Up @@ -236,6 +246,10 @@ export function objectEntries(object: { [key: string]: unknown }) {
return Object.keys(object).map((key) => [key, object[key]])
}

export function isEmptyObject(object: object) {
return Object.keys(object).length === 0
}

export function getGlobalObject<T>(): T {
// tslint:disable-next-line: function-constructor no-function-constructor-with-string-args
return (typeof globalThis === 'object' ? globalThis : Function('return this')()) as T
Expand Down
12 changes: 6 additions & 6 deletions packages/core/test/oldCookiesMigration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ import {
OLD_SESSION_COOKIE_NAME,
tryOldCookiesMigration,
} from '../src/oldCookiesMigration'
import { EXPIRATION_DELAY } from '../src/sessionManagement'
import { SESSION_EXPIRATION_DELAY } from '../src/sessionManagement'

describe('old cookies migration', () => {
it('should not touch current cookie', () => {
setCookie(SESSION_COOKIE_NAME, 'id=abcde&rum=0&logs=1', EXPIRATION_DELAY)
setCookie(SESSION_COOKIE_NAME, 'id=abcde&rum=0&logs=1', SESSION_EXPIRATION_DELAY)

tryOldCookiesMigration(cacheCookieAccess(SESSION_COOKIE_NAME))

expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=abcde&rum=0&logs=1')
})

it('should create new cookie from old cookie values', () => {
setCookie(OLD_SESSION_COOKIE_NAME, 'abcde', EXPIRATION_DELAY)
setCookie(OLD_LOGS_COOKIE_NAME, '1', EXPIRATION_DELAY)
setCookie(OLD_RUM_COOKIE_NAME, '0', EXPIRATION_DELAY)
setCookie(OLD_SESSION_COOKIE_NAME, 'abcde', SESSION_EXPIRATION_DELAY)
setCookie(OLD_LOGS_COOKIE_NAME, '1', SESSION_EXPIRATION_DELAY)
setCookie(OLD_RUM_COOKIE_NAME, '0', SESSION_EXPIRATION_DELAY)

tryOldCookiesMigration(cacheCookieAccess(SESSION_COOKIE_NAME))

Expand All @@ -30,7 +30,7 @@ describe('old cookies migration', () => {
})

it('should create new cookie from a single old cookie', () => {
setCookie(OLD_RUM_COOKIE_NAME, '0', EXPIRATION_DELAY)
setCookie(OLD_RUM_COOKIE_NAME, '0', SESSION_EXPIRATION_DELAY)

tryOldCookiesMigration(cacheCookieAccess(SESSION_COOKIE_NAME))

Expand Down
4 changes: 2 additions & 2 deletions packages/core/test/requestCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
trackFetch,
trackXhr,
} from '../src/requestCollection'
import { FetchStub, FetchStubBuilder, FetchStubPromise, isFirefox, isIE } from '../src/specHelper'
import { FetchStub, FetchStubBuilder, FetchStubPromise, isIE } from '../src/specHelper'
import { find, includes } from '../src/utils'

describe('fetch tracker', () => {
Expand Down Expand Up @@ -211,7 +211,7 @@ describe('xhr tracker', () => {
expectedMethod: string | jasmine.Any
expectedStatus: number | jasmine.Any
expectedURL: string
expectedResponse?: string | jasmine.Any
expectedResponse?: string | jasmine.AsymmetricMatcher<string>
expectXHR?: (xhr: XMLHttpRequest) => void
}) {
const xhr = new XMLHttpRequest()
Expand Down
Loading