@embrace-io/web-sdk
Version:
233 lines (232 loc) • 13.6 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const require_constants_attributes = require("../../../constants/attributes.cjs");
const require_instrumentations_EmbraceInstrumentationBase_EmbraceInstrumentationBase = require("../../EmbraceInstrumentationBase/EmbraceInstrumentationBase.cjs");
require("./enums/AttributeNames.cjs");
const require_instrumentations_document_load_DocumentLoadInstrumentation_utils = require("./utils.cjs");
let _opentelemetry_api = require("@opentelemetry/api");
let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
let _opentelemetry_instrumentation = require("@opentelemetry/instrumentation");
let _opentelemetry_sdk_trace_web = require("@opentelemetry/sdk-trace-web");
let _opentelemetry_semantic_conventions_incubating = require("@opentelemetry/semantic-conventions/incubating");
let _opentelemetry_core = require("@opentelemetry/core");
//#region src/instrumentations/document-load/DocumentLoadInstrumentation/DocumentLoadInstrumentation.ts
const ATTR_HTTP_RESPONSE_DELIVERY_TYPE = "http.response.delivery_type";
const ATTR_HTTP_RESPONSE_DECODED_BODY_SIZE = "http.response.decoded_body_size";
const ATTR_HTTP_REQUEST_INITIATOR_TYPE = "http.request.initiator_type";
const ATTR_HTTP_REQUEST_RENDER_BLOCKING_STATUS = "http.request.render_blocking_status";
const ATTR_HTTP_RESPONSE_CORS_OPAQUE = "http.response.cors_opaque";
const ATTR_HTTP_RESPONSE_CACHE_REVALIDATED = "http.response.cache_revalidated";
const ATTR_HTTP_REQUEST_INCOMPLETE = "http.request.incomplete";
const ATTR_HTTP_REQUEST_PREVENTED = "http.request.prevented";
var DocumentLoadInstrumentation = class extends require_instrumentations_EmbraceInstrumentationBase_EmbraceInstrumentationBase.EmbraceInstrumentationBase {
_onDocumentLoaded;
_performanceCollected = false;
constructor({ diag, perf, enabled, applyCustomAttributesOnSpan, ignorePerformancePaintEvents = false, ignoreNetworkEvents = false } = {}) {
super({
instrumentationName: "DocumentLoadInstrumentation",
instrumentationVersion: "1.0.0",
diag,
perf,
config: {
enabled,
applyCustomAttributesOnSpan,
ignorePerformancePaintEvents,
ignoreNetworkEvents
}
});
this._onDocumentLoaded = () => {
window.setTimeout(() => {
this._collectPerformance();
}, 0);
};
if (this._config.enabled) this.enable();
}
init() {
this._diag.debug("Initializing document load instrumentation");
}
/**
* Adds spans for all resources
* @param rootSpan
*/
_addResourcesSpans(rootSpan) {
performance.getEntriesByType("resource").forEach((resource) => {
this._initResourceSpan(resource, rootSpan);
});
}
/**
* Collects information about performance and creates appropriate spans
*/
_collectPerformance() {
if (this._performanceCollected) return;
this._performanceCollected = true;
const metaElement = Array.from(document.getElementsByTagName("meta")).find((e) => e.getAttribute("name") === _opentelemetry_core.TRACE_PARENT_HEADER);
const entries = require_instrumentations_document_load_DocumentLoadInstrumentation_utils.getPerformanceNavigationEntries();
const traceparent = metaElement?.content || "";
_opentelemetry_api.context.with(_opentelemetry_api.propagation.extract(_opentelemetry_api.ROOT_CONTEXT, { traceparent }), () => {
const rootSpan = this._startSpan("documentLoad", _opentelemetry_sdk_trace_web.PerformanceTimingNames.FETCH_START, entries);
if (!rootSpan) return;
_opentelemetry_api.context.with(_opentelemetry_api.trace.setSpan(_opentelemetry_api.context.active(), rootSpan), () => {
const fetchSpan = this._startSpan("documentFetch", _opentelemetry_sdk_trace_web.PerformanceTimingNames.FETCH_START, entries);
if (fetchSpan) {
fetchSpan.setAttribute(_opentelemetry_semantic_conventions_incubating.ATTR_URL_FULL, location.href);
_opentelemetry_api.context.with(_opentelemetry_api.trace.setSpan(_opentelemetry_api.context.active(), fetchSpan), () => {
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvents)(fetchSpan, entries, this.getConfig().ignoreNetworkEvents);
this._addCustomAttributesOnSpan(fetchSpan, this.getConfig().applyCustomAttributesOnSpan?.documentFetch);
this._endSpan(fetchSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.RESPONSE_END, entries);
});
}
});
rootSpan.setAttribute(require_constants_attributes.KEY_EMB_TYPE, "ux.document_load");
rootSpan.setAttribute(_opentelemetry_semantic_conventions_incubating.ATTR_URL_FULL, location.href);
rootSpan.setAttribute(_opentelemetry_semantic_conventions_incubating.ATTR_USER_AGENT_ORIGINAL, navigator.userAgent);
this._addResourcesSpans(rootSpan);
if (!this.getConfig().ignoreNetworkEvents) {
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvent)(rootSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.FETCH_START, entries);
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvent)(rootSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.UNLOAD_EVENT_START, entries);
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvent)(rootSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.UNLOAD_EVENT_END, entries);
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvent)(rootSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.DOM_INTERACTIVE, entries);
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvent)(rootSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.DOM_CONTENT_LOADED_EVENT_START, entries);
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvent)(rootSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.DOM_CONTENT_LOADED_EVENT_END, entries);
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvent)(rootSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.DOM_COMPLETE, entries);
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvent)(rootSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.LOAD_EVENT_START, entries);
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvent)(rootSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.LOAD_EVENT_END, entries);
}
if (!this.getConfig().ignorePerformancePaintEvents) require_instrumentations_document_load_DocumentLoadInstrumentation_utils.addSpanPerformancePaintEvents(rootSpan, this.perf);
this._addCustomAttributesOnSpan(rootSpan, this.getConfig().applyCustomAttributesOnSpan?.documentLoad);
this._endSpan(rootSpan, _opentelemetry_sdk_trace_web.PerformanceTimingNames.LOAD_EVENT_END, entries);
});
}
/**
* Helper function for ending a span
* @param span
* @param performanceName name of performance entry for end time
* @param entries
*/
_endSpan(span, performanceName, entries) {
if (span) if ((0, _opentelemetry_sdk_trace_web.hasKey)(entries, performanceName) && typeof entries[performanceName] === "number") span.end(this.perf.epochMillisFromOrigin(entries[performanceName]));
else span.end();
}
/**
* Creates and ends a span with network information about a resource added as timed events
* @param resource
* @param parentSpan
*/
_initResourceSpan(resource, parentSpan) {
const span = this._startSpan("resourceFetch", _opentelemetry_sdk_trace_web.PerformanceTimingNames.FETCH_START, resource, parentSpan);
if (!span) return;
span.setAttribute(require_constants_attributes.KEY_EMB_TYPE, "ux.resource_fetch");
span.setAttribute(_opentelemetry_semantic_conventions_incubating.ATTR_URL_FULL, resource.name);
(0, _opentelemetry_sdk_trace_web.addSpanNetworkEvents)(span, resource, this.getConfig().ignoreNetworkEvents);
if (resource.deliveryType) span.setAttribute(ATTR_HTTP_RESPONSE_DELIVERY_TYPE, resource.deliveryType);
if (resource.initiatorType) span.setAttribute(ATTR_HTTP_REQUEST_INITIATOR_TYPE, resource.initiatorType);
if (resource.renderBlockingStatus) span.setAttribute(ATTR_HTTP_REQUEST_RENDER_BLOCKING_STATUS, resource.renderBlockingStatus);
if (resource.responseStatus) span.setAttribute(_opentelemetry_semantic_conventions.ATTR_HTTP_RESPONSE_STATUS_CODE, resource.responseStatus);
if (typeof resource.encodedBodySize === "number" && resource.encodedBodySize >= 0) span.setAttribute(_opentelemetry_semantic_conventions_incubating.ATTR_HTTP_RESPONSE_BODY_SIZE, resource.encodedBodySize);
if (typeof resource.transferSize === "number" && resource.transferSize >= 0) span.setAttribute(_opentelemetry_semantic_conventions_incubating.ATTR_HTTP_RESPONSE_SIZE, resource.transferSize);
if (typeof resource.decodedBodySize === "number" && resource.decodedBodySize >= 0) span.setAttribute(ATTR_HTTP_RESPONSE_DECODED_BODY_SIZE, resource.decodedBodySize);
this._addResourceDiagnosticAttributes(span, resource);
this._addCustomAttributesOnResourceSpan(span, resource, this.getConfig().applyCustomAttributesOnSpan?.resourceFetch);
this._endSpan(span, _opentelemetry_sdk_trace_web.PerformanceTimingNames.RESPONSE_END, resource);
}
/**
* Helper function for starting a span
* @param spanName name of span
* @param performanceName name of performance entry for time start
* @param entries
* @param parentSpan
*/
_startSpan(spanName, performanceName, entries, parentSpan) {
if ((0, _opentelemetry_sdk_trace_web.hasKey)(entries, performanceName) && typeof entries[performanceName] === "number") return this.tracer.startSpan(spanName, { startTime: this.perf.epochMillisFromOrigin(entries[performanceName]) }, parentSpan ? _opentelemetry_api.trace.setSpan(_opentelemetry_api.context.active(), parentSpan) : void 0);
}
/**
* Executes callback {_onDocumentLoaded} when the page is loaded
*/
_waitForPageLoad() {
if (window.document.readyState === "complete") this._onDocumentLoaded();
else window.addEventListener("load", this._onDocumentLoaded);
}
/**
* Adds custom attributes to span if configured
* Used for both documentFetch and documentLoad spans
*/
_addCustomAttributesOnSpan(span, applyCustomAttributesOnSpan) {
if (applyCustomAttributesOnSpan) (0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => {
applyCustomAttributesOnSpan(span);
}, (error) => {
if (!error) return;
this._diag.error("addCustomAttributesOnSpan", error);
}, true);
}
/**
* Adds custom attributes to resource span if configured
*/
_addCustomAttributesOnResourceSpan(span, resource, applyCustomAttributesOnSpan) {
if (applyCustomAttributesOnSpan) (0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => {
applyCustomAttributesOnSpan(span, resource);
}, (error) => {
if (!error) return;
this._diag.error("addCustomAttributesOnResourceSpan", error);
}, true);
}
_hasNoSizeData(resource) {
const transferSize = typeof resource.transferSize === "number" ? resource.transferSize : 0;
const decodedBodySize = typeof resource.decodedBodySize === "number" ? resource.decodedBodySize : 0;
const encodedBodySize = typeof resource.encodedBodySize === "number" ? resource.encodedBodySize : 0;
return transferSize === 0 && decodedBodySize === 0 && encodedBodySize === 0;
}
_hasTimingData(resource) {
const fetchStart = typeof resource.fetchStart === "number" ? resource.fetchStart : 0;
const responseEnd = typeof resource.responseEnd === "number" ? resource.responseEnd : 0;
return fetchStart > 0 && responseEnd > 0;
}
_isCorsRestricted(resource) {
return this._hasNoSizeData(resource) && this._hasTimingData(resource);
}
_isFetchIncomplete(resource) {
const fetchStart = typeof resource.fetchStart === "number" ? resource.fetchStart : 0;
const responseEnd = typeof resource.responseEnd === "number" ? resource.responseEnd : 0;
return this._hasNoSizeData(resource) && fetchStart > 0 && responseEnd === 0;
}
_isFetchPrevented(resource) {
const fetchStart = typeof resource.fetchStart === "number" ? resource.fetchStart : 0;
return this._hasNoSizeData(resource) && fetchStart === 0;
}
/**
* Detect cache validation (304 Not Modified responses)
*
* 304 responses show transferSize of ~300 bytes (headers only, no body).
* Use deliveryType to distinguish from cache hits: 'cache' = no network, otherwise = 304.
*
* Spec: https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-transfersize
*/
_isCacheValidated(resource) {
const transferSize = typeof resource.transferSize === "number" ? resource.transferSize : 0;
const deliveryType = typeof resource.deliveryType === "string" ? resource.deliveryType : "";
return transferSize === 300 && deliveryType !== "cache";
}
/**
* Add diagnostic attributes to identify resource loading issues
*
* Diagnostic attributes help identify why resources may have incomplete timing data:
* - CORS restrictions (opaque responses without Timing-Allow-Origin header)
* - Cache revalidation (304 Not Modified responses)
* - Request incomplete (started but didn't complete - network error, aborted)
* - Request prevented (never started - blocked by CSP, browser, extension)
*/
_addResourceDiagnosticAttributes(span, resource) {
if (this._isCorsRestricted(resource)) span.setAttribute(ATTR_HTTP_RESPONSE_CORS_OPAQUE, true);
else if (this._isFetchIncomplete(resource)) span.setAttribute(ATTR_HTTP_REQUEST_INCOMPLETE, true);
else if (this._isFetchPrevented(resource)) span.setAttribute(ATTR_HTTP_REQUEST_PREVENTED, true);
if (this._isCacheValidated(resource)) span.setAttribute(ATTR_HTTP_RESPONSE_CACHE_REVALIDATED, true);
}
onEnable() {
window.removeEventListener("load", this._onDocumentLoaded);
this._waitForPageLoad();
}
onDisable() {
window.removeEventListener("load", this._onDocumentLoaded);
}
};
//#endregion
exports.DocumentLoadInstrumentation = DocumentLoadInstrumentation;
//# sourceMappingURL=DocumentLoadInstrumentation.cjs.map