diff --git a/packages/core/src/browser/cookie.ts b/packages/core/src/browser/cookie.ts index 553c324749..bab3f49e93 100644 --- a/packages/core/src/browser/cookie.ts +++ b/packages/core/src/browser/cookie.ts @@ -1,6 +1,11 @@ import { display } from '../tools/display' import { ONE_MINUTE, ONE_SECOND } from '../tools/utils/timeUtils' -import { findCommaSeparatedValue, findCommaSeparatedValues, generateUUID } from '../tools/utils/stringUtils' +import { + findAllCommaSeparatedValues, + findCommaSeparatedValue, + findCommaSeparatedValues, + generateUUID, +} from '../tools/utils/stringUtils' import { buildUrl } from '../tools/utils/urlPolyfill' export interface CookieOptions { @@ -21,15 +26,28 @@ export function setCookie(name: string, value: string, expireDelay: number = 0, document.cookie = `${name}=${value};${expires};path=/;samesite=${sameSite}${domain}${secure}${partitioned}` } +/** + * Returns the value of the cookie with the given name + * If there are multiple cookies with the same name, returns the first one + */ export function getCookie(name: string) { return findCommaSeparatedValue(document.cookie, name) } +/** + * Returns all the values of the cookies with the given name + */ +export function getCookies(name: string): string[] { + return findAllCommaSeparatedValues(document.cookie).get(name) || [] +} + let initCookieParsed: Map | undefined /** * Returns a cached value of the cookie. Use this during SDK initialization (and whenever possible) * to avoid accessing document.cookie multiple times. + * + * ⚠️ If there are multiple cookies with the same name, returns the LAST one (unlike `getCookie()`) */ export function getInitCookie(name: string) { if (!initCookieParsed) { diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 84dcffe6e2..8ed4a53d46 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -14,12 +14,13 @@ import { getCurrentSite } from '../../browser/cookie' import { ExperimentalFeature, isExperimentalFeatureEnabled } from '../../tools/experimentalFeatures' import { findLast } from '../../tools/utils/polyfills' import { monitorError } from '../../tools/monitor' -import { SESSION_NOT_TRACKED, SESSION_TIME_OUT_DELAY } from './sessionConstants' +import { SESSION_NOT_TRACKED, SESSION_TIME_OUT_DELAY, SessionPersistence } from './sessionConstants' import { startSessionStore } from './sessionStore' import type { SessionState } from './sessionState' import { toSessionState } from './sessionState' import { retrieveSessionCookie } from './storeStrategies/sessionInCookie' import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' +import { retrieveSessionFromLocalStorage } from './storeStrategies/sessionInLocalStorage' export interface SessionManager { findSession: ( @@ -107,7 +108,7 @@ export function startSessionManager( const session = sessionStore.getSession() if (!session) { - reportUnexpectedSessionState().catch(() => void 0) // Ignore errors + reportUnexpectedSessionState(configuration).catch(() => void 0) // Ignore errors return { id: 'invalid', @@ -172,16 +173,33 @@ function trackResume(configuration: Configuration, cb: () => void) { stopCallbacks.push(stop) } -async function reportUnexpectedSessionState() { - const rawSession = retrieveSessionCookie() +async function reportUnexpectedSessionState(configuration: Configuration) { + const sessionStoreStrategyType = configuration.sessionStoreStrategyType + if (!sessionStoreStrategyType) { + return + } + + let rawSession + let cookieContext + + if (sessionStoreStrategyType.type === SessionPersistence.COOKIE) { + rawSession = retrieveSessionCookie(sessionStoreStrategyType.cookieOptions) + + cookieContext = { + cookie: await getSessionCookies(), + currentDomain: `${window.location.protocol}//${window.location.hostname}`, + } + } else { + rawSession = retrieveSessionFromLocalStorage() + } // monitor-until: forever, could be handy to troubleshoot issues until session manager rework addTelemetryDebug('Unexpected session state', { + sessionStoreStrategyType: sessionStoreStrategyType.type, session: rawSession, isSyntheticsTest: isSyntheticsTest(), createdTimestamp: rawSession?.created, expireTimestamp: rawSession?.expire, - cookie: await getSessionCookies(), - currentDomain: `${window.location.protocol}//${window.location.hostname}`, + ...cookieContext, }) } diff --git a/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts index 1085f07f7b..7057f94c3d 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts @@ -1,26 +1,34 @@ -import { mockClock, getSessionState } from '../../../../test' +import { ExperimentalFeature } from '../../../tools/experimentalFeatures' +import { mockClock, getSessionState, registerCleanupTask, mockExperimentalFeatures } from '../../../../test' import { setCookie, deleteCookie, getCookie, getCurrentSite } from '../../../browser/cookie' import type { SessionState } from '../sessionState' -import type { Configuration } from '../../configuration' +import { validateAndBuildConfiguration } from '../../configuration' +import type { InitConfiguration } from '../../configuration' import { SESSION_COOKIE_EXPIRATION_DELAY, SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from '../sessionConstants' import { buildCookieOptions, selectCookieStrategy, initCookieStrategy } from './sessionInCookie' -import type { SessionStoreStrategy } from './sessionStoreStrategy' import { SESSION_STORE_KEY } from './sessionStoreStrategy' -export const DEFAULT_INIT_CONFIGURATION = { trackAnonymousUser: true } as Configuration -describe('session in cookie strategy', () => { - const sessionState: SessionState = { id: '123', created: '0' } - let cookieStorageStrategy: SessionStoreStrategy +const DEFAULT_INIT_CONFIGURATION = { clientToken: 'abc', trackAnonymousUser: true } - beforeEach(() => { - cookieStorageStrategy = initCookieStrategy(DEFAULT_INIT_CONFIGURATION, {}) - }) +function setupCookieStrategy(partialInitConfiguration: Partial = {}) { + const initConfiguration = { + ...DEFAULT_INIT_CONFIGURATION, + ...partialInitConfiguration, + } as InitConfiguration - afterEach(() => { - deleteCookie(SESSION_STORE_KEY) - }) + const configuration = validateAndBuildConfiguration(initConfiguration)! + const cookieOptions = buildCookieOptions(initConfiguration)! + + registerCleanupTask(() => deleteCookie(SESSION_STORE_KEY, cookieOptions)) + + return initCookieStrategy(configuration, cookieOptions) +} + +describe('session in cookie strategy', () => { + const sessionState: SessionState = { id: '123', created: '0' } it('should persist a session in a cookie', () => { + const cookieStorageStrategy = setupCookieStrategy() cookieStorageStrategy.persistSession(sessionState) const session = cookieStorageStrategy.retrieveSession() expect(session).toEqual({ ...sessionState }) @@ -28,6 +36,7 @@ describe('session in cookie strategy', () => { }) it('should set `isExpired=1` and `aid` to the cookie holding the session', () => { + const cookieStorageStrategy = setupCookieStrategy() spyOn(Math, 'random').and.callFake(() => 0) cookieStorageStrategy.persistSession(sessionState) cookieStorageStrategy.expireSession(sessionState) @@ -37,6 +46,7 @@ describe('session in cookie strategy', () => { }) it('should return an empty object if session string is invalid', () => { + const cookieStorageStrategy = setupCookieStrategy() setCookie(SESSION_STORE_KEY, '{test:42}', 1000) const session = cookieStorageStrategy.retrieveSession() expect(session).toEqual({}) @@ -101,23 +111,53 @@ describe('session in cookie strategy', () => { }) }) }) + + describe('encode cookie options', () => { + beforeEach(() => { + mockExperimentalFeatures([ExperimentalFeature.ENCODE_COOKIE_OPTIONS]) + }) + + it('should encode cookie options in the cookie value', () => { + // Some older browsers don't support partitioned cross-site session cookies + // so instead of testing the cookie value, we test the call to the cookie setter + const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') + const cookieStorageStrategy = setupCookieStrategy({ usePartitionedCrossSiteSessionCookie: true }) + cookieStorageStrategy.persistSession({ id: '123' }) + + const calls = cookieSetSpy.calls.all() + const lastCall = calls[calls.length - 1] + expect(lastCall.args[0]).toMatch(/^_dd_s=id=123&c=1/) + }) + + it('should not encode cookie options in the cookie value if the session is empty (deleting the cookie)', () => { + const cookieStorageStrategy = setupCookieStrategy({ usePartitionedCrossSiteSessionCookie: true }) + cookieStorageStrategy.persistSession({}) + + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() + }) + + it('should return the correct session state from the cookies', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('_dd_s=id=123&c=0;_dd_s=id=456&c=1;_dd_s=id=789&c=2') + const cookieStorageStrategy = setupCookieStrategy({ usePartitionedCrossSiteSessionCookie: true }) + + expect(cookieStorageStrategy.retrieveSession()).toEqual({ id: '456' }) + }) + + it('should return the session state from the first cookie if there is no match', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('_dd_s=id=123&c=0;_dd_s=id=789&c=2') + const cookieStorageStrategy = setupCookieStrategy({ usePartitionedCrossSiteSessionCookie: true }) + + expect(cookieStorageStrategy.retrieveSession()).toEqual({ id: '123' }) + }) + }) }) describe('session in cookie strategy when opt-in anonymous user tracking', () => { const anonymousId = 'device-123' const sessionState: SessionState = { id: '123', created: '0' } - let cookieStorageStrategy: SessionStoreStrategy - beforeEach(() => { - cookieStorageStrategy = initCookieStrategy( - { ...DEFAULT_INIT_CONFIGURATION, trackAnonymousUser: true } as Configuration, - {} - ) - }) - afterEach(() => { - deleteCookie(SESSION_STORE_KEY) - }) it('should persist with anonymous id', () => { + const cookieStorageStrategy = setupCookieStrategy() cookieStorageStrategy.persistSession({ ...sessionState, anonymousId }) const session = cookieStorageStrategy.retrieveSession() expect(session).toEqual({ ...sessionState, anonymousId }) @@ -125,6 +165,7 @@ describe('session in cookie strategy when opt-in anonymous user tracking', () => }) it('should expire with anonymous id', () => { + const cookieStorageStrategy = setupCookieStrategy() cookieStorageStrategy.expireSession({ ...sessionState, anonymousId }) const session = cookieStorageStrategy.retrieveSession() expect(session).toEqual({ isExpired: '1', anonymousId }) @@ -132,6 +173,7 @@ describe('session in cookie strategy when opt-in anonymous user tracking', () => }) it('should persist for one year when opt-in', () => { + const cookieStorageStrategy = setupCookieStrategy() const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') const clock = mockClock() cookieStorageStrategy.persistSession({ ...sessionState, anonymousId }) @@ -141,6 +183,7 @@ describe('session in cookie strategy when opt-in anonymous user tracking', () => }) it('should expire in one year when opt-in', () => { + const cookieStorageStrategy = setupCookieStrategy() const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') const clock = mockClock() cookieStorageStrategy.expireSession({ ...sessionState, anonymousId }) @@ -153,17 +196,9 @@ describe('session in cookie strategy when opt-in anonymous user tracking', () => describe('session in cookie strategy when opt-out anonymous user tracking', () => { const anonymousId = 'device-123' const sessionState: SessionState = { id: '123', created: '0' } - let cookieStorageStrategy: SessionStoreStrategy - - beforeEach(() => { - cookieStorageStrategy = initCookieStrategy({ trackAnonymousUser: false } as Configuration, {}) - }) - - afterEach(() => { - deleteCookie(SESSION_STORE_KEY) - }) it('should not extend cookie expiration time when opt-out', () => { + const cookieStorageStrategy = setupCookieStrategy({ trackAnonymousUser: false }) const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') const clock = mockClock() cookieStorageStrategy.expireSession({ ...sessionState, anonymousId }) @@ -171,12 +206,14 @@ describe('session in cookie strategy when opt-out anonymous user tracking', () = }) it('should not persist with one year when opt-out', () => { + const cookieStorageStrategy = setupCookieStrategy({ trackAnonymousUser: false }) const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') cookieStorageStrategy.persistSession({ ...sessionState, anonymousId }) expect(cookieSetSpy.calls.argsFor(0)[0]).toContain(new Date(Date.now() + SESSION_EXPIRATION_DELAY).toUTCString()) }) it('should not persist or expire a session with anonymous id when opt-out', () => { + const cookieStorageStrategy = setupCookieStrategy({ trackAnonymousUser: false }) cookieStorageStrategy.persistSession({ ...sessionState, anonymousId }) cookieStorageStrategy.expireSession({ ...sessionState, anonymousId }) const session = cookieStorageStrategy.retrieveSession() diff --git a/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts index 3022f2cf74..75d0d45a80 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts @@ -1,6 +1,8 @@ +import { isExperimentalFeatureEnabled, ExperimentalFeature } from '../../../tools/experimentalFeatures' +import { isEmptyObject } from '../../../tools/utils/objectUtils' import { isChromium } from '../../../tools/utils/browserDetection' import type { CookieOptions } from '../../../browser/cookie' -import { getCurrentSite, areCookiesAuthorized, getCookie, setCookie } from '../../../browser/cookie' +import { getCurrentSite, areCookiesAuthorized, getCookies, setCookie, getCookie } from '../../../browser/cookie' import type { InitConfiguration, Configuration } from '../../configuration' import { tryOldCookiesMigration } from '../oldCookiesMigration' import { @@ -14,6 +16,8 @@ import { toSessionString, toSessionState, getExpiredSessionState } from '../sess import type { SessionStoreStrategy, SessionStoreStrategyType } from './sessionStoreStrategy' import { SESSION_STORE_KEY } from './sessionStoreStrategy' +const SESSION_COOKIE_VERSION = 0 + export function selectCookieStrategy(initConfiguration: InitConfiguration): SessionStoreStrategyType | undefined { const cookieOptions = buildCookieOptions(initConfiguration) return cookieOptions && areCookiesAuthorized(cookieOptions) @@ -30,7 +34,7 @@ export function initCookieStrategy(configuration: Configuration, cookieOptions: isLockEnabled: isChromium(), persistSession: (sessionState: SessionState) => storeSessionCookie(cookieOptions, configuration, sessionState, SESSION_EXPIRATION_DELAY), - retrieveSession: retrieveSessionCookie, + retrieveSession: () => retrieveSessionCookie(cookieOptions), expireSession: (sessionState: SessionState) => storeSessionCookie( cookieOptions, @@ -51,15 +55,34 @@ function storeSessionCookie( sessionState: SessionState, defaultTimeout: number ) { + let sessionStateString = toSessionString(sessionState) + + if (isExperimentalFeatureEnabled(ExperimentalFeature.ENCODE_COOKIE_OPTIONS)) { + sessionStateString = toSessionString({ + ...sessionState, + // deleting a cookie is writing a new cookie with an empty value + // we don't want to store the cookie options in this case otherwise the cookie will not be deleted + ...(!isEmptyObject(sessionState) ? { c: encodeCookieOptions(options) } : {}), + }) + } + setCookie( SESSION_STORE_KEY, - toSessionString(sessionState), + sessionStateString, configuration.trackAnonymousUser ? SESSION_COOKIE_EXPIRATION_DELAY : defaultTimeout, options ) } -export function retrieveSessionCookie(): SessionState { +/** + * Retrieve the session state from the cookie that was set with the same cookie options + * If there is no match, return the first cookie, because that's how `getCookie()` works + */ +export function retrieveSessionCookie(cookieOptions: CookieOptions): SessionState { + if (isExperimentalFeatureEnabled(ExperimentalFeature.ENCODE_COOKIE_OPTIONS)) { + return retrieveSessionCookieFromEncodedCookie(cookieOptions) + } + const sessionString = getCookie(SESSION_STORE_KEY) const sessionState = toSessionState(sessionString) return sessionState @@ -83,3 +106,42 @@ export function buildCookieOptions(initConfiguration: InitConfiguration): Cookie return cookieOptions } + +function encodeCookieOptions(cookieOptions: CookieOptions): string { + const domainCount = cookieOptions.domain ? cookieOptions.domain.split('.').length - 1 : 0 + + /* eslint-disable no-bitwise */ + let byte = 0 + byte |= SESSION_COOKIE_VERSION << 5 // Store version in upper 3 bits + byte |= domainCount << 1 // Store domain count in next 4 bits + byte |= cookieOptions.crossSite ? 1 : 0 // Store useCrossSiteScripting in next bit + /* eslint-enable no-bitwise */ + + return byte.toString(16) // Convert to hex string +} + +/** + * Retrieve the session state from the cookie that was set with the same cookie options. + * If there is no match, fallback to the first cookie, (because that's how `getCookie()` works) + * and this allows to keep the current session id when we release this feature. + */ +function retrieveSessionCookieFromEncodedCookie(cookieOptions: CookieOptions): SessionState { + const cookies = getCookies(SESSION_STORE_KEY) + const opts = encodeCookieOptions(cookieOptions) + + let sessionState: SessionState | undefined + + // reverse the cookies so that if there is no match, the cookie returned is the first one + for (const cookie of cookies.reverse()) { + sessionState = toSessionState(cookie) + + if (sessionState.c === opts) { + break + } + } + + // remove the cookie options from the session state as this is not part of the session state + delete sessionState?.c + + return sessionState ?? {} +} diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts index 187b970ec8..4308a5d244 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts @@ -34,7 +34,7 @@ function persistInLocalStorage(sessionState: SessionState) { localStorage.setItem(SESSION_STORE_KEY, toSessionString(sessionState)) } -function retrieveSessionFromLocalStorage(): SessionState { +export function retrieveSessionFromLocalStorage(): SessionState { const sessionString = localStorage.getItem(SESSION_STORE_KEY) return toSessionState(sessionString) } diff --git a/packages/core/src/tools/experimentalFeatures.ts b/packages/core/src/tools/experimentalFeatures.ts index 82f4b7495b..e096d20629 100644 --- a/packages/core/src/tools/experimentalFeatures.ts +++ b/packages/core/src/tools/experimentalFeatures.ts @@ -18,6 +18,7 @@ export enum ExperimentalFeature { USE_TREE_WALKER_FOR_ACTION_NAME = 'use_tree_walker_for_action_name', FEATURE_OPERATION_VITAL = 'feature_operation_vital', SHORT_SESSION_INVESTIGATION = 'short_session_investigation', + ENCODE_COOKIE_OPTIONS = 'encode_cookie_options', } const enabledExperimentalFeatures: Set = new Set() diff --git a/packages/core/src/tools/utils/stringUtils.spec.ts b/packages/core/src/tools/utils/stringUtils.spec.ts index e117dac456..2a3e8e1c82 100644 --- a/packages/core/src/tools/utils/stringUtils.spec.ts +++ b/packages/core/src/tools/utils/stringUtils.spec.ts @@ -1,4 +1,9 @@ -import { safeTruncate, findCommaSeparatedValue, findCommaSeparatedValues } from './stringUtils' +import { + safeTruncate, + findCommaSeparatedValue, + findCommaSeparatedValues, + findAllCommaSeparatedValues, +} from './stringUtils' describe('stringUtils', () => { describe('safeTruncate', () => { @@ -57,4 +62,13 @@ describe('stringUtils', () => { expect(findCommaSeparatedValues('foo=a;bar=b')).toEqual(expectedValues) }) }) + + describe('findAllCommaSeparatedValues', () => { + it('returns all the values from a comma separated hash', () => { + const expectedValues = new Map() + expectedValues.set('foo', ['a', 'c']) + expectedValues.set('bar', ['b']) + expect(findAllCommaSeparatedValues('foo=a;bar=b;foo=c')).toEqual(expectedValues) + }) + }) }) diff --git a/packages/core/src/tools/utils/stringUtils.ts b/packages/core/src/tools/utils/stringUtils.ts index ff2ddc6323..e2ed146734 100644 --- a/packages/core/src/tools/utils/stringUtils.ts +++ b/packages/core/src/tools/utils/stringUtils.ts @@ -11,6 +11,10 @@ export function generateUUID(placeholder?: string): string { const COMMA_SEPARATED_KEY_VALUE = /([\w-]+)\s*=\s*([^;]+)/g +/** + * Returns the value of the key with the given name + * If there are multiple values with the same key, returns the first one + */ export function findCommaSeparatedValue(rawString: string, name: string): string | undefined { COMMA_SEPARATED_KEY_VALUE.lastIndex = 0 while (true) { @@ -25,6 +29,36 @@ export function findCommaSeparatedValue(rawString: string, name: string): string } } +/** + * Returns a map of all the values with the given key + * If there are multiple values with the same key, returns all the values + */ +export function findAllCommaSeparatedValues(rawString: string): Map { + const result = new Map() + COMMA_SEPARATED_KEY_VALUE.lastIndex = 0 + while (true) { + const match = COMMA_SEPARATED_KEY_VALUE.exec(rawString) + if (match) { + const key = match[1] + const value = match[2] + if (result.has(key)) { + result.get(key)!.push(value) + } else { + result.set(key, [value]) + } + } else { + break + } + } + return result +} + +/** + * Returns a map of the values with the given key + * ⚠️ If there are multiple values with the same key, returns the LAST one + * + * @deprecated use `findAllCommaSeparatedValues()` instead + */ export function findCommaSeparatedValues(rawString: string): Map { const result = new Map() COMMA_SEPARATED_KEY_VALUE.lastIndex = 0 diff --git a/packages/core/test/cookie.ts b/packages/core/test/cookie.ts index 5fd9573ec1..3bf52f0209 100644 --- a/packages/core/test/cookie.ts +++ b/packages/core/test/cookie.ts @@ -8,7 +8,11 @@ export function expireCookie() { } export function getSessionState(sessionStoreKey: string) { - return toSessionState(getCookie(sessionStoreKey)) + const sessionState = toSessionState(getCookie(sessionStoreKey)) + // remove the cookie options from the session state so the test works the same way as the code + // see: packages/core/src/domain/session/storeStrategies/sessionInCookie.ts:148 + delete sessionState.c + return sessionState } interface Cookie { diff --git a/test/e2e/scenario/sessionStore.scenario.ts b/test/e2e/scenario/sessionStore.scenario.ts index d34b97f063..68ad3d70b1 100644 --- a/test/e2e/scenario/sessionStore.scenario.ts +++ b/test/e2e/scenario/sessionStore.scenario.ts @@ -1,4 +1,4 @@ -import { SESSION_STORE_KEY } from '@datadog/browser-core' +import { ExperimentalFeature, SESSION_STORE_KEY } from '@datadog/browser-core' import type { BrowserContext, Page } from '@playwright/test' import { test, expect } from '@playwright/test' import type { RumPublicApi } from '@datadog/browser-rum-core' @@ -107,6 +107,60 @@ test.describe('Session Stores', () => { }) }) + for (const encodeCookieOptions of [true, false]) { + const enableExperimentalFeatures = encodeCookieOptions ? [ExperimentalFeature.ENCODE_COOKIE_OPTIONS] : [] + + createTest( + encodeCookieOptions + ? 'should not fails when RUM and LOGS are initialized with different trackSessionAcrossSubdomains values when Encode Cookie Options is enabled' + : 'should fails when RUM and LOGS are initialized with different trackSessionAcrossSubdomains values when Encode Cookie Options is disabled' + ) + .withRum({ trackSessionAcrossSubdomains: true, enableExperimentalFeatures }) + .withLogs({ trackSessionAcrossSubdomains: false, enableExperimentalFeatures }) + .withHostName(FULL_HOSTNAME) + .run(async ({ page }) => { + await page.waitForTimeout(1000) + + if (!encodeCookieOptions) { + // ensure the test is failing when the Feature Flag is disabled + test.fail() + } + + const [rumInternalContext, logsInternalContext] = await page.evaluate(() => [ + window.DD_RUM?.getInternalContext(), + window.DD_LOGS?.getInternalContext(), + ]) + + expect(rumInternalContext).toBeDefined() + expect(logsInternalContext).toBeDefined() + }) + + createTest( + encodeCookieOptions + ? 'should not fails when RUM and LOGS are initialized with different usePartitionedCrossSiteSessionCookie values when Encode Cookie Options is enabled' + : 'should fails when RUM and LOGS are initialized with different usePartitionedCrossSiteSessionCookie values when Encode Cookie Options is disabled' + ) + .withRum({ usePartitionedCrossSiteSessionCookie: true, enableExperimentalFeatures }) + .withLogs({ usePartitionedCrossSiteSessionCookie: false, enableExperimentalFeatures }) + .withHostName(FULL_HOSTNAME) + .run(async ({ page }) => { + await page.waitForTimeout(1000) + + if (!encodeCookieOptions) { + // ensure the test is failing when the Feature Flag is disabled + test.fail() + } + + const [rumInternalContext, logsInternalContext] = await page.evaluate(() => [ + window.DD_RUM?.getInternalContext(), + window.DD_LOGS?.getInternalContext(), + ]) + + expect(rumInternalContext).toBeDefined() + expect(logsInternalContext).toBeDefined() + }) + } + async function injectSdkInAnIframe(page: Page, bundleUrl: string) { await page.evaluate( (browserSdkUrl) =>