@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
JavaScript
;
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