UNPKG

@embrace-io/web-sdk

Version:
232 lines (231 loc) 11.8 kB
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