@embrace-io/web-sdk
Version:
260 lines (259 loc) • 13.5 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const require_utils_getSelector = require("../../../utils/getSelector.cjs");
const require_constants_attributes = require("../../../constants/attributes.cjs");
const require_utils_performanceObserver_performanceObserver = require("../../../utils/performanceObserver/performanceObserver.cjs");
const require_api_page_pageAPI = require("../../../api-page/pageAPI.cjs");
const require_instrumentations_EmbraceInstrumentationBase_EmbraceInstrumentationBase = require("../../EmbraceInstrumentationBase/EmbraceInstrumentationBase.cjs");
const require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes = require("./attributes.cjs");
const require_instrumentations_web_vitals_WebVitalsInstrumentation_constants = require("./constants.cjs");
let _opentelemetry_api_logs = require("@opentelemetry/api-logs");
let _opentelemetry_instrumentation = require("@opentelemetry/instrumentation");
let _opentelemetry_core = require("@opentelemetry/core");
//#region src/instrumentations/web-vitals/WebVitalsInstrumentation/WebVitalsInstrumentation.ts
const roundClamp = (value) => Math.round(Math.max(0, value));
const roundRect = (rect) => ({
x: Math.round(rect?.x ?? 0),
y: Math.round(rect?.y ?? 0),
width: Math.round(rect?.width ?? 0),
height: Math.round(rect?.height ?? 0)
});
const isPrimitiveValue = (value) => {
const type = typeof value;
return type === "number" || type === "string" || type === "boolean";
};
const loafScriptsAttribution = (metric, diag) => {
const attributes = {};
const attribution = metric.attribution;
try {
if (attribution.longAnimationFrameEntries.length > 0) {
const scripts = /* @__PURE__ */ new Map();
for (const entry of attribution.longAnimationFrameEntries) for (const script of entry.scripts) {
let url = script.sourceURL || "(inline)";
if (url.length > 2048) url = `${url.substring(0, require_instrumentations_web_vitals_WebVitalsInstrumentation_constants.MAX_LOAF_SCRIPT_URL_LENGTH)}...`;
const existing = scripts.get(url);
if (existing) {
existing.totalDuration += script.duration;
existing.styleAndLayoutDuration += script.forcedStyleAndLayoutDuration;
existing.count++;
} else scripts.set(url, {
totalDuration: script.duration,
styleAndLayoutDuration: script.forcedStyleAndLayoutDuration,
count: 1
});
}
if (scripts.size > 0) attributes[`${require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_EMB_WEB_VITAL_ATTRIBUTION_PREFIX}loaf_scripts`] = JSON.stringify(Object.fromEntries([...scripts].sort((a, b) => b[1].totalDuration - a[1].totalDuration).slice(0, 250).map(([url, script]) => [url, {
total_duration: Math.round(script.totalDuration),
style_and_layout_duration: Math.round(script.styleAndLayoutDuration),
count: script.count
}])));
}
} catch (e) {
diag.error("error building loaf scripts for INP", e);
}
return attributes;
};
const clsLayoutShiftsAttribution = (metric, diag) => {
const attributes = {};
try {
const entries = metric.entries;
if (entries.length > 0) {
const shifts = entries.slice(0, 50).map((entry) => {
const source = entry.sources[0];
return {
selector: require_utils_getSelector.getSelector(source?.node ?? null),
startRect: roundRect(source?.previousRect),
endRect: roundRect(source?.currentRect)
};
});
const prefix = require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_EMB_WEB_VITAL_ATTRIBUTION_PREFIX;
attributes[`${prefix}clsLayoutShifts`] = JSON.stringify(shifts);
if (entries.length > 50) attributes[`${prefix}clsLayoutShiftsDroppedCount`] = entries.length - 50;
}
} catch (e) {
diag.error("error building CLS layout shifts", e);
}
return attributes;
};
const inpAttribution = (metric, diag) => {
const attributes = {};
try {
const tagName = (metric.entries.find((entry) => entry.target)?.target)?.tagName?.toLowerCase();
if (tagName) attributes[`${require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_EMB_WEB_VITAL_ATTRIBUTION_PREFIX}element_type`] = tagName;
} catch (e) {
diag.error("error building INP element type attribution", e);
}
return attributes;
};
const ttfbSubPartsAttribution = (metric, diag) => {
const attributes = {};
const entry = metric.attribution.navigationEntry;
if (entry) try {
const redirect = roundClamp(entry.redirectEnd - entry.redirectStart);
const domainLookup = roundClamp(entry.domainLookupEnd - entry.domainLookupStart);
const tcpConnection = roundClamp(entry.secureConnectionStart > 0 ? entry.secureConnectionStart - entry.connectStart : entry.connectEnd - entry.connectStart);
const tlsNegotiation = roundClamp(entry.secureConnectionStart > 0 ? entry.connectEnd - entry.secureConnectionStart : 0);
const effectiveResponseStart = Math.max(entry.finalResponseHeadersStart ?? 0, entry.responseStart);
const serverResponse = roundClamp(effectiveResponseStart - entry.requestStart);
const total = Math.round(entry.responseEnd - entry.startTime);
const unattributed = roundClamp(total - redirect - domainLookup - tcpConnection - tlsNegotiation - serverResponse);
const prefix = require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_EMB_WEB_VITAL_ATTRIBUTION_PREFIX;
attributes[`${prefix}redirect`] = redirect;
attributes[`${prefix}domainLookup`] = domainLookup;
attributes[`${prefix}tcpConnection`] = tcpConnection;
attributes[`${prefix}tlsNegotiation`] = tlsNegotiation;
attributes[`${prefix}serverResponse`] = serverResponse;
attributes[`${prefix}unattributed`] = unattributed;
} catch (e) {
diag.error("error computing TTFB timing breakdown", e);
}
else diag.debug("TTFB navigationEntry unavailable, skipping timing breakdown");
return attributes;
};
const lcpElementAttribution = (metric, diag) => {
const attributes = {};
try {
const element = metric.attribution.lcpEntry?.element;
if (element) {
const elementType = element.tagName.toLowerCase();
const elementBoundingRect = JSON.stringify(roundRect(element.getBoundingClientRect()));
const prefix = require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_EMB_WEB_VITAL_ATTRIBUTION_PREFIX;
attributes[`${prefix}elementType`] = elementType;
attributes[`${prefix}elementBoundingRect`] = elementBoundingRect;
}
} catch (e) {
diag.error("error building LCP element attribution", e);
}
return attributes;
};
var WebVitalsInstrumentation = class extends require_instrumentations_EmbraceInstrumentationBase_EmbraceInstrumentationBase.EmbraceInstrumentationBase {
_listeners;
_urlDocument;
_urlAttribution;
_includeRawAttribution;
_softNavsActive;
_pageManager;
_attributedPage = {
INP: void 0,
LCP: void 0,
CLS: void 0,
FCP: void 0,
TTFB: void 0
};
_largestShiftTargetForCLS = null;
_clsMetricId = void 0;
_applyCustomLogRecordData;
_listenersRegistered = false;
constructor({ diag, perf, listeners = require_instrumentations_web_vitals_WebVitalsInstrumentation_constants.WEB_VITALS_ID_TO_LISTENER, urlDocument, urlAttribution = true, includeRawAttribution = true, reportSoftNavs = true, pageManager, applyCustomLogRecordData, ...config } = {}) {
super({
instrumentationName: "WebVitalsInstrumentation",
instrumentationVersion: "1.0.0",
diag,
perf,
config
});
this._listeners = listeners;
this._urlDocument = urlDocument ?? window.document;
this._urlAttribution = urlAttribution;
this._includeRawAttribution = includeRawAttribution;
this._softNavsActive = reportSoftNavs && require_utils_performanceObserver_performanceObserver.isEntryTypeSupported("soft-navigation");
this._pageManager = pageManager ?? require_api_page_pageAPI.page.getPageManager();
this._applyCustomLogRecordData = applyCustomLogRecordData;
if (this._config.enabled !== false) this.enable();
}
onDisable() {
this._diag.debug("WebVitalsInstrumentation disabled, pausing emission");
}
enable() {
if (typeof PerformanceObserver !== "undefined") super.enable();
else this._diag.debug("PerformanceObserver not supported, web vitals will not be collected");
}
onEnable() {
if (this._listenersRegistered) {
this._diag.debug("WebVitalsInstrumentation listeners already registered, resuming emission");
return;
}
this._listenersRegistered = true;
require_instrumentations_web_vitals_WebVitalsInstrumentation_constants.ALL_WEB_VITALS.forEach((name) => {
const reportSoftNavs = name !== "TTFB" && this._softNavsActive;
if (this._urlAttribution) this._listeners[name]?.((metric) => {
if (!this._isEnabled) return;
if (metric.name === "CLS") {
const clsMetric = metric;
const target = clsMetric.attribution.largestShiftTarget;
if (this._clsMetricId !== clsMetric.id) {
this._clsMetricId = clsMetric.id;
this._largestShiftTargetForCLS = target;
this._attributedPage.CLS = this._currentAttributedPage();
} else if (this._largestShiftTargetForCLS !== target) {
this._largestShiftTargetForCLS = target;
this._attributedPage.CLS = this._currentAttributedPage();
}
} else this._attributedPage[metric.name] = this._currentAttributedPage();
}, {
reportAllChanges: true,
reportSoftNavs
});
this._listeners[name]?.((metric) => {
if (!this._isEnabled) return;
this._emitWebVital(metric);
}, { reportSoftNavs });
});
}
_currentAttributedPage() {
return {
fullURL: this._urlDocument.URL,
path: this._pageManager.getCurrentRoute()?.path,
pageID: this._pageManager.getCurrentPageId() ?? void 0,
label: this._pageManager.getPageLabel() ?? void 0
};
}
_getTimeForMetric(metric) {
if (metric.name === "INP" && metric.attribution.interactionTime !== void 0) return this.perf.epochMillisFromOrigin(metric.attribution.interactionTime);
if (metric.name === "CLS") {
const windowStart = metric.entries.length > 0 ? Math.min(...metric.entries.map((entry) => entry.startTime)) : metric.attribution.largestShiftTime;
if (windowStart !== void 0) return this.perf.epochMillisFromOrigin(windowStart);
}
if (metric.name === "TTFB") return this.perf.getZeroTime();
const metricStartTime = metric.entries[metric.entries.length - 1]?.startTime;
if (metricStartTime !== void 0) return this.perf.epochMillisFromOrigin(metricStartTime);
return this.perf.getNowMillis();
}
_emitWebVital(metric) {
const attributedPage = this._attributedPage[metric.name];
const logRecord = {
eventName: require_instrumentations_web_vitals_WebVitalsInstrumentation_constants.WEB_VITAL_EVENT_NAME,
severityNumber: _opentelemetry_api_logs.SeverityNumber.INFO,
attributes: {
[require_constants_attributes.KEY_EMB_TYPE]: "ux.web_vital",
[require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_BROWSER_WEB_VITAL_NAME]: metric.name.toLowerCase(),
[require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_BROWSER_WEB_VITAL_VALUE]: metric.value,
[require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_BROWSER_WEB_VITAL_DELTA]: metric.delta,
[require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_BROWSER_WEB_VITAL_RATING]: metric.rating,
[require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_BROWSER_WEB_VITAL_ID]: metric.id,
[require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_BROWSER_WEB_VITAL_NAVIGATION_TYPE]: metric.navigationType,
...metric.navigationId !== void 0 ? { [require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_BROWSER_WEB_VITAL_NAVIGATION_ID]: metric.navigationId } : {},
...metric.navigationInteractionId !== void 0 ? { [require_instrumentations_web_vitals_WebVitalsInstrumentation_attributes.KEY_BROWSER_WEB_VITAL_INTERACTION_ID]: metric.navigationInteractionId } : {},
...attributedPage ? {
[require_constants_attributes.KEY_BROWSER_URL_FULL]: metric.navigationURL != null && (metric.navigationType === "soft-navigation" || this._softNavsActive) ? metric.navigationURL : attributedPage.fullURL,
[require_constants_attributes.KEY_EMB_PAGE_PATH]: attributedPage.path,
[require_constants_attributes.KEY_EMB_PAGE_ID]: attributedPage.pageID,
[require_constants_attributes.KEY_APP_SURFACE_LABEL]: attributedPage.label
} : {},
...metric.name === "INP" ? loafScriptsAttribution(metric, this._diag) : {},
...metric.name === "INP" ? inpAttribution(metric, this._diag) : {},
...metric.name === "TTFB" ? ttfbSubPartsAttribution(metric, this._diag) : {},
...metric.name === "CLS" ? clsLayoutShiftsAttribution(metric, this._diag) : {},
...metric.name === "LCP" ? lcpElementAttribution(metric, this._diag) : {}
},
body: this._includeRawAttribution && metric.attribution != null ? JSON.stringify(Object.fromEntries(Object.entries(metric.attribution).filter(([, v]) => isPrimitiveValue(v)))) : void 0,
timestamp: (0, _opentelemetry_core.millisToHrTime)(this._getTimeForMetric(metric))
};
if (this._applyCustomLogRecordData) (0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => this._applyCustomLogRecordData?.(logRecord), (error) => {
if (error) this._diag.error("applyCustomLogRecordData hook failed", error);
}, true);
this.logger.emit(logRecord);
}
};
//#endregion
exports.WebVitalsInstrumentation = WebVitalsInstrumentation;
//# sourceMappingURL=WebVitalsInstrumentation.cjs.map