UNPKG

@zendesk/retrace

Version:

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

289 lines 11 kB
"use strict"; 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