Skip to content

✨ [RUMF-1580] Implement storage fallback #2261

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

Merged
merged 42 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
119cc26
🎨 Renamed cookieSession to sessionState
yannickadam May 9, 2023
3aaa1fc
♻️ Create a SessionStorage interface
yannickadam May 9, 2023
c021c67
♻️ Move withCookieLockAccess to SessionStore and rename processStora…
yannickadam May 9, 2023
ce2f815
♻️ Refactor SessionCookieStore to use SessionStorage interface
yannickadam May 11, 2023
f26717e
♻️ Update session storage interface and move back lock configuration…
yannickadam May 15, 2023
44d3ed7
🎨 Remove remaining references to cookies from sessionStore
yannickadam May 15, 2023
20b0d29
🎨 Session components naming update
yannickadam May 25, 2023
17920d8
👌 Removed StoreAccessOptions
yannickadam Jun 1, 2023
bc24152
👌 Extract sessionStoreOperations in its own file
yannickadam Jun 1, 2023
97f9e72
👌 Renamed expandOrRenewCookie
yannickadam Jun 1, 2023
72f4303
👌 Added tests to oldCookiesMigration
yannickadam Jun 1, 2023
fdc1996
👌 Updated tests
yannickadam Jun 5, 2023
99d6f97
♻️ Extract session utilities from CookieStore
yannickadam May 11, 2023
59e2610
✨ Implement Local Storage store
yannickadam May 12, 2023
2de32d1
👌 Naming toSessionState
yannickadam Jun 5, 2023
6fa5709
👌 Improved tests
yannickadam Jun 5, 2023
d176b79
👌 Added test for MAX_LOCK_TRIES + used getCookie to check session per…
yannickadam Jun 6, 2023
a770083
Merge branch 'yannick.adam/RUMF-1580_Decouple_storage_mechanism' into…
yannickadam Jun 6, 2023
81618f4
✅ Added tests on session handling capabilities
yannickadam May 15, 2023
8e5b802
♻️ Moved old cookie migration invocation to cookie storage
yannickadam May 17, 2023
008e86e
✨ Implement fallback
yannickadam May 17, 2023
7b718f1
✅ Add tests for localStorage in Session Store
yannickadam May 19, 2023
fbc6d79
♻️ Remove StoreInitOptions and propagate initConfiguration
yannickadam Jun 1, 2023
75c104d
♻️ Changed naming
yannickadam Jun 2, 2023
83acc68
♻️ Removed sessionStore from configuration
yannickadam Jun 5, 2023
7159723
👌 Remove allowFallbackToLocalStorage from SessionStoreOptions
yannickadam Jun 7, 2023
c25cc2c
👌 Match cookie test key
yannickadam Jun 7, 2023
8ca99ae
👌 Merge SessionStoreOptions in SessionStoreStrategyType
yannickadam Jun 7, 2023
b73cec6
👌 Non-null assertion for sessionManager init
yannickadam Jun 8, 2023
8ba024e
👌 Added tests for configuration
yannickadam Jun 8, 2023
2ab51bd
👌 Add e2e tests
yannickadam Jun 9, 2023
df3d491
👌 Report allowFallbackToLocalStorage in configuration telemetry
yannickadam Jun 9, 2023
306e128
👌 Fix E2E linting
yannickadam Jun 9, 2023
c041354
👌 Remove redundant unit tests
yannickadam Jun 9, 2023
5dcf990
👌 Use snake case for telemetry
yannickadam Jun 9, 2023
f49e86a
👌 Use the same key for cookie and localStorage
yannickadam Jun 12, 2023
1de407c
👌 Rename sessionStoreStrategyType to STORE_TYPE + factorize testKey
yannickadam Jun 12, 2023
dae11c9
👌 Remove COOKIE_ACCESS_DELAY in favor of STORAGE_POLL_DELAY
yannickadam Jun 12, 2023
8ac8692
👌 Add a test on configuration
yannickadam Jun 12, 2023
f3f7b92
👌 Factorize into expandSessionState
yannickadam Jun 12, 2023
d5f6633
👌 Fix typo in sessionState
yannickadam Jun 14, 2023
9d9f1fe
Merge branch 'main' into yannick.adam/RUMF-1580_Implement_fallback
yannickadam Jun 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/core/src/browser/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { display } from '../tools/display'
import { ONE_MINUTE, ONE_SECOND } from '../tools/utils/timeUtils'
import { findCommaSeparatedValue, generateUUID } from '../tools/utils/stringUtils'

