Skip to content

Commit cb85d18

Browse files
committed
Add ci visibility context form cookie
1 parent b3cc811 commit cb85d18

15 files changed

+308
-79
lines changed

packages/core/src/browser/addEventListener.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { monitor } from '../tools/monitor'
22
import { getZoneJsOriginalValue } from '../tools/getZoneJsOriginalValue'
33
import type { Configuration } from '../domain/configuration'
4-
import type { VisualViewport, VisualViewportEventMap } from './types'
4+
import type { CookieStore, CookieStoreEventMap, VisualViewport, VisualViewportEventMap } from './types'
55

66
export type TrustableEvent<E extends Event = Event> = E & { __ddIsTrusted?: boolean }
77

@@ -75,7 +75,9 @@ type EventMapFor<T> = T extends Window
7575
? PerformanceEventMap
7676
: T extends Worker
7777
? WorkerEventMap
78-
: Record<never, never>
78+
: T extends CookieStore
79+
? CookieStoreEventMap
80+
: Record<never, never>
7981

8082
/**
8183
* Add an event listener to an event target object (Window, Element, mock object...). This provides

packages/core/src/browser/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,21 @@ export interface VisualViewport extends EventTarget {
3838
options?: boolean | EventListenerOptions
3939
): void
4040
}
41+
42+
// Those are native API types that are not official supported by TypeScript yet
43+
// https://github.com/microsoft/TypeScript/blob/13c374a868c926f6a907666a5599992c1351b773/src/lib/dom.generated.d.ts#L15399-L15418
44+
45+
export interface CookieStore extends EventTarget {}
46+
47+
export interface CookieStoreEventMap {
48+
change: CookieChangeEvent
49+
}
50+
51+
export type CookieChangeItem = { name: string; value: string | undefined }
52+
53+
export type CookieChangeEvent = Event & {
54+
changed: CookieChangeItem[]
55+
deleted: CookieChangeItem[]
56+
}
57+
58+
export interface CookieStore extends EventTarget {}

packages/core/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,14 @@ export {
8787
} from './domain/error/error'
8888
export { NonErrorPrefix } from './domain/error/error.types'
8989
export { Context, ContextArray, ContextValue } from './tools/serialisation/context'
90-
export { areCookiesAuthorized, getCookie, setCookie, deleteCookie } from './browser/cookie'
90+
export {
91+
areCookiesAuthorized,
92+
getCookie,
93+
getInitCookie,
94+
setCookie,
95+
deleteCookie,
96+
resetInitCookies,
97+
} from './browser/cookie'
9198
export { initXhrObservable, XhrCompleteContext, XhrStartContext } from './browser/xhrObservable'
9299
export { initFetchObservable, FetchResolveContext, FetchStartContext, FetchContext } from './browser/fetchObservable'
93100
export { createPageExitObservable, PageExitEvent, PageExitReason, isPageExitReason } from './browser/pageExitObservable'

packages/rum-core/src/boot/startRum.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { startPageStateHistory } from '../domain/contexts/pageStateHistory'
4646
import type { CommonContext } from '../domain/contexts/commonContext'
4747
import { startDisplayContext } from '../domain/contexts/displayContext'
4848
import { startVitalCollection } from '../domain/vital/vitalCollection'
49+
import { startCiVisibilityContext } from '../domain/contexts/ciVisibilityContext'
4950
import type { RecorderApi } from './rumPublicApi'
5051

5152
export type StartRum = typeof startRum
@@ -230,6 +231,7 @@ export function startRumEventCollection(
230231
)
231232

232233
const displayContext = startDisplayContext(configuration)
234+
const ciVisibilityContext = startCiVisibilityContext(configuration)
233235

234236
startRumAssembly(
235237
configuration,
@@ -239,6 +241,7 @@ export function startRumEventCollection(
239241
urlContexts,
240242
actionContexts,
241243
displayContext,
244+
ciVisibilityContext,
242245
getCommonContext,
243246
reportError
244247
)
@@ -250,6 +253,7 @@ export function startRumEventCollection(
250253
addAction,
251254
actionContexts,
252255
stop: () => {
256+
ciVisibilityContext.stop()
253257
displayContext.stop()
254258
pageStateHistory.stop()
255259
urlContexts.stop()
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { Subscription } from '@datadog/browser-core'
2+
import { ONE_MINUTE, STORAGE_POLL_DELAY, deleteCookie, setCookie } from '@datadog/browser-core'
3+
import type { Clock } from '@datadog/browser-core/test'
4+
import { mockClock } from '@datadog/browser-core/test'
5+
import type { CookieChangeItem } from 'packages/core/src/browser/types'
6+
import type { RumConfiguration } from '../domain/configuration'
7+
import { createCookieObservable } from './cookieObservable'
8+
import type { CookieObservable } from './cookieObservable'
9+
10+
const COOKIE_NAME = 'cookie_name'
11+
const COOKIE_DURATION = ONE_MINUTE
12+
13+
describe('cookieObservable', () => {
14+
let observable: CookieObservable
15+
let subscription: Subscription
16+
let originalSupportedEntryTypes: PropertyDescriptor | undefined
17+
let clock: Clock
18+
beforeEach(() => {
19+
clock = mockClock()
20+
observable = createCookieObservable({} as RumConfiguration, COOKIE_NAME)
21+
originalSupportedEntryTypes = Object.getOwnPropertyDescriptor(window, 'cookieStore')
22+
})
23+
24+
afterEach(() => {
25+
subscription?.unsubscribe()
26+
if (originalSupportedEntryTypes) {
27+
Object.defineProperty(window, 'cookieStore', originalSupportedEntryTypes)
28+
}
29+
clock.cleanup()
30+
deleteCookie(COOKIE_NAME)
31+
})
32+
33+
it('should notify observers on cookie change', (done) => {
34+
subscription = observable.subscribe((cookieChange) => {
35+
expect(cookieChange).toEqual({
36+
name: COOKIE_NAME,
37+
value: 'foo',
38+
})
39+
40+
done()
41+
})
42+
setCookie(COOKIE_NAME, 'foo', COOKIE_DURATION)
43+
})
44+
45+
it('should notify observers on cookie change when cookieStore is not supported', () => {
46+
Object.defineProperty(window, 'cookieStore', { get: () => undefined })
47+
let cookieChange: CookieChangeItem | undefined
48+
observable.subscribe((change) => (cookieChange = change))
49+
50+
setCookie(COOKIE_NAME, 'foo', COOKIE_DURATION)
51+
clock.tick(STORAGE_POLL_DELAY)
52+
53+
expect(cookieChange).toEqual({
54+
name: COOKIE_NAME,
55+
value: 'foo',
56+
})
57+
})
58+
})
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { Configuration } from '@datadog/browser-core'
2+
import {
3+
setInterval,
4+
clearInterval,
5+
Observable,
6+
addEventListener,
7+
ONE_SECOND,
8+
findCommaSeparatedValue,
9+
} from '@datadog/browser-core'
10+
import type { CookieChangeItem, CookieStore } from 'packages/core/src/browser/types'
11+
12+
export interface CookieStoreWindow extends Window {
13+
cookieStore: CookieStore
14+
}
15+
16+
export type CookieObservable = ReturnType<typeof createCookieObservable>
17+
18+
export function createCookieObservable(configuration: Configuration, cookieName: string) {
19+
return new Observable<CookieChangeItem>(
20+
(observable) =>
21+
listenToCookieStoreChange(configuration, cookieName, (event) => observable.notify(event)) ??
22+
watchCookieFallback(cookieName, (event) => observable.notify(event))
23+
)
24+
}
25+
26+
function listenToCookieStoreChange(
27+
configuration: Configuration,
28+
cookieName: string,
29+
callback: (event: CookieChangeItem) => void
30+
) {
31+
if (!('cookieStore' in window)) {
32+
return
33+
}
34+
35+
const listener = addEventListener(configuration, (window as CookieStoreWindow).cookieStore, 'change', (event) => {
36+
event.changed
37+
.concat(event.deleted)
38+
.filter((change) => change.name === cookieName)
39+
.forEach((change) => {
40+
callback({
41+
name: change.name,
42+
value: change.value,
43+
})
44+
})
45+
})
46+
47+
return listener.stop
48+
}
49+
50+
export const WATCH_COOKIE_INTERVAL_DELAY = ONE_SECOND
51+
52+
function watchCookieFallback(
53+
cookieName: string,
54+
callback: (event: { name: string; value: string | undefined }) => void
55+
) {
56+
const watchCookieIntervalId = setInterval(() => {
57+
callback({ name: cookieName, value: findCommaSeparatedValue(document.cookie, cookieName) })
58+
}, WATCH_COOKIE_INTERVAL_DELAY)
59+
60+
return () => {
61+
clearInterval(watchCookieIntervalId)
62+
}
63+
}

packages/rum-core/src/domain/assembly.spec.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,7 @@ import {
99
setNavigatorConnection,
1010
} from '@datadog/browser-core/test'
1111
import type { TestSetupBuilder } from '../../test'
12-
import {
13-
createRumSessionManagerMock,
14-
mockCiVisibilityWindowValues,
15-
setup,
16-
createRawRumEvent,
17-
cleanupCiVisibilityWindowValues,
18-
} from '../../test'
12+
import { createRumSessionManagerMock, setup, createRawRumEvent, cleanupCiVisibilityValues } from '../../test'
1913
import type { RumEventDomainContext } from '../domainContext.types'
2014
import type { RawRumActionEvent, RawRumEvent } from '../rawRumEvent.types'
2115
import { RumEventType } from '../rawRumEvent.types'
@@ -26,6 +20,7 @@ import { LifeCycleEventType } from './lifeCycle'
2620
import type { RumConfiguration } from './configuration'
2721
import type { ViewContext } from './contexts/viewContexts'
2822
import type { CommonContext } from './contexts/commonContext'
23+
import type { CiVisibilityContext } from './contexts/ciVisibilityContext'
2924

3025
describe('rum assembly', () => {
3126
let setupBuilder: TestSetupBuilder
@@ -34,6 +29,8 @@ describe('rum assembly', () => {
3429
let extraConfigurationOptions: Partial<RumConfiguration> = {}
3530
let findView: () => ViewContext
3631
let reportErrorSpy: jasmine.Spy<jasmine.Func>
32+
let ciVisibilityContext: { test_execution_id: string } | undefined
33+
3734
beforeEach(() => {
3835
findView = () => ({
3936
id: '7890',
@@ -46,6 +43,8 @@ describe('rum assembly', () => {
4643
user: {},
4744
hasReplay: undefined,
4845
}
46+
ciVisibilityContext = undefined
47+
4948
setupBuilder = setup()
5049
.withViewContexts({
5150
findView: () => findView(),
@@ -67,6 +66,7 @@ describe('rum assembly', () => {
6766
urlContexts,
6867
actionContexts,
6968
displayContext,
69+
{ get: () => ciVisibilityContext } as CiVisibilityContext,
7070
() => commonContext,
7171
reportErrorSpy
7272
)
@@ -76,7 +76,7 @@ describe('rum assembly', () => {
7676

7777
afterEach(() => {
7878
cleanupSyntheticsWorkerValues()
79-
cleanupCiVisibilityWindowValues()
79+
cleanupCiVisibilityValues()
8080
})
8181

8282
describe('beforeSend', () => {
@@ -674,8 +674,8 @@ describe('rum assembly', () => {
674674
expect(serverRumEvents[0].session.type).toEqual('synthetics')
675675
})
676676

677-
it('should detect ci visibility tests based on ci visibility global window values', () => {
678-
mockCiVisibilityWindowValues('traceId')
677+
it('should detect ci visibility tests', () => {
678+
ciVisibilityContext = { test_execution_id: 'traceId' }
679679

680680
const { lifeCycle } = setupBuilder.build()
681681
notifyRawRumEvent(lifeCycle, {
@@ -798,7 +798,7 @@ describe('rum assembly', () => {
798798

799799
describe('ci visibility context', () => {
800800
it('includes the ci visibility context', () => {
801-
mockCiVisibilityWindowValues('traceId')
801+
ciVisibilityContext = { test_execution_id: 'traceId' }
802802

803803
const { lifeCycle } = setupBuilder.build()
804804
notifyRawRumEvent(lifeCycle, {

packages/rum-core/src/domain/assembly.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type {
2424
import { RumEventType } from '../rawRumEvent.types'
2525
import type { RumEvent } from '../rumEvent.types'
2626
import { getSyntheticsContext } from './contexts/syntheticsContext'
27-
import { getCiTestContext } from './contexts/ciTestContext'
27+
import type { CiVisibilityContext } from './contexts/ciVisibilityContext'
2828
import type { LifeCycle } from './lifeCycle'
2929
import { LifeCycleEventType } from './lifeCycle'
3030
import type { ViewContexts } from './contexts/viewContexts'
@@ -68,6 +68,7 @@ export function startRumAssembly(
6868
urlContexts: UrlContexts,
6969
actionContexts: ActionContexts,
7070
displayContext: DisplayContext,
71+
ciVisibilityContext: CiVisibilityContext,
7172
getCommonContext: () => CommonContext,
7273
reportError: (error: RawError) => void
7374
) {
@@ -122,7 +123,6 @@ export function startRumAssembly(
122123
}
123124

124125
const syntheticsContext = getSyntheticsContext()
125-
const ciTestContext = getCiTestContext()
126126
lifeCycle.subscribe(
127127
LifeCycleEventType.RAW_RUM_EVENT_COLLECTED,
128128
({ startTime, rawRumEvent, domainContext, savedCommonContext, customerContext }) => {
@@ -152,7 +152,11 @@ export function startRumAssembly(
152152
source: 'browser',
153153
session: {
154154
id: session.id,
155-
type: syntheticsContext ? SessionType.SYNTHETICS : ciTestContext ? SessionType.CI_TEST : SessionType.USER,
155+
type: syntheticsContext
156+
? SessionType.SYNTHETICS
157+
: ciVisibilityContext.get()
158+
? SessionType.CI_TEST
159+
: SessionType.USER,
156160
},
157161
view: {
158162
id: viewContext.id,
@@ -162,7 +166,7 @@ export function startRumAssembly(
162166
},
163167
action: needToAssembleWithAction(rawRumEvent) && actionId ? { id: actionId } : undefined,
164168
synthetics: syntheticsContext,
165-
ci_test: ciTestContext,
169+
ci_test: ciVisibilityContext.get(),
166170
display: displayContext.get(),
167171
connectivity: getConnectivity(),
168172
}

packages/rum-core/src/domain/contexts/ciTestContext.spec.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

packages/rum-core/src/domain/contexts/ciTestContext.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)