UNPKG

@embrace-io/web-sdk

Version:
1 lines 14.7 kB
{"version":3,"file":"SoftNavigationPerformanceInstrumentation.cjs","names":["EmbraceInstrumentationBase","isEntryTypeSupported","createPerformanceObserver","KEY_EMB_SOFT_NAVIGATION_SPAN_IDS","KEY_EMB_SOFT_NAVIGATION_SPAN_ID_TYPES","KEY_EMB_SOFT_NAVIGATION_LOG_IDS","KEY_EMB_SOFT_NAVIGATION_LOG_ID_TYPES","SOFT_NAVIGATION_SPAN_NAME","KEY_BROWSER_URL_FULL","KEY_EMB_SOFT_NAVIGATION_SOURCE","SOFT_NAVIGATION_SOURCES","KEY_EMB_SOFT_NAVIGATION_NAVIGATION_ID","KEY_EMB_SOFT_NAVIGATION_INTERACTION_ID","KEY_EMB_SOFT_NAVIGATION_START_TIME","KEY_EMB_SOFT_NAVIGATION_DURATION","KEY_EMB_SOFT_NAVIGATION_PAINT_TIME","KEY_EMB_SOFT_NAVIGATION_PRESENTATION_TIME"],"sources":["../../../../src/instrumentations/soft-navigation-performance/SoftNavigationPerformanceInstrumentation/SoftNavigationPerformanceInstrumentation.ts"],"sourcesContent":["/* eslint-disable baseline-js/use-baseline */\nimport type { NavigationHost } from '../../../common/index.ts';\nimport { KEY_BROWSER_URL_FULL } from '../../../constants/index.ts';\nimport type { SignalBuffer } from '../../../processors/utils/SignalBuffer.ts';\nimport {\n createPerformanceObserver,\n isEntryTypeSupported,\n} from '../../../utils/index.ts';\nimport { EmbraceInstrumentationBase } from '../../EmbraceInstrumentationBase/index.ts';\nimport {\n KEY_EMB_SOFT_NAVIGATION_DURATION,\n KEY_EMB_SOFT_NAVIGATION_INTERACTION_ID,\n KEY_EMB_SOFT_NAVIGATION_LOG_ID_TYPES,\n KEY_EMB_SOFT_NAVIGATION_LOG_IDS,\n KEY_EMB_SOFT_NAVIGATION_NAVIGATION_ID,\n KEY_EMB_SOFT_NAVIGATION_PAINT_TIME,\n KEY_EMB_SOFT_NAVIGATION_PRESENTATION_TIME,\n KEY_EMB_SOFT_NAVIGATION_SOURCE,\n KEY_EMB_SOFT_NAVIGATION_SPAN_ID_TYPES,\n KEY_EMB_SOFT_NAVIGATION_SPAN_IDS,\n KEY_EMB_SOFT_NAVIGATION_START_TIME,\n SOFT_NAVIGATION_SOURCES,\n SOFT_NAVIGATION_SPAN_NAME,\n} from './constants.ts';\nimport type {\n PerformanceSoftNavigationTiming,\n SoftNavigationPerformanceInstrumentationArgs,\n} from './types.ts';\n\n/**\n * Returns the click entry whose processing window contains the given navigation\n * timestamp, or null if no match is found.\n *\n * For pushState navigations, currententrychange fires synchronously during the\n * click handler, so the timestamp falls within the click entry's\n * [startTime, startTime + duration] window.\n */\nexport function getNavigationEventTrigger(\n navigationTimestamp: number,\n entry: PerformanceEventTiming,\n): PerformanceEventTiming | null {\n return entry.startTime <= navigationTimestamp &&\n navigationTimestamp <= entry.startTime + entry.duration\n ? entry\n : null;\n}\n\nconst PENDING_NAVIGATION_TTL_MS = 60_000;\n\ntype PendingNavigation = { timestamp: number; url: string };\n\nexport class SoftNavigationPerformanceInstrumentation extends EmbraceInstrumentationBase {\n private _observer: PerformanceObserver | null = null;\n private _eventObserver: PerformanceObserver | null = null;\n private _pendingNavigations: PendingNavigation[] = [];\n private _usePolyfill = false;\n private readonly _navigationHost: NavigationHost;\n private readonly _signalBuffer: SignalBuffer | undefined;\n\n private readonly _handleCurrentEntryChange = (event: Event): void => {\n const url = this._navigationHost.navigation?.currentEntry?.url;\n if (!url) {\n this._diag.debug('currententrychange fired with no URL, skipping');\n return;\n }\n\n this._pendingNavigations.push({ timestamp: event.timeStamp, url });\n };\n\n public constructor({\n diag,\n perf,\n limitManager,\n navigationHost = window as NavigationHost,\n signalBuffer,\n }: SoftNavigationPerformanceInstrumentationArgs = {}) {\n super({\n instrumentationName: 'SoftNavigationPerformanceInstrumentation',\n instrumentationVersion: '1.0.0',\n diag,\n perf,\n limitManager,\n config: {},\n });\n\n this._navigationHost = navigationHost;\n this._signalBuffer = signalBuffer;\n\n if (this._config.enabled) {\n this.enable();\n }\n }\n\n public override enable(): void {\n if (isEntryTypeSupported('soft-navigation')) {\n this._usePolyfill = false;\n super.enable();\n } else if (\n this._navigationHost.navigation &&\n isEntryTypeSupported('event')\n ) {\n this._usePolyfill = true;\n super.enable();\n } else {\n this._diag.debug(\n 'soft-navigation and navigation API not supported, skipping',\n );\n }\n }\n\n public override onEnable(): void {\n if (this._usePolyfill) {\n this._enablePolyfill();\n } else {\n this._enableNative();\n }\n }\n\n private _enableNative(): void {\n if (this._observer) {\n this._observer.disconnect();\n }\n\n this._observer = createPerformanceObserver<PerformanceSoftNavigationTiming>(\n 'soft-navigation',\n (entry) => this._processEntry(entry),\n { diag: this._diag },\n );\n\n if (!this._observer) {\n this._isEnabled = false;\n this._diag.error('failed to enable');\n return;\n }\n }\n\n private _enablePolyfill(): void {\n if (this._eventObserver) {\n this._eventObserver.disconnect();\n }\n\n // biome-ignore lint/style/noNonNullAssertion: enable() is only called when navigationHost.navigation is defined\n const nav = this._navigationHost.navigation!;\n\n nav.addEventListener('currententrychange', this._handleCurrentEntryChange);\n\n // A plain click listener only provides event.timeStamp (= startTime).\n // To correlate a click with the navigation it triggered we need the full [startTime, startTime + duration] window.\n // PerformanceEventTiming is the only API that exposes duration for event processing.\n this._eventObserver = createPerformanceObserver<PerformanceEventTiming>(\n 'event',\n (entry) => this._processClickEntry(entry),\n // SPA routers commit navigations synchronously inside the click handler,\n // so the entry duration is well under the 104ms default threshold\n // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver/observe#durationthreshold\n // 16ms (one frame) is the spec minimum and captures any real interaction.\n { diag: this._diag, durationThreshold: 16 },\n );\n\n if (!this._eventObserver) {\n nav.removeEventListener(\n 'currententrychange',\n this._handleCurrentEntryChange,\n );\n this._isEnabled = false;\n this._diag.error('failed to enable polyfill');\n return;\n }\n }\n\n public onDisable(): void {\n if (this._observer) {\n this._observer.disconnect();\n this._observer = null;\n }\n\n if (this._eventObserver) {\n this._eventObserver.disconnect();\n this._eventObserver = null;\n }\n\n this._navigationHost.navigation?.removeEventListener(\n 'currententrychange',\n this._handleCurrentEntryChange,\n );\n this._pendingNavigations.length = 0;\n }\n\n // A soft-navigation span is only ever built after its window has already\n // closed, so this is the sole opportunity to back-fill it with the ids of\n // the spans and logs that started within that window.\n private _correlationAttributes(\n startTimeEpochMillis: number,\n endTimeEpochMillis: number,\n ): Record<string, string[]> {\n if (!this._signalBuffer) {\n return {};\n }\n\n const { spanIds, spanTypes, logIds, logTypes } =\n this._signalBuffer.collectWindow(\n startTimeEpochMillis,\n endTimeEpochMillis,\n );\n\n return {\n [KEY_EMB_SOFT_NAVIGATION_SPAN_IDS]: spanIds,\n [KEY_EMB_SOFT_NAVIGATION_SPAN_ID_TYPES]: spanTypes,\n [KEY_EMB_SOFT_NAVIGATION_LOG_IDS]: logIds,\n [KEY_EMB_SOFT_NAVIGATION_LOG_ID_TYPES]: logTypes,\n };\n }\n\n private _processEntry(entry: PerformanceSoftNavigationTiming): void {\n if (!this._isEnabled) {\n return;\n }\n\n if (this.limitManager?.limitSoftNavigationEntry()) {\n return;\n }\n\n const startTimeEpochMillis = this.perf.epochMillisFromOrigin(\n entry.startTime,\n );\n const endTimeEpochMillis = this.perf.epochMillisFromOrigin(\n entry.startTime + entry.duration,\n );\n\n const span = this.tracer.startSpan(SOFT_NAVIGATION_SPAN_NAME, {\n startTime: startTimeEpochMillis,\n attributes: {\n [KEY_BROWSER_URL_FULL]: entry.name,\n [KEY_EMB_SOFT_NAVIGATION_SOURCE]:\n SOFT_NAVIGATION_SOURCES.performanceObserver,\n [KEY_EMB_SOFT_NAVIGATION_NAVIGATION_ID]: entry.navigationId,\n [KEY_EMB_SOFT_NAVIGATION_INTERACTION_ID]: entry.interactionId,\n [KEY_EMB_SOFT_NAVIGATION_START_TIME]: this.perf.millisFromZeroTime(\n entry.startTime,\n ),\n [KEY_EMB_SOFT_NAVIGATION_DURATION]: entry.duration,\n [KEY_EMB_SOFT_NAVIGATION_PAINT_TIME]:\n entry.paintTime != null\n ? this.perf.millisFromZeroTime(entry.paintTime)\n : undefined,\n [KEY_EMB_SOFT_NAVIGATION_PRESENTATION_TIME]:\n entry.presentationTime != null\n ? this.perf.millisFromZeroTime(entry.presentationTime)\n : undefined,\n ...this._correlationAttributes(\n startTimeEpochMillis,\n endTimeEpochMillis,\n ),\n },\n });\n span.end(endTimeEpochMillis);\n }\n\n private _processClickEntry(entry: PerformanceEventTiming): void {\n if (!this._isEnabled) {\n return;\n }\n\n if (entry.name !== 'click') {\n return;\n }\n\n // Remove any pending navigations that are older than the TTL. This prevents\n // the pending navigation list from growing indefinitely.\n this._pendingNavigations = this._pendingNavigations.filter(\n ({ timestamp }) =>\n entry.startTime - timestamp < PENDING_NAVIGATION_TTL_MS,\n );\n\n const index = this._pendingNavigations.findIndex(\n ({ timestamp }) => getNavigationEventTrigger(timestamp, entry) !== null,\n );\n\n if (index === -1) {\n return;\n }\n\n const [matched] = this._pendingNavigations.splice(index, 1);\n this._emitPolyfillSpan(entry, matched.timestamp, matched.url);\n }\n\n private _emitPolyfillSpan(\n clickEntry: PerformanceEventTiming,\n navigationTimestamp: number,\n url: string,\n ): void {\n if (!this._isEnabled) {\n return;\n }\n\n if (this.limitManager?.limitSoftNavigationEntry()) {\n return;\n }\n\n const startTimeEpochMillis = this.perf.epochMillisFromOrigin(\n clickEntry.startTime,\n );\n const endTimeEpochMillis =\n this.perf.epochMillisFromOrigin(navigationTimestamp);\n\n const span = this.tracer.startSpan(SOFT_NAVIGATION_SPAN_NAME, {\n startTime: startTimeEpochMillis,\n attributes: {\n [KEY_BROWSER_URL_FULL]: url,\n [KEY_EMB_SOFT_NAVIGATION_SOURCE]: SOFT_NAVIGATION_SOURCES.polyfill,\n [KEY_EMB_SOFT_NAVIGATION_START_TIME]: this.perf.millisFromZeroTime(\n clickEntry.startTime,\n ),\n [KEY_EMB_SOFT_NAVIGATION_DURATION]:\n navigationTimestamp - clickEntry.startTime,\n [KEY_EMB_SOFT_NAVIGATION_INTERACTION_ID]:\n clickEntry.interactionId !== 0 ? clickEntry.interactionId : undefined,\n ...this._correlationAttributes(\n startTimeEpochMillis,\n endTimeEpochMillis,\n ),\n },\n });\n span.end(endTimeEpochMillis);\n }\n}\n"],"mappings":"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"}