@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
JavaScript
"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