UNPKG

@embrace-io/web-sdk

Version:
158 lines (157 loc) 7.3 kB
import { KEY_BROWSER_URL_FULL } from "../../../constants/attributes.js"; import { createPerformanceObserver, isEntryTypeSupported } from "../../../utils/performanceObserver/performanceObserver.js"; import { KEY_EMB_SOFT_NAVIGATION_DURATION, KEY_EMB_SOFT_NAVIGATION_INTERACTION_ID, KEY_EMB_SOFT_NAVIGATION_LOG_IDS, KEY_EMB_SOFT_NAVIGATION_LOG_ID_TYPES, KEY_EMB_SOFT_NAVIGATION_NAVIGATION_ID, KEY_EMB_SOFT_NAVIGATION_PAINT_TIME, KEY_EMB_SOFT_NAVIGATION_PRESENTATION_TIME, KEY_EMB_SOFT_NAVIGATION_SOURCE, KEY_EMB_SOFT_NAVIGATION_SPAN_IDS, KEY_EMB_SOFT_NAVIGATION_SPAN_ID_TYPES, KEY_EMB_SOFT_NAVIGATION_START_TIME, SOFT_NAVIGATION_SOURCES, SOFT_NAVIGATION_SPAN_NAME } from "./constants.js"; import { EmbraceInstrumentationBase } from "../../EmbraceInstrumentationBase/EmbraceInstrumentationBase.js"; //#region src/instrumentations/soft-navigation-performance/SoftNavigationPerformanceInstrumentation/SoftNavigationPerformanceInstrumentation.ts /** * Returns the click entry whose processing window contains the given navigation * timestamp, or null if no match is found. * * For pushState navigations, currententrychange fires synchronously during the * click handler, so the timestamp falls within the click entry's * [startTime, startTime + duration] window. */ function getNavigationEventTrigger(navigationTimestamp, entry) { return entry.startTime <= navigationTimestamp && navigationTimestamp <= entry.startTime + entry.duration ? entry : null; } const PENDING_NAVIGATION_TTL_MS = 6e4; var SoftNavigationPerformanceInstrumentation = class extends EmbraceInstrumentationBase { _observer = null; _eventObserver = null; _pendingNavigations = []; _usePolyfill = false; _navigationHost; _signalBuffer; _handleCurrentEntryChange = (event) => { const url = this._navigationHost.navigation?.currentEntry?.url; if (!url) { this._diag.debug("currententrychange fired with no URL, skipping"); return; } this._pendingNavigations.push({ timestamp: event.timeStamp, url }); }; constructor({ diag, perf, limitManager, navigationHost = window, signalBuffer } = {}) { super({ instrumentationName: "SoftNavigationPerformanceInstrumentation", instrumentationVersion: "1.0.0", diag, perf, limitManager, config: {} }); this._navigationHost = navigationHost; this._signalBuffer = signalBuffer; if (this._config.enabled) this.enable(); } enable() { if (isEntryTypeSupported("soft-navigation")) { this._usePolyfill = false; super.enable(); } else if (this._navigationHost.navigation && isEntryTypeSupported("event")) { this._usePolyfill = true; super.enable(); } else this._diag.debug("soft-navigation and navigation API not supported, skipping"); } onEnable() { if (this._usePolyfill) this._enablePolyfill(); else this._enableNative(); } _enableNative() { if (this._observer) this._observer.disconnect(); this._observer = createPerformanceObserver("soft-navigation", (entry) => this._processEntry(entry), { diag: this._diag }); if (!this._observer) { this._isEnabled = false; this._diag.error("failed to enable"); return; } } _enablePolyfill() { if (this._eventObserver) this._eventObserver.disconnect(); const nav = this._navigationHost.navigation; nav.addEventListener("currententrychange", this._handleCurrentEntryChange); this._eventObserver = createPerformanceObserver("event", (entry) => this._processClickEntry(entry), { diag: this._diag, durationThreshold: 16 }); if (!this._eventObserver) { nav.removeEventListener("currententrychange", this._handleCurrentEntryChange); this._isEnabled = false; this._diag.error("failed to enable polyfill"); return; } } onDisable() { if (this._observer) { this._observer.disconnect(); this._observer = null; } if (this._eventObserver) { this._eventObserver.disconnect(); this._eventObserver = null; } this._navigationHost.navigation?.removeEventListener("currententrychange", this._handleCurrentEntryChange); this._pendingNavigations.length = 0; } _correlationAttributes(startTimeEpochMillis, endTimeEpochMillis) { if (!this._signalBuffer) return {}; const { spanIds, spanTypes, logIds, logTypes } = this._signalBuffer.collectWindow(startTimeEpochMillis, endTimeEpochMillis); return { [KEY_EMB_SOFT_NAVIGATION_SPAN_IDS]: spanIds, [KEY_EMB_SOFT_NAVIGATION_SPAN_ID_TYPES]: spanTypes, [KEY_EMB_SOFT_NAVIGATION_LOG_IDS]: logIds, [KEY_EMB_SOFT_NAVIGATION_LOG_ID_TYPES]: logTypes }; } _processEntry(entry) { if (!this._isEnabled) return; if (this.limitManager?.limitSoftNavigationEntry()) return; const startTimeEpochMillis = this.perf.epochMillisFromOrigin(entry.startTime); const endTimeEpochMillis = this.perf.epochMillisFromOrigin(entry.startTime + entry.duration); this.tracer.startSpan(SOFT_NAVIGATION_SPAN_NAME, { startTime: startTimeEpochMillis, attributes: { [KEY_BROWSER_URL_FULL]: entry.name, [KEY_EMB_SOFT_NAVIGATION_SOURCE]: SOFT_NAVIGATION_SOURCES.performanceObserver, [KEY_EMB_SOFT_NAVIGATION_NAVIGATION_ID]: entry.navigationId, [KEY_EMB_SOFT_NAVIGATION_INTERACTION_ID]: entry.interactionId, [KEY_EMB_SOFT_NAVIGATION_START_TIME]: this.perf.millisFromZeroTime(entry.startTime), [KEY_EMB_SOFT_NAVIGATION_DURATION]: entry.duration, [KEY_EMB_SOFT_NAVIGATION_PAINT_TIME]: entry.paintTime != null ? this.perf.millisFromZeroTime(entry.paintTime) : void 0, [KEY_EMB_SOFT_NAVIGATION_PRESENTATION_TIME]: entry.presentationTime != null ? this.perf.millisFromZeroTime(entry.presentationTime) : void 0, ...this._correlationAttributes(startTimeEpochMillis, endTimeEpochMillis) } }).end(endTimeEpochMillis); } _processClickEntry(entry) { if (!this._isEnabled) return; if (entry.name !== "click") return; this._pendingNavigations = this._pendingNavigations.filter(({ timestamp }) => entry.startTime - timestamp < PENDING_NAVIGATION_TTL_MS); const index = this._pendingNavigations.findIndex(({ timestamp }) => getNavigationEventTrigger(timestamp, entry) !== null); if (index === -1) return; const [matched] = this._pendingNavigations.splice(index, 1); this._emitPolyfillSpan(entry, matched.timestamp, matched.url); } _emitPolyfillSpan(clickEntry, navigationTimestamp, url) { if (!this._isEnabled) return; if (this.limitManager?.limitSoftNavigationEntry()) return; const startTimeEpochMillis = this.perf.epochMillisFromOrigin(clickEntry.startTime); const endTimeEpochMillis = this.perf.epochMillisFromOrigin(navigationTimestamp); this.tracer.startSpan(SOFT_NAVIGATION_SPAN_NAME, { startTime: startTimeEpochMillis, attributes: { [KEY_BROWSER_URL_FULL]: url, [KEY_EMB_SOFT_NAVIGATION_SOURCE]: SOFT_NAVIGATION_SOURCES.polyfill, [KEY_EMB_SOFT_NAVIGATION_START_TIME]: this.perf.millisFromZeroTime(clickEntry.startTime), [KEY_EMB_SOFT_NAVIGATION_DURATION]: navigationTimestamp - clickEntry.startTime, [KEY_EMB_SOFT_NAVIGATION_INTERACTION_ID]: clickEntry.interactionId !== 0 ? clickEntry.interactionId : void 0, ...this._correlationAttributes(startTimeEpochMillis, endTimeEpochMillis) } }).end(endTimeEpochMillis); } }; //#endregion export { SoftNavigationPerformanceInstrumentation, getNavigationEventTrigger }; //# sourceMappingURL=SoftNavigationPerformanceInstrumentation.js.map