UNPKG

@zendesk/retrace

Version:

define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API

1,309 lines 73.4 kB
"use strict"; /* eslint-disable jest/no-conditional-in-test */ /* eslint-disable @typescript-eslint/no-floating-promises */ /** * Copyright Zendesk, Inc. * * Use of this source code is governed under the Apache License, Version 2.0 * found at http://www.apache.org/licenses/LICENSE-2.0. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const react_1 = require("react"); const React = __importStar(require("react")); const react_test_renderer_1 = require("react-test-renderer"); const ActionLog_1 = require("./ActionLog"); const constants_1 = require("./constants"); const generateReport_1 = require("./generateReport"); const performanceMock = __importStar(require("./performanceMark")); const useTimingMeasurement_1 = require("./useTimingMeasurement"); const utilities_1 = require("./utilities"); jest.mock('./performanceMark', () => ({ performanceMark: jest.fn(), performanceMeasure: jest.fn(), })); const performanceMarkMock = performanceMock.performanceMark; const performanceMeasureMock = performanceMock.performanceMeasure; const assert = (condition, message) => { if (!condition) { throw new Error(message); } }; const createPerfObserverEntryList = (entryList) => ({ getEntries: () => entryList, getEntriesByName: () => [], getEntriesByType: () => [], }); describe('useTiming', () => { const mockObserve = jest.fn(); const mockDisconnect = jest.fn(); const PerformanceObserverMock = jest.fn().mockImplementation(() => ({ observe: mockObserve, disconnect: mockDisconnect, takeRecords: jest.fn(), })); const mockGetSupportedEntryTypes = jest.fn(() => []); // Set static property supportedEntryTypes on PerformanceObserver Object.defineProperty(PerformanceObserverMock, 'supportedEntryTypes', { get: mockGetSupportedEntryTypes, configurable: true, }); const originalPerformanceObserver = window.PerformanceObserver; const mockReportFn = jest.fn(); const id = 'test-component'; const timeIncrement = 100; const debounceMs = 5 * timeIncrement; const timeoutMs = 10 * timeIncrement; const originalConsoleError = console.error; const consoleErrorMock = jest.fn(); const onInternalError = jest.fn(); let currentTime; let stageChangeDuration; const originalTimeOrigin = performance.timeOrigin; beforeAll(() => { performanceMarkMock.mockImplementation((name) => ({ name, duration: 0, startTime: currentTime, entryType: 'mark', toJSON: () => `(toJSON:${name})`, detail: null, })); performanceMeasureMock.mockImplementation((name, startMark) => { if (name.includes('-till-') || name.includes('/start-')) currentTime += stageChangeDuration; else if (!name.endsWith('timeout')) currentTime += timeIncrement; return { name, duration: currentTime - startMark.startTime, startTime: startMark.startTime, entryType: 'measure', toJSON: () => `(toJSON:${name})`, detail: null, }; }); console.error = consoleErrorMock; globalThis.PerformanceObserver = PerformanceObserverMock; jest.useFakeTimers(); Object.defineProperty(performance, 'timeOrigin', { configurable: true, enumerable: true, get() { return 0; }, }); }); afterAll(() => { jest.useRealTimers(); console.error = originalConsoleError; globalThis.PerformanceObserver = originalPerformanceObserver; Object.defineProperty(performance, 'timeOrigin', { configurable: true, enumerable: true, get() { return originalTimeOrigin; }, }); }); beforeEach(() => { currentTime = 0; stageChangeDuration = 0; }); afterEach(() => { jest.clearAllMocks(); (0, utilities_1.resetMemoizedCurrentBrowserSupportForNonResponsiveStateDetection)(); // For whatever reason, if this line isn't here, jest's timers go all wack-o // even though we're clearing them a line below // this one has to be run; otherwise if a test fails, subsequent tests will also fail. // Note that for some reason switching to useRealTimers() doesn't clear the timers 🤦‍ jest.runOnlyPendingTimers(); jest.clearAllTimers(); }); describe('without the beacon hook and stages', () => { it('should report metrics when ready', () => { const simulatedFirstRenderTimeMs = timeIncrement; let renderer; const actionLog = new ActionLog_1.ActionLog({ reportFn: mockReportFn, debounceMs, timeoutMs, onInternalError, }); const TimedTestComponent = () => { (0, useTimingMeasurement_1.useTimingMeasurement)({ id, placement: 'manager', actionLog, }, []); return React.createElement("div", null, "Hello!"); }; (0, react_test_renderer_1.act)(() => { renderer = (0, react_test_renderer_1.create)(React.createElement(TimedTestComponent, null)); }); // the hook shouldn't affect the contents being rendered: expect(renderer.toJSON()).toMatchInlineSnapshot(` <div> Hello! </div> `); // we're simulating a browser that doesn't support observing frozen states: expect(mockObserve).not.toHaveBeenCalled(); // exhaust React's next tick timer jest.advanceTimersToNextTimer(); // jest.advanceTimersByTime(1); // report shouldn't have been called yet -- it's debounced: expect(mockReportFn).not.toHaveBeenCalled(); // we should have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); jest.advanceTimersByTime(simulatedFirstRenderTimeMs); // we should *still* have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); // report shouldn't have been called yet -- it's debounced: expect(mockReportFn).not.toHaveBeenCalled(); jest.advanceTimersByTime(debounceMs); // no more timers should be set by this time: expect(jest.getTimerCount()).toBe(0); expect(mockReportFn).toHaveBeenCalledTimes(1); const report = { id, isFirstLoad: true, tti: null, ttr: simulatedFirstRenderTimeMs, lastStage: constants_1.INFORMATIVE_STAGES.INITIAL, counts: { manager: 1, }, durations: { manager: [timeIncrement], }, timeSpent: { manager: timeIncrement, }, spans: expect.anything(), loadingStagesDuration: 0, includedStages: [], hadError: false, handled: true, flushReason: 'debounce', }; const reportArgs = mockReportFn.mock.calls.at(-1)?.[0]; expect(reportArgs).toBeDefined(); const generatedReport = (0, generateReport_1.generateReport)(reportArgs); expect(generatedReport).toEqual(report); expect(generatedReport.spans).toMatchInlineSnapshot(` Array [ Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (1)", "endTime": 100, "relativeEndTime": 100, "startTime": 0, "type": "render", }, Object { "data": Object { "dependencyChanges": 0, "mountedPlacements": Array [ "manager", ], "previousStage": "initial", "stage": "rendered", "timeToStage": 100, "timingId": "test-component", }, "description": "render", "endTime": 100, "relativeEndTime": 100, "startTime": 0, "type": "ttr", }, ] `); renderer.unmount(); jest.runAllTimers(); // an un-mount shouldn't change anything expect(mockReportFn).toHaveBeenCalledTimes(1); expect(jest.getTimerCount()).toBe(0); }); it('should report new metrics after updating', () => { const simulatedFirstRenderTimeMs = timeIncrement; let renderer; const actionLog = new ActionLog_1.ActionLog({ reportFn: mockReportFn, debounceMs, timeoutMs, onInternalError, }); const TimedTestComponent = ({ action }) => { (0, useTimingMeasurement_1.useTimingMeasurement)({ id, placement: 'manager', actionLog, }, [action]); return React.createElement("div", null, "Hello!"); }; (0, react_test_renderer_1.act)(() => { renderer = (0, react_test_renderer_1.create)(React.createElement(TimedTestComponent, { action: "mount" })); }); // the hook shouldn't affect the contents being rendered: expect(renderer.toJSON()).toMatchInlineSnapshot(` <div> Hello! </div> `); // we're simulating a browser that doesn't support observing frozen states: expect(mockObserve).not.toHaveBeenCalled(); // exhaust React's (?) next tick timer jest.advanceTimersToNextTimer(); // report shouldn't have been called yet -- it's debounced: expect(mockReportFn).not.toHaveBeenCalled(); // we should have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); jest.advanceTimersByTime(simulatedFirstRenderTimeMs); // we should *still* have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); // report shouldn't have been called yet -- it's debounced: expect(mockReportFn).not.toHaveBeenCalled(); jest.advanceTimersByTime(debounceMs); // no more timers should be set by this time: expect(jest.getTimerCount()).toBe(0); expect(mockReportFn).toHaveBeenCalledTimes(1); const report = { id, isFirstLoad: true, tti: null, ttr: simulatedFirstRenderTimeMs, lastStage: constants_1.INFORMATIVE_STAGES.INITIAL, counts: { manager: 1, }, durations: { manager: [timeIncrement], }, timeSpent: { manager: timeIncrement, }, loadingStagesDuration: 0, spans: expect.anything(), includedStages: [], hadError: false, handled: true, flushReason: 'debounce', }; expect(mockReportFn).toHaveBeenCalledTimes(1); const reportArgs = mockReportFn.mock.calls.at(-1)?.[0]; expect(reportArgs).toBeDefined(); const generatedReport = (0, generateReport_1.generateReport)(reportArgs); expect(generatedReport).toEqual(report); expect(generatedReport.spans).toMatchInlineSnapshot(` Array [ Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (1)", "endTime": 100, "relativeEndTime": 100, "startTime": 0, "type": "render", }, Object { "data": Object { "dependencyChanges": 0, "mountedPlacements": Array [ "manager", ], "previousStage": "initial", "stage": "rendered", "timeToStage": 100, "timingId": "test-component", }, "description": "render", "endTime": 100, "relativeEndTime": 100, "startTime": 0, "type": "ttr", }, ] `); jest.runAllTimers(); actionLog.disableReporting(); (0, react_test_renderer_1.act)(() => { renderer.update(React.createElement(TimedTestComponent, { action: "update" })); }); jest.advanceTimersByTime(debounceMs); // a new report should have been generated expect(mockReportFn).toHaveBeenCalledTimes(2); }); it(`should keep debouncing and timeoutMs if the component keeps doing stuff for too long`, () => { expect(jest.getTimerCount()).toBe(0); let renderer; let keepRerendering = true; const actionLog = new ActionLog_1.ActionLog({ debounceMs, timeoutMs, }); const TimedTestComponentThatKeepsDoingStuffForever = ({ children, }) => { (0, useTimingMeasurement_1.useTimingMeasurement)({ id, placement: 'manager', onInternalError, reportFn: mockReportFn, actionLog, }, []); const [state, setState] = (0, react_1.useState)(0); (0, react_1.useEffect)(() => { if (keepRerendering) { setTimeout(() => { setState(state + 1); }, timeIncrement); } }); return (React.createElement("div", null, children, state)); }; (0, react_test_renderer_1.act)(() => { renderer = (0, react_test_renderer_1.create)(React.createElement(TimedTestComponentThatKeepsDoingStuffForever, null, "Hello world! Your lucky number (re-render count) is:")); }); // the hook shouldn't affect the contents being rendered: expect(renderer.toJSON()).toMatchInlineSnapshot(` <div> Hello world! Your lucky number (re-render count) is: 0 </div> `); // exhaust React's next tick timer jest.advanceTimersToNextTimer(); // report shouldn't have been called yet -- it's debounced: expect(mockReportFn).not.toHaveBeenCalled(); // we should have timers for: debounce and timeoutMs, and the one from the component expect(jest.getTimerCount()).toBe(3); jest.advanceTimersByTime(timeoutMs - timeIncrement); // timeoutMs should still be going expect(jest.getTimerCount()).toBe(3); jest.advanceTimersByTime(timeIncrement); // by this time the timeoutMs should have executed and now the only timer is the one from the component expect(jest.getTimerCount()).toBe(1); // we should be after 9 re-renders at this point: expect(renderer.toJSON()).toMatchInlineSnapshot(` <div> Hello world! Your lucky number (re-render count) is: 9 </div> `); expect(mockReportFn).toHaveBeenCalledTimes(1); const expectedRenderCount = timeoutMs / timeIncrement; const startToEndTime = timeoutMs; const report = { id, isFirstLoad: true, tti: null, ttr: null, lastStage: constants_1.INFORMATIVE_STAGES.TIMEOUT, counts: { manager: expectedRenderCount, }, durations: { manager: Array.from({ length: expectedRenderCount }).map(() => timeIncrement), }, spans: expect.anything(), timeSpent: { manager: startToEndTime, }, loadingStagesDuration: 0, includedStages: [ constants_1.INFORMATIVE_STAGES.INITIAL, constants_1.INFORMATIVE_STAGES.TIMEOUT, ], hadError: false, handled: false, flushReason: 'timeout', }; expect(mockReportFn).toHaveBeenCalledTimes(1); const reportArgs = mockReportFn.mock.calls.at(-1)?.[0]; expect(reportArgs).toBeDefined(); const generatedReport = (0, generateReport_1.generateReport)(reportArgs); expect(generatedReport).toEqual(report); expect(generatedReport.spans).toMatchInlineSnapshot(` Array [ Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (1)", "endTime": 100, "relativeEndTime": 100, "startTime": 0, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (2)", "endTime": 200, "relativeEndTime": 200, "startTime": 100, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (3)", "endTime": 300, "relativeEndTime": 300, "startTime": 200, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (4)", "endTime": 400, "relativeEndTime": 400, "startTime": 300, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (5)", "endTime": 500, "relativeEndTime": 500, "startTime": 400, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (6)", "endTime": 600, "relativeEndTime": 600, "startTime": 500, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (7)", "endTime": 700, "relativeEndTime": 700, "startTime": 600, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (8)", "endTime": 800, "relativeEndTime": 800, "startTime": 700, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (9)", "endTime": 900, "relativeEndTime": 900, "startTime": 800, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (10)", "endTime": 1000, "relativeEndTime": 1000, "startTime": 900, "type": "render", }, Object { "data": Object { "dependencyChanges": 0, "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "previousStage": "initial", "source": "timeout", "stage": "timeout", "timeToStage": 1000, "timingId": "test-component", }, "description": "initial to timeout", "endTime": 1000, "relativeEndTime": 1000, "startTime": 0, "type": "stage-change", }, ] `); jest.advanceTimersByTime(timeIncrement); // re-rendering should still be happening, even though we're not measuring anymore: expect(jest.getTimerCount()).toBe(1); expect(renderer.toJSON()).toMatchInlineSnapshot(` <div> Hello world! Your lucky number (re-render count) is: 10 </div> `); keepRerendering = false; jest.advanceTimersByTime(timeIncrement); // no more timers should be set by this time: expect(jest.getTimerCount()).toBe(0); // ...and we reported only once expect(mockReportFn).toHaveBeenCalledTimes(1); renderer.unmount(); }); }); describe('with stages, but without the beacon hook', () => { it('should report metrics when ready', () => { let renderer; let setStage; const actionLog = new ActionLog_1.ActionLog({ debounceMs, timeoutMs, finalStages: [constants_1.DEFAULT_STAGES.READY], }); const TimedTestComponent = () => { const [stage, _setStage] = (0, react_1.useState)(constants_1.INFORMATIVE_STAGES.INITIAL); setStage = _setStage; (0, useTimingMeasurement_1.useTimingMeasurement)({ id, placement: 'manager', onInternalError, reportFn: mockReportFn, stage, actionLog, }, []); return React.createElement("div", null, "Hello! We are at stage:", stage); }; (0, react_test_renderer_1.act)(() => { renderer = (0, react_test_renderer_1.create)(React.createElement(TimedTestComponent, null)); }); // the hook shouldn't affect the contents being rendered: expect(renderer.toJSON()).toMatchInlineSnapshot(` <div> Hello! We are at stage: initial </div> `); // we're simulating a browser that doesn't support observing frozen states: expect(mockObserve).not.toHaveBeenCalled(); // exhaust React's next tick timer jest.advanceTimersToNextTimer(); // report shouldn't have been called yet -- it's debounced: expect(mockReportFn).not.toHaveBeenCalled(); // we should have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); jest.advanceTimersByTime(timeIncrement); // we should *still* have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); // still debounced: expect(mockReportFn).not.toHaveBeenCalled(); setStage(constants_1.DEFAULT_STAGES.LOADING); // re-rendered with new data: expect(renderer.toJSON()).toMatchInlineSnapshot(` <div> Hello! We are at stage: loading </div> `); // exhaust React's next tick timer jest.advanceTimersToNextTimer(); // we should have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); jest.advanceTimersByTime(timeIncrement); setStage(constants_1.DEFAULT_STAGES.READY); // re-rendered with new data: expect(renderer.toJSON()).toMatchInlineSnapshot(` <div> Hello! We are at stage: ready </div> `); // exhaust React's next tick timer jest.advanceTimersToNextTimer(); // we should have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); // still debounced: expect(mockReportFn).not.toHaveBeenCalled(); jest.advanceTimersByTime(debounceMs); expect(mockReportFn).toHaveBeenCalledTimes(1); const expectedRenderCount = 3; const timeSpent = timeIncrement * expectedRenderCount; const report = { id, isFirstLoad: true, tti: null, ttr: timeIncrement * expectedRenderCount, lastStage: constants_1.DEFAULT_STAGES.READY, counts: { manager: expectedRenderCount, }, durations: { manager: [timeIncrement, timeIncrement, timeIncrement], }, timeSpent: { manager: timeSpent, }, spans: expect.anything(), loadingStagesDuration: timeIncrement, includedStages: [ constants_1.INFORMATIVE_STAGES.INITIAL, constants_1.DEFAULT_STAGES.LOADING, constants_1.DEFAULT_STAGES.READY, ], hadError: false, handled: true, flushReason: 'debounce', }; expect(mockReportFn).toHaveBeenCalledTimes(1); const reportArgs = mockReportFn.mock.calls.at(-1)?.[0]; expect(reportArgs).toBeDefined(); const generatedReport = (0, generateReport_1.generateReport)(reportArgs); expect(generatedReport).toEqual(report); expect(generatedReport.spans).toMatchInlineSnapshot(` Array [ Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "initial", "timingId": "test-component", }, "description": "<manager> (1)", "endTime": 100, "relativeEndTime": 100, "startTime": 0, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "loading", "timingId": "test-component", }, "description": "<manager> (2)", "endTime": 200, "relativeEndTime": 200, "startTime": 100, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "source": "manager", "stage": "ready", "timingId": "test-component", }, "description": "<manager> (3)", "endTime": 300, "relativeEndTime": 300, "startTime": 200, "type": "render", }, Object { "data": Object { "dependencyChanges": 0, "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "previousStage": "initial", "source": "manager", "stage": "loading", "timeToStage": 100, "timingId": "test-component", }, "description": "initial to loading", "endTime": 100, "relativeEndTime": 100, "startTime": 0, "type": "stage-change", }, Object { "data": Object { "dependencyChanges": 0, "metadata": Object {}, "mountedPlacements": Array [ "manager", ], "previousStage": "loading", "source": "manager", "stage": "ready", "timeToStage": 100, "timingId": "test-component", }, "description": "loading to ready", "endTime": 200, "relativeEndTime": 200, "startTime": 100, "type": "stage-change", }, Object { "data": Object { "dependencyChanges": 0, "mountedPlacements": Array [ "manager", ], "previousStage": "ready", "stage": "rendered", "timeToStage": 100, "timingId": "test-component", }, "description": "render", "endTime": 300, "relativeEndTime": 300, "startTime": 0, "type": "ttr", }, ] `); // no more timers should be set by this time: expect(jest.getTimerCount()).toBe(0); renderer.unmount(); jest.runAllTimers(); // an un-mount shouldn't change anything expect(mockReportFn).toHaveBeenCalledTimes(1); expect(jest.getTimerCount()).toBe(0); }); }); describe('with stages, the beacon hook, custom activation from the beacon and non-responsive state detection', () => { it('should report metrics when ready', () => { let renderer; // mock a browser that supports 'longtask' monitoring mockGetSupportedEntryTypes.mockReturnValue(['longtask']); let setBeaconState; const actionLog = new ActionLog_1.ActionLog({ finalStages: [constants_1.DEFAULT_STAGES.READY, constants_1.DEFAULT_STAGES.ERROR], debounceMs, timeoutMs, }); const BeaconComponent = () => { const [{ stage, isActive }, _setState] = (0, react_1.useState)({ stage: constants_1.INFORMATIVE_STAGES.INITIAL, isActive: false, }); setBeaconState = _setState; (0, useTimingMeasurement_1.useTimingMeasurement)({ id, placement: 'beacon', onInternalError, stage, isActive, actionLog, }, []); return (React.createElement(React.Fragment, null, React.createElement("div", null, "We are at stage:", stage), React.createElement("div", null, "We are:", isActive ? 'measuring' : 'not measuring'))); }; const TimedTestComponent = () => { (0, useTimingMeasurement_1.useTimingMeasurement)({ id, placement: 'manager', onInternalError, reportFn: mockReportFn, actionLog, }, []); return (React.createElement(React.Fragment, null, React.createElement("div", null, "Hello!"), React.createElement(BeaconComponent, null))); }; (0, react_test_renderer_1.act)(() => { renderer = (0, react_test_renderer_1.create)(React.createElement(TimedTestComponent, null)); }); // the hook shouldn't affect the contents being rendered: expect(renderer.toJSON()).toMatchInlineSnapshot(` [ <div> Hello! </div>, <div> We are at stage: initial </div>, <div> We are: not measuring </div>, ] `); // exhaust React's next tick timer jest.advanceTimersToNextTimer(); // we're simulating a browser that does support observing frozen states expect(PerformanceObserverMock).toHaveBeenCalledTimes(1); expect(PerformanceObserverMock).toHaveBeenLastCalledWith(expect.any(Function)); // but we aren't active yet: expect(mockObserve).not.toHaveBeenCalled(); assert(PerformanceObserverMock.mock.calls[0]); const performanceObserverCallback = PerformanceObserverMock.mock.calls[0][0]; // report shouldn't have been called yet -- we're inactive: expect(mockReportFn).not.toHaveBeenCalled(); // no timers should be present expect(jest.getTimerCount()).toBe(0); setBeaconState({ stage: constants_1.DEFAULT_STAGES.LOADING, isActive: true }); // re-rendered with new data: expect(renderer.toJSON()).toMatchInlineSnapshot(` [ <div> Hello! </div>, <div> We are at stage: loading </div>, <div> We are: measuring </div>, ] `); // exhaust React's next tick timer jest.advanceTimersToNextTimer(); // now we're active! expect(mockObserve).toHaveBeenCalledTimes(1); expect(mockObserve).toHaveBeenLastCalledWith({ entryTypes: ['longtask'] }); jest.advanceTimersByTime(timeIncrement); // report shouldn't have been called yet -- it's debounced: expect(mockReportFn).not.toHaveBeenCalled(); // we should have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); jest.advanceTimersByTime(timeIncrement); // we should *still* have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); // still debounced: expect(mockReportFn).not.toHaveBeenCalled(); setBeaconState({ stage: constants_1.DEFAULT_STAGES.READY, isActive: true }); // re-rendered with new data: expect(renderer.toJSON()).toMatchInlineSnapshot(` [ <div> Hello! </div>, <div> We are at stage: ready </div>, <div> We are: measuring </div>, ] `); // exhaust React's next tick timer jest.advanceTimersToNextTimer(); // we should have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); // still debounced: expect(mockReportFn).not.toHaveBeenCalled(); const lagStartTime = currentTime + timeIncrement; const lagDuration = timeIncrement * 5; // simulate non-responsiveness for 500ms performanceObserverCallback(createPerfObserverEntryList([ { duration: lagDuration, entryType: 'longtask', name: 'longtask', startTime: lagStartTime, toJSON: () => '', }, ]), // the 2nd argument isn't used, but we need it to make TypeScript happy new PerformanceObserver(() => { /* noop */ })); jest.advanceTimersByTime(timeIncrement); // report shouldn't have been called yet -- it's debounced: expect(mockReportFn).not.toHaveBeenCalled(); // we should have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); jest.advanceTimersByTime(timeIncrement); // we should *still* have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); // still debounced: expect(mockReportFn).not.toHaveBeenCalled(); jest.advanceTimersByTime(debounceMs); expect(mockReportFn).toHaveBeenCalledTimes(1); // we should have disconnected now: expect(mockDisconnect).toHaveBeenCalledTimes(1); // we should only start counting renders AFTER we activate, that's why we only have 2 (loading and ready): const expectedBeaconRenderCount = 2; const ttr = timeIncrement * expectedBeaconRenderCount; const tti = ttr + timeIncrement + lagDuration; const report = { id, isFirstLoad: true, tti, ttr, lastStage: constants_1.DEFAULT_STAGES.READY, counts: { beacon: expectedBeaconRenderCount, observer: 1, // manager wouldn't have re-rendered }, durations: { beacon: [timeIncrement, timeIncrement], observer: [lagDuration], }, timeSpent: { beacon: ttr, observer: lagDuration, }, spans: expect.anything(), loadingStagesDuration: timeIncrement, includedStages: [constants_1.DEFAULT_STAGES.LOADING, constants_1.DEFAULT_STAGES.READY], hadError: false, handled: true, flushReason: 'debounce', }; expect(mockReportFn).toHaveBeenCalledTimes(1); const reportArgs = mockReportFn.mock.calls.at(-1)?.[0]; expect(reportArgs).toBeDefined(); const generatedReport = (0, generateReport_1.generateReport)(reportArgs); expect(generatedReport).toEqual(report); expect(generatedReport.spans).toMatchInlineSnapshot(` Array [ Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", "beacon", ], "source": "beacon", "stage": "loading", "timingId": "test-component", }, "description": "<beacon> (1)", "endTime": 300, "relativeEndTime": 100, "startTime": 200, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", "beacon", ], "source": "beacon", "stage": "ready", "timingId": "test-component", }, "description": "<beacon> (2)", "endTime": 400, "relativeEndTime": 200, "startTime": 300, "type": "render", }, Object { "data": Object { "metadata": Object {}, "mountedPlacements": Array [ "manager", "beacon", ], "source": "observer", "stage": "ready", "timingId": "test-component", }, "description": "unresponsive", "endTime": 1000, "relativeEndTime": 800, "startTime": 500, "type": "unresponsive", }, Object { "data": Object { "dependencyChanges": 0, "metadata": Object {}, "mountedPlacements": Array [ "manager", "beacon", ], "previousStage": "loading", "source": "beacon", "stage": "ready", "timeToStage": 100, "timingId": "test-component", }, "description": "loading to ready", "endTime": 300, "relativeEndTime": 100, "startTime": 200, "type": "stage-change", }, Object { "data": Object { "dependencyChanges": 0, "mountedPlacements": Array [ "manager", "beacon", ], "previousStage": "ready", "stage": "rendered", "timeToStage": 100, "timingId": "test-component", }, "description": "render", "endTime": 400, "relativeEndTime": 200, "startTime": 200, "type": "ttr", }, Object { "data": Object { "dependencyChanges": 0, "mountedPlacements": Array [ "manager", "beacon", ], "previousStage": "rendered", "stage": "interactive", "timeToStage": 600, "timingId": "test-component", }, "description": "interactive", "endTime": 1000, "relativeEndTime": 800, "startTime": 200, "type": "tti", }, ] `); // no more timers should be set by this time: expect(jest.getTimerCount()).toBe(0); renderer.unmount(); jest.runAllTimers(); // an un-mount shouldn't change anything expect(mockReportFn).toHaveBeenCalledTimes(1); expect(jest.getTimerCount()).toBe(0); }); it('should wait until beacon is activated', () => { let renderer; // mock a browser that supports 'longtask' monitoring mockGetSupportedEntryTypes.mockReturnValue(['longtask']); let setBeaconState; const actionLog = new ActionLog_1.ActionLog({ waitForBeaconActivation: ['beacon'], debounceMs, timeoutMs, }); const BeaconComponent = () => { (0, useTimingMeasurement_1.useTimingMeasurement)({ id, placement: 'beacon', onInternalError, actionLog, }, []); return (React.createElement(React.Fragment, null, React.createElement("div", null, "We are a beacon"))); }; const TimedTestComponent = () => { const [{ isActive }, _setState] = (0, react_1.useState)({ isActive: false, }); setBeaconState = _setState; (0, useTimingMeasurement_1.useTimingMeasurement)({ id, placement: 'manager', onInternalError, reportFn: mockReportFn, actionLog, }, []); return (React.createElement(React.Fragment, null, React.createElement("div", null, "Hello!"), isActive && React.createElement(BeaconComponent, null))); }; (0, react_test_renderer_1.act)(() => { renderer = (0, react_test_renderer_1.create)(React.createElement(TimedTestComponent, null)); }); // the hook shouldn't affect the contents being rendered: expect(renderer.toJSON()).toMatchInlineSnapshot(` <div> Hello! </div> `); // exhaust React's next tick timer jest.advanceTimersToNextTimer(); // we're simulating a browser that does support observing frozen states expect(PerformanceObserverMock).toHaveBeenCalledTimes(1); expect(PerformanceObserverMock).toHaveBeenLastCalledWith(expect.any(Function)); // but we aren't active yet: expect(mockObserve).not.toHaveBeenCalled(); assert(PerformanceObserverMock.mock.calls[0]); const performanceObserverCallback = PerformanceObserverMock.mock.calls[0][0]; // report shouldn't have been called yet -- waitForBeaconActivation has not been satisfied: expect(mockReportFn).not.toHaveBeenCalled(); // no timers should be present expect(jest.getTimerCount()).toBe(0); setBeaconState({ isActive: true }); // re-rendered with new data: expect(renderer.toJSON()).toMatchInlineSnapshot(` [ <div> Hello! </div>, <div> We are a beacon </div>, ] `); // exhaust React's next tick timer jest.advanceTimersToNextTimer(); // now we're active! expect(mockObserve).toHaveBeenCalledTimes(1); expect(mockObserve).toHaveBeenLastCalledWith({ entryTypes: ['longtask'] }); jest.advanceTimersByTime(timeIncrement); // report shouldn't have been called yet -- it's debounced: expect(mockReportFn).not.toHaveBeenCalled(); // we should have timers for: debounce and timeoutMs expect(jest.getTimerCount()).toBe(2); const lagStartTime = currentTime + timeIncrement; const lagDuration = debounceMs; // simulate non-responsiveness for 500ms performanceObserverCallback(createPerfObserverEntryList([ { duration: lagDuration, entryType: 'longtask', name: 'longtask', startTime: lagStartTime, toJSON: () => '', }, ]), // the 2nd argument isn't used, b