export const COOKIE_ACCESS_DELAY = ONE_SECOND

export interface CookieOptions {
secure?: boolean
crossSite?: boolean
Expand Down
41 changes: 28 additions & 13 deletions packages/core/src/domain/configuration/configuration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,25 +117,40 @@ describe('validateAndBuildConfiguration', () => {
})
})

describe('cookie options', () => {
it('should not be secure nor crossSite by default', () => {
const configuration = validateAndBuildConfiguration({ clientToken })!
expect(configuration.cookieOptions).toEqual({ secure: false, crossSite: false })
describe('sessionStoreStrategyType', () => {
it('allowFallbackToLocalStorage should not be enabled by default', () => {
spyOnProperty(document, 'cookie', 'get').and.returnValue('')
const configuration = validateAndBuildConfiguration({ clientToken })
expect(configuration?.sessionStoreStrategyType).toBeUndefined()
})

it('should contain cookie in the configuration by default', () => {
const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: false })
expect(configuration?.sessionStoreStrategyType).toEqual({
type: 'Cookie',
cookieOptions: { secure: false, crossSite: false },
})
})

it('should be secure when `useSecureSessionCookie` is truthy', () => {
const configuration = validateAndBuildConfiguration({ clientToken, useSecureSessionCookie: true })!
expect(configuration.cookieOptions).toEqual({ secure: true, crossSite: false })
it('should contain cookie in the configuration when fallback is enabled and cookies are available', () => {
const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: true })
expect(configuration?.sessionStoreStrategyType).toEqual({
type: 'Cookie',
cookieOptions: { secure: false, crossSite: false },
})
})

it('should be secure and crossSite when `useCrossSiteSessionCookie` is truthy', () => {
const configuration = validateAndBuildConfiguration({ clientToken, useCrossSiteSessionCookie: true })!
expect(configuration.cookieOptions).toEqual({ secure: true, crossSite: true })
it('should contain local storage in the configuration when fallback is enabled and cookies are not available', () => {
spyOnProperty(document, 'cookie', 'get').and.returnValue('')
const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: true })
expect(configuration?.sessionStoreStrategyType).toEqual({ type: 'LocalStorage' })
})

it('should have domain when `trackSessionAcrossSubdomains` is truthy', () => {
const configuration = validateAndBuildConfiguration({ clientToken, trackSessionAcrossSubdomains: true })!
expect(configuration.cookieOptions).toEqual({ secure: false, crossSite: false, domain: jasmine.any(String) })
it('should not contain any storage if both cookies and local storage are unavailable', () => {
spyOnProperty(document, 'cookie', 'get').and.returnValue('')
spyOn(Storage.prototype, 'getItem').and.throwError('unavailable')
const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: true })
expect(configuration?.sessionStoreStrategyType).toBeUndefined()
})
})

Expand Down
53 changes: 20 additions & 33 deletions packages/core/src/domain/configuration/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { CookieOptions } from '../../browser/cookie'
import { getCurrentSite } from '../../browser/cookie'
import { catchUserErrors } from '../../tools/catchUserErrors'
import { display } from '../../tools/display'
import type { RawTelemetryConfiguration } from '../telemetry'
Expand All @@ -10,6 +8,8 @@ import { isPercentage } from '../../tools/utils/numberUtils'
import { ONE_KIBI_BYTE } from '../../tools/utils/byteUtils'
import { objectHasValue } from '../../tools/utils/objectUtils'
import { assign } from '../../tools/utils/polyfills'
import { selectSessionStoreStrategyType } from '../session/sessionStore'
import type { SessionStoreStrategyType } from '../session/storeStrategies/sessionStoreStrategy'
import type { TransportConfiguration } from './transportConfiguration'
import { computeTransportConfiguration } from './transportConfiguration'

Expand Down Expand Up @@ -52,6 +52,9 @@ export interface InitConfiguration {
useSecureSessionCookie?: boolean | undefined
trackSessionAcrossSubdomains?: boolean | undefined

// alternate storage option
allowFallbackToLocalStorage?: boolean | undefined

// internal options
enableExperimentalFeatures?: string[] | undefined
replica?: ReplicaUserConfiguration | undefined
Expand All @@ -73,7 +76,7 @@ interface ReplicaUserConfiguration {
export interface Configuration extends TransportConfiguration {
// Built from init configuration
beforeSend: GenericBeforeSendCallback | undefined
cookieOptions: CookieOptions
sessionStoreStrategyType: SessionStoreStrategyType | undefined
sessionSampleRate: number
telemetrySampleRate: number
telemetryConfigurationSampleRate: number
Expand Down Expand Up @@ -129,7 +132,7 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati
{
beforeSend:
initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'),
cookieOptions: buildCookieOptions(initConfiguration),
sessionStoreStrategyType: selectSessionStoreStrategyType(initConfiguration),
sessionSampleRate: sessionSampleRate ?? 100,
telemetrySampleRate: initConfiguration.telemetrySampleRate ?? 20,
telemetryConfigurationSampleRate: initConfiguration.telemetryConfigurationSampleRate ?? 5,
Expand Down Expand Up @@ -161,36 +164,20 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati
)
}

export function buildCookieOptions(initConfiguration: InitConfiguration) {
const cookieOptions: CookieOptions = {}

cookieOptions.secure = mustUseSecureCookie(initConfiguration)
cookieOptions.crossSite = !!initConfiguration.useCrossSiteSessionCookie

if (initConfiguration.trackSessionAcrossSubdomains) {
cookieOptions.domain = getCurrentSite()
}

return cookieOptions
}

function mustUseSecureCookie(initConfiguration: InitConfiguration) {
return !!initConfiguration.useSecureSessionCookie || !!initConfiguration.useCrossSiteSessionCookie
}

export function serializeConfiguration(configuration: InitConfiguration): Partial<RawTelemetryConfiguration> {
const proxy = configuration.proxy ?? configuration.proxyUrl
export function serializeConfiguration(initConfiguration: InitConfiguration): Partial<RawTelemetryConfiguration> {
const proxy = initConfiguration.proxy ?? initConfiguration.proxyUrl
return {
session_sample_rate: configuration.sessionSampleRate ?? configuration.sampleRate,
telemetry_sample_rate: configuration.telemetrySampleRate,
telemetry_configuration_sample_rate: configuration.telemetryConfigurationSampleRate,
use_before_send: !!configuration.beforeSend,
use_cross_site_session_cookie: configuration.useCrossSiteSessionCookie,
use_secure_session_cookie: configuration.useSecureSessionCookie,
session_sample_rate: initConfiguration.sessionSampleRate ?? initConfiguration.sampleRate,
telemetry_sample_rate: initConfiguration.telemetrySampleRate,
telemetry_configuration_sample_rate: initConfiguration.telemetryConfigurationSampleRate,
use_before_send: !!initConfiguration.beforeSend,
use_cross_site_session_cookie: initConfiguration.useCrossSiteSessionCookie,
use_secure_session_cookie: initConfiguration.useSecureSessionCookie,
use_proxy: proxy !== undefined ? !!proxy : undefined,
silent_multiple_init: configuration.silentMultipleInit,
track_session_across_subdomains: configuration.trackSessionAcrossSubdomains,
track_resources: configuration.trackResources,
track_long_task: configuration.trackLongTasks,
silent_multiple_init: initConfiguration.silentMultipleInit,
track_session_across_subdomains: initConfiguration.trackSessionAcrossSubdomains,
track_resources: initConfiguration.trackResources,
track_long_task: initConfiguration.trackLongTasks,
allow_fallback_to_local_storage: !!initConfiguration.allowFallbackToLocalStorage,
}
}
1 change: 0 additions & 1 deletion packages/core/src/domain/configuration/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export {
Configuration,
InitConfiguration,
buildCookieOptions,
DefaultPrivacyLevel,
validateAndBuildConfiguration,
serializeConfiguration,
Expand Down
34 changes: 17 additions & 17 deletions packages/core/src/domain/session/oldCookiesMigration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { CookieOptions } from '../../browser/cookie'
import { getCookie, setCookie } from '../../browser/cookie'
import {
OLD_LOGS_COOKIE_NAME,
Expand All @@ -7,44 +6,45 @@ import {
tryOldCookiesMigration,
} from './oldCookiesMigration'
import { SESSION_EXPIRATION_DELAY } from './sessionConstants'
import { SESSION_COOKIE_NAME } from './sessionCookieStore'
import { initCookieStrategy } from './storeStrategies/sessionInCookie'
import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy'

describe('old cookies migration', () => {
const options: CookieOptions = {}
const sessionStoreStrategy = initCookieStrategy({})

it('should not touch current cookie', () => {
setCookie(SESSION_COOKIE_NAME, 'id=abcde&rum=0&logs=1&expire=1234567890', SESSION_EXPIRATION_DELAY)
setCookie(SESSION_STORE_KEY, 'id=abcde&rum=0&logs=1&expire=1234567890', SESSION_EXPIRATION_DELAY)

tryOldCookiesMigration(options)
tryOldCookiesMigration(sessionStoreStrategy)

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

it('should create new cookie from old cookie values', () => {
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(options)
tryOldCookiesMigration(sessionStoreStrategy)

expect(getCookie(SESSION_COOKIE_NAME)).toContain('id=abcde')
expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0')
expect(getCookie(SESSION_COOKIE_NAME)).toContain('logs=1')
expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/expire=\d+/)
expect(getCookie(SESSION_STORE_KEY)).toContain('id=abcde')
expect(getCookie(SESSION_STORE_KEY)).toContain('rum=0')
expect(getCookie(SESSION_STORE_KEY)).toContain('logs=1')
expect(getCookie(SESSION_STORE_KEY)).toMatch(/expire=\d+/)
})

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

tryOldCookiesMigration(options)
tryOldCookiesMigration(sessionStoreStrategy)

expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=')
expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0')
expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/expire=\d+/)
expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=')
expect(getCookie(SESSION_STORE_KEY)).toContain('rum=0')
expect(getCookie(SESSION_STORE_KEY)).toMatch(/expire=\d+/)
})

it('should not create a new cookie if no old cookie is present', () => {
tryOldCookiesMigration(options)
expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined()
tryOldCookiesMigration(sessionStoreStrategy)
expect(getCookie(SESSION_STORE_KEY)).toBeUndefined()
})
})
25 changes: 12 additions & 13 deletions packages/core/src/domain/session/oldCookiesMigration.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { CookieOptions } from '../../browser/cookie'
import { getCookie } from '../../browser/cookie'
import { dateNow } from '../../tools/utils/timeUtils'
import type { SessionState } from './sessionStore'
import { isSessionInExpiredState } from './sessionStore'
import { SESSION_COOKIE_NAME, persistSessionCookie } from './sessionCookieStore'
import { SESSION_EXPIRATION_DELAY } from './sessionConstants'
import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy'
import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy'
import type { SessionState } from './sessionState'
import { expandSessionState, isSessionInExpiredState } from './sessionState'

export const OLD_SESSION_COOKIE_NAME = '_dd'
export const OLD_RUM_COOKIE_NAME = '_dd_r'
Expand All @@ -18,13 +16,14 @@ export const LOGS_SESSION_KEY = 'logs'
* This migration should remain in the codebase as long as older versions are available/live
* to allow older sdk versions to be upgraded to newer versions without compatibility issues.
*/
export function tryOldCookiesMigration(options: CookieOptions) {
const sessionString = getCookie(SESSION_COOKIE_NAME)
const oldSessionId = getCookie(OLD_SESSION_COOKIE_NAME)
const oldRumType = getCookie(OLD_RUM_COOKIE_NAME)
const oldLogsType = getCookie(OLD_LOGS_COOKIE_NAME)
export function tryOldCookiesMigration(cookieStoreStrategy: SessionStoreStrategy) {
const sessionString = getCookie(SESSION_STORE_KEY)
if (!sessionString) {
const oldSessionId = getCookie(OLD_SESSION_COOKIE_NAME)
const oldRumType = getCookie(OLD_RUM_COOKIE_NAME)
const oldLogsType = getCookie(OLD_LOGS_COOKIE_NAME)
const session: SessionState = {}

if (oldSessionId) {
session.id = oldSessionId
}
Expand All @@ -36,8 +35,8 @@ export function tryOldCookiesMigration(options: CookieOptions) {
}

if (!isSessionInExpiredState(session)) {
session.expire = String(dateNow() + SESSION_EXPIRATION_DELAY)
persistSessionCookie(options)(session)
expandSessionState(session)
cookieStoreStrategy.persistSession(session)
}
}
}
40 changes: 0 additions & 40 deletions packages/core/src/domain/session/sessionCookieStore.spec.ts

This file was deleted.

32 changes: 0 additions & 32 deletions packages/core/src/domain/session/sessionCookieStore.ts

This file was deleted.

This file was deleted.

Loading