@zendesk/retrace
Version:
define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API
418 lines • 14.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ActionLogCache_1 = require("./ActionLogCache");
const generateReport_1 = require("./generateReport");
const getExternalApi_1 = require("./getExternalApi");
const garbageCollectMs = 100_000;
const debounceMs = 500;
const renderTime = 100;
const timeoutMs = 45_000;
describe('getExternalApi', () => {
const originalTimeOrigin = performance.timeOrigin;
beforeEach(() => {
jest.useFakeTimers();
Object.defineProperty(performance, 'timeOrigin', {
configurable: true,
enumerable: true,
get() {
return 0;
},
});
});
afterEach(() => {
jest.useRealTimers();
Object.defineProperty(performance, 'timeOrigin', {
configurable: true,
enumerable: true,
get() {
return originalTimeOrigin;
},
});
});
it('sends the report after debounce when no final stages are specified', () => {
const reportFn = jest.fn();
const onInternalError = jest.fn();
const actionLogCache = new ActionLogCache_1.ActionLogCache({
garbageCollectMs,
debounceMs,
reportFn,
});
const api = (0, getExternalApi_1.getExternalApi)({
actionLogCache,
idPrefix: 'test',
placement: 'beacon',
});
api.markRenderStart('1');
const actionLog = api.getActionLogForIdIfExists('1');
expect(actionLog).toBeDefined();
// no actions yet cause we didn't mark render end
expect(actionLog?.getActions()).toHaveLength(0);
expect(actionLog?.isCapturingData).toBe(true);
jest.advanceTimersByTime(renderTime);
api.markRenderEnd('1');
// start and end render expected:
expect(actionLog?.getActions()).toHaveLength(2);
expect(actionLog?.getActions()).toMatchInlineSnapshot(`
[
{
"entry": {},
"marker": "start",
"mountedPlacements": [
"beacon",
],
"source": "beacon",
"timestamp": 0,
"timingId": "test/1",
"type": "render",
},
{
"entry": {},
"marker": "end",
"mountedPlacements": [
"beacon",
],
"source": "beacon",
"timestamp": 100,
"timingId": "test/1",
"type": "render",
},
]
`);
// not send yet cause we're waiting for debounce
expect(reportFn).not.toHaveBeenCalled();
jest.advanceTimersByTime(debounceMs);
expect(reportFn).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(garbageCollectMs);
api.dispose('1');
expect(actionLogCache.get('test/1')).toBeUndefined();
expect(onInternalError).not.toHaveBeenCalled();
});
it('sends the report after debounce when a final stage is specified', () => {
const reportFn = jest.fn();
const onInternalError = jest.fn();
const actionLogCache = new ActionLogCache_1.ActionLogCache({
garbageCollectMs,
debounceMs,
finalStages: ['ready'],
reportFn,
});
const api = (0, getExternalApi_1.getExternalApi)({
actionLogCache,
idPrefix: 'test',
placement: 'beacon',
});
api.markRenderStart('1');
const actionLog = api.getActionLogForIdIfExists('1');
expect(actionLog).toBeDefined();
// no actions yet cause we didn't mark render end
expect(actionLog?.getActions()).toHaveLength(0);
expect(actionLog?.isCapturingData).toBe(true);
jest.advanceTimersByTime(renderTime);
api.markRenderEnd('1');
// start and end render expected:
expect(actionLog?.getActions()).toHaveLength(2);
api.markStage('1', 'ready');
// not send yet cause we're waiting for debounce
expect(reportFn).not.toHaveBeenCalled();
expect(actionLog?.getActions()).toHaveLength(3);
expect(actionLog?.getActions()[2]).toMatchInlineSnapshot(`
{
"entry": {},
"marker": "point",
"metadata": undefined,
"mountedPlacements": [
"beacon",
],
"source": "beacon",
"stage": "ready",
"timestamp": 100,
"timingId": "test/1",
"type": "stage-change",
}
`);
jest.advanceTimersByTime(debounceMs);
expect(reportFn).toHaveBeenCalledTimes(1);
expect(onInternalError).not.toHaveBeenCalled();
});
it('switches stages correctly and without duplication', () => {
const reportFn = jest.fn();
const onInternalError = jest.fn();
const actionLogCache = new ActionLogCache_1.ActionLogCache({
garbageCollectMs,
debounceMs,
finalStages: ['ready'],
reportFn,
});
const api1 = (0, getExternalApi_1.getExternalApi)({
actionLogCache,
idPrefix: 'test',
placement: 'beacon1',
});
const api2 = (0, getExternalApi_1.getExternalApi)({
actionLogCache,
idPrefix: 'test',
placement: 'beacon2',
});
api1.markStage('1', 'first');
jest.advanceTimersByTime(renderTime);
api1.markStage('1', 'first');
jest.advanceTimersByTime(renderTime);
api1.markStage('1', 'second');
jest.advanceTimersByTime(renderTime);
api2.markStage('1', 'second');
jest.advanceTimersByTime(renderTime);
api2.markStage('1', 'ready');
const actionLog = api1.getActionLogForIdIfExists('1');
expect(actionLog).toBeDefined();
expect(actionLog?.isCapturingData).toBe(true);
// not send yet cause we're waiting for debounce
expect(reportFn).not.toHaveBeenCalled();
expect(actionLog?.getActions()).toHaveLength(3);
expect(actionLog?.getActions()).toMatchInlineSnapshot(`
[
{
"entry": {},
"marker": "point",
"metadata": undefined,
"mountedPlacements": [
"beacon1",
],
"source": "beacon1",
"stage": "first",
"timestamp": 0,
"timingId": "test/1",
"type": "stage-change",
},
{
"entry": {},
"marker": "point",
"metadata": undefined,
"mountedPlacements": [
"beacon1",
],
"source": "beacon1",
"stage": "second",
"timestamp": 200,
"timingId": "test/1",
"type": "stage-change",
},
{
"entry": {},
"marker": "point",
"metadata": undefined,
"mountedPlacements": [
"beacon1",
"beacon2",
],
"source": "beacon2",
"stage": "ready",
"timestamp": 400,
"timingId": "test/1",
"type": "stage-change",
},
]
`);
jest.advanceTimersByTime(debounceMs);
expect(reportFn).toHaveBeenCalledTimes(1);
expect(onInternalError).not.toHaveBeenCalled();
});
it('ignores stages changed before all "waitForBeaconActivation" are active', () => {
const reportFn = jest.fn();
const onInternalError = jest.fn();
const actionLogCache = new ActionLogCache_1.ActionLogCache({
garbageCollectMs,
debounceMs,
timeoutMs,
finalStages: ['ready'],
waitForBeaconActivation: ['beacon2'],
minimumExpectedSimultaneousBeacons: 2,
reportFn,
});
const api1 = (0, getExternalApi_1.getExternalApi)({
actionLogCache,
idPrefix: 'test',
placement: 'beacon1',
});
const api2 = (0, getExternalApi_1.getExternalApi)({
actionLogCache,
idPrefix: 'test',
placement: 'beacon2',
});
// this will be ignored:
api1.markStage('1', 'first');
jest.advanceTimersByTime(renderTime);
// this will be ignored:
api1.markStage('1', 'second');
jest.advanceTimersByTime(renderTime);
const actionLog = api1.getActionLogForIdIfExists('1');
expect(actionLog).toBeDefined();
expect(actionLog?.isCapturingData).toBe(false);
// this will be the first stage change:
api2.markStage('1', 'second');
jest.advanceTimersByTime(renderTime);
api2.markStage('1', 'ready');
expect(actionLog?.isCapturingData).toBe(true);
// not send yet cause we're waiting for debounce
expect(reportFn).not.toHaveBeenCalled();
expect(actionLog?.getActions()).toHaveLength(2);
expect(actionLog?.getActions()).toMatchInlineSnapshot(`
[
{
"entry": {},
"marker": "point",
"metadata": undefined,
"mountedPlacements": [
"beacon1",
"beacon2",
],
"source": "beacon2",
"stage": "second",
"timestamp": 200,
"timingId": "test/1",
"type": "stage-change",
},
{
"entry": {},
"marker": "point",
"metadata": undefined,
"mountedPlacements": [
"beacon1",
"beacon2",
],
"source": "beacon2",
"stage": "ready",
"timestamp": 300,
"timingId": "test/1",
"type": "stage-change",
},
]
`);
jest.advanceTimersByTime(debounceMs);
expect(reportFn).toHaveBeenCalledTimes(1);
expect(onInternalError).not.toHaveBeenCalled();
});
it('does not report when minimumExpectedSimultaneousBeacons is not reached within expected time', () => {
const reportFn = jest.fn();
const onInternalError = jest.fn();
const actionLogCache = new ActionLogCache_1.ActionLogCache({
garbageCollectMs,
debounceMs,
timeoutMs,
waitForBeaconActivation: ['beacon1'],
minimumExpectedSimultaneousBeacons: 2,
reportFn,
onInternalError,
});
const api1 = (0, getExternalApi_1.getExternalApi)({
actionLogCache,
idPrefix: 'test',
placement: 'beacon1',
});
api1.markStage('1', 'first');
jest.advanceTimersByTime(renderTime);
api1.markStage('1', 'second');
jest.advanceTimersByTime(renderTime);
const actionLog = api1.getActionLogForIdIfExists('1');
expect(actionLog).toBeDefined();
expect(actionLog?.isCapturingData).toBe(true);
// not send yet cause we're waiting for debounce
expect(reportFn).not.toHaveBeenCalled();
expect(actionLog?.getActions()).toHaveLength(2);
jest.advanceTimersByTime(debounceMs);
expect(reportFn).not.toHaveBeenCalled();
expect(onInternalError).not.toHaveBeenCalled();
jest.advanceTimersByTime(timeoutMs);
expect(reportFn).not.toHaveBeenCalled();
expect(onInternalError).not.toHaveBeenCalled();
// recover and report after the timeout has passed:
api1.markStage('1', 'third');
jest.advanceTimersByTime(renderTime);
expect(actionLog?.getActions()).toHaveLength(1);
});
it('timeouts when final stage is not reached within expected time', () => {
const reportFn = jest.fn();
const onInternalError = jest.fn();
const actionLogCache = new ActionLogCache_1.ActionLogCache({
garbageCollectMs,
debounceMs,
timeoutMs,
finalStages: ['ready'],
reportFn,
onInternalError,
});
const api1 = (0, getExternalApi_1.getExternalApi)({
actionLogCache,
idPrefix: 'test',
placement: 'beacon1',
});
api1.markStage('1', 'first');
// api1.markRenderStart('1')
const actionLog = api1.getActionLogForIdIfExists('1');
expect(actionLog).toBeDefined();
expect(actionLog?.isCapturingData).toBe(true);
jest.advanceTimersByTime(renderTime);
expect(reportFn).not.toHaveBeenCalled();
expect(actionLog?.getActions()).toHaveLength(1);
jest.advanceTimersByTime(debounceMs);
expect(reportFn).not.toHaveBeenCalled();
expect(onInternalError).not.toHaveBeenCalled();
expect(actionLog?.isCapturingData).toBe(true);
jest.advanceTimersByTime(timeoutMs);
// we stopped capturing after timeout
expect(actionLog?.isCapturingData).toBe(false);
// and sent out a report with the timeout
expect(reportFn).toHaveBeenCalledTimes(1);
expect(onInternalError).not.toHaveBeenCalled();
expect(actionLog?.getActions()).toHaveLength(0);
expect((0, generateReport_1.generateReport)(reportFn.mock.calls[0][0]).spans)
.toMatchInlineSnapshot(`
[
{
"data": {
"dependencyChanges": 0,
"metadata": {},
"mountedPlacements": [
"beacon1",
],
"previousStage": "first",
"source": "timeout",
"stage": "timeout",
"timeToStage": 45000,
"timingId": "test/1",
},
"description": "first to timeout",
"endTime": 45000,
"relativeEndTime": 45000,
"startTime": 0,
"type": "stage-change",
},
]
`);
reportFn.mockClear();
// an entirely new marker after the timeout has passed should work too:
api1.markStage('1', 'ready');
expect(actionLog?.isCapturingData).toBe(true);
expect(actionLog?.getActions()).toHaveLength(1);
expect(actionLog?.getActions()).toMatchInlineSnapshot(`
[
{
"entry": {},
"marker": "point",
"metadata": undefined,
"mountedPlacements": [
"beacon1",
],
"source": "beacon1",
"stage": "ready",
"timestamp": 45600,
"timingId": "test/1",
"type": "stage-change",
},
]
`);
jest.advanceTimersByTime(debounceMs);
// and sent out a report with the ready state
expect(reportFn).toHaveBeenCalledTimes(1);
});
});
//# sourceMappingURL=getExternalApi.test.js.map