@zendesk/retrace
Version:
define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API
269 lines • 11.9 kB
JavaScript
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateReport = generateReport;
const constants_1 = require("./constants");
const utilities_1 = require("./utilities");
function generateReport({ actions, timingId, isFirstLoad = true, immediateSendReportStages = [], loadingStages = constants_1.DEFAULT_LOADING_STAGES, flushReason = 'auto', measures, }) {
const lastStart = {};
let lastRenderEnd = null;
const timeSpent = {};
let startTime = null;
let endTime = null;
let lastDependencyChange = null;
let dependencyChanges = 0;
const counts = {};
let previousStageEndTime = null;
let previousStage = constants_1.INFORMATIVE_STAGES.INITIAL;
const stageDescriptions = [];
const durations = {};
const hasObserverSupport = (0, utilities_1.getCurrentBrowserSupportForNonResponsiveStateDetection)();
const allImmediateSendReportStages = [
...immediateSendReportStages,
constants_1.INFORMATIVE_STAGES.TIMEOUT,
];
const lastAction = [...actions].reverse()[0];
const includedStages = new Set();
const spans = [];
const markStage = ({ stage, action, }) => {
// guard for the case where the initial stage is customized by the initial render
if (action.timestamp !== startTime) {
includedStages.add(previousStage);
const lastStageTime = previousStageEndTime ?? startTime;
const timeToStage = action.timestamp - lastStageTime;
const lastStageDescription = stageDescriptions[stageDescriptions.length - 1];
if (stage === previousStage && lastStageDescription) {
// since we're still in the same stage (possibly set by a different source this time),
// we just update previous stage description:
lastStageDescription.timeToStage = timeToStage;
lastStageDescription.timestamp = action.timestamp - startTime;
lastStageDescription.metadata = Object.assign(lastStageDescription.metadata ?? {}, action.metadata);
lastStageDescription.mountedPlacements = action.mountedPlacements;
lastStageDescription.timingId = action.timingId;
lastStageDescription.dependencyChanges = dependencyChanges;
}
else if (stage !== previousStage) {
stageDescriptions.push({
type: action.type,
source: action.source,
previousStage,
stage,
timeToStage,
previousStageTimestamp: (lastStageTime ?? 0) - startTime,
timestamp: action.timestamp - startTime,
...(action.metadata
? {
metadata: action.metadata,
}
: {}),
mountedPlacements: action.mountedPlacements,
timingId: action.timingId,
dependencyChanges,
});
}
}
if (stage !== previousStage) {
// update for next time
previousStageEndTime = action.timestamp;
dependencyChanges = 0;
}
includedStages.add(stage);
previousStage = stage;
};
actions.forEach((action, index) => {
if (index === 0) {
startTime = action.timestamp;
previousStageEndTime = action.timestamp;
lastDependencyChange = action.timestamp;
}
else {
endTime = action.timestamp;
}
// eslint-disable-next-line default-case
switch (action.marker) {
case constants_1.MARKER.START: {
// action's start time should never be before overall start time
lastStart[action.source] = Math.max(action.timestamp, startTime ?? 0);
break;
}
case constants_1.MARKER.END: {
if (action.source !== constants_1.OBSERVER_SOURCE)
lastRenderEnd = action.timestamp;
counts[action.source] = (counts[action.source] ?? 0) + 1;
const sourceDurations = durations[action.source] ?? [];
let { duration } = action.entry;
const actionStartTime = action.timestamp - duration;
if (actionStartTime < startTime) {
// correct for the special case where the observer is initialized before the first action
duration -= startTime - actionStartTime;
}
sourceDurations.push(duration);
durations[action.source] = sourceDurations;
timeSpent[action.source] = (timeSpent[action.source] ?? 0) + duration;
spans.push({
type: action.type,
description: action.type === constants_1.ACTION_TYPE.UNRESPONSIVE
? 'unresponsive'
: `<${action.source}> (${sourceDurations.length})`,
startTime: action.timestamp - duration,
endTime: action.timestamp,
relativeEndTime: action.timestamp - (startTime ?? 0),
entry: action.entry,
data: {
mountedPlacements: action.mountedPlacements,
timingId: action.timingId,
source: action.source,
metadata: action.metadata ?? {},
stage: previousStage,
},
});
break;
}
case constants_1.MARKER.POINT: {
if (action.type === constants_1.ACTION_TYPE.DEPENDENCY_CHANGE) {
dependencyChanges++;
spans.push({
type: action.type,
description: 'dependency change',
startTime: lastDependencyChange,
endTime: action.timestamp,
relativeEndTime: action.timestamp - (startTime ?? 0),
entry: action.entry,
data: {
mountedPlacements: action.mountedPlacements,
timingId: action.timingId,
source: action.source,
metadata: action.metadata ?? {},
stage: previousStage,
},
});
lastDependencyChange = action.timestamp;
}
else {
markStage({ stage: action.stage, action });
}
break;
}
}
});
if (!lastRenderEnd)
lastRenderEnd = 0;
const lastTimedEvent = Math.max(lastRenderEnd, previousStageEndTime ?? 0);
const isInCompleteState = Boolean(lastAction && lastAction.type !== constants_1.ACTION_TYPE.STAGE_CHANGE);
const didImmediateSend = allImmediateSendReportStages.includes(previousStage);
spans.push(...stageDescriptions.map(({ type, previousStage: pStage, stage, previousStageTimestamp, timestamp, timeToStage, ...data }) => ({
type,
description: `${pStage} to ${stage}`,
startTime: startTime + previousStageTimestamp,
endTime: startTime + timestamp,
relativeEndTime: timestamp,
data: {
stage,
previousStage: pStage,
timeToStage,
mountedPlacements: data.mountedPlacements,
timingId: data.timingId,
source: data.source,
metadata: data.metadata ?? {},
dependencyChanges: data.dependencyChanges,
},
})));
const tti = startTime !== null &&
endTime !== null &&
hasObserverSupport &&
isInCompleteState &&
!didImmediateSend
? endTime - startTime
: null;
const ttr = startTime !== null && previousStage !== constants_1.INFORMATIVE_STAGES.TIMEOUT
? lastTimedEvent - startTime
: null;
if (lastAction &&
endTime !== null &&
previousStageEndTime !== null &&
previousStage !== constants_1.INFORMATIVE_STAGES.TIMEOUT) {
const lastStageToLastRender = lastRenderEnd - previousStageEndTime;
const lastStageToEnd = endTime - previousStageEndTime;
spans.push({
type: 'ttr',
description: 'render',
startTime: startTime,
endTime: lastRenderEnd,
relativeEndTime: lastRenderEnd - (startTime ?? 0),
entry: measures.ttr,
data: {
mountedPlacements: lastAction.mountedPlacements,
timingId: lastAction.timingId,
stage: isInCompleteState
? constants_1.INFORMATIVE_STAGES.RENDERED
: constants_1.INFORMATIVE_STAGES.INCOMPLETE_RENDER,
previousStage,
timeToStage: lastStageToLastRender,
dependencyChanges,
},
});
if (hasObserverSupport && isInCompleteState && !didImmediateSend) {
const lastRenderToEndTime = endTime - lastRenderEnd;
spans.push({
type: 'tti',
description: 'interactive',
startTime: startTime,
endTime: endTime,
relativeEndTime: endTime - (startTime ?? 0),
entry: measures.tti,
data: {
stage: constants_1.INFORMATIVE_STAGES.INTERACTIVE,
previousStage: constants_1.INFORMATIVE_STAGES.RENDERED,
timeToStage: lastRenderToEndTime,
mountedPlacements: lastAction.mountedPlacements,
timingId: lastAction.timingId,
dependencyChanges: 0,
},
});
}
else if (lastStageToEnd > lastStageToLastRender) {
const difference = lastStageToEnd - lastStageToLastRender;
spans.push({
type: 'render',
description: 'incomplete render',
startTime: lastRenderEnd,
endTime: lastRenderEnd + difference,
relativeEndTime: lastRenderEnd + difference,
data: {
stage: constants_1.INFORMATIVE_STAGES.INCOMPLETE_RENDER,
previousStage: isInCompleteState
? constants_1.INFORMATIVE_STAGES.RENDERED
: constants_1.INFORMATIVE_STAGES.INCOMPLETE_RENDER,
timeToStage: difference,
mountedPlacements: lastAction.mountedPlacements,
timingId: lastAction.timingId,
dependencyChanges: 0,
},
});
}
}
const loadingStagesSpans = Object.values(spans).filter(({ data: { previousStage: pStage } }) => pStage && loadingStages.includes(pStage));
const loadingStagesDuration = loadingStagesSpans.reduce((total, { data: { timeToStage = 0 } }) => total + timeToStage, 0);
return {
id: timingId ?? lastAction?.timingId ?? 'unknown',
tti,
ttr,
isFirstLoad,
lastStage: previousStage,
timeSpent,
counts,
durations,
includedStages: [...includedStages],
handled: isInCompleteState,
hadError: constants_1.ERROR_STAGES.includes(previousStage),
loadingStagesDuration,
spans,
flushReason,
};
}
//# sourceMappingURL=generateReport.js.map