@embrace-io/web-sdk
Version:
232 lines (231 loc) • 11.8 kB
JavaScript
import { KEY_EMB_TYPE } from "../../../constants/attributes.js";
import { EmbraceInstrumentationBase } from "../../EmbraceInstrumentationBase/EmbraceInstrumentationBase.js";
import "./enums/AttributeNames.js";
import { addSpanPerformancePaintEvents, getPerformanceNavigationEntries } from "./utils.js";
import { ROOT_CONTEXT, context, propagation, trace } from "@opentelemetry/api";
import { ATTR_HTTP_RESPONSE_STATUS_CODE } from "@opentelemetry/semantic-conventions";
import { safeExecuteInTheMiddle } from "@opentelemetry/instrumentation";
import { PerformanceTimingNames, addSpanNetworkEvent, addSpanNetworkEvents, hasKey } from "@opentelemetry/sdk-trace-web";
import { ATTR_HTTP_RESPONSE_BODY_SIZE, ATTR_HTTP_RESPONSE_SIZE, ATTR_URL_FULL as ATTR_URL_FULL$1, ATTR_USER_AGENT_ORIGINAL as ATTR_USER_AGENT_ORIGINAL$1 } from "@opentelemetry/semantic-conventions/incubating";
import { TRACE_PARENT_HEADER } from "@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 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") === TRACE_PARENT_HEADER);
const entries = getPerformanceNavigationEntries();
const traceparent = metaElement?.content || "";
context.with(propagation.extract(ROOT_CONTEXT, { traceparent }), () => {
const rootSpan = this._startSpan("documentLoad", PerformanceTimingNames.FETCH_START, entries);
if (!rootSpan) return;
context.with(trace.setSpan(context.active(), rootSpan), () => {
const fetchSpan = this._startSpan("documentFetch", PerformanceTimingNames.FETCH_START, entries);
if (fetchSpan) {
fetchSpan.setAttribute(ATTR_URL_FULL$1, location.href);
context.with(trace.setSpan(context.active(), fetchSpan), () => {
addSpanNetworkEvents(fetchSpan, entries, this.getConfig().ignoreNetworkEvents);
this._addCustomAttributesOnSpan(fetchSpan, this.getConfig().applyCustomAttributesOnSpan?.documentFetch);
this._endSpan(fetchSpan, PerformanceTimingNames.RESPONSE_END, entries);
});
}
});
rootSpan.setAttribute(KEY_EMB_TYPE, "ux.document_load");
rootSpan.setAttribute(ATTR_URL_FULL$1, location.href);
rootSpan.setAttribute(ATTR_USER_AGENT_ORIGINAL$1, navigator.userAgent);
this._addResourcesSpans(rootSpan);
if (!this.getConfig().ignoreNetworkEvents) {
addSpanNetworkEvent(rootSpan, PerformanceTimingNames.FETCH_START, entries);
addSpanNetworkEvent(rootSpan, PerformanceTimingNames.UNLOAD_EVENT_START, entries);
addSpanNetworkEvent(rootSpan, PerformanceTimingNames.UNLOAD_EVENT_END, entries);
addSpanNetworkEvent(rootSpan, PerformanceTimingNames.DOM_INTERACTIVE, entries);
addSpanNetworkEvent(rootSpan, PerformanceTimingNames.DOM_CONTENT_LOADED_EVENT_START, entries);
addSpanNetworkEvent(rootSpan, PerformanceTimingNames.DOM_CONTENT_LOADED_EVENT_END, entries);
addSpanNetworkEvent(rootSpan, PerformanceTimingNames.DOM_COMPLETE, entries);
addSpanNetworkEvent(rootSpan, PerformanceTimingNames.LOAD_EVENT_START, entries);
addSpanNetworkEvent(rootSpan, PerformanceTimingNames.LOAD_EVENT_END, entries);
}
if (!this.getConfig().ignorePerformancePaintEvents) addSpanPerformancePaintEvents(rootSpan, this.perf);
this._addCustomAttributesOnSpan(rootSpan, this.getConfig().applyCustomAttributesOnSpan?.documentLoad);
this._endSpan(rootSpan, 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 (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", PerformanceTimingNames.FETCH_START, resource, parentSpan);
if (!span) return;
span.setAttribute(KEY_EMB_TYPE, "ux.resource_fetch");
span.setAttribute(ATTR_URL_FULL$1, resource.name);
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(ATTR_HTTP_RESPONSE_STATUS_CODE, resource.responseStatus);
if (typeof resource.encodedBodySize === "number" && resource.encodedBodySize >= 0) span.setAttribute(ATTR_HTTP_RESPONSE_BODY_SIZE, resource.encodedBodySize);
if (typeof resource.transferSize === "number" && resource.transferSize >= 0) span.setAttribute(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, 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 (hasKey(entries, performanceName) && typeof entries[performanceName] === "number") return this.tracer.startSpan(spanName, { startTime: this.perf.epochMillisFromOrigin(entries[performanceName]) }, parentSpan ? trace.setSpan(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) 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) 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
export { DocumentLoadInstrumentation };
//# sourceMappingURL=DocumentLoadInstrumentation.js.map