diff --git a/packages/browser-core/src/index.ts b/packages/browser-core/src/index.ts index 26b5aedc27..ed0655b830 100644 --- a/packages/browser-core/src/index.ts +++ b/packages/browser-core/src/index.ts @@ -179,6 +179,7 @@ export * from './tools/utils/numberUtils' export * from './tools/utils/byteUtils' export * from './tools/utils/objectUtils' export * from './tools/utils/functionUtils' +export * from './tools/queueMicrotask' export * from './tools/serialisation/jsonStringify' export * from './tools/serialisation/stringify' export * from './tools/utils/stringUtils' diff --git a/packages/browser-core/test/emulate/mockClock.ts b/packages/browser-core/test/emulate/mockClock.ts index 51f12b566e..531fad9240 100644 --- a/packages/browser-core/test/emulate/mockClock.ts +++ b/packages/browser-core/test/emulate/mockClock.ts @@ -1,5 +1,7 @@ import type { TimeStamp, RelativeTime } from '@datadog/js-core/time' +import { queueMicrotask } from '../../src/tools/queueMicrotask' import { registerCleanupTask } from '../registerCleanupTask' +import { replaceMockable } from '../replaceMockable' export type Clock = ReturnType @@ -15,6 +17,17 @@ export function mockClock() { registerCleanupTask(() => jasmine.clock().uninstall()) + const pendingMicroTasks: Array<() => void> = [] + replaceMockable(queueMicrotask, (callback) => { + pendingMicroTasks.push(callback) + }) + + function flushPendingMicroTasks() { + while (pendingMicroTasks.length > 0) { + pendingMicroTasks.shift()!() + } + } + return { /** * Returns a RelativeTime representing the time it was X milliseconds after the `mockClock()` @@ -26,7 +39,11 @@ export function mockClock() { * invokation (the start of the test). */ timeStamp: (duration: number) => (timeStampStart + duration) as TimeStamp, - tick: (ms: number) => jasmine.clock().tick(ms), + tick: (ms: number) => { + flushPendingMicroTasks() + jasmine.clock().tick(ms) + flushPendingMicroTasks() + }, setDate: (date: Date) => jasmine.clock().mockDate(date), } } diff --git a/packages/browser-rum-core/src/boot/startRum.spec.ts b/packages/browser-rum-core/src/boot/startRum.spec.ts index d9a80328b6..843779e878 100644 --- a/packages/browser-rum-core/src/boot/startRum.spec.ts +++ b/packages/browser-rum-core/src/boot/startRum.spec.ts @@ -92,11 +92,13 @@ describe('session expiration lifecycle', () => { describe('rum session', () => { let serverRumEvents: RumEvent[] + let clock: Clock let lifeCycle: LifeCycle let sessionManager: SessionManagerMock beforeEach(() => { lifeCycle = new LifeCycle() + clock = mockClock() sessionManager = createSessionManagerMock().setId('42') serverRumEvents = collectServerEvents(lifeCycle) @@ -106,22 +108,24 @@ describe('rum session', () => { }) it('when the session is renewed, a new view event should be sent', () => { - expect(serverRumEvents.length).toEqual(1) - expect(serverRumEvents[0].type).toEqual('view') - expect(serverRumEvents[0].session.id).toEqual('42') + const getViewEvents = () => + serverRumEvents.filter((event): event is RumViewEvent => event.type === RumEventType.VIEW) + + clock.tick(0) + expect(getViewEvents().length).toEqual(1) + expect(getViewEvents()[0].session.id).toEqual('42') lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED, { endClocks: relativeToClocks(relativeNow()) }) - expect(serverRumEvents.length).toEqual(2) + expect(getViewEvents().length).toEqual(2) sessionManager.setId('43') lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + clock.tick(0) - expect(serverRumEvents.length).toEqual(3) - - // New view event - expect(serverRumEvents[2].type).toEqual('view') - expect(serverRumEvents[2].session.id).toEqual('43') - expect(serverRumEvents[2].view.id).not.toEqual(serverRumEvents[0].view.id) + const viewEvents = getViewEvents() + expect(viewEvents.length).toEqual(3) + expect(viewEvents[2].session.id).toEqual('43') + expect(viewEvents[2].view.id).not.toEqual(viewEvents[0].view.id) }) }) diff --git a/packages/browser-rum-core/src/domain/view/setupViewTest.specHelper.ts b/packages/browser-rum-core/src/domain/view/setupViewTest.specHelper.ts index 2c64017e1e..a8bcce7a51 100644 --- a/packages/browser-rum-core/src/domain/view/setupViewTest.specHelper.ts +++ b/packages/browser-rum-core/src/domain/view/setupViewTest.specHelper.ts @@ -1,8 +1,8 @@ import { Observable } from '@datadog/browser-core' import { deepClone } from '@datadog/js-core/util' +import { registerCleanupTask } from '@datadog/browser-core/test' import { mockRumConfiguration, setupLocationObserver } from '../../../test' -import type { LifeCycle } from '../lifeCycle' -import { LifeCycleEventType } from '../lifeCycle' +import { LifeCycle, LifeCycleEventType } from '../lifeCycle' import type { RumConfiguration } from '../configuration' import type { RumMutationRecord } from '../../browser/domMutationObservable' import type { ViewCreatedEvent, ViewEvent, ViewOptions, ViewEndedEvent } from './trackViews' @@ -10,16 +10,14 @@ import { trackViews } from './trackViews' export type ViewTest = ReturnType -interface ViewTrackingContext { - lifeCycle: LifeCycle +interface ViewTestOptions { initialLocation?: string partialConfig?: Partial + initialViewOptions?: ViewOptions } -export function setupViewTest( - { lifeCycle, initialLocation, partialConfig }: ViewTrackingContext, - initialViewOptions?: ViewOptions -) { +export function setupViewTest({ initialLocation, partialConfig, initialViewOptions }: ViewTestOptions = {}) { + const lifeCycle = new LifeCycle() const domMutationObservable = new Observable() const windowOpenObservable = new Observable() const configuration = mockRumConfiguration(partialConfig) @@ -64,8 +62,11 @@ export function setupViewTest( !configuration.trackViewsManually, initialViewOptions ) + + registerCleanupTask(stop) + return { - stop, + lifeCycle, startView, setViewContext, setViewContextProperty, @@ -80,9 +81,6 @@ export function setupViewTest( getViewCreateCount, getViewEnd, getViewEndCount, - getLatestViewContext: () => ({ - id: getViewCreate(getViewCreateCount() - 1).id, - }), } } diff --git a/packages/browser-rum-core/src/domain/view/trackViews.spec.ts b/packages/browser-rum-core/src/domain/view/trackViews.spec.ts index 9d9b4cbf65..5b27f88ae4 100644 --- a/packages/browser-rum-core/src/domain/view/trackViews.spec.ts +++ b/packages/browser-rum-core/src/domain/view/trackViews.spec.ts @@ -3,11 +3,12 @@ import { timeStampNow, relativeToClocks, relativeNow } from '@datadog/js-core/ti import { PageExitReason, display } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' -import { mockClock, registerCleanupTask, createNewEvent } from '@datadog/browser-core/test' +import { mockClock, createNewEvent } from '@datadog/browser-core/test' import { createPerformanceEntry, mockPerformanceObserver } from '../../../test' import type { AssembledRumEvent } from '../../rawRumEvent.types' import { RumEventType, ViewLoadingType } from '../../rawRumEvent.types' -import { LifeCycle, LifeCycleEventType } from '../lifeCycle' +import type { LifeCycle } from '../lifeCycle' +import { LifeCycleEventType } from '../lifeCycle' import type { RumPerformanceEntry } from '../../browser/performanceObservable' import { RumPerformanceEntryType } from '../../browser/performanceObservable' import { PAGE_ACTIVITY_END_DELAY } from '../waitPageActivityEnd' @@ -22,7 +23,6 @@ function expireSession(lifeCycle: LifeCycle, endClocks: ClocksState = relativeTo } describe('track views automatically', () => { - const lifeCycle = new LifeCycle() let changeLocation: (to: string) => void let viewTest: ViewTest let clock: Clock @@ -30,12 +30,11 @@ describe('track views automatically', () => { beforeEach(() => { clock = mockClock() - viewTest = setupViewTest({ lifeCycle, initialLocation: 'http://foo.com/foo' }, { name: 'initial view name' }) - changeLocation = viewTest.changeLocation - - registerCleanupTask(() => { - viewTest.stop() + viewTest = setupViewTest({ + initialLocation: 'http://foo.com/foo', + initialViewOptions: { name: 'initial view name' }, }) + changeLocation = viewTest.changeLocation }) describe('initial view', () => { @@ -98,9 +97,11 @@ describe('track views automatically', () => { it('should keep the same URL when updating a view ended by a URL change', () => { const { getViewUpdateCount, getViewUpdate } = viewTest + clock.tick(0) expect(getViewUpdateCount()).toEqual(1) changeLocation('/bar') + clock.tick(0) expect(getViewUpdateCount()).toEqual(3) expect(getViewUpdate(0).location.href).toEqual('http://foo.com/foo') @@ -109,6 +110,7 @@ describe('track views automatically', () => { }) it('should ignore URL parameter changes', () => { + clock.tick(0) expect(viewTest.getViewUpdateCount()).toEqual(1) changeLocation('/foo?bar=bar') @@ -119,11 +121,13 @@ describe('track views automatically', () => { }) it('should keep the same URL when updating an ended view', () => { + clock.tick(0) const initialView = viewTest.getViewUpdate(0) changeLocation('/bar') + clock.tick(0) expect(viewTest.getViewUpdateCount()).toEqual(3) - lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { + viewTest.lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.ERROR, view: { id: initialView.id }, } as AssembledRumEvent) @@ -138,7 +142,6 @@ describe('track views automatically', () => { }) describe('view lifecycle', () => { - let lifeCycle: LifeCycle let viewTest: ViewTest let clock: Clock let notifySpy: jasmine.Spy @@ -146,28 +149,32 @@ describe('view lifecycle', () => { beforeEach(() => { clock = mockClock() - lifeCycle = new LifeCycle() - notifySpy = spyOn(lifeCycle, 'notify').and.callThrough() - viewTest = setupViewTest( - { lifeCycle, initialLocation: '/foo' }, - { + viewTest = setupViewTest({ + initialLocation: '/foo', + initialViewOptions: { name: 'initial view name', service: 'initial service', version: 'initial version', - } - ) + }, + }) + notifySpy = spyOn(viewTest.lifeCycle, 'notify').and.callThrough() changeLocation = viewTest.changeLocation + }) - registerCleanupTask(() => { - viewTest.stop() - }) + it('notifies a View update asynchronously', () => { + const { getViewUpdateCount } = viewTest + + expect(getViewUpdateCount()).toBe(0) + clock.tick(0) + + expect(getViewUpdateCount()).toBe(1) }) describe('expire session', () => { it('should end the view when the session expires', () => { - const { getViewEndCount } = viewTest + const { lifeCycle, getViewEndCount } = viewTest expect(getViewEndCount()).toBe(0) @@ -177,6 +184,7 @@ describe('view lifecycle', () => { }) it('should end the view at the session expiration clocks', () => { + const { lifeCycle } = viewTest const endClocks = relativeToClocks(clock.relative(123)) expireSession(lifeCycle, endClocks) @@ -185,7 +193,9 @@ describe('view lifecycle', () => { }) it('should send a final view update', () => { - const { getViewUpdateCount, getViewUpdate } = viewTest + const { lifeCycle, getViewUpdateCount, getViewUpdate } = viewTest + + clock.tick(0) // wait for the first view update to be sent expect(getViewUpdateCount()).toBe(1) @@ -197,7 +207,7 @@ describe('view lifecycle', () => { }) it('should not start a new view if the session expired', () => { - const { getViewCreateCount } = viewTest + const { lifeCycle, getViewCreateCount } = viewTest expect(getViewCreateCount()).toBe(1) @@ -207,25 +217,25 @@ describe('view lifecycle', () => { }) it('should not end the view again if the view is already ended', () => { - const { getViewEndCount, getViewUpdateCount } = viewTest + const { lifeCycle, getViewEndCount, getViewUpdateCount } = viewTest expect(getViewEndCount()).toBe(0) expireSession(lifeCycle) expect(getViewEndCount()).toBe(1) - expect(getViewUpdateCount()).toBe(2) + expect(getViewUpdateCount()).toBe(1) expireSession(lifeCycle) expect(getViewEndCount()).toBe(1) - expect(getViewUpdateCount()).toBe(2) + expect(getViewUpdateCount()).toBe(1) }) }) describe('renew session', () => { it('should create new view on renew session', () => { - const { getViewCreateCount } = viewTest + const { lifeCycle, getViewCreateCount } = viewTest expect(getViewCreateCount()).toBe(1) @@ -236,16 +246,17 @@ describe('view lifecycle', () => { }) it('should use session_renewal loading type for the new view', () => { - const { getViewUpdate, getViewUpdateCount } = viewTest + const { lifeCycle, getViewUpdate, getViewUpdateCount } = viewTest expireSession(lifeCycle) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + clock.tick(0) expect(getViewUpdate(getViewUpdateCount() - 1).loadingType).toBe(ViewLoadingType.SESSION_RENEWAL) }) it('should use the current view name, service and version for the new view', () => { - const { getViewCreateCount, getViewCreate, startView } = viewTest + const { lifeCycle, getViewCreateCount, getViewCreate, startView } = viewTest expireSession(lifeCycle) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) @@ -333,7 +344,7 @@ describe('view lifecycle', () => { }) it('should not send periodical updates after the session has expired', () => { - const { getViewUpdateCount } = viewTest + const { lifeCycle, getViewUpdateCount } = viewTest clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) // make sure we don't have pending update expireSession(lifeCycle) @@ -353,7 +364,7 @@ describe('view lifecycle', () => { { exitReason: PageExitReason.HIDDEN }, ].forEach(({ exitReason }) => { it(`should not end the current view when the page is exiting for reason ${exitReason}`, () => { - const { getViewEndCount } = viewTest + const { lifeCycle, getViewEndCount } = viewTest expect(getViewEndCount()).toEqual(0) @@ -364,17 +375,17 @@ describe('view lifecycle', () => { }) it('should trigger a view update on unloading', () => { - const { getViewUpdateCount } = viewTest + const { lifeCycle, getViewUpdateCount } = viewTest - expect(getViewUpdateCount()).toEqual(1) + expect(getViewUpdateCount()).toEqual(0) lifeCycle.notify(LifeCycleEventType.PREPARE_URGENT_FLUSH, PageExitReason.UNLOADING) - expect(getViewUpdateCount()).toEqual(2) + expect(getViewUpdateCount()).toEqual(1) }) it('should not create a new view when ending the view on unloading', () => { - const { getViewCreateCount } = viewTest + const { lifeCycle, getViewCreateCount } = viewTest expect(getViewCreateCount()).toEqual(1) @@ -384,7 +395,7 @@ describe('view lifecycle', () => { }) it('should not set the view as inactive on unloading', () => { - const { getViewUpdate, getViewUpdateCount } = viewTest + const { lifeCycle, getViewUpdate, getViewUpdateCount } = viewTest lifeCycle.notify(LifeCycleEventType.PREPARE_URGENT_FLUSH, PageExitReason.UNLOADING) @@ -393,64 +404,54 @@ describe('view lifecycle', () => { }) it('should notify BEFORE_VIEW_CREATED before VIEW_CREATED', () => { - expect(notifySpy.calls.argsFor(0)[0]).toEqual(LifeCycleEventType.BEFORE_VIEW_CREATED) - expect(notifySpy.calls.argsFor(1)[0]).toEqual(LifeCycleEventType.VIEW_CREATED) + notifySpy.calls.reset() + + viewTest.changeLocation('/bar') + + const beforeViewCreatedIndex = notifySpy.calls + .allArgs() + .findIndex((args) => args[0] === LifeCycleEventType.BEFORE_VIEW_CREATED) + expect(notifySpy.calls.argsFor(beforeViewCreatedIndex + 1)[0]).toEqual(LifeCycleEventType.VIEW_CREATED) }) it('should notify AFTER_VIEW_ENDED after VIEW_ENDED', () => { - const callsCount = notifySpy.calls.count() + notifySpy.calls.reset() - viewTest.stop() + viewTest.changeLocation('/bar') - expect(notifySpy.calls.argsFor(callsCount)[0]).toEqual(LifeCycleEventType.VIEW_ENDED) - expect(notifySpy.calls.argsFor(callsCount + 1)[0]).toEqual(LifeCycleEventType.AFTER_VIEW_ENDED) + expect(notifySpy.calls.argsFor(0)[0]).toEqual(LifeCycleEventType.VIEW_ENDED) + expect(notifySpy.calls.argsFor(1)[0]).toEqual(LifeCycleEventType.AFTER_VIEW_ENDED) }) }) describe('view loading type', () => { - const lifeCycle = new LifeCycle() - let viewTest: ViewTest - - beforeEach(() => { - mockClock() - - viewTest = setupViewTest({ lifeCycle }) - - registerCleanupTask(() => { - viewTest.stop() - }) - }) - it('should collect initial view type as "initial_load"', () => { - const { getViewUpdate } = viewTest + const clock = mockClock() + const { getViewUpdate } = setupViewTest() + clock.tick(0) expect(getViewUpdate(0).loadingType).toEqual(ViewLoadingType.INITIAL_LOAD) }) it('should collect view type as "route_change" after a view change', () => { - const { getViewUpdate, startView } = viewTest + const clock = mockClock() + const { getViewUpdate, startView } = setupViewTest() startView() + clock.tick(0) - expect(getViewUpdate(1).loadingType).toEqual(ViewLoadingType.INITIAL_LOAD) - expect(getViewUpdate(2).loadingType).toEqual(ViewLoadingType.ROUTE_CHANGE) + expect(getViewUpdate(0).loadingType).toEqual(ViewLoadingType.INITIAL_LOAD) + expect(getViewUpdate(1).loadingType).toEqual(ViewLoadingType.ROUTE_CHANGE) }) }) describe('view metrics', () => { - const lifeCycle = new LifeCycle() let clock: Clock - let viewTest: ViewTest let notifyPerformanceEntries: (entries: RumPerformanceEntry[]) => void beforeEach(() => { clock = mockClock() ;({ notifyPerformanceEntries } = mockPerformanceObserver()) - viewTest = setupViewTest({ lifeCycle }) - - registerCleanupTask(() => { - viewTest.stop() - }) }) describe('common view metrics', () => { @@ -458,7 +459,8 @@ describe('view metrics', () => { if (!isLayoutShiftSupported()) { pending('CLS web vital not supported') } - const { getViewUpdateCount, getViewUpdate } = viewTest + const { getViewUpdateCount, getViewUpdate } = setupViewTest() + clock.tick(0) expect(getViewUpdateCount()).toEqual(1) expect(getViewUpdate(0).initialViewMetrics).toEqual({}) @@ -484,7 +486,7 @@ describe('view metrics', () => { if (!isLayoutShiftSupported()) { pending('CLS web vital not supported') } - const { getViewUpdate, getViewUpdateCount, getViewCreateCount, startView } = viewTest + const { getViewUpdate, getViewUpdateCount, getViewCreateCount, startView } = setupViewTest() startView() clock.tick(0) // run immediate timeouts (mostly for `trackNavigationTimings`) expect(getViewCreateCount()).toEqual(2) @@ -501,9 +503,8 @@ describe('view metrics', () => { describe('initial view metrics', () => { it('updates should be throttled', () => { - const { getViewUpdateCount, getViewUpdate } = viewTest - expect(getViewUpdateCount()).toEqual(1) - expect(getViewUpdate(0).initialViewMetrics).toEqual({}) + const { getViewUpdateCount, getViewUpdate } = setupViewTest() + expect(getViewUpdateCount()).toEqual(0) clock.tick(THROTTLE_VIEW_UPDATE_PERIOD - 1) @@ -517,7 +518,7 @@ describe('view metrics', () => { }) it('should be updated for 5 min after view end', () => { - const { getViewCreateCount, getViewUpdate, getViewUpdateCount, startView } = viewTest + const { getViewCreateCount, getViewUpdate, getViewUpdateCount, startView } = setupViewTest() startView() expect(getViewCreateCount()).toEqual(2) @@ -535,7 +536,7 @@ describe('view metrics', () => { }) it('should not be updated 5 min after view end', () => { - const { getViewCreateCount, getViewUpdate, getViewUpdateCount, startView } = viewTest + const { getViewCreateCount, getViewUpdate, getViewUpdateCount, startView } = setupViewTest() startView() expect(getViewCreateCount()).toEqual(2) @@ -559,7 +560,8 @@ describe('view metrics', () => { let viewDuration: Duration beforeEach(() => { - const { getViewUpdateCount, getViewUpdate, startView } = viewTest + const { getViewUpdateCount, getViewUpdate, startView } = setupViewTest() + clock.tick(0) expect(getViewUpdateCount()).toEqual(1) @@ -570,6 +572,7 @@ describe('view metrics', () => { viewDuration = relativeNow() startView() + clock.tick(0) expect(getViewUpdateCount()).toEqual(3) @@ -630,45 +633,33 @@ describe('view metrics', () => { }) describe('view is active', () => { - const lifeCycle = new LifeCycle() - let viewTest: ViewTest - - beforeEach(() => { - viewTest = setupViewTest({ lifeCycle }) - - registerCleanupTask(() => { - viewTest.stop() - }) - }) - it('should set initial view as active', () => { - const { getViewUpdate } = viewTest + const clock = mockClock() + const { getViewUpdate } = setupViewTest() + clock.tick(0) expect(getViewUpdate(0).isActive).toBe(true) }) it('should set old view as inactive and new one as active after a route change', () => { - const { getViewUpdate, startView } = viewTest + const clock = mockClock() + const { getViewUpdate, startView } = setupViewTest() startView() + clock.tick(0) - expect(getViewUpdate(1).isActive).toBe(false) - expect(getViewUpdate(2).isActive).toBe(true) + expect(getViewUpdate(0).isActive).toBe(false) + expect(getViewUpdate(1).isActive).toBe(true) }) }) describe('view custom timings', () => { - const lifeCycle = new LifeCycle() let clock: Clock let viewTest: ViewTest beforeEach(() => { clock = mockClock() - viewTest = setupViewTest({ lifeCycle, initialLocation: '/foo' }) - - registerCleanupTask(() => { - viewTest.stop() - }) + viewTest = setupViewTest({ initialLocation: '/foo' }) }) it('should add custom timing to current view', () => { @@ -676,6 +667,7 @@ describe('view custom timings', () => { const { getViewUpdate, startView, addTiming } = viewTest startView() + clock.tick(0) // wait for the first view update to be sent const currentViewId = getViewUpdate(2).id clock.tick(20) @@ -779,7 +771,7 @@ describe('view custom timings', () => { it('should not add custom timing when the session has expired', () => { clock.tick(0) // run immediate timeouts (mostly for `trackNavigationTimings`) - const { getViewUpdateCount, addTiming } = viewTest + const { lifeCycle, getViewUpdateCount, addTiming } = viewTest expireSession(lifeCycle) @@ -794,17 +786,12 @@ describe('view custom timings', () => { }) describe('manual loading time', () => { - const lifeCycle = new LifeCycle() let clock: Clock let viewTest: ViewTest beforeEach(() => { clock = mockClock() - viewTest = setupViewTest({ lifeCycle }) - - registerCleanupTask(() => { - viewTest.stop() - }) + viewTest = setupViewTest() }) it('should set loading time on the current view', () => { @@ -839,7 +826,7 @@ describe('manual loading time', () => { it('should not set loading time when the session has expired', () => { clock.tick(0) // run immediate timeouts (mostly for `trackNavigationTimings`) - const { getViewUpdateCount, setLoadingTime } = viewTest + const { lifeCycle, getViewUpdateCount, setLoadingTime } = viewTest expireSession(lifeCycle) @@ -994,27 +981,22 @@ describe('manual loading time', () => { }) describe('start view', () => { - const lifeCycle = new LifeCycle() let clock: Clock - let viewTest: ViewTest beforeEach(() => { clock = mockClock() - viewTest = setupViewTest({ lifeCycle }) - - registerCleanupTask(() => { - viewTest.stop() - }) }) it('should start a new view', () => { - const { getViewUpdateCount, getViewUpdate, startView } = viewTest + const { getViewUpdateCount, getViewUpdate, startView } = setupViewTest() + clock.tick(0) expect(getViewUpdateCount()).toBe(1) const initialViewId = getViewUpdate(0).id clock.tick(10) startView() + clock.tick(0) expect(getViewUpdateCount()).toBe(3) @@ -1029,37 +1011,39 @@ describe('start view', () => { }) it('should name the view', () => { - const { getViewUpdate, startView } = viewTest + const { getViewUpdate, startView } = setupViewTest() startView() startView({ name: 'foo' }) startView({ name: 'bar' }) + clock.tick(0) - expect(getViewUpdate(2).name).toBeUndefined() - expect(getViewUpdate(4).name).toBe('foo') - expect(getViewUpdate(6).name).toBe('bar') + expect(getViewUpdate(1).name).toBeUndefined() + expect(getViewUpdate(2).name).toBe('foo') + expect(getViewUpdate(3).name).toBe('bar') }) it('should have service and version', () => { - const { getViewUpdate, startView } = viewTest + const { getViewUpdate, startView } = setupViewTest() startView() startView({ service: 'service 1', version: 'version 1' }) startView({ service: 'service 2', version: 'version 2' }) + clock.tick(0) - expect(getViewUpdate(2)).toEqual( + expect(getViewUpdate(1)).toEqual( jasmine.objectContaining({ service: undefined, version: undefined, }) ) - expect(getViewUpdate(4)).toEqual( + expect(getViewUpdate(2)).toEqual( jasmine.objectContaining({ service: 'service 1', version: 'version 1', }) ) - expect(getViewUpdate(6)).toEqual( + expect(getViewUpdate(3)).toEqual( jasmine.objectContaining({ service: 'service 2', version: 'version 2', @@ -1068,10 +1052,12 @@ describe('start view', () => { }) it('should ignore null service/version', () => { - const { getViewUpdate, startView } = viewTest + const { getViewUpdate, startView } = setupViewTest() startView({ service: null, version: null }) - expect(getViewUpdate(2)).toEqual( + clock.tick(0) + + expect(getViewUpdate(1)).toEqual( jasmine.objectContaining({ service: undefined, version: undefined, @@ -1080,43 +1066,37 @@ describe('start view', () => { }) it('should use the provided clock to stop the current view and start the new one', () => { - const { getViewUpdate, startView } = viewTest + const { getViewUpdate, startView } = setupViewTest() clock.tick(100) startView({ name: 'foo' }, relativeToClocks(50 as RelativeTime)) + clock.tick(0) expect(getViewUpdate(1).duration).toBe(50 as Duration) expect(getViewUpdate(2).startClocks.relative).toBe(50 as RelativeTime) }) it('should create view with handling stack', () => { - const { startView, getViewUpdate } = viewTest + const { startView, getViewUpdate } = setupViewTest() startView({ name: 'foo', handlingStack: 'Error\n at foo\n at bar' }) + clock.tick(0) - // The new view is at index 2 (after the initial view end and the new view start) - expect(getViewUpdate(2).handlingStack).toBe('Error\n at foo\n at bar') + expect(getViewUpdate(1).handlingStack).toBe('Error\n at foo\n at bar') }) }) describe('view event count', () => { - const lifeCycle = new LifeCycle() let clock: Clock - let viewTest: ViewTest beforeEach(() => { clock = mockClock() - - registerCleanupTask(() => { - viewTest.stop() - }) }) it('should be updated when notified with a RUM_EVENT_COLLECTED event', () => { - viewTest = setupViewTest({ lifeCycle }) - const { getViewUpdate, getViewUpdateCount } = viewTest + const { lifeCycle, getViewCreate, getViewUpdate, getViewUpdateCount } = setupViewTest() - lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, createFakeActionEvent()) + lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, createFakeActionEvent(getViewCreate(0).id)) clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) @@ -1124,21 +1104,21 @@ describe('view event count', () => { }) it('should take child events occurring on view end into account', () => { - viewTest = setupViewTest({ lifeCycle, initialLocation: 'http://foo.com' }) - const { getViewUpdate, getViewUpdateCount } = viewTest + const { lifeCycle, changeLocation, getViewCreate, getViewUpdate, getViewUpdateCount } = setupViewTest({ + initialLocation: 'http://foo.com', + }) lifeCycle.subscribe(LifeCycleEventType.VIEW_ENDED, () => { - lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, createFakeActionEvent()) + lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, createFakeActionEvent(getViewCreate(0).id)) }) - viewTest.changeLocation('/bar') + changeLocation('/bar') - expect(getViewUpdate(getViewUpdateCount() - 2).eventCounts.actionCount).toBe(1) + expect(getViewUpdate(getViewUpdateCount() - 1).eventCounts.actionCount).toBe(1) }) it('should be updated for 5 min after view end', () => { - viewTest = setupViewTest({ lifeCycle }) - const { getViewUpdate, getViewUpdateCount, getViewCreateCount, startView } = viewTest + const { lifeCycle, getViewUpdate, getViewUpdateCount, getViewCreateCount, startView } = setupViewTest() startView() expect(getViewCreateCount()).toEqual(2) const firstView = getViewUpdate(0) @@ -1158,8 +1138,7 @@ describe('view event count', () => { }) it('should not be updated 5 min after view end', () => { - viewTest = setupViewTest({ lifeCycle }) - const { getViewUpdate, getViewUpdateCount, getViewCreateCount, startView } = viewTest + const { lifeCycle, getViewUpdate, getViewUpdateCount, getViewCreateCount, startView } = setupViewTest() startView() expect(getViewCreateCount()).toEqual(2) const firstView = getViewUpdate(0) @@ -1177,37 +1156,37 @@ describe('view event count', () => { expect(latestUpdate.id).not.toEqual(firstView.id) }) - function createFakeActionEvent() { + function createFakeActionEvent(viewId: string) { return { type: RumEventType.ACTION, action: {}, - view: viewTest.getLatestViewContext(), + view: { id: viewId }, } as AssembledRumEvent } describe('view specific context', () => { it('should update view context if startView has context parameter', () => { - viewTest = setupViewTest({ lifeCycle }) - const { getViewUpdate, startView } = viewTest + const { getViewUpdate, startView } = setupViewTest() startView({ context: { foo: 'bar' } }) - expect(getViewUpdate(2).context).toEqual({ foo: 'bar' }) + clock.tick(0) + expect(getViewUpdate(1).context).toEqual({ foo: 'bar' }) }) it('should replace current context set on view event', () => { - viewTest = setupViewTest({ lifeCycle }) - const { getViewUpdate, startView } = viewTest + const { getViewUpdate, startView } = setupViewTest() startView({ context: { foo: 'bar' } }) - expect(getViewUpdate(2).context).toEqual({ foo: 'bar' }) + clock.tick(0) + expect(getViewUpdate(1).context).toEqual({ foo: 'bar' }) startView({ context: { bar: 'baz' } }) - expect(getViewUpdate(4).context).toEqual({ bar: 'baz' }) + clock.tick(0) + expect(getViewUpdate(3).context).toEqual({ bar: 'baz' }) }) it('should set view context with setViewContext', () => { - viewTest = setupViewTest({ lifeCycle }) - const { getViewUpdate, setViewContext } = viewTest + const { getViewUpdate, setViewContext } = setupViewTest() setViewContext({ foo: 'bar' }) clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) @@ -1216,8 +1195,7 @@ describe('view event count', () => { }) it('should set view context with setViewContextProperty', () => { - viewTest = setupViewTest({ lifeCycle }) - const { getViewUpdate, setViewContextProperty } = viewTest + const { getViewUpdate, setViewContextProperty } = setupViewTest() setViewContextProperty('foo', 'bar') clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) @@ -1226,8 +1204,7 @@ describe('view event count', () => { }) it('should get view context with getViewContext', () => { - viewTest = setupViewTest({ lifeCycle }) - const { getViewContext, setViewContextProperty } = viewTest + const { getViewContext, setViewContextProperty } = setupViewTest() setViewContextProperty('foo', 'bar') expect(getViewContext()).toEqual({ foo: 'bar' }) @@ -1236,58 +1213,47 @@ describe('view event count', () => { describe('set view name', () => { it('should set an undefined view name', () => { - viewTest = setupViewTest({ lifeCycle }) - - const { getViewUpdate, startView, setViewName } = viewTest + const { getViewUpdate, startView, setViewName } = setupViewTest() startView() setViewName('foo') - expect(getViewUpdate(3).name).toEqual('foo') + clock.tick(0) + + expect(getViewUpdate(1).name).toEqual('foo') }) it('should set a defined view name', () => { - viewTest = setupViewTest({ lifeCycle }) - - const { getViewUpdate, startView, setViewName } = viewTest + const { getViewUpdate, startView, setViewName } = setupViewTest() startView({ name: 'initial view name' }) setViewName('foo') - expect(getViewUpdate(3).name).toEqual('foo') + clock.tick(0) + + expect(getViewUpdate(1).name).toEqual('foo') }) }) }) describe('service and version', () => { - const lifeCycle = new LifeCycle() - let viewTest: ViewTest - - beforeEach(() => { - mockClock() - - registerCleanupTask(() => { - viewTest.stop() - }) - }) - it('should come from the init configuration by default', () => { - viewTest = setupViewTest({ lifeCycle, partialConfig: { service: 'service', version: 'version' } }) - - const { getViewUpdate } = viewTest + const clock = mockClock() + const { getViewUpdate } = setupViewTest({ partialConfig: { service: 'service', version: 'version' } }) + clock.tick(0) expect(getViewUpdate(0).service).toEqual('service') expect(getViewUpdate(0).version).toEqual('version') }) it('should come from the view option if defined', () => { - viewTest = setupViewTest( - { lifeCycle, partialConfig: { service: 'service', version: 'version' } }, - { + const clock = mockClock() + const { getViewUpdate } = setupViewTest({ + partialConfig: { service: 'service', version: 'version' }, + initialViewOptions: { service: 'view service', version: 'view version', - } - ) - - const { getViewUpdate } = viewTest + }, + }) + clock.tick(0) expect(getViewUpdate(0).service).toEqual('view service') expect(getViewUpdate(0).version).toEqual('view version') @@ -1295,19 +1261,9 @@ describe('service and version', () => { }) describe('BFCache views', () => { - const lifeCycle = new LifeCycle() - let viewTest: ViewTest - - beforeEach(() => { - viewTest = setupViewTest({ lifeCycle }) - - registerCleanupTask(() => { - viewTest.stop() - }) - }) - it('should create a new "bf_cache" view when restoring from the BFCache', () => { - const { getViewCreateCount, getViewEndCount, getViewUpdate, getViewUpdateCount } = viewTest + const clock = mockClock() + const { getViewCreateCount, getViewEndCount, getViewUpdate, getViewUpdateCount } = setupViewTest() expect(getViewCreateCount()).toBe(1) expect(getViewEndCount()).toBe(0) @@ -1315,6 +1271,7 @@ describe('BFCache views', () => { const event = createNewEvent('pageshow', { persisted: true }) window.dispatchEvent(event) + clock.tick(0) expect(getViewEndCount()).toBe(1) expect(getViewCreateCount()).toBe(2) diff --git a/packages/browser-rum-core/src/domain/view/trackViews.ts b/packages/browser-rum-core/src/domain/view/trackViews.ts index d28f3eb36e..1b372c6dc7 100644 --- a/packages/browser-rum-core/src/domain/view/trackViews.ts +++ b/packages/browser-rum-core/src/domain/view/trackViews.ts @@ -21,6 +21,7 @@ import { setTimeout, Observable, createContextManager, + queueMicrotask, mockable, } from '@datadog/browser-core' import type { ViewCustomTimings } from '../../rawRumEvent.types' @@ -286,7 +287,11 @@ function newView( }) // Initial view update - triggerViewUpdate() + mockable(queueMicrotask)(() => { + if (documentVersion === 0) { + triggerViewUpdate() + } + }) // View context update should always be throttled contextManager.changeObservable.subscribe(scheduleViewUpdate) diff --git a/test/e2e/scenario/rum/init.scenario.ts b/test/e2e/scenario/rum/init.scenario.ts index fe0af44515..c75935e8c0 100644 --- a/test/e2e/scenario/rum/init.scenario.ts +++ b/test/e2e/scenario/rum/init.scenario.ts @@ -207,6 +207,24 @@ test.describe('API calls and events around init', () => { const viewContext = await page.evaluate(() => window.DD_RUM?.getViewContext()) expect(viewContext).toEqual({ foo: 'bar' }) }) + + createTest('context set right after init should be applied to events generated during init') + .withRum() + .withRumInit((configuration) => { + window.DD_RUM!.init(configuration) + window.DD_RUM!.setViewContext({ viewContext: true }) + window.DD_RUM!.setGlobalContext({ globalContext: true }) + window.DD_RUM!.setUser({ id: 'user-id' }) + window.DD_RUM!.addFeatureFlagEvaluation('foo', true) + }) + .run(async ({ intakeRegistry, flushEvents }) => { + await flushEvents() + + const initialView = intakeRegistry.rumViewEvents[0] + expect(initialView.context).toEqual({ viewContext: true, globalContext: true }) + expect(initialView.usr).toEqual(expect.objectContaining({ id: 'user-id' })) + expect(initialView.feature_flags).toEqual({ foo: true }) + }) }) test.describe('beforeSend', () => {