UNPKG

@dash0hq/sdk-web

Version:

Dash0's Web SDK to collect telemetry from end-users' web browsers

204 lines (174 loc) 6.84 kB
import { doc, perf, win } from "./globals"; import { now } from "./time"; import { noop } from "./fn"; import { setTimeout } from "./timers"; import { addEventListener, removeEventListener } from "./listeners"; const TEN_MINUTES_IN_MILLIS = 1000 * 60 * 10; const ONE_DAY_IN_MILLIS = 1000 * 60 * 60 * 24; export const isResourceTimingAvailable = !!(perf && perf.getEntriesByType); export const isPerformanceObserverAvailable = perf && typeof (win as any)["PerformanceObserver"] === "function" && typeof perf["now"] === "function"; export const PerformanceTimingNames = Object.freeze({ CONNECT_END: "connectEnd", CONNECT_START: "connectStart", DECODED_BODY_SIZE: "decodedBodySize", DOM_COMPLETE: "domComplete", DOM_CONTENT_LOADED_EVENT_END: "domContentLoadedEventEnd", DOM_CONTENT_LOADED_EVENT_START: "domContentLoadedEventStart", DOM_INTERACTIVE: "domInteractive", DOMAIN_LOOKUP_END: "domainLookupEnd", DOMAIN_LOOKUP_START: "domainLookupStart", ENCODED_BODY_SIZE: "encodedBodySize", FETCH_START: "fetchStart", LOAD_EVENT_END: "loadEventEnd", LOAD_EVENT_START: "loadEventStart", NAVIGATION_START: "navigationStart", REDIRECT_END: "redirectEnd", REDIRECT_START: "redirectStart", REQUEST_START: "requestStart", RESPONSE_END: "responseEnd", RESPONSE_START: "responseStart", SECURE_CONNECTION_START: "secureConnectionStart", START_TIME: "startTime", UNLOAD_EVENT_END: "unloadEventEnd", UNLOAD_EVENT_START: "unloadEventStart", }); export type ObserveResourcePerformanceResult = { duration: number; resource?: PerformanceResourceTiming | null; }; type ObserveResourcePerformanceResultCallback = (arg: ObserveResourcePerformanceResult) => any; type ObserveResourcePerformanceOptions = { resourceMatcher: (arg: PerformanceResourceTiming) => boolean; maxWaitForResourceMillis: number; maxToleranceForResourceTimingsMillis: number; onEnd: ObserveResourcePerformanceResultCallback; }; type PerformanceObservationContoller = { start: () => void; end: () => void; cancel: () => void; }; export function observeResourcePerformance(opts: ObserveResourcePerformanceOptions): PerformanceObservationContoller { if (!isPerformanceObserverAvailable) return observeWithoutPerformanceObserverSupport(opts.onEnd); // Used to calculate the duration when no resource was found. let startTime: number; let endTime: number; let resource: PerformanceResourceTiming | undefined; // Global resources that will need to be disposed let observer: PerformanceObserver | undefined; let fallbackNoResourceFoundTimerHandle: ReturnType<typeof setTimeout> | undefined; let fallbackEndNeverCalledTimerHandle: ReturnType<typeof setTimeout> | undefined; return { start: onStart, end: onEnd, cancel: disposeGlobalResources, }; function onStart() { startTime = perf.now(); try { const PerformanceObserver = win?.PerformanceObserver; if (PerformanceObserver) { observer = new PerformanceObserver(onResourceFound); observer?.observe({ type: "resource" }); } } catch (_e) { // Some browsers may not support the passed entryTypes and decide to throw an error. // This would then result in an error with a message like: // // entryTypes only contained unsupported types // // Swallow and ignore the error. Treat it like unavailable performance observer data. } fallbackEndNeverCalledTimerHandle = setTimeout(disposeGlobalResources, TEN_MINUTES_IN_MILLIS); } function onEnd() { endTime = perf.now(); cancelFallbackEndNeverCalledTimer(); if (resource || !isWaitingAcceptable()) { end(); } else { addEventListener(doc!, "visibilitychange", onVisibilityChanged); fallbackNoResourceFoundTimerHandle = setTimeout(end, opts.maxWaitForResourceMillis); } } function end() { disposeGlobalResources(); // In some old web browsers, e.g. Chrome 31, the value provided as the duration // can be very wrong. We have seen cases where this value is measured in years. // If this does seem be the case, then we will ignore the duration property and // instead prefer our approximation. if (resource?.duration && resource.duration < ONE_DAY_IN_MILLIS) { opts.onEnd({ resource, duration: Math.round(resource.duration) }); } else { opts.onEnd({ resource, duration: Math.round(endTime - startTime) }); } } function onResourceFound(list: PerformanceObserverEntryList) { const r = list.getEntriesByType("resource").find((e) => { // This polymorphism is not properly represented in the api types. The cast is safe since we're only accessing the resource timings. const entry = e as PerformanceResourceTiming; return ( entry.startTime >= startTime && (!endTime || endTime + opts.maxToleranceForResourceTimingsMillis >= entry.responseEnd) && opts.resourceMatcher(entry) ); }); if (!r) return; resource = r as PerformanceResourceTiming; if (endTime) end(); } function onVisibilityChanged() { if (!isWaitingAcceptable()) end(); } function disposeGlobalResources() { disconnectResourceObserver(); cancelFallbackNoResourceFoundTimer(); cancelFallbackEndNeverCalledTimer(); stopVisibilityObservation(); } function disconnectResourceObserver() { if (observer) { try { observer?.disconnect(); } catch (_e) { // Observer disconnect may throw when connect attempt wasn't successful. Ignore this. } observer = undefined; } } function cancelFallbackNoResourceFoundTimer() { if (fallbackNoResourceFoundTimerHandle) { clearTimeout(fallbackNoResourceFoundTimerHandle); fallbackNoResourceFoundTimerHandle = undefined; } } function cancelFallbackEndNeverCalledTimer() { if (fallbackEndNeverCalledTimerHandle) { clearTimeout(fallbackEndNeverCalledTimerHandle); fallbackEndNeverCalledTimerHandle = undefined; } } function stopVisibilityObservation() { if (!doc) return; removeEventListener(doc, "visibilitychange", onVisibilityChanged); } } // We may only wait for resource data to arrive as long as the document is visible or in the process // of becoming visible. In all other cases we might lose data when waiting, e.g. when the document // is in the process of being disposed. function isWaitingAcceptable() { return doc?.visibilityState === "visible" || (doc?.visibilityState as string) === "prerender"; } function observeWithoutPerformanceObserverSupport( onEnd: ObserveResourcePerformanceResultCallback ): PerformanceObservationContoller { let start: number = 0; return { start: () => { start = now(); }, end: () => onEnd({ duration: now() - start }), cancel: noop, }; }