UNPKG

@zendesk/retrace

Version:

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

180 lines 7.64 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultEmbedSpanSelector = void 0; exports.isRenderEntry = isRenderEntry; exports.getSpanSummaryAttributes = getSpanSummaryAttributes; exports.findLongestSpan = findLongestSpan; exports.convertTraceToRUM = convertTraceToRUM; /* eslint-disable @typescript-eslint/consistent-indexed-object-style */ const getSpanKey_1 = require("./getSpanKey"); function isRenderEntry(span) { return (span.type === 'component-render' || span.type === 'component-render-start' || span.type === 'component-unmount'); } function updateEmbeddedEntry(embeddedEntry, spanAndAnnotation) { const { annotation, span } = spanAndAnnotation; return { count: embeddedEntry.count + 1, totalDuration: embeddedEntry.totalDuration + span.duration, spans: [ ...embeddedEntry.spans, { startOffset: annotation.operationRelativeStartTime, duration: span.duration, }, ], }; } function createEmbeddedEntry({ span, annotation, }) { return { count: 1, totalDuration: span.duration, spans: [ { startOffset: annotation.operationRelativeStartTime, duration: span.duration, }, ], }; } const defaultEmbedSpanSelector = (spanAndAnnotation) => { const { span } = spanAndAnnotation; return isRenderEntry(span); }; exports.defaultEmbedSpanSelector = defaultEmbedSpanSelector; function getSpanSummaryAttributes(recordedItems) { // loop through recorded items, create a entry based on the name const spanAttributes = {}; for (const { span } of recordedItems) { const { attributes, name } = span; const existingAttributes = spanAttributes[name] ?? {}; if (attributes && Object.keys(attributes).length > 0) { spanAttributes[name] = { ...existingAttributes, ...attributes, }; } } return spanAttributes; } function findLongestSpan(spanAndAnnotations, filter) { const filteredSpans = filter ? spanAndAnnotations.filter(filter) : spanAndAnnotations; if (filteredSpans.length === 0) { return undefined; } let longestSpanAndAnnotation = filteredSpans[0]; let maxDuration = filteredSpans[0].span.duration; for (const spanAndAnnotation of filteredSpans) { if (spanAndAnnotation.span.duration > maxDuration) { maxDuration = spanAndAnnotation.span.duration; longestSpanAndAnnotation = spanAndAnnotation; } } return longestSpanAndAnnotation; } function recursivelyRoundValues(obj, roundFunc = (x) => Math.round(x)) { const result = {}; for (const [key, value] of Object.entries(obj)) { if (typeof value === 'number') { result[key] = roundFunc(value); } else if (Array.isArray(value)) { result[key] = value.map((item) => typeof item === 'number' ? roundFunc(item) : // Keep strings intact - don't process them typeof item === 'string' ? item : recursivelyRoundValues(item, roundFunc)); } else if (value && typeof value === 'object') { result[key] = recursivelyRoundValues(value, roundFunc); } else { result[key] = value; } } return result; } function filterRenderSpanAttributes(computedRenderBeaconSpans, keepComputedRenderBeaconSpanAttributes) { let computedRenderBeaconSpansTransformed = {}; if (keepComputedRenderBeaconSpanAttributes !== undefined) { computedRenderBeaconSpansTransformed = Object.fromEntries(Object.entries(computedRenderBeaconSpans).map(([key, { attributes, ...span }]) => { if (keepComputedRenderBeaconSpanAttributes === false || !attributes) { return [key, { ...span }]; } const filteredAttributes = {}; for (const attr of keepComputedRenderBeaconSpanAttributes) { if (attr in attributes) { filteredAttributes[attr] = attributes[attr]; } } return [key, { ...span, attributes: filteredAttributes }]; })); } return computedRenderBeaconSpansTransformed; } function convertTraceToRUM({ traceRecording, context, embedSpanSelector = exports.defaultEmbedSpanSelector, keepComputedRenderBeaconSpanAttributes, }) { const { entries, computedRenderBeaconSpans, ...otherTraceRecordingAttributes } = traceRecording; const embeddedEntries = []; const nonEmbeddedSpans = new Set(); const spanAttributes = getSpanSummaryAttributes(traceRecording.entries); const childOperations = {}; for (const spanAndAnnotation of entries) { if (spanAndAnnotation.span.type === 'operation') { // note: if there were multiple child operations of the same name, // we will only keep the last one, as they are keyed by name childOperations[spanAndAnnotation.span.name] = { ...spanAndAnnotation.span, computedRenderBeaconSpans: filterRenderSpanAttributes(spanAndAnnotation.span.computedRenderBeaconSpans, keepComputedRenderBeaconSpanAttributes), operationRelativeStartTime: spanAndAnnotation.annotation.operationRelativeStartTime, operationRelativeEndTime: spanAndAnnotation.annotation.operationRelativeEndTime, }; } const isEmbedded = embedSpanSelector(spanAndAnnotation, context); if (isEmbedded) { embeddedEntries.push(spanAndAnnotation); } else { nonEmbeddedSpans.add((0, getSpanKey_1.getSpanKey)(spanAndAnnotation.span)); } } const embeddedSpans = new Map(); for (const spanAndAnnotation of embeddedEntries) { const { span } = spanAndAnnotation; const typeAndName = (0, getSpanKey_1.getSpanKey)(span); const existingEmbeddedEntry = embeddedSpans.get(typeAndName); if (existingEmbeddedEntry) { embeddedSpans.set(typeAndName, updateEmbeddedEntry(existingEmbeddedEntry, spanAndAnnotation)); } else { embeddedSpans.set(typeAndName, createEmbeddedEntry(spanAndAnnotation)); } } // Filter out entries with zero duration for (const [key, value] of embeddedSpans) { if (value.totalDuration === 0) { embeddedSpans.delete(key); } } const computedRenderBeaconSpansTransformed = filterRenderSpanAttributes(computedRenderBeaconSpans, keepComputedRenderBeaconSpanAttributes); const longestSpanAndAnnotation = findLongestSpan(entries, ({ span }) => span.type !== 'operation'); const result = { ...otherTraceRecordingAttributes, computedRenderBeaconSpans: computedRenderBeaconSpansTransformed, embeddedSpans: Object.fromEntries(embeddedSpans), nonEmbeddedSpans: [...nonEmbeddedSpans], spanAttributes, // this can be used to create a query like "list top 10 longest spans" for an operation longestSpan: longestSpanAndAnnotation && { ...longestSpanAndAnnotation, key: (0, getSpanKey_1.getSpanKey)(longestSpanAndAnnotation.span), }, childOperations, }; // we want to decrease precision to improve readability of the output, and decrease the payload size return recursivelyRoundValues(result); } //# sourceMappingURL=convertToRum.js.map