@embrace-io/web-sdk
Version:
158 lines (157 loc) • 7.3 kB
JavaScript
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