diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 24180d753c..20cf5283c5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -49,7 +49,7 @@ export * from './tools/browserDetection' export { instrumentMethod, instrumentMethodAndCallOriginal } from './tools/instrumentMethod' export { ErrorSource, ErrorHandling, formatUnknownError, createHandlingStack, RawError } from './tools/error' export { Context, ContextArray, ContextValue } from './tools/context' -export { areCookiesAuthorized, getCookie, setCookie, COOKIE_ACCESS_DELAY } from './browser/cookie' +export { areCookiesAuthorized, getCookie, setCookie, deleteCookie, COOKIE_ACCESS_DELAY } from './browser/cookie' export { initXhrObservable, XhrCompleteContext, XhrStartContext } from './browser/xhrObservable' export { initFetchObservable, FetchCompleteContext, FetchStartContext, FetchContext } from './browser/fetchObservable' export { EndpointBuilder } from './domain/configuration/endpointBuilder' diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index d1744c8424..b63d581a9b 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -9,7 +9,13 @@ import { resetExperimentalFeatures, } from '@datadog/browser-core' import { initEventBridgeStub, deleteEventBridgeStub } from '../../../core/test/specHelper' -import { noopRecorderApi, setup, TestSetupBuilder } from '../../test/specHelper' +import { + cleanupSyntheticsWorkerValues, + mockSyntheticsWorkerValues, + noopRecorderApi, + setup, + TestSetupBuilder, +} from '../../test/specHelper' import { ActionType } from '../rawRumEvent.types' import { makeRumPublicApi, @@ -143,6 +149,26 @@ describe('rum public api', () => { }) }) + describe('init', () => { + let rumPublicApi: RumPublicApi + let startRumSpy: jasmine.Spy + + beforeEach(() => { + startRumSpy = jasmine.createSpy() + rumPublicApi = makeRumPublicApi(startRumSpy, noopRecorderApi) + }) + + afterEach(() => { + cleanupSyntheticsWorkerValues() + }) + + it('does not call startRum if Synthetics will inject its own instance of RUM', () => { + mockSyntheticsWorkerValues({ injectsRum: true }) + rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + expect(startRumSpy).not.toHaveBeenCalled() + }) + }) + describe('getInternalContext', () => { let getInternalContextSpy: jasmine.Spy['getInternalContext']> let rumPublicApi: RumPublicApi diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index 8291addb29..6d42f32f70 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -28,6 +28,7 @@ import { RumSession } from '../domain/rumSession' import { RumEventDomainContext } from '../domainContext.types' import { CommonContext, User, ActionType, ReplayStats } from '../rawRumEvent.types' import { RumEvent } from '../rumEvent.types' +import { willSyntheticsInjectRum } from '../domain/syntheticsContext' import { buildEnv } from './buildEnv' import { startRum } from './startRum' @@ -97,6 +98,10 @@ export function makeRumPublicApi(startRumImpl: S } function initRum(initConfiguration: C) { + if (willSyntheticsInjectRum()) { + return + } + if (canUseEventBridge()) { initConfiguration = overrideInitConfigurationForBridge(initConfiguration) } else if (!canHandleSession(initConfiguration)) { diff --git a/packages/rum-core/src/domain/assembly.spec.ts b/packages/rum-core/src/domain/assembly.spec.ts index 3a617f795b..d4b116cfd4 100644 --- a/packages/rum-core/src/domain/assembly.spec.ts +++ b/packages/rum-core/src/domain/assembly.spec.ts @@ -1,23 +1,19 @@ -import { ErrorSource, ONE_MINUTE, RawError, RelativeTime, display, setCookie } from '@datadog/browser-core' -import { deleteCookie } from 'packages/core/src/browser/cookie' -import { createRumSessionMock } from 'packages/rum-core/test/mockRumSession' +import { ErrorSource, ONE_MINUTE, RawError, RelativeTime, display } from '@datadog/browser-core' +import { createRumSessionMock } from '../../test/mockRumSession' import { createRawRumEvent } from '../../test/fixtures' -import { setup, TestSetupBuilder } from '../../test/specHelper' +import { + cleanupSyntheticsWorkerValues, + mockSyntheticsWorkerValues, + setup, + TestSetupBuilder, +} from '../../test/specHelper' import { RumEventDomainContext } from '../domainContext.types' import { CommonContext, RawRumActionEvent, RawRumErrorEvent, RawRumEvent, RumEventType } from '../rawRumEvent.types' import { RumActionEvent, RumErrorEvent, RumEvent } from '../rumEvent.types' -import { - BrowserWindow, - startRumAssembly, - SYNTHETICS_RESULT_ID_COOKIE_NAME, - SYNTHETICS_TEST_ID_COOKIE_NAME, -} from './assembly' +import { startRumAssembly } from './assembly' import { LifeCycle, LifeCycleEventType, RawRumEventCollectedData } from './lifeCycle' import { RumSessionPlan } from './rumSession' -// Duration to create a cookie lasting at least until the end of the test -const COOKIE_DURATION = 1000 - describe('rum assembly', () => { let setupBuilder: TestSetupBuilder let commonContext: CommonContext @@ -65,9 +61,7 @@ describe('rum assembly', () => { afterEach(() => { setupBuilder.cleanup() - cleanupSyntheticsGlobals() - deleteCookie(SYNTHETICS_TEST_ID_COOKIE_NAME) - deleteCookie(SYNTHETICS_RESULT_ID_COOKIE_NAME) + cleanupSyntheticsWorkerValues() }) describe('beforeSend', () => { @@ -527,20 +521,8 @@ describe('rum assembly', () => { }) }) - it('should detect synthetics sessions from global', () => { - setSyntheticsGlobals('foo', 'bar') - - const { lifeCycle } = setupBuilder.build() - notifyRawRumEvent(lifeCycle, { - rawRumEvent: createRawRumEvent(RumEventType.VIEW), - }) - - expect(serverRumEvents[0].session.type).toEqual('synthetics') - }) - - it('should detect synthetics sessions from cookies', () => { - setCookie(SYNTHETICS_TEST_ID_COOKIE_NAME, 'foo', COOKIE_DURATION) - setCookie(SYNTHETICS_RESULT_ID_COOKIE_NAME, 'bar', COOKIE_DURATION) + it('should detect synthetics sessions based on synthetics worker values', () => { + mockSyntheticsWorkerValues() const { lifeCycle } = setupBuilder.build() notifyRawRumEvent(lifeCycle, { @@ -572,66 +554,15 @@ describe('rum assembly', () => { }) describe('synthetics context', () => { - it('sets the synthetics context defined by global variables', () => { - setSyntheticsGlobals('foo', 'bar') - - const { lifeCycle } = setupBuilder.build() - notifyRawRumEvent(lifeCycle, { - rawRumEvent: createRawRumEvent(RumEventType.VIEW), - }) - - expect(serverRumEvents[0].synthetics).toEqual({ - test_id: 'foo', - result_id: 'bar', - }) - }) - - it('sets the synthetics context defined by global cookie', () => { - setCookie(SYNTHETICS_TEST_ID_COOKIE_NAME, 'foo', COOKIE_DURATION) - setCookie(SYNTHETICS_RESULT_ID_COOKIE_NAME, 'bar', COOKIE_DURATION) - - const { lifeCycle } = setupBuilder.build() - notifyRawRumEvent(lifeCycle, { - rawRumEvent: createRawRumEvent(RumEventType.VIEW), - }) - - expect(serverRumEvents[0].synthetics).toEqual({ - test_id: 'foo', - result_id: 'bar', - }) - }) - - it('does not set synthetics context if one global variable is undefined', () => { - setSyntheticsGlobals('foo') - - const { lifeCycle } = setupBuilder.build() - notifyRawRumEvent(lifeCycle, { - rawRumEvent: createRawRumEvent(RumEventType.VIEW), - }) - - expect(serverRumEvents[0].synthetics).toBeUndefined() - }) - - it('does not set synthetics context if global variables are not strings', () => { - setSyntheticsGlobals(1, 2) + it('includes the synthetics context', () => { + mockSyntheticsWorkerValues() const { lifeCycle } = setupBuilder.build() notifyRawRumEvent(lifeCycle, { rawRumEvent: createRawRumEvent(RumEventType.VIEW), }) - expect(serverRumEvents[0].synthetics).toBeUndefined() - }) - - it('does not set synthetics context if one cookie is undefined', () => { - setCookie(SYNTHETICS_TEST_ID_COOKIE_NAME, 'foo', COOKIE_DURATION) - - const { lifeCycle } = setupBuilder.build() - notifyRawRumEvent(lifeCycle, { - rawRumEvent: createRawRumEvent(RumEventType.VIEW), - }) - - expect(serverRumEvents[0].synthetics).toBeUndefined() + expect(serverRumEvents[0].synthetics).toBeTruthy() }) }) @@ -783,13 +714,3 @@ function notifyRawRumEvent( } lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, fullData) } - -function setSyntheticsGlobals(publicId: any, resultId?: any) { - ;(window as BrowserWindow)._DATADOG_SYNTHETICS_PUBLIC_ID = publicId - ;(window as BrowserWindow)._DATADOG_SYNTHETICS_RESULT_ID = resultId -} - -function cleanupSyntheticsGlobals() { - delete (window as BrowserWindow)._DATADOG_SYNTHETICS_PUBLIC_ID - delete (window as BrowserWindow)._DATADOG_SYNTHETICS_RESULT_ID -} diff --git a/packages/rum-core/src/domain/assembly.ts b/packages/rum-core/src/domain/assembly.ts index 51eb68a558..6d88bac094 100644 --- a/packages/rum-core/src/domain/assembly.ts +++ b/packages/rum-core/src/domain/assembly.ts @@ -11,7 +11,6 @@ import { RawError, createEventRateLimiter, EventRateLimiter, - getCookie, } from '@datadog/browser-core' import { RumEventDomainContext } from '../domainContext.types' import { @@ -25,16 +24,12 @@ import { User, } from '../rawRumEvent.types' import { RumEvent } from '../rumEvent.types' +import { getSyntheticsContext } from './syntheticsContext' import { LifeCycle, LifeCycleEventType } from './lifeCycle' import { ParentContexts } from './parentContexts' import { RumSession, RumSessionPlan } from './rumSession' import { UrlContexts } from './urlContexts' -export interface BrowserWindow extends Window { - _DATADOG_SYNTHETICS_PUBLIC_ID?: string - _DATADOG_SYNTHETICS_RESULT_ID?: string -} - enum SessionType { SYNTHETICS = 'synthetics', USER = 'user', @@ -158,19 +153,3 @@ function needToAssembleWithAction( ): event is RawRumErrorEvent | RawRumResourceEvent | RawRumLongTaskEvent { return [RumEventType.ERROR, RumEventType.RESOURCE, RumEventType.LONG_TASK].indexOf(event.type) !== -1 } - -export const SYNTHETICS_TEST_ID_COOKIE_NAME = 'datadog-synthetics-public-id' -export const SYNTHETICS_RESULT_ID_COOKIE_NAME = 'datadog-synthetics-result-id' - -function getSyntheticsContext() { - const testId = (window as BrowserWindow)._DATADOG_SYNTHETICS_PUBLIC_ID || getCookie(SYNTHETICS_TEST_ID_COOKIE_NAME) - const resultId = - (window as BrowserWindow)._DATADOG_SYNTHETICS_RESULT_ID || getCookie(SYNTHETICS_RESULT_ID_COOKIE_NAME) - - if (typeof testId === 'string' && typeof resultId === 'string') { - return { - test_id: testId, - result_id: resultId, - } - } -} diff --git a/packages/rum-core/src/domain/parentContexts.spec.ts b/packages/rum-core/src/domain/parentContexts.spec.ts index a7168b313d..0e145d7644 100644 --- a/packages/rum-core/src/domain/parentContexts.spec.ts +++ b/packages/rum-core/src/domain/parentContexts.spec.ts @@ -1,6 +1,7 @@ import { RelativeTime, relativeToClocks } from '@datadog/browser-core' import { createRumSessionMock, RumSessionMock } from '../../test/mockRumSession' import { setup, TestSetupBuilder } from '../../test/specHelper' +import { CLEAR_OLD_CONTEXTS_INTERVAL } from '../tools/contextHistory' import { LifeCycleEventType } from './lifeCycle' import { ACTION_CONTEXT_TIME_OUT_DELAY, @@ -10,7 +11,6 @@ import { } from './parentContexts' import { AutoAction } from './rumEventsCollection/action/trackActions' import { ViewCreatedEvent } from './rumEventsCollection/view/trackViews' -import { CLEAR_OLD_CONTEXTS_INTERVAL } from './contextHistory' describe('parentContexts', () => { const FAKE_ID = 'fake' diff --git a/packages/rum-core/src/domain/parentContexts.ts b/packages/rum-core/src/domain/parentContexts.ts index 49d9056207..390c129e07 100644 --- a/packages/rum-core/src/domain/parentContexts.ts +++ b/packages/rum-core/src/domain/parentContexts.ts @@ -1,10 +1,10 @@ import { ONE_MINUTE, RelativeTime, SESSION_TIME_OUT_DELAY } from '@datadog/browser-core' import { ActionContext, ViewContext } from '../rawRumEvent.types' +import { ContextHistory } from '../tools/contextHistory' import { LifeCycle, LifeCycleEventType } from './lifeCycle' import { AutoAction, AutoActionCreatedEvent } from './rumEventsCollection/action/trackActions' import { ViewCreatedEvent } from './rumEventsCollection/view/trackViews' import { RumSession } from './rumSession' -import { ContextHistory } from './contextHistory' export const VIEW_CONTEXT_TIME_OUT_DELAY = SESSION_TIME_OUT_DELAY export const ACTION_CONTEXT_TIME_OUT_DELAY = 5 * ONE_MINUTE // arbitrary diff --git a/packages/rum-core/src/domain/syntheticsContext.spec.ts b/packages/rum-core/src/domain/syntheticsContext.spec.ts new file mode 100644 index 0000000000..e0fb42f719 --- /dev/null +++ b/packages/rum-core/src/domain/syntheticsContext.spec.ts @@ -0,0 +1,74 @@ +import { mockSyntheticsWorkerValues, cleanupSyntheticsWorkerValues } from '../../test/specHelper' +import { getSyntheticsContext, willSyntheticsInjectRum } from './syntheticsContext' + +describe('getSyntheticsContext', () => { + afterEach(() => { + cleanupSyntheticsWorkerValues() + }) + + it('sets the synthetics context defined by global variables', () => { + mockSyntheticsWorkerValues({ publicId: 'foo', resultId: 'bar' }, 'globals') + + expect(getSyntheticsContext()).toEqual({ + test_id: 'foo', + result_id: 'bar', + }) + }) + + it('sets the synthetics context defined by global cookie', () => { + mockSyntheticsWorkerValues({ publicId: 'foo', resultId: 'bar' }, 'cookies') + + expect(getSyntheticsContext()).toEqual({ + test_id: 'foo', + result_id: 'bar', + }) + }) + + it('does not set synthetics context if one global variable is undefined', () => { + mockSyntheticsWorkerValues({ publicId: 'foo' }, 'globals') + + expect(getSyntheticsContext()).toBeUndefined() + }) + + it('does not set synthetics context if global variables are not strings', () => { + mockSyntheticsWorkerValues({ publicId: 1, resultId: 2 }, 'globals') + + expect(getSyntheticsContext()).toBeUndefined() + }) + + it('does not set synthetics context if one cookie is undefined', () => { + mockSyntheticsWorkerValues({ publicId: 'foo' }, 'cookies') + + expect(getSyntheticsContext()).toBeUndefined() + }) +}) + +describe('willSyntheticsInjectRum', () => { + afterEach(() => { + cleanupSyntheticsWorkerValues() + }) + + it('returns false if nothing is defined', () => { + mockSyntheticsWorkerValues({}, 'globals') + + expect(willSyntheticsInjectRum()).toBeFalse() + }) + + it('returns false if the INJECTS_RUM global variable is false', () => { + mockSyntheticsWorkerValues({ injectsRum: false }, 'globals') + + expect(willSyntheticsInjectRum()).toBeFalse() + }) + + it('returns true if the INJECTS_RUM global variable is truthy', () => { + mockSyntheticsWorkerValues({ injectsRum: true }, 'globals') + + expect(willSyntheticsInjectRum()).toBeTrue() + }) + + it('returns true if the INJECTS_RUM cookie is truthy', () => { + mockSyntheticsWorkerValues({ injectsRum: true }, 'cookies') + + expect(willSyntheticsInjectRum()).toBeTrue() + }) +}) diff --git a/packages/rum-core/src/domain/syntheticsContext.ts b/packages/rum-core/src/domain/syntheticsContext.ts new file mode 100644 index 0000000000..3a6862a21b --- /dev/null +++ b/packages/rum-core/src/domain/syntheticsContext.ts @@ -0,0 +1,30 @@ +import { getCookie } from '@datadog/browser-core' + +export const SYNTHETICS_TEST_ID_COOKIE_NAME = 'datadog-synthetics-public-id' +export const SYNTHETICS_RESULT_ID_COOKIE_NAME = 'datadog-synthetics-result-id' +export const SYNTHETICS_INJECTS_RUM_COOKIE_NAME = 'datadog-synthetics-injects-rum' + +export interface BrowserWindow extends Window { + _DATADOG_SYNTHETICS_PUBLIC_ID?: string + _DATADOG_SYNTHETICS_RESULT_ID?: string + _DATADOG_SYNTHETICS_INJECTS_RUM?: boolean +} + +export function getSyntheticsContext() { + const testId = (window as BrowserWindow)._DATADOG_SYNTHETICS_PUBLIC_ID || getCookie(SYNTHETICS_TEST_ID_COOKIE_NAME) + const resultId = + (window as BrowserWindow)._DATADOG_SYNTHETICS_RESULT_ID || getCookie(SYNTHETICS_RESULT_ID_COOKIE_NAME) + + if (typeof testId === 'string' && typeof resultId === 'string') { + return { + test_id: testId, + result_id: resultId, + } + } +} + +export function willSyntheticsInjectRum() { + return Boolean( + (window as BrowserWindow)._DATADOG_SYNTHETICS_INJECTS_RUM || getCookie(SYNTHETICS_INJECTS_RUM_COOKIE_NAME) + ) +} diff --git a/packages/rum-core/src/domain/urlContexts.ts b/packages/rum-core/src/domain/urlContexts.ts index 2121ab9ef2..f039cffc0a 100644 --- a/packages/rum-core/src/domain/urlContexts.ts +++ b/packages/rum-core/src/domain/urlContexts.ts @@ -1,7 +1,7 @@ import { RelativeTime, Observable, SESSION_TIME_OUT_DELAY, relativeNow } from '@datadog/browser-core' import { UrlContext } from '../rawRumEvent.types' import { LocationChange } from '../browser/locationChangeObservable' -import { ContextHistory } from './contextHistory' +import { ContextHistory } from '../tools/contextHistory' import { LifeCycle, LifeCycleEventType } from './lifeCycle' /** diff --git a/packages/rum-core/src/domain/contextHistory.spec.ts b/packages/rum-core/src/tools/contextHistory.spec.ts similarity index 100% rename from packages/rum-core/src/domain/contextHistory.spec.ts rename to packages/rum-core/src/tools/contextHistory.spec.ts diff --git a/packages/rum-core/src/domain/contextHistory.ts b/packages/rum-core/src/tools/contextHistory.ts similarity index 100% rename from packages/rum-core/src/domain/contextHistory.ts rename to packages/rum-core/src/tools/contextHistory.ts diff --git a/packages/rum-core/test/specHelper.ts b/packages/rum-core/test/specHelper.ts index b31c71e1fd..8739a033e5 100644 --- a/packages/rum-core/test/specHelper.ts +++ b/packages/rum-core/test/specHelper.ts @@ -6,6 +6,8 @@ import { Observable, TimeStamp, noop, + setCookie, + deleteCookie, } from '@datadog/browser-core' import { SPEC_ENDPOINTS, mockClock, Clock, buildLocation } from '../../core/test/specHelper' import { RecorderApi } from '../src/boot/rumPublicApi' @@ -17,6 +19,12 @@ import { RumSession, RumSessionPlan } from '../src/domain/rumSession' import { RawRumEvent, RumContext, ViewContext, UrlContext } from '../src/rawRumEvent.types' import { LocationChange } from '../src/browser/locationChangeObservable' import { UrlContexts } from '../src/domain/urlContexts' +import { + BrowserWindow, + SYNTHETICS_INJECTS_RUM_COOKIE_NAME, + SYNTHETICS_RESULT_ID_COOKIE_NAME, + SYNTHETICS_TEST_ID_COOKIE_NAME, +} from '../src/domain/syntheticsContext' import { validateFormat } from './formatValidation' import { createRumSessionMock } from './mockRumSession' @@ -252,3 +260,43 @@ export const noopRecorderApi: RecorderApi = { onRumStart: noop, getReplayStats: () => undefined, } + +// Duration to create a cookie lasting at least until the end of the test +const COOKIE_DURATION = 1000 + +export function mockSyntheticsWorkerValues( + { publicId, resultId, injectsRum }: { publicId?: any; resultId?: any; injectsRum?: any } = { + publicId: 'synthetics_public_id', + resultId: 'synthetics_result_id', + injectsRum: false, + }, + method: 'globals' | 'cookies' = 'globals' +) { + switch (method) { + case 'globals': + ;(window as BrowserWindow)._DATADOG_SYNTHETICS_PUBLIC_ID = publicId + ;(window as BrowserWindow)._DATADOG_SYNTHETICS_RESULT_ID = resultId + ;(window as BrowserWindow)._DATADOG_SYNTHETICS_INJECTS_RUM = injectsRum + break + case 'cookies': + if (publicId !== undefined) { + setCookie(SYNTHETICS_TEST_ID_COOKIE_NAME, publicId, COOKIE_DURATION) + } + if (resultId !== undefined) { + setCookie(SYNTHETICS_RESULT_ID_COOKIE_NAME, resultId, COOKIE_DURATION) + } + if (injectsRum !== undefined) { + setCookie(SYNTHETICS_INJECTS_RUM_COOKIE_NAME, injectsRum, COOKIE_DURATION) + } + break + } +} + +export function cleanupSyntheticsWorkerValues() { + delete (window as BrowserWindow)._DATADOG_SYNTHETICS_PUBLIC_ID + delete (window as BrowserWindow)._DATADOG_SYNTHETICS_RESULT_ID + delete (window as BrowserWindow)._DATADOG_SYNTHETICS_INJECTS_RUM + deleteCookie(SYNTHETICS_TEST_ID_COOKIE_NAME) + deleteCookie(SYNTHETICS_RESULT_ID_COOKIE_NAME) + deleteCookie(SYNTHETICS_INJECTS_RUM_COOKIE_NAME) +}