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-648] add cookie configuration options #523

Merged
merged 4 commits into from
Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
21 changes: 21 additions & 0 deletions packages/core/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CookieOptions, getCurrentSite } from './cookie'
import { BuildEnv, BuildMode, Datacenter, INTAKE_SITE } from './init'
import { haveSameOrigin } from './urlPolyfill'
import { includes, ONE_KILO_BYTE, ONE_SECOND } from './utils'
Expand Down Expand Up @@ -36,6 +37,8 @@ export const DEFAULT_CONFIGURATION = {
batchBytesLimit: 16 * ONE_KILO_BYTE,
}

const DEFAULT_COOKIE_OPTIONS: CookieOptions = { secure: false, thirdPartyContext: false }

export interface UserConfiguration {
publicApiKey?: string // deprecated
clientToken: string
Expand All @@ -56,6 +59,10 @@ export interface UserConfiguration {
env?: string
version?: string

allowThirdPartyContextExecution?: boolean
enforceSecureContextExecution?: boolean
trackSessionAcrossSubdomains?: boolean

// only on staging build mode
replica?: ReplicaUserConfiguration

Expand All @@ -71,6 +78,7 @@ interface ReplicaUserConfiguration {
}

export type Configuration = typeof DEFAULT_CONFIGURATION & {
cookieOptions: CookieOptions
logsEndpoint: string
rumEndpoint: string
traceEndpoint: string
Expand Down Expand Up @@ -121,6 +129,7 @@ export function buildConfiguration(userConfiguration: UserConfiguration, buildEn
: []

const configuration: Configuration = {
cookieOptions: { ...DEFAULT_COOKIE_OPTIONS },
isEnabled: (feature: string) => {
return includes(enableExperimentalFeatures, feature)
},
Expand Down Expand Up @@ -158,6 +167,18 @@ export function buildConfiguration(userConfiguration: UserConfiguration, buildEn
configuration.trackInteractions = !!userConfiguration.trackInteractions
}

if ('allowThirdPartyContextExecution' in userConfiguration) {
configuration.cookieOptions.thirdPartyContext = !!userConfiguration.allowThirdPartyContextExecution
}

if ('enforceSecureContextExecution' in userConfiguration) {
configuration.cookieOptions.secure = !!userConfiguration.enforceSecureContextExecution
}

if ('trackSessionAcrossSubdomains' in userConfiguration && !!userConfiguration.trackSessionAcrossSubdomains) {
configuration.cookieOptions.domain = getCurrentSite()
}

if (transportConfiguration.buildMode === BuildMode.E2E_TEST) {
if (userConfiguration.internalMonitoringEndpoint !== undefined) {
configuration.internalMonitoringEndpoint = userConfiguration.internalMonitoringEndpoint
Expand Down
43 changes: 35 additions & 8 deletions packages/core/src/cookie.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { findCommaSeparatedValue } from './utils'
import { findCommaSeparatedValue, ONE_SECOND } from './utils'

export const COOKIE_ACCESS_DELAY = 1000
export const COOKIE_ACCESS_DELAY = ONE_SECOND

export interface CookieOptions {
secure?: boolean
thirdPartyContext?: boolean
domain?: string
}

export interface CookieCache {
get: () => string | undefined
set: (value: string, expireDelay: number) => void
}

export function cacheCookieAccess(name: string): CookieCache {
export function cacheCookieAccess(name: string, options: CookieOptions): CookieCache {
let timeout: number
let cache: string | undefined
let hasCache = false
Expand All @@ -30,18 +36,21 @@ export function cacheCookieAccess(name: string): CookieCache {
return cache
},
set: (value: string, expireDelay: number) => {
setCookie(name, value, expireDelay)
setCookie(name, value, expireDelay, options)
cache = value
cacheAccess()
},
}
}

export function setCookie(name: string, value: string, expireDelay: number) {
export function setCookie(name: string, value: string, expireDelay: number, options?: CookieOptions) {
const date = new Date()
date.setTime(date.getTime() + expireDelay)
const expires = `expires=${date.toUTCString()}`
document.cookie = `${name}=${value};${expires};path=/;samesite=strict`
const sameSite = options && options.thirdPartyContext ? 'none' : 'strict'
const domain = options && options.domain ? `;domain=${options.domain}` : ''
const secure = options && (options.secure || options.thirdPartyContext) ? `;secure` : ''
document.cookie = `${name}=${value};${expires};path=/;samesite=${sameSite}${domain}${secure}`
}

export function getCookie(name: string) {
Expand All @@ -53,12 +62,30 @@ export function areCookiesAuthorized(): boolean {
return false
}
try {
const testCookieName = 'dd_rum_test'
const testCookieName = 'dd_cookie_test'
const testCookieValue = 'test'
setCookie(testCookieName, testCookieValue, 1000)
setCookie(testCookieName, testCookieValue, ONE_SECOND)
return getCookie(testCookieName) === testCookieValue
} catch (error) {
console.error(error)
return false
}
}

/**
* No API to retrieve it, number of levels for subdomain and suffix are unknown
* strategy: find the minimal domain on which cookies are allowed to be set
* https://web.dev/same-site-same-origin/#site
*/
export function getCurrentSite() {
const testCookieName = 'dd_site_test'
const testCookieValue = 'test'

const domainLevels = window.location.hostname.split('.')
let candidateDomain = domainLevels.pop()
while (domainLevels.length && !getCookie(testCookieName)) {
candidateDomain = `${domainLevels.pop()}.${candidateDomain}`
setCookie(testCookieName, testCookieValue, ONE_SECOND, { domain: candidateDomain })
}
return candidateDomain
}
5 changes: 3 additions & 2 deletions packages/core/src/sessionManagement.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cacheCookieAccess, COOKIE_ACCESS_DELAY, CookieCache } from './cookie'
import { cacheCookieAccess, COOKIE_ACCESS_DELAY, CookieCache, CookieOptions } from './cookie'
import { monitor } from './internalMonitoring'
import { Observable } from './observable'
import { tryOldCookiesMigration } from './oldCookiesMigration'
Expand Down Expand Up @@ -26,10 +26,11 @@ export interface SessionState {
* Limit access to cookie to avoid performance issues
*/
export function startSessionManagement<TrackingType extends string>(
options: CookieOptions,
productKey: string,
computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean }
): Session<TrackingType> {
const sessionCookie = cacheCookieAccess(SESSION_COOKIE_NAME)
const sessionCookie = cacheCookieAccess(SESSION_COOKIE_NAME, options)
tryOldCookiesMigration(sessionCookie)
const renewObservable = new Observable<void>()
let currentSessionId = retrieveActiveSession(sessionCookie).id
Expand Down
10 changes: 6 additions & 4 deletions packages/core/test/oldCookiesMigration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getCookie, SESSION_COOKIE_NAME, setCookie } from '../src'
import { cacheCookieAccess } from '../src/cookie'
import { cacheCookieAccess, CookieOptions } from '../src/cookie'
import {
OLD_LOGS_COOKIE_NAME,
OLD_RUM_COOKIE_NAME,
Expand All @@ -9,10 +9,12 @@ import {
import { SESSION_EXPIRATION_DELAY } from '../src/sessionManagement'

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

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

tryOldCookiesMigration(cacheCookieAccess(SESSION_COOKIE_NAME))
tryOldCookiesMigration(cacheCookieAccess(SESSION_COOKIE_NAME, options))

expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=abcde&rum=0&logs=1')
})
Expand All @@ -22,7 +24,7 @@ describe('old cookies migration', () => {
setCookie(OLD_LOGS_COOKIE_NAME, '1', SESSION_EXPIRATION_DELAY)
setCookie(OLD_RUM_COOKIE_NAME, '0', SESSION_EXPIRATION_DELAY)

tryOldCookiesMigration(cacheCookieAccess(SESSION_COOKIE_NAME))
tryOldCookiesMigration(cacheCookieAccess(SESSION_COOKIE_NAME, options))

expect(getCookie(SESSION_COOKIE_NAME)).toContain('id=abcde')
expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0')
Expand All @@ -32,7 +34,7 @@ describe('old cookies migration', () => {
it('should create new cookie from a single old cookie', () => {
setCookie(OLD_RUM_COOKIE_NAME, '0', SESSION_EXPIRATION_DELAY)

tryOldCookiesMigration(cacheCookieAccess(SESSION_COOKIE_NAME))
tryOldCookiesMigration(cacheCookieAccess(SESSION_COOKIE_NAME, options))

expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=')
expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0')
Expand Down
Loading