@zendesk/retrace
Version:
define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API
289 lines • 11 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.mapOperationForVisualizationHierarchical = exports.mapOperationForVisualization = void 0;
const ensureTimestamp_1 = require("../v3/ensureTimestamp");
const buildSpanHierarchy_1 = require("./utils/buildSpanHierarchy");
const orderArray = [
'longtask',
'long-animation-frame',
'computed-span',
'component-render',
'hook-render',
'measure',
'resource',
'resource-ember',
'asset',
'iframe',
];
const ASSET_EXTENSIONS = [
'.mp3',
'.mp4',
'.webm',
'.wav',
'.ogg',
'.flac',
'.aac',
'.aiff',
'.wma',
'.m4a',
'.flv',
'.avi',
'.mov',
'.wmv',
'.mpg',
'.mpeg',
'.mkv',
'.jpg',
'.jpeg',
'.png',
'.gif',
'.webp',
'.bmp',
'.tiff',
'.svg',
'.ico',
'.css',
'.scss',
'.less',
'.styl',
'.html',
'.htm',
'.xml',
'.js',
];
const order = Object.fromEntries(orderArray.map((type, idx) => [type, idx]));
const mapOperationForVisualization = (traceRecording, { collapseRenders = true, collapseAssets = true, collapseEmberResources = false, collapseIframes = false, displayResources = true, displayMeasures = true, } = {}) => {
const allEntries = traceRecording.entries;
if (!allEntries)
return null;
const preMappedEntries = allEntries.flatMap((entry) => {
if (entry.span.type === 'component-render-start') {
return [];
}
const mapped = {
span: entry.span,
annotation: entry.annotation,
groupName: entry.span.name,
type: entry.span.type,
};
let overrideGroupName;
let { type } = mapped;
if (type === 'resource') {
const filename = (mapped.span.performanceEntry?.name ?? mapped.span.name)
.split('/')
.at(-1)
?.split('?')
.at(0);
const extension = filename?.split('.').at(-1);
if (filename === '$file' ||
(extension && ASSET_EXTENSIONS.includes(`.${extension}`))) {
overrideGroupName = overrideGroupName ?? extension;
type = 'asset';
}
}
if (collapseRenders && type === 'component-render') {
overrideGroupName = 'renders';
}
if (collapseAssets && type === 'asset') {
overrideGroupName = 'assets';
}
if (collapseIframes && type === 'iframe') {
overrideGroupName = 'iframes';
}
if (type === 'asset' || type === 'iframe') {
overrideGroupName =
overrideGroupName ?? mapped.groupName.split('/').at(-1);
}
if (type === 'measure' &&
(entry.span.name.endsWith('/tti') || entry.span.name.endsWith('/ttr'))) {
// remove suffix from measure name
overrideGroupName = entry.span.name.split('/').slice(0, -1).join('/');
}
if (entry.span.name.startsWith('https://')) {
const shortenedName = entry.span.name.split('zendesk.com').at(-1);
if (mapped.span.attributes?.initiatorType === 'xmlhttprequest') {
overrideGroupName = collapseEmberResources
? 'ember-resource'
: (overrideGroupName ?? shortenedName);
type = 'resource-ember';
}
if (type === 'resource') {
overrideGroupName = overrideGroupName ?? shortenedName;
}
}
return {
...mapped,
overrideGroupName,
type,
};
});
const mappedEntries = preMappedEntries.map((mapped) => {
if (mapped.groupName.startsWith('graphql/')) {
const clientName = mapped.span.attributes?.apolloClientName;
const commonName = mapped.overrideGroupName ?? mapped.groupName;
if (clientName === 'local' && mapped.span.attributes?.feature) {
const { feature } = mapped.span.attributes;
const matchingResourceTask = preMappedEntries.find((t) => t.span.attributes?.feature === feature && t.type === 'resource');
if (matchingResourceTask) {
matchingResourceTask.groupName = commonName;
}
return {
...mapped,
groupName: commonName,
type: 'resource',
};
}
return {
...mapped,
groupName: commonName,
type: 'resource',
};
}
return {
...mapped,
groupName: mapped.overrideGroupName ?? mapped.groupName,
};
});
const entriesFromComputedSpans = Object.entries(traceRecording.computedSpans).map(([name, computedSpan]) => ({
groupName: name,
type: 'computed-span',
span: {
id: `computed-span-${name}-${traceRecording.id}`,
type: 'measure',
duration: computedSpan.duration,
name,
startTime: (0, ensureTimestamp_1.adjustTimestampBy)(traceRecording.startTime, computedSpan.startOffset),
relatedTo: traceRecording.relatedTo,
},
annotation: {
id: traceRecording.id,
occurrence: 1,
operationRelativeStartTime: computedSpan.startOffset,
operationRelativeEndTime: computedSpan.startOffset + computedSpan.duration,
},
}));
// Add computedRenderBeaconSpans as three computed-span entries per beacon
const entriesFromComputedRenderBeaconSpans = Object.entries(traceRecording.computedRenderBeaconSpans || {}).flatMap(([beaconName, beaconSpan]) => [
{
groupName: beaconName,
type: 'computed-span',
span: {
id: `computed-span-${beaconName}-${traceRecording.id}`,
type: 'measure',
duration: beaconSpan.firstRenderTillContent,
name: `${beaconName}/firstRenderTillContent`,
startTime: (0, ensureTimestamp_1.adjustTimestampBy)(traceRecording.startTime, beaconSpan.startOffset),
relatedTo: traceRecording.relatedTo,
},
annotation: {
id: traceRecording.id,
occurrence: 1,
operationRelativeStartTime: beaconSpan.startOffset,
operationRelativeEndTime: beaconSpan.startOffset + beaconSpan.firstRenderTillContent,
},
},
{
groupName: beaconName,
type: 'computed-span',
span: {
id: `computed-span-${beaconName}-${traceRecording.id}`,
type: 'measure',
duration: beaconSpan.firstRenderTillLoading,
name: `${beaconName}/firstRenderTillLoading`,
startTime: (0, ensureTimestamp_1.adjustTimestampBy)(traceRecording.startTime, beaconSpan.startOffset),
relatedTo: traceRecording.relatedTo,
},
annotation: {
id: traceRecording.id,
occurrence: 1,
operationRelativeStartTime: beaconSpan.startOffset,
operationRelativeEndTime: beaconSpan.startOffset + beaconSpan.firstRenderTillLoading,
},
},
{
groupName: beaconName,
type: 'computed-span',
span: {
id: `computed-span-${beaconName}-${traceRecording.id}`,
type: 'measure',
duration: beaconSpan.firstRenderTillData,
name: `${beaconName}/firstRenderTillData`,
startTime: (0, ensureTimestamp_1.adjustTimestampBy)(traceRecording.startTime, beaconSpan.startOffset),
relatedTo: traceRecording.relatedTo,
},
annotation: {
id: traceRecording.id,
occurrence: 1,
operationRelativeStartTime: beaconSpan.startOffset,
operationRelativeEndTime: beaconSpan.startOffset + beaconSpan.firstRenderTillData,
},
},
]);
const entriesWithComputedSpans = [
...mappedEntries,
...entriesFromComputedSpans,
...entriesFromComputedRenderBeaconSpans,
].sort((a, b) => {
const orderA = order[a.type] ?? 100;
const orderB = order[b.type] ?? 100;
return orderA - orderB;
});
const spans = entriesWithComputedSpans
// .filter((task) => task.span.duration > 0)
.filter((task) => (displayResources || task.type !== 'resource') &&
(displayMeasures || task.type !== 'measure'));
// const spanEvents = entriesWithComputedSpans.filter(
// (entry) => entry.span.duration === 0,
// )
const spanTypes = new Set(spans.map((entry) => entry.type));
const uniqueGroups = [...new Set(spans.map((task) => task.groupName))];
return {
name: traceRecording.name,
spans,
uniqueGroups,
spanTypes,
duration: traceRecording.duration ??
traceRecording.entries.at(-1)?.annotation.operationRelativeEndTime ??
0,
};
};
exports.mapOperationForVisualization = mapOperationForVisualization;
/**
* Enhanced version of mapOperationForVisualization that builds hierarchical span structures
*/
const mapOperationForVisualizationHierarchical = (traceRecording, options = {}) => {
// First get the flat mapped operation
const flatOperation = (0, exports.mapOperationForVisualization)(traceRecording, options);
if (!flatOperation)
return null;
// Validate the hierarchy before building it
const validation = (0, buildSpanHierarchy_1.validateSpanHierarchy)(flatOperation.spans);
if (!validation.isValid) {
// eslint-disable-next-line no-console
console.warn('Span hierarchy validation failed:', validation.errors);
// Fall back to flat structure if hierarchy is invalid
return {
...flatOperation,
spans: flatOperation.spans.map((span) => ({
...span,
children: [],
isExpanded: false,
depth: 0,
parentId: span.span.parentSpanId,
})),
expandedSpanIds: new Set(),
};
}
// Build hierarchical structures
const hierarchicalSpans = (0, buildSpanHierarchy_1.buildSpanHierarchy)(flatOperation.spans);
return {
name: flatOperation.name,
spans: hierarchicalSpans,
uniqueGroups: flatOperation.uniqueGroups,
spanTypes: flatOperation.spanTypes,
duration: flatOperation.duration,
expandedSpanIds: new Set(),
};
};
exports.mapOperationForVisualizationHierarchical = mapOperationForVisualizationHierarchical;
//# sourceMappingURL=mapOperationForVisualization.js.map