Skip to content

Commit 0c6284c

Browse files
👷 run latest firefox and webkit in CI, default local e2e to chromium (#4618)
1 parent fbd89d3 commit 0c6284c

15 files changed

Lines changed: 405 additions & 235 deletions

‎.gitlab-ci.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
variables:
22
CURRENT_STAGING: staging-22
33
APP: 'browser-sdk'
4-
CURRENT_CI_IMAGE: 107
4+
CURRENT_CI_IMAGE: 108
55
BUILD_STABLE_REGISTRY: 'registry.ddbuild.io'
66
CI_IMAGE: '$BUILD_STABLE_REGISTRY/ci/$APP:$CURRENT_CI_IMAGE'
77
GIT_REPOSITORY: 'git@github.com:DataDog/browser-sdk.git'
@@ -244,7 +244,7 @@ e2e:
244244
interruptible: true
245245
parallel:
246246
matrix:
247-
- BROWSER: [chromium, chromium-pinned, firefox-pinned, webkit-pinned]
247+
- BROWSER: [chromium, firefox, webkit, chromium-pinned, firefox-pinned, webkit-pinned]
248248
artifacts:
249249
when: always
250250
reports:

‎Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ RUN curl --silent --show-error --fail http://dl.google.com/linux/chrome/deb/pool
3737

3838
# Current Playwright's Chromium (used by the e2e job)
3939
ARG PLAYWRIGHT_VERSION
40-
RUN npx -y playwright@${PLAYWRIGHT_VERSION} install --with-deps chromium
40+
RUN npx -y playwright@${PLAYWRIGHT_VERSION} install --with-deps chromium firefox webkit
4141

4242
# Pinned Playwright browsers: Chromium 120 + Firefox 119 + WebKit 17.4 (used by the e2e-pinned job)
4343
ARG PINNED_PLAYWRIGHT_VERSION=1.40.1

‎packages/core/src/browser/cookie.ts‎

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { display } from '../tools/display'
2-
import { ONE_MINUTE, ONE_SECOND } from '../tools/utils/timeUtils'
1+
import { ONE_SECOND } from '../tools/utils/timeUtils'
32
import {
43
findAllCommaSeparatedValues,
54
findCommaSeparatedValue,
@@ -70,25 +69,6 @@ export function deleteCookie(name: string, options?: CookieOptions) {
7069
setCookie(name, '', 0, options)
7170
}
7271

73-
export function areCookiesAuthorized(options: CookieOptions): boolean {
74-
if (document.cookie === undefined || document.cookie === null) {
75-
return false
76-
}
77-
try {
78-
// Use a unique cookie name to avoid issues when the SDK is initialized multiple times during
79-
// the test cookie lifetime
80-
const testCookieName = `dd_cookie_test_${generateUUID()}`
81-
const testCookieValue = 'test'
82-
setCookie(testCookieName, testCookieValue, ONE_MINUTE, options)
83-
const isCookieCorrectlySet = getCookie(testCookieName) === testCookieValue
84-
deleteCookie(testCookieName, options)
85-
return isCookieCorrectlySet
86-
} catch (error) {
87-
display.error(error)
88-
return false
89-
}
90-
}
91-
9272
/**
9373
* No API to retrieve it, number of levels for subdomain and suffix are unknown
9474
* strategy: find the minimal domain on which cookies are allowed to be set

‎packages/core/src/browser/cookieAccess.spec.ts‎

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import type { Clock } from '../../test'
22
import { collectAsyncCalls, mockClock, registerCleanupTask, replaceMockable } from '../../test'
33
import type { Configuration } from '../domain/configuration'
4+
import { display } from '../tools/display'
45
import { detectVersion, isChromium } from '../tools/utils/browserDetection'
56
import { dateNow, ONE_MINUTE } from '../tools/utils/timeUtils'
67
import type { CookieOptions } from './cookie'
78
import { deleteCookie, getCookie, setCookie } from './cookie'
89
import type { CookieStoreWindow } from './browser.types'
9-
import { createCookieAccess, WATCH_COOKIE_INTERVAL_DELAY } from './cookieAccess'
10+
import type { CookieAccess } from './cookieAccess'
11+
import {
12+
areCookiesAuthorized,
13+
createCookieStoreAccess,
14+
createDocumentCookieAccess,
15+
WATCH_COOKIE_INTERVAL_DELAY,
16+
} from './cookieAccess'
1017

1118
const COOKIE_NAME = 'test_cookie'
1219
const COOKIE_OPTIONS = { secure: false, crossSite: false, partitioned: false }
@@ -18,6 +25,7 @@ function disableCookieStore() {
1825

1926
interface setupResult {
2027
clock: Clock
28+
createCookieAccess: (name: string, options: CookieOptions) => CookieAccess
2129
flushObservable: (spy: jasmine.Spy) => Promise<void>
2230
setCookieWithCleanup: (
2331
this: void,
@@ -38,6 +46,7 @@ describe('cookieAccess', () => {
3846

3947
return {
4048
clock,
49+
createCookieAccess: (name: string, options: CookieOptions) => createDocumentCookieAccess(name, options),
4150
flushObservable(this: void, _spy: jasmine.Spy) {
4251
clock.tick(WATCH_COOKIE_INTERVAL_DELAY)
4352
return Promise.resolve()
@@ -68,6 +77,8 @@ describe('cookieAccess', () => {
6877

6978
return {
7079
clock,
80+
createCookieAccess: (name: string, options: CookieOptions) =>
81+
createCookieStoreAccess(name, options, MOCK_CONFIGURATION),
7182
async flushObservable(this: void, spy: jasmine.Spy) {
7283
await collectAsyncCalls(spy, 1)
7384
// Reset the spy calls to avoid throwing on unexpected calls during teardown
@@ -107,10 +118,10 @@ describe('cookieAccess', () => {
107118
describe(title, () => {
108119
describe('getAllAndSet', () => {
109120
it('should pass current cookie values to callback', async () => {
110-
const { setCookieWithCleanup } = setup()
121+
const { createCookieAccess, setCookieWithCleanup } = setup()
111122
await setCookieWithCleanup(COOKIE_NAME, 'value1', ONE_MINUTE)
112123

113-
const cookieAccess = createCookieAccess(COOKIE_NAME, MOCK_CONFIGURATION, COOKIE_OPTIONS)
124+
const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS)
114125

115126
let capturedValues: string[] | undefined
116127
await cookieAccess.getAllAndSet((values) => {
@@ -122,8 +133,8 @@ describe('cookieAccess', () => {
122133
})
123134

124135
it('should pass empty array when cookie does not exist', async () => {
125-
setup()
126-
const cookieAccess = createCookieAccess(COOKIE_NAME, MOCK_CONFIGURATION, COOKIE_OPTIONS)
136+
const { createCookieAccess } = setup()
137+
const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS)
127138

128139
let capturedValues: string[] | undefined
129140
await cookieAccess.getAllAndSet((values) => {
@@ -135,8 +146,8 @@ describe('cookieAccess', () => {
135146
})
136147

137148
it('should write the value returned by the callback', async () => {
138-
setup()
139-
const cookieAccess = createCookieAccess(COOKIE_NAME, MOCK_CONFIGURATION, COOKIE_OPTIONS)
149+
const { createCookieAccess } = setup()
150+
const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS)
140151

141152
await cookieAccess.getAllAndSet(() => ({ value: 'hello', expireDelay: ONE_MINUTE }))
142153

@@ -149,11 +160,11 @@ describe('cookieAccess', () => {
149160
pending('Only Recent Chromium supports multiple cookies with the same name with different options')
150161
}
151162

152-
const { setCookieWithCleanup } = setup()
163+
const { createCookieAccess, setCookieWithCleanup } = setup()
153164
await setCookieWithCleanup(COOKIE_NAME, 'value1', ONE_MINUTE)
154165
await setCookieWithCleanup(COOKIE_NAME, 'value2', ONE_MINUTE, { secure: true, partitioned: true })
155166

156-
const cookieAccess = createCookieAccess(COOKIE_NAME, MOCK_CONFIGURATION, COOKIE_OPTIONS)
167+
const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS)
157168

158169
let capturedValues: string[] | undefined
159170
await cookieAccess.getAllAndSet((values) => {
@@ -167,8 +178,8 @@ describe('cookieAccess', () => {
167178

168179
describe('observable', () => {
169180
it('should notify when cookie is changed externally', async () => {
170-
const { flushObservable, setCookieWithCleanup } = setup()
171-
const cookieAccess = createCookieAccess(COOKIE_NAME, MOCK_CONFIGURATION, COOKIE_OPTIONS)
181+
const { createCookieAccess, flushObservable, setCookieWithCleanup } = setup()
182+
const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS)
172183
const spy = jasmine.createSpy('observer')
173184
const subscription = cookieAccess.observable.subscribe(spy)
174185
registerCleanupTask(() => subscription.unsubscribe())
@@ -180,10 +191,10 @@ describe('cookieAccess', () => {
180191
})
181192

182193
it('should notify when cookie is deleted externally', async () => {
183-
const { flushObservable, setCookieWithCleanup } = setup()
194+
const { createCookieAccess, flushObservable, setCookieWithCleanup } = setup()
184195
await setCookieWithCleanup(COOKIE_NAME, 'existing', ONE_MINUTE)
185196

186-
const cookieAccess = createCookieAccess(COOKIE_NAME, MOCK_CONFIGURATION, COOKIE_OPTIONS)
197+
const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS)
187198
const spy = jasmine.createSpy('observer')
188199
const subscription = cookieAccess.observable.subscribe(spy)
189200
registerCleanupTask(() => subscription.unsubscribe())
@@ -195,10 +206,10 @@ describe('cookieAccess', () => {
195206
})
196207

197208
it('should not notify when cookie value is unchanged', async () => {
198-
const { clock, setCookieWithCleanup } = setup()
209+
const { createCookieAccess, clock, setCookieWithCleanup } = setup()
199210
await setCookieWithCleanup(COOKIE_NAME, 'stable', ONE_MINUTE)
200211

201-
const cookieAccess = createCookieAccess(COOKIE_NAME, MOCK_CONFIGURATION, COOKIE_OPTIONS)
212+
const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS)
202213
const spy = jasmine.createSpy('observer')
203214
const subscription = cookieAccess.observable.subscribe(spy)
204215
registerCleanupTask(() => subscription.unsubscribe())
@@ -209,8 +220,8 @@ describe('cookieAccess', () => {
209220
})
210221

211222
it('should notify the observable after writing', async () => {
212-
const { flushObservable } = setup()
213-
const cookieAccess = createCookieAccess(COOKIE_NAME, MOCK_CONFIGURATION, COOKIE_OPTIONS)
223+
const { createCookieAccess, flushObservable } = setup()
224+
const cookieAccess = createCookieAccess(COOKIE_NAME, COOKIE_OPTIONS)
214225
const spy = jasmine.createSpy('observer')
215226
const subscription = cookieAccess.observable.subscribe(spy)
216227
registerCleanupTask(() => subscription.unsubscribe())
@@ -223,4 +234,85 @@ describe('cookieAccess', () => {
223234
})
224235
})
225236
}
237+
238+
describe('areCookiesAuthorized', () => {
239+
it('returns true when the access can write and read back the test cookie', async () => {
240+
const access: CookieAccess = {
241+
getAll: jasmine.createSpy('getAll').and.returnValue(Promise.resolve(['test'])),
242+
getAllAndSet: jasmine.createSpy('getAllAndSet').and.returnValue(Promise.resolve()),
243+
observable: null as any,
244+
}
245+
const factory = jasmine.createSpy('factory').and.returnValue(access)
246+
247+
const result = await areCookiesAuthorized(factory, COOKIE_OPTIONS, MOCK_CONFIGURATION)
248+
249+
expect(result).toBe(true)
250+
expect(factory).toHaveBeenCalledWith(jasmine.any(String), COOKIE_OPTIONS, MOCK_CONFIGURATION)
251+
})
252+
253+
it('returns false when the access cannot read back the test cookie', async () => {
254+
const access: CookieAccess = {
255+
getAll: () => Promise.resolve([]),
256+
getAllAndSet: () => Promise.resolve(),
257+
observable: null as any,
258+
}
259+
260+
const result = await areCookiesAuthorized(() => access, COOKIE_OPTIONS, MOCK_CONFIGURATION)
261+
262+
expect(result).toBe(false)
263+
})
264+
265+
it('returns false and logs when the access throws', async () => {
266+
const displayErrorSpy = spyOn(display, 'error')
267+
const access: CookieAccess = {
268+
getAll: () => Promise.resolve([]),
269+
getAllAndSet: () => Promise.reject(new Error('boom')),
270+
observable: null as any,
271+
}
272+
273+
const result = await areCookiesAuthorized(() => access, COOKIE_OPTIONS, MOCK_CONFIGURATION)
274+
275+
expect(result).toBe(false)
276+
expect(displayErrorSpy).toHaveBeenCalled()
277+
})
278+
279+
it('cleans up the test cookie after the check', async () => {
280+
const calls: Array<{ value: string; expireDelay: number }> = []
281+
const access: CookieAccess = {
282+
getAll: () => Promise.resolve(['test']),
283+
getAllAndSet: (cb) => {
284+
calls.push(cb([]))
285+
return Promise.resolve()
286+
},
287+
observable: null as any,
288+
}
289+
290+
await areCookiesAuthorized(() => access, COOKIE_OPTIONS, MOCK_CONFIGURATION)
291+
292+
expect(calls).toEqual([
293+
{ value: 'test', expireDelay: jasmine.any(Number) as unknown as number },
294+
{ value: '', expireDelay: 0 },
295+
])
296+
})
297+
298+
it('works with the real createDocumentCookieAccess', async () => {
299+
disableCookieStore()
300+
const result = await areCookiesAuthorized(createDocumentCookieAccess, COOKIE_OPTIONS, MOCK_CONFIGURATION)
301+
expect(result).toBe(true)
302+
})
303+
304+
it('works with the real createCookieStoreAccess', async () => {
305+
if (!(window as CookieStoreWindow).cookieStore) {
306+
pending('CookieStore API not available')
307+
}
308+
const result = await areCookiesAuthorized(createCookieStoreAccess, COOKIE_OPTIONS, MOCK_CONFIGURATION)
309+
expect(result).toBe(true)
310+
})
311+
312+
it('returns false when document.cookie is empty', async () => {
313+
spyOnProperty(document, 'cookie', 'get').and.returnValue('')
314+
const result = await areCookiesAuthorized(createDocumentCookieAccess, COOKIE_OPTIONS, MOCK_CONFIGURATION)
315+
expect(result).toBe(false)
316+
})
317+
})
226318
})

‎packages/core/src/browser/cookieAccess.ts‎

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { setInterval, clearInterval } from '../tools/timer'
2-
import { dateNow, ONE_SECOND } from '../tools/utils/timeUtils'
2+
import { dateNow, ONE_MINUTE, ONE_SECOND } from '../tools/utils/timeUtils'
33
import { Observable } from '../tools/observable'
44
import { mockable } from '../tools/mockable'
5+
import { display } from '../tools/display'
6+
import { generateUUID } from '../tools/utils/stringUtils'
57
import type { Configuration } from '../domain/configuration'
68
import { addTelemetryDebug } from '../domain/telemetry'
79
import { addEventListener, DOM_EVENT } from './addEventListener'
@@ -21,24 +23,44 @@ export interface CookieAccess {
2123
observable: Observable<void>
2224
}
2325

24-
export function createCookieAccess(
26+
export type CookieAccessFactory = (
2527
cookieName: string,
26-
configuration: Configuration,
27-
cookieOptions: CookieOptions
28-
): CookieAccess {
29-
const cookieStore = mockable((window as CookieStoreWindow).cookieStore)
30-
if (cookieStore) {
31-
return createCookieStoreAccess(cookieName, configuration, cookieOptions, cookieStore)
28+
cookieOptions: CookieOptions,
29+
configuration: Configuration
30+
) => CookieAccess
31+
32+
export async function areCookiesAuthorized(
33+
createAccess: CookieAccessFactory,
34+
cookieOptions: CookieOptions,
35+
configuration: Configuration
36+
): Promise<boolean> {
37+
// Use a unique cookie name to avoid issues when the SDK is initialized multiple times during
38+
// the test cookie lifetime
39+
const testCookieName = `dd_cookie_test_${generateUUID()}`
40+
const testCookieValue = 'test'
41+
const access = createAccess(testCookieName, cookieOptions, configuration)
42+
try {
43+
await access.getAllAndSet(() => ({ value: testCookieValue, expireDelay: ONE_MINUTE }))
44+
const values = await access.getAll()
45+
return values.includes(testCookieValue)
46+
} catch (error) {
47+
display.error(error)
48+
return false
49+
} finally {
50+
try {
51+
await access.getAllAndSet(() => ({ value: '', expireDelay: 0 }))
52+
} catch {
53+
// Best-effort cleanup
54+
}
3255
}
33-
return createDocumentCookieAccess(cookieName, cookieOptions)
3456
}
3557

36-
function createCookieStoreAccess(
58+
export function createCookieStoreAccess(
3759
cookieName: string,
38-
configuration: Configuration,
3960
cookieOptions: CookieOptions,
40-
cookieStore: NonNullable<CookieStoreWindow['cookieStore']>
61+
configuration: Configuration
4162
): CookieAccess {
63+
const cookieStore = mockable((window as CookieStoreWindow).cookieStore)!
4264
const observable = new Observable<void>(() => {
4365
const listener = addEventListener(configuration, cookieStore, DOM_EVENT.CHANGE, (event) => {
4466
// Based on our experimentation, we're assuming that entries for the same cookie cannot be in both the 'changed' and 'deleted' arrays.
@@ -99,7 +121,11 @@ function createCookieStoreAccess(
99121
}
100122

101123
export const WATCH_COOKIE_INTERVAL_DELAY = ONE_SECOND
102-
function createDocumentCookieAccess(cookieName: string, cookieOptions: CookieOptions): CookieAccess {
124+
export function createDocumentCookieAccess(
125+
cookieName: string,
126+
cookieOptions: CookieOptions,
127+
_configuration?: Configuration
128+
): CookieAccess {
103129
let previousCookieValues = getCookies(cookieName)
104130

105131
const observable = new Observable<void>(() => {

0 commit comments

Comments
 (0)