From 0ae8db544c0639221a594a7755fa251f40065d8a Mon Sep 17 00:00:00 2001 From: Billy Date: Wed, 31 Jan 2024 23:28:19 -0800 Subject: [PATCH] feat!: capture all W3C fields in NavigationEvents (#494) --- .../__tests__/EventCache.integ.test.ts | 1 - ...son => performance-navigation-timing.json} | 90 +++-- .../__tests__/Orchestration.test.ts | 3 +- src/plugins/event-plugins/NavigationPlugin.ts | 322 ++++-------------- .../__integ__/NavigationPlugin.test.ts | 202 ++++------- .../__tests__/NavigationPlugin.test.ts | 140 ++++---- src/plugins/utils/constant.ts | 2 +- src/sessions/VirtualPageLoadTimer.ts | 11 +- src/sessions/__tests__/SessionManager.test.ts | 11 +- src/utils/common-utils.ts | 4 + 10 files changed, 273 insertions(+), 513 deletions(-) rename src/event-schemas/{navigation-event.json => performance-navigation-timing.json} (60%) diff --git a/src/event-cache/__tests__/EventCache.integ.test.ts b/src/event-cache/__tests__/EventCache.integ.test.ts index 67eefefc..01c08482 100644 --- a/src/event-cache/__tests__/EventCache.integ.test.ts +++ b/src/event-cache/__tests__/EventCache.integ.test.ts @@ -84,7 +84,6 @@ describe('EventCache tests', () => { allowCookies: false, sessionLengthSeconds: 0, sessionAttributes: { - version: '2.0.0', domain: 'overridden.console.aws.amazon.com', browserLanguage: 'en-UK', browserName: 'Chrome', diff --git a/src/event-schemas/navigation-event.json b/src/event-schemas/performance-navigation-timing.json similarity index 60% rename from src/event-schemas/navigation-event.json rename to src/event-schemas/performance-navigation-timing.json index a269fbae..09bbc44c 100644 --- a/src/event-schemas/navigation-event.json +++ b/src/event-schemas/performance-navigation-timing.json @@ -1,45 +1,38 @@ { - "$id": "com.amazon.rum.performance_navigation_event", + "$id": "com.amazon.rum.performance_navigation_timing", "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "NavigationEvent", + "title": "PerformanceNavigationTimingEvent", "type": "object", "properties": { - "version": { - "const": "1.0.0", - "type": "string", - "description": "Schema version." - }, - "initiatorType": { - "type": "string", - "enum": ["navigation", "route_change"] + "name": { + "type": "string" }, - "navigationType": { - "description": "An unsigned short which indicates how the navigation to this page was done. Possible values are:TYPE_NAVIGATE (0), TYPE_RELOAD (1), TYPE_BACK_FORWARD (2), TYPE_RESERVED (255)", - "type": "string", - "enum": ["navigate", "reload", "back_forward", "reserved"] + "entryType": { + "const": "navigation", + "type": "string" }, "startTime": { - "type": "number" - }, - "unloadEventStart": { - "type": "number" + "type": "number", + "description": "StartTime value is always '0' for PerformanceNavigationTimingEvents created by the PerformanceAPI. However, non-W3C 'route_changes' created by RUM's polyfill for SinglePageApplications can have startTimes >= 0." }, - "promptForUnload": { + "duration": { "type": "number" }, - "redirectCount": { - "type": "integer" + "initiatorType": { + "type": "string", + "enum": ["navigation", "route_change"], + "description": "InitiatorType value is always 'navigation' for PerformanceNavigationTimingEvents created by the PerformanceAPI. However, RUM adds the non-W3C concept 'route_change' as a polyfill because the PerformanceAPI currently does not support Single Page Applications." }, - "redirectStart": { - "type": "number" + "nextHopProtocol": { + "type": "string" }, - "redirectTime": { + "workerStart": { "type": "number" }, - "workerStart": { + "redirectStart": { "type": "number" }, - "workerTime": { + "redirectEnd": { "type": "number" }, "fetchStart": { @@ -48,73 +41,68 @@ "domainLookupStart": { "type": "number" }, - "dns": { + "domainLookupEnd": { "type": "number" }, - "nextHopProtocol": { - "type": "string" - }, "connectStart": { "type": "number" }, - "connect": { + "connectEnd": { "type": "number" }, "secureConnectionStart": { "type": "number" }, - "tlsTime": { - "type": "number" - }, "requestStart": { "type": "number" }, - "timeToFirstByte": { - "type": "number" - }, "responseStart": { "type": "number" }, - "responseTime": { + "responseEnd": { "type": "number" }, - "domInteractive": { + "transferSize": { "type": "number" }, - "domContentLoadedEventStart": { + "encodedBodySize": { "type": "number" }, - "domContentLoaded": { + "decodedBodySize": { "type": "number" }, "domComplete": { "type": "number" }, - "domProcessingTime": { + "domContentLoadedEventEnd": { "type": "number" }, - "loadEventStart": { + "domContentLoadedEventStart": { "type": "number" }, - "loadEventTime": { + "domInteractive": { "type": "number" }, - "duration": { + "loadEventEnd": { "type": "number" }, - "headerSize": { + "loadEventStart": { "type": "number" }, - "transferSize": { - "type": "number" + "redirectCount": { + "type": "integer" }, - "compressionRatio": { + "type": { + "type": "string", + "enum": ["navigate", "reload", "back_forward", "prerender"] + }, + "unloadEventEnd": { "type": "number" }, - "navigationTimingLevel": { + "unloadEventStart": { "type": "number" } }, "additionalProperties": false, - "required": ["version", "initiatorType", "startTime", "duration"] + "required": ["entryType", "startTime", "duration", "initiatorType"] } diff --git a/src/orchestration/__tests__/Orchestration.test.ts b/src/orchestration/__tests__/Orchestration.test.ts index 429258f1..70a75120 100644 --- a/src/orchestration/__tests__/Orchestration.test.ts +++ b/src/orchestration/__tests__/Orchestration.test.ts @@ -28,7 +28,8 @@ jest.mock('../../utils/common-utils', () => { return { __esModule: true, ...originalModule, - isLCPSupported: jest.fn().mockReturnValue(true) + isLCPSupported: jest.fn().mockReturnValue(true), + isNavigationSupported: jest.fn().mockReturnValue(true) }; }); diff --git a/src/plugins/event-plugins/NavigationPlugin.ts b/src/plugins/event-plugins/NavigationPlugin.ts index d2f09ad1..f1475ba5 100644 --- a/src/plugins/event-plugins/NavigationPlugin.ts +++ b/src/plugins/event-plugins/NavigationPlugin.ts @@ -1,26 +1,26 @@ import { InternalPlugin } from '../InternalPlugin'; -import { NavigationEvent } from '../../events/navigation-event'; +import { PerformanceNavigationTimingEvent } from '../../events/performance-navigation-timing'; import { PERFORMANCE_NAVIGATION_EVENT_TYPE } from '../utils/constant'; import { PerformancePluginConfig, defaultPerformancePluginConfig } from '../utils/performance-utils'; +import { isNavigationSupported } from '../../utils/common-utils'; export const NAVIGATION_EVENT_PLUGIN_ID = 'navigation'; - const NAVIGATION = 'navigation'; -const LOAD = 'load'; -/** - * This plugin records performance timing events generated during every page load/re-load activity. - * Paint, resource and performance event types make sense only if all or none are included. - * For RUM, these event types are inter-dependent. So they are recorded under one plugin. - */ +/** This plugin records performance timing events generated during every page load/re-load activity. */ export class NavigationPlugin extends InternalPlugin { private config: PerformancePluginConfig; + private po?: PerformanceObserver; + constructor(config?: Partial) { super(NAVIGATION_EVENT_PLUGIN_ID); this.config = { ...defaultPerformancePluginConfig, ...config }; + this.po = isNavigationSupported() + ? new PerformanceObserver(this.performanceEntryHandler) + : undefined; } enable(): void { @@ -28,7 +28,7 @@ export class NavigationPlugin extends InternalPlugin { return; } this.enabled = true; - window.addEventListener(LOAD, this.eventListener); + this.observe(); } disable(): void { @@ -36,262 +36,72 @@ export class NavigationPlugin extends InternalPlugin { return; } this.enabled = false; - if (this.eventListener) { - window.removeEventListener(LOAD, this.eventListener); - } + this.po?.disconnect(); } /** - * Use the loadEventEnd field from window.performance to check if the website - * has loaded already. - * - * @returns boolean - */ - hasTheWindowLoadEventFired() { - if ( - window.performance && - window.performance.getEntriesByType(NAVIGATION).length - ) { - const navData = window.performance.getEntriesByType( - NAVIGATION - )[0] as PerformanceNavigationTiming; - return Boolean(navData.loadEventEnd); - } - return false; - } - - /** - * Use Navigation timing Level 1 for all browsers by default - - * https://developer.mozilla.org/en-US/docs/Web/API/Performance/timing - * - * If browser provides support, use Navigation Timing Level 2 specification - + * Callback to record PerformanceNavigationTiming as RUM PerformanceNavigationTimingEvent * https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming - * - * Only the current document resource is included in the performance timeline; - * there is only one PerformanceNavigationTiming object in the performance timeline. - * https://www.w3.org/TR/navigation-timing-2/ */ - eventListener = () => { - if (performance.getEntriesByType(NAVIGATION).length === 0) { - this.performanceNavigationEventHandlerTimingLevel1(); - } else { - const navigationObserver = new PerformanceObserver((list) => { - list.getEntries() - .filter((e) => e.entryType === NAVIGATION) - .filter((e) => !this.config.ignore(e)) - .forEach((event) => { - this.performanceNavigationEventHandlerTimingLevel2( - event as PerformanceNavigationTiming - ); - }); - }); - navigationObserver.observe({ - entryTypes: [NAVIGATION] - }); - } - }; - - /** - * W3C specification: https://www.w3.org/TR/navigation-timing/#sec-navigation-timing-interface - */ - performanceNavigationEventHandlerTimingLevel1 = () => { - const recordNavigation = () => { - const entryData = performance.timing; - const origin = entryData.navigationStart; - const eventDataNavigationTimingLevel1: NavigationEvent = { - version: '1.0.0', - initiatorType: 'navigation', - startTime: 0, - unloadEventStart: - entryData.unloadEventStart > 0 - ? entryData.unloadEventStart - origin - : 0, - promptForUnload: - entryData.unloadEventEnd - entryData.unloadEventStart, - redirectStart: - entryData.redirectStart > 0 - ? entryData.redirectStart - origin - : 0, - redirectTime: entryData.redirectEnd - entryData.redirectStart, - fetchStart: - entryData.fetchStart > 0 - ? entryData.fetchStart - origin - : 0, - domainLookupStart: - entryData.domainLookupStart > 0 - ? entryData.domainLookupStart - origin - : 0, - dns: entryData.domainLookupEnd - entryData.domainLookupStart, - connectStart: - entryData.connectStart > 0 - ? entryData.connectStart - origin - : 0, - connect: entryData.connectEnd - entryData.connectStart, - secureConnectionStart: - entryData.secureConnectionStart > 0 - ? entryData.secureConnectionStart - origin - : 0, - tlsTime: - entryData.secureConnectionStart > 0 - ? entryData.connectEnd - entryData.secureConnectionStart - : 0, - - requestStart: - entryData.requestStart > 0 - ? entryData.requestStart - origin - : 0, - timeToFirstByte: - entryData.responseStart - entryData.requestStart, - responseStart: - entryData.responseStart > 0 - ? entryData.responseStart - origin - : 0, - responseTime: - entryData.responseStart > 0 - ? entryData.responseEnd - entryData.responseStart - : 0, - - domInteractive: - entryData.domInteractive > 0 - ? entryData.domInteractive - origin - : 0, - domContentLoadedEventStart: - entryData.domContentLoadedEventStart > 0 - ? entryData.domContentLoadedEventStart - origin - : 0, - domContentLoaded: - entryData.domContentLoadedEventEnd - - entryData.domContentLoadedEventStart, - domComplete: - entryData.domComplete > 0 - ? entryData.domComplete - origin - : 0, - domProcessingTime: - entryData.loadEventStart - entryData.responseEnd, - loadEventStart: - entryData.loadEventStart > 0 - ? entryData.loadEventStart - origin - : 0, - loadEventTime: - entryData.loadEventEnd - entryData.loadEventStart, - duration: entryData.loadEventEnd - entryData.navigationStart, - navigationTimingLevel: 1 - }; - if (this.context?.record) { - this.context.record( - PERFORMANCE_NAVIGATION_EVENT_TYPE, - eventDataNavigationTimingLevel1 - ); + performanceEntryHandler: PerformanceObserverCallback = ( + list: PerformanceObserverEntryList + ) => { + list.getEntries().forEach((entry) => { + if (!this.enabled || this.config.ignore(entry)) { + return; } - }; - // Timeout is required for loadEventEnd to complete - setTimeout(recordNavigation, 0); - }; - - /** - * W3C specification: https://www.w3.org/TR/navigation-timing-2/#bib-navigation-timing - */ - performanceNavigationEventHandlerTimingLevel2 = ( - entryData: PerformanceNavigationTiming - ): void => { - const eventDataNavigationTimingLevel2: NavigationEvent = { - version: '1.0.0', - initiatorType: entryData.initiatorType as - | 'navigation' - | 'route_change', - navigationType: entryData.type as - | 'back_forward' - | 'navigate' - | 'reload' - | 'reserved' - | undefined, - startTime: entryData.startTime, - unloadEventStart: entryData.unloadEventStart, - promptForUnload: - entryData.unloadEventEnd - entryData.unloadEventStart, - redirectCount: entryData.redirectCount, - redirectStart: entryData.redirectStart, - redirectTime: entryData.redirectEnd - entryData.redirectStart, - - workerStart: entryData.workerStart, - workerTime: - entryData.workerStart > 0 - ? entryData.fetchStart - entryData.workerStart - : 0, - - fetchStart: entryData.fetchStart, - domainLookupStart: entryData.domainLookupStart, - dns: entryData.domainLookupEnd - entryData.domainLookupStart, - - nextHopProtocol: entryData.nextHopProtocol, - connectStart: entryData.connectStart, - connect: entryData.connectEnd - entryData.connectStart, - secureConnectionStart: entryData.secureConnectionStart, - tlsTime: - entryData.secureConnectionStart > 0 - ? entryData.connectEnd - entryData.secureConnectionStart - : 0, - - requestStart: entryData.requestStart, - timeToFirstByte: entryData.responseStart - entryData.requestStart, - responseStart: entryData.responseStart, - responseTime: - entryData.responseStart > 0 - ? entryData.responseEnd - entryData.responseStart - : 0, - - domInteractive: entryData.domInteractive, - domContentLoadedEventStart: entryData.domContentLoadedEventStart, - domContentLoaded: - entryData.domContentLoadedEventEnd - - entryData.domContentLoadedEventStart, - domComplete: entryData.domComplete, - domProcessingTime: entryData.loadEventStart - entryData.responseEnd, - loadEventStart: entryData.loadEventStart, - loadEventTime: entryData.loadEventEnd - entryData.loadEventStart, - - duration: entryData.duration, - - headerSize: - entryData.transferSize > 0 - ? entryData.transferSize - entryData.encodedBodySize - : 0, - transferSize: entryData.transferSize, - compressionRatio: - entryData.encodedBodySize > 0 - ? entryData.decodedBodySize / entryData.encodedBodySize - : 0, - navigationTimingLevel: 2 - }; - if (this.context?.record) { - this.context.record( - PERFORMANCE_NAVIGATION_EVENT_TYPE, - eventDataNavigationTimingLevel2 - ); - } + // Record + const e = entry as PerformanceNavigationTiming; + this.context?.record(PERFORMANCE_NAVIGATION_EVENT_TYPE, { + name: this.context.config.recordResourceUrl + ? e.name + : undefined, + entryType: NAVIGATION, + initiatorType: NAVIGATION, + startTime: e.startTime, + duration: e.duration, + nextHopProtocol: e.nextHopProtocol, + workerStart: e.workerStart, + redirectStart: e.redirectStart, + redirectEnd: e.redirectEnd, + fetchStart: e.fetchStart, + domainLookupStart: e.domainLookupStart, + domainLookupEnd: e.domainLookupEnd, + connectStart: e.connectStart, + connectEnd: e.connectEnd, + secureConnectionStart: e.secureConnectionStart, + requestStart: e.requestStart, + responseStart: e.responseStart, + responseEnd: e.responseEnd, + transferSize: e.transferSize, + encodedBodySize: e.encodedBodySize, + decodedBodySize: e.decodedBodySize, + domComplete: e.domComplete, + domContentLoadedEventEnd: e.domContentLoadedEventEnd, + domContentLoadedEventStart: e.domContentLoadedEventStart, + domInteractive: e.domInteractive, + loadEventEnd: e.loadEventEnd, + loadEventStart: e.loadEventStart, + redirectCount: e.redirectCount, + type: e.type, + unloadEventEnd: e.unloadEventEnd, + unloadEventStart: e.unloadEventStart + } as PerformanceNavigationTimingEvent); + + // Teardown + this.po?.disconnect(); + }); }; - /** - * loadEventEnd is populated as 0 if the web page has not loaded completely, even though LOAD has been fired. - * As a result, if loadEventEnd is populated, we can ignore eventListener and record the data directly. - * On the other hand, if not, we have to use eventListener to initializes PerformanceObserver. - * PerformanceObserver will act as a second check for the final load timings. - */ + private observe() { + this.po?.observe({ + type: NAVIGATION, + buffered: true + }); + } + protected onload(): void { - if (this.enabled) { - if (this.hasTheWindowLoadEventFired()) { - window.performance - .getEntriesByType(NAVIGATION) - .filter((e) => !this.config.ignore(e)) - .forEach((event) => - this.performanceNavigationEventHandlerTimingLevel2( - event - ) - ); - } else { - window.addEventListener(LOAD, this.eventListener); - } - } + this.observe(); } } diff --git a/src/plugins/event-plugins/__integ__/NavigationPlugin.test.ts b/src/plugins/event-plugins/__integ__/NavigationPlugin.test.ts index 01f0afa9..5d323a64 100644 --- a/src/plugins/event-plugins/__integ__/NavigationPlugin.test.ts +++ b/src/plugins/event-plugins/__integ__/NavigationPlugin.test.ts @@ -1,51 +1,12 @@ import { - STATUS_202, DISPATCH_COMMAND, COMMAND, PAYLOAD, SUBMIT, - REQUEST_BODY, - RESPONSE_STATUS, - ID, - TIMESTAMP + REQUEST_BODY } from '../../../test-utils/integ-test-utils'; import { PERFORMANCE_NAVIGATION_EVENT_TYPE } from '../../utils/constant'; -const INITIATOR_TYPE = 'initiatorType'; -const NAVIGATION_TYPE = 'navigationType'; -const START_TIME = 'startTime'; -const UNLOAD_EVENT_START = 'unloadEventStart'; -const PROMPT_FOR_UNLOAD = 'promptForUnload'; -const REDIRECT_COUNT = 'redirectCount'; -const REDIRECT_START = 'redirectStart'; -const REDIRECT_TIME = 'redirectTime'; -const WORKER_START = 'workerStart'; -const WORKER_TIME = 'workerTime'; -const FETCH_START = 'fetchStart'; -const DOMAIN_LOOKUP_START = 'domainLookupStart'; -const DNS = 'dns'; -const NEXT_HOP_PROTOCOL = 'nextHopProtocol'; -const CONNECT_START = 'connectStart'; -const CONNECT = 'connect'; -const SECURE_CONNECTION_START = 'secureConnectionStart'; -const TLS_TIME = 'tlsTime'; -const REQUEST_START = 'requestStart'; -const TIME_TO_FIRST_BYTE = 'timeToFirstByte'; -const RESPONSE_START = 'responseStart'; -const RESPONSE_TIME = 'responseTime'; -const DOM_INTERACTIVE = 'domInteractive'; -const DOM_CONTENT_LOADED_EVENT_START = 'domContentLoadedEventStart'; -const DOM_CONTENT_LOADED = 'domContentLoaded'; -const DOM_COMPLETE = 'domComplete'; -const DOM_PROCESSING_TIME = 'domProcessingTime'; -const LOAD_EVENT_START = 'loadEventStart'; -const LOAD_EVENT_TIME = 'loadEventTime'; -const DURATION = 'duration'; -const HEADER_SIZE = 'headerSize'; -const TRANSFER_SIZE = 'transferSize'; -const COMPRESSION_RATIO = 'compressionRatio'; -const SAFARI = 'Safari'; - fixture('NavigationEvent Plugin').page( 'http://localhost:8080/delayed_page.html' ); @@ -55,98 +16,77 @@ test('when plugin loads after window.load then navigation timing events are reco .typeText(COMMAND, DISPATCH_COMMAND, { replace: true }) .click(PAYLOAD) .pressKey('ctrl+a delete') - .click(SUBMIT); - - const isBrowserSafari = - (await REQUEST_BODY.textContent).indexOf(SAFARI) > -1; - - await t - .expect(REQUEST_BODY.textContent) - .contains(PERFORMANCE_NAVIGATION_EVENT_TYPE) - .expect(REQUEST_BODY.textContent) - .contains(ID) - .expect(REQUEST_BODY.textContent) - .contains(TIMESTAMP) - - .expect(REQUEST_BODY.textContent) - .contains(INITIATOR_TYPE) + .click(SUBMIT) .expect(REQUEST_BODY.textContent) - .contains(START_TIME) - .expect(REQUEST_BODY.textContent) - .contains(UNLOAD_EVENT_START) - .expect(REQUEST_BODY.textContent) - .contains(PROMPT_FOR_UNLOAD) - .expect(REQUEST_BODY.textContent) - .contains(REDIRECT_START) - .expect(REQUEST_BODY.textContent) - .contains(REDIRECT_TIME) + .contains('BatchId'); - .expect(REQUEST_BODY.textContent) - .contains(FETCH_START) - .expect(REQUEST_BODY.textContent) - .contains(DOMAIN_LOOKUP_START) - .expect(REQUEST_BODY.textContent) - .contains(DNS) + const navigationEvent = JSON.parse( + JSON.parse(await REQUEST_BODY.textContent).RumEvents?.find( + (e: any) => e.type === PERFORMANCE_NAVIGATION_EVENT_TYPE + )?.details + ); - .expect(REQUEST_BODY.textContent) - .contains(CONNECT_START) - .expect(REQUEST_BODY.textContent) - .contains(CONNECT) - .expect(REQUEST_BODY.textContent) - .contains(SECURE_CONNECTION_START) - .expect(REQUEST_BODY.textContent) - .contains(TLS_TIME) - .expect(REQUEST_BODY.textContent) - .contains(REQUEST_START) - .expect(REQUEST_BODY.textContent) - .contains(TIME_TO_FIRST_BYTE) - .expect(REQUEST_BODY.textContent) - .contains(RESPONSE_START) - .expect(REQUEST_BODY.textContent) - .contains(RESPONSE_TIME) - .expect(REQUEST_BODY.textContent) - .contains(DOM_INTERACTIVE) - .expect(REQUEST_BODY.textContent) - .contains(DOM_CONTENT_LOADED_EVENT_START) - .expect(REQUEST_BODY.textContent) - .contains(DOM_CONTENT_LOADED) - .expect(REQUEST_BODY.textContent) - .contains(DOM_COMPLETE) - .expect(REQUEST_BODY.textContent) - .contains(DOM_PROCESSING_TIME) - .expect(REQUEST_BODY.textContent) - .contains(LOAD_EVENT_START) - .expect(REQUEST_BODY.textContent) - .contains(LOAD_EVENT_TIME) - .expect(REQUEST_BODY.textContent) - .contains(DURATION) - - .expect(RESPONSE_STATUS.textContent) - .eql(STATUS_202.toString()) - .expect((await REQUEST_BODY.textContent).indexOf(DURATION)) - .gt(0); - - /** - * Deprecated Timing Level1 used for Safari browser do not contain following attributes - * https://nicj.net/navigationtiming-in-practice/ - */ - if (!isBrowserSafari) { - await t - .expect(REQUEST_BODY.textContent) - .contains(REDIRECT_COUNT) - .expect(REQUEST_BODY.textContent) - .contains(NAVIGATION_TYPE) - .expect(REQUEST_BODY.textContent) - .contains(WORKER_START) - .expect(REQUEST_BODY.textContent) - .contains(WORKER_TIME) - .expect(REQUEST_BODY.textContent) - .contains(NEXT_HOP_PROTOCOL) - .expect(REQUEST_BODY.textContent) - .contains(HEADER_SIZE) - .expect(REQUEST_BODY.textContent) - .contains(TRANSFER_SIZE) - .expect(REQUEST_BODY.textContent) - .contains(COMPRESSION_RATIO); - } + await t + .expect(navigationEvent) + .ok() + .expect(navigationEvent.name) + .ok() + .expect(navigationEvent.entryType) + .eql('navigation') + .expect(navigationEvent.startTime) + .gte(0) + .expect(navigationEvent.duration) + .gte(0) + .expect(navigationEvent.initiatorType) + .eql('navigation') + .expect(navigationEvent.nextHopProtocol) + .typeOf('string') + .expect(navigationEvent.redirectStart) + .gte(0) + .expect(navigationEvent.redirectEnd) + .gte(0) + .expect(navigationEvent.fetchStart) + .gte(0) + .expect(navigationEvent.domainLookupStart) + .gte(0) + .expect(navigationEvent.domainLookupEnd) + .gte(0) + .expect(navigationEvent.connectStart) + .gte(0) + .expect(navigationEvent.connectEnd) + .gte(0) + .expect(navigationEvent.secureConnectionStart) + .gte(0) + .expect(navigationEvent.requestStart) + .gte(0) + .expect(navigationEvent.responseStart) + .gte(0) + .expect(navigationEvent.responseEnd) + .gte(0) + .expect(navigationEvent.transferSize) + .gte(0) + .expect(navigationEvent.encodedBodySize) + .gte(0) + .expect(navigationEvent.decodedBodySize) + .gte(0) + .expect(navigationEvent.domComplete) + .gte(0) + .expect(navigationEvent.domContentLoadedEventEnd) + .gte(0) + .expect(navigationEvent.domContentLoadedEventStart) + .gte(0) + .expect(navigationEvent.domInteractive) + .gte(0) + .expect(navigationEvent.loadEventEnd) + .gte(0) + .expect(navigationEvent.loadEventStart) + .gte(0) + .expect(navigationEvent.redirectCount) + .gte(0) + .expect(navigationEvent.type) + .ok() + .expect(navigationEvent.unloadEventEnd) + .gte(0) + .expect(navigationEvent.unloadEventStart) + .gte(0); }); diff --git a/src/plugins/event-plugins/__tests__/NavigationPlugin.test.ts b/src/plugins/event-plugins/__tests__/NavigationPlugin.test.ts index 580f9fae..c9c314dd 100644 --- a/src/plugins/event-plugins/__tests__/NavigationPlugin.test.ts +++ b/src/plugins/event-plugins/__tests__/NavigationPlugin.test.ts @@ -1,11 +1,19 @@ +let isNavigationSupported = true; +jest.mock('../../../utils/common-utils', () => { + const originalModule = jest.requireActual('../../../utils/common-utils'); + return { + __esModule: true, + ...originalModule, + isNavigationSupported: jest + .fn() + .mockImplementation(() => isNavigationSupported) + }; +}); + import { navigationEvent, performanceEvent, - performanceEventNotLoaded, - mockPerformanceObserver, - MockPerformanceTiming, - mockPerformanceObjectWith, - putRumEventsDocument + performanceEventNotLoaded } from '../../../test-utils/mock-data'; import { NavigationPlugin } from '../NavigationPlugin'; import { context, record } from '../../../test-utils/test-utils'; @@ -34,64 +42,44 @@ describe('NavigationPlugin tests', () => { window.dispatchEvent(new Event('load')); plugin.disable(); - expect(record.mock.calls[0][0]).toEqual( - PERFORMANCE_NAVIGATION_EVENT_TYPE - ); - expect(record.mock.calls[0][1]).toEqual( - expect.objectContaining({ - duration: navigationEvent.duration, - startTime: navigationEvent.startTime, - navigationType: navigationEvent.type - }) - ); - }); - - test('When transferSize is 0 then headerSize is 0', async () => { - const plugin: NavigationPlugin = buildNavigationPlugin(); - // Run - plugin.load(context); - window.dispatchEvent(new Event('load')); - plugin.disable(); + const e = navigationEvent; expect(record.mock.calls[0][0]).toEqual( PERFORMANCE_NAVIGATION_EVENT_TYPE ); expect(record.mock.calls[0][1]).toEqual( expect.objectContaining({ - headerSize: 0 - }) - ); - }); - - test('When navigation timing level 2 API is not present then navigation timing level 1 API is recorded', async () => { - jest.useFakeTimers(); - mockPerformanceObjectWith([putRumEventsDocument], [], []); - mockPerformanceObserver(); - - const plugin: NavigationPlugin = buildNavigationPlugin(); - - plugin.load(context); - window.dispatchEvent(new Event('load')); - plugin.disable(); - - jest.runAllTimers(); - - expect(record).toHaveBeenCalledTimes(1); - expect(record.mock.calls[0][0]).toEqual( - PERFORMANCE_NAVIGATION_EVENT_TYPE - ); - - expect(record.mock.calls[0][1]).toEqual( - expect.objectContaining({ - domComplete: - MockPerformanceTiming.domComplete - - MockPerformanceTiming.navigationStart, - responseStart: - MockPerformanceTiming.responseStart - - MockPerformanceTiming.navigationStart, - initiatorType: 'navigation', - redirectStart: MockPerformanceTiming.redirectStart, - navigationTimingLevel: 1 + name: e.name, + entryType: 'navigation', + startTime: e.startTime, + duration: e.duration, + initiatorType: e.initiatorType, + nextHopProtocol: e.nextHopProtocol, + workerStart: e.workerStart, + redirectStart: e.redirectStart, + redirectEnd: e.redirectEnd, + fetchStart: e.fetchStart, + domainLookupStart: e.domainLookupStart, + domainLookupEnd: e.domainLookupEnd, + connectStart: e.connectStart, + connectEnd: e.connectEnd, + secureConnectionStart: e.secureConnectionStart, + requestStart: e.requestStart, + responseStart: e.responseStart, + responseEnd: e.responseEnd, + transferSize: e.transferSize, + encodedBodySize: e.encodedBodySize, + decodedBodySize: e.decodedBodySize, + domComplete: e.domComplete, + domContentLoadedEventEnd: e.domContentLoadedEventEnd, + domContentLoadedEventStart: e.domContentLoadedEventStart, + domInteractive: e.domInteractive, + loadEventEnd: e.loadEventEnd, + loadEventStart: e.loadEventStart, + redirectCount: e.redirectCount, + type: e.type, + unloadEventEnd: e.unloadEventEnd, + unloadEventStart: e.unloadEventStart }) ); }); @@ -123,7 +111,7 @@ describe('NavigationPlugin tests', () => { // Assert expect(record).toHaveBeenCalledTimes(0); }); - test('when entry is ignored then level 2 navigation is not recorded', async () => { + test('when entry is ignored then navigation is not recorded', async () => { // enables plugin by default const plugin: NavigationPlugin = buildNavigationPlugin({ ignore: (event) => true @@ -141,15 +129,16 @@ describe('NavigationPlugin tests', () => { // Setting up new mocked window that has not loaded (window as any).performance = performanceEventNotLoaded.performance(); - // enables plugin by default and loads + // run const plugin: NavigationPlugin = buildNavigationPlugin(); plugin.load(context); - // assert that the plugin did not fire - expect(record).toHaveBeenCalledTimes(0); - - // now that the page has loaded, we should fire window.dispatchEvent(new Event('load')); - expect(record).toHaveBeenCalledTimes(1); + + // assert + expect(record).toHaveBeenCalledWith( + PERFORMANCE_NAVIGATION_EVENT_TYPE, + expect.anything() + ); }); test('when window.load fires before plugin loads then navigation timing is recorded', async () => { @@ -160,6 +149,27 @@ describe('NavigationPlugin tests', () => { // so when we load the plugin now, it should still record event plugin.load(context); // Assert - expect(record).toHaveBeenCalled(); + expect(record).toHaveBeenCalledWith( + PERFORMANCE_NAVIGATION_EVENT_TYPE, + expect.anything() + ); + }); + + test('when PerformanceNavigationTiming is not supported, then the NavigationPlugin does not initialize an observer', async () => { + // init + isNavigationSupported = false; + // jest.mock('../NavigationPlugin'); + + // enables plugin by default + const plugin: NavigationPlugin = buildNavigationPlugin(); + + // window by default has already loaded before the plugin + // so when we load the plugin now, it should still record event + plugin.load(context); + // Assert + expect((plugin as any).po).toBeUndefined(); + + // restore + isNavigationSupported = true; }); }); diff --git a/src/plugins/utils/constant.ts b/src/plugins/utils/constant.ts index c6e67e21..67541d50 100644 --- a/src/plugins/utils/constant.ts +++ b/src/plugins/utils/constant.ts @@ -12,7 +12,7 @@ export const FID_EVENT_TYPE = `${RUM_AMZ_PREFIX}.first_input_delay_event`; export const CLS_EVENT_TYPE = `${RUM_AMZ_PREFIX}.cumulative_layout_shift_event`; // Page load event schemas -export const PERFORMANCE_NAVIGATION_EVENT_TYPE = `${RUM_AMZ_PREFIX}.performance_navigation_event`; +export const PERFORMANCE_NAVIGATION_EVENT_TYPE = `${RUM_AMZ_PREFIX}.performance_navigation_timing`; export const PERFORMANCE_RESOURCE_EVENT_TYPE = `${RUM_AMZ_PREFIX}.performance_resource_event`; // DOM event schemas diff --git a/src/sessions/VirtualPageLoadTimer.ts b/src/sessions/VirtualPageLoadTimer.ts index b6eab0d4..46824725 100644 --- a/src/sessions/VirtualPageLoadTimer.ts +++ b/src/sessions/VirtualPageLoadTimer.ts @@ -1,4 +1,4 @@ -import { NavigationEvent } from '../events/navigation-event'; +import { PerformanceNavigationTimingEvent } from '../events/performance-navigation-timing'; import { PERFORMANCE_NAVIGATION_EVENT_TYPE } from '../plugins/utils/constant'; import { MonkeyPatched } from '../plugins/MonkeyPatched'; import { Config } from '../orchestration/Orchestration'; @@ -200,7 +200,7 @@ export class VirtualPageLoadTimer extends MonkeyPatched< * Checks whether the virtual page is still being loaded. * If completed: * (1) Clear the timers - * (2) Record data using NavigationEvent + * (2) Record data using PerformanceNavigationTimingEvent * (3) Indicate current page has finished loading */ private checkLoadStatus = () => { @@ -240,13 +240,12 @@ export class VirtualPageLoadTimer extends MonkeyPatched< }; private recordRouteChangeNavigationEvent(page: Page) { - const virtualPageNavigationEvent: NavigationEvent = { - version: '1.0.0', + const virtualPageNavigationEvent = { initiatorType: 'route_change', - navigationType: 'navigate', + type: 'navigate', startTime: page.start, duration: this.latestEndTime - page.start - }; + } as PerformanceNavigationTimingEvent; if (this.record) { this.record( PERFORMANCE_NAVIGATION_EVENT_TYPE, diff --git a/src/sessions/__tests__/SessionManager.test.ts b/src/sessions/__tests__/SessionManager.test.ts index d502816f..44aedbc6 100644 --- a/src/sessions/__tests__/SessionManager.test.ts +++ b/src/sessions/__tests__/SessionManager.test.ts @@ -1,3 +1,13 @@ +jest.mock('../../utils/common-utils', () => { + const originalModule = jest.requireActual('../../utils/common-utils'); + return { + __esModule: true, + ...originalModule, + isLCPSupported: jest.fn().mockReturnValue(true), + isNavigationSupported: jest.fn().mockReturnValue(true) + }; +}); + import { Attributes, NIL_UUID, @@ -22,7 +32,6 @@ import { DEFAULT_CONFIG, mockFetch } from '../../test-utils/test-utils'; -import { advanceTo } from 'jest-date-mock'; global.fetch = mockFetch; const NAVIGATION = 'navigation'; diff --git a/src/utils/common-utils.ts b/src/utils/common-utils.ts index 75d0edfa..75ec0396 100644 --- a/src/utils/common-utils.ts +++ b/src/utils/common-utils.ts @@ -223,6 +223,10 @@ export const isLongTaskSupported = () => { return PerformanceObserver.supportedEntryTypes.includes('longtask'); }; +export const isNavigationSupported = () => { + return PerformanceObserver.supportedEntryTypes.includes('navigation'); +}; + /** PutRumEvents regex pattern */ const putRumEventsPattern = /.*\/application\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/events/;