Merge branch 'dev' into tracker-fetch-priority-low

This commit is contained in:
Mike Cao authored and GitHub committed 2026-04-04 17:12:29 -04:00
commit f8f86d72aa
1360 files changed
+67072 -141449

No files matched your search

+1 -1
View File
@@ -148,6 +148,6 @@ export type UmamiTracker = {
};
};
interface Window {
export interface Window {
umami: UmamiTracker;
}
+194 -16
View File
@@ -1,4 +1,7 @@
(window => {
if (window.__umami_tracker_loaded) return;
window.__umami_tracker_loaded = true;
const {
screen: { width, height },
navigator: { language, doNotTrack: ndnt, msDoNotTrack: msdnt },
@@ -12,28 +15,39 @@
if (!currentScript) return;
const { hostname, href, origin } = location;
const localStorage = href.startsWith('data:') ? undefined : window.localStorage;
let localStorage;
try {
localStorage = href.startsWith('data:') ? undefined : window.localStorage;
} catch {
/* (DOMException) SecurityError: Access is denied for this document. */
}
const _data = 'data-';
const _false = 'false';
const _true = 'true';
const attr = currentScript.getAttribute.bind(currentScript);
const website = attr(_data + 'website-id');
const hostUrl = attr(_data + 'host-url');
const beforeSend = attr(_data + 'before-send');
const tag = attr(_data + 'tag') || undefined;
const autoTrack = attr(_data + 'auto-track') !== _false;
const dnt = attr(_data + 'do-not-track') === _true;
const excludeSearch = attr(_data + 'exclude-search') === _true;
const excludeHash = attr(_data + 'exclude-hash') === _true;
const domain = attr(_data + 'domains') || '';
const config = value => attr(`${_data}${value}`);
const website = config('website-id');
const hostUrl = config('host-url');
const beforeSend = config('before-send');
const tag = config('tag') || undefined;
const autoTrack = config('auto-track') !== _false;
const dnt = config('do-not-track') === _true;
const excludeSearch = config('exclude-search') === _true;
const excludeHash = config('exclude-hash') === _true;
const domain = config('domains') || '';
const credentials = config('fetch-credentials') || 'omit';
const perf = config('performance') === _true;
const domains = domain.split(',').map(n => n.trim());
const host =
hostUrl || '__COLLECT_API_HOST__' || currentScript.src.split('/').slice(0, -1).join('/');
const endpoint = `${host.replace(/\/$/, '')}__COLLECT_API_ENDPOINT__`;
const screen = `${width}x${height}`;
const eventRegex = /data-umami-event-([\w-_]+)/;
const eventNameAttribute = _data + 'umami-event';
const eventNameAttribute = `${_data}umami-event`;
const delayDuration = 300;
/* Helper functions */
@@ -45,7 +59,7 @@
if (excludeSearch) u.search = '';
if (excludeHash) u.hash = '';
return u.toString();
} catch (e) {
} catch {
return raw;
}
};
@@ -72,6 +86,10 @@
const handlePush = (_state, _title, url) => {
if (!url) return;
if (typeof flushPerformance === 'function') {
flushPerformance();
}
currentRef = currentUrl;
currentUrl = normalize(new URL(url, location.href).toString());
@@ -141,7 +159,7 @@
const trackingDisabled = () =>
disabled ||
!website ||
(localStorage && localStorage.getItem('umami.disabled')) ||
localStorage?.getItem('umami.disabled') ||
(domain && !domains.includes(hostname)) ||
(dnt && hasDoNotTrack());
@@ -151,7 +169,7 @@
const callback = window[beforeSend];
if (typeof callback === 'function') {
payload = callback(type, payload);
payload = await Promise.resolve(callback(type, payload));
}
if (!payload) return;
@@ -165,7 +183,7 @@
'Content-Type': 'application/json',
...(typeof cache !== 'undefined' && { 'x-umami-cache': cache }),
},
credentials: 'omit',
credentials,
priority: 'low',
});
@@ -174,7 +192,8 @@
disabled = !!data.disabled;
cache = data.cache;
}
} catch (e) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_e) {
/* no-op */
}
};
@@ -185,6 +204,7 @@
track();
handlePathChanges();
handleClicks();
if (perf) initPerformance();
}
};
@@ -210,12 +230,169 @@
);
};
/* Performance */
const initPerformance = () => {
const metrics = {};
let sent = false;
let timeoutId;
let isInitialLoad = true;
let activationStart = 0;
let pageStartTime = 0;
const observe = (type, callback) => {
try {
const observer = new PerformanceObserver(list => {
list.getEntries().forEach(callback);
});
observer.observe({ type, buffered: true });
} catch {
/* not supported */
}
};
// TTFB
observe('navigation', entry => {
activationStart = entry.activationStart || 0;
metrics.ttfb = Math.max(entry.responseStart - activationStart, 0);
});
// FCP
observe('paint', entry => {
if (entry.name === 'first-contentful-paint') {
metrics.fcp = Math.max(entry.startTime - activationStart, 0);
}
});
// LCP
observe('largest-contentful-paint', entry => {
metrics.lcp = Math.max(entry.startTime - activationStart, 0);
});
// CLS - session windows algorithm (gap < 1s, max 5s duration; report worst window)
let clsSessionValue = 0;
let clsSessionEntries = [];
observe('layout-shift', entry => {
if (!entry.hadRecentInput) {
const lastEntry = clsSessionEntries[clsSessionEntries.length - 1];
const firstEntry = clsSessionEntries[0];
if (
lastEntry &&
entry.startTime - lastEntry.startTime - lastEntry.duration < 1000 &&
entry.startTime - firstEntry.startTime < 5000
) {
clsSessionValue += entry.value;
clsSessionEntries.push(entry);
} else {
clsSessionValue = entry.value;
clsSessionEntries = [entry];
}
if (clsSessionValue > (metrics.cls || 0)) {
metrics.cls = clsSessionValue;
}
}
});
// INP - group by interactionId, 98th percentile, 40ms threshold
let interactions = {};
try {
const observer = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
if (entry.interactionId) {
const existing = interactions[entry.interactionId];
if (!existing || entry.duration > existing) {
interactions[entry.interactionId] = entry.duration;
}
const values = Object.values(interactions).sort((a, b) => b - a);
if (values.length) {
const p98Index = Math.floor(Math.max(values.length, 10) * 0.02);
metrics.inp = values[Math.min(p98Index, values.length - 1)];
}
}
});
});
observer.observe({ type: 'event', buffered: true, durationThreshold: 40 });
} catch {
/* not supported */
}
const getEntriesByType = type => {
try {
return window.performance?.getEntriesByType?.(type) || [];
} catch {
return [];
}
};
const applyFallbackMetrics = () => {
if (!isInitialLoad) return;
if (metrics.ttfb === undefined) {
const navigation = getEntriesByType('navigation')?.[0];
if (navigation) {
metrics.ttfb = Math.max(navigation.responseStart - (navigation.activationStart || 0), 0);
}
}
if (metrics.fcp === undefined) {
const fcpEntry = getEntriesByType('paint')?.find(
entry => entry.name === 'first-contentful-paint',
);
if (fcpEntry) {
metrics.fcp = Math.max(fcpEntry.startTime - activationStart, 0);
}
}
if (metrics.lcp === undefined) {
const lcpEntries = getEntriesByType('largest-contentful-paint');
const lcpEntry = lcpEntries?.[lcpEntries.length - 1];
if (lcpEntry) {
metrics.lcp = Math.max(lcpEntry.startTime - activationStart, 0);
}
}
};
const sendPerformance = () => {
if (sent) return;
applyFallbackMetrics();
metrics.duration = Math.round(performance.now() - pageStartTime);
sent = true;
if (timeoutId) clearTimeout(timeoutId);
send({ ...getPayload(), ...metrics }, 'performance');
};
flushPerformance = () => {
sendPerformance();
isInitialLoad = false;
Object.keys(metrics).forEach(k => {
delete metrics[k];
});
activationStart = 0;
pageStartTime = performance.now();
clsSessionValue = 0;
clsSessionEntries = [];
interactions = {};
sent = false;
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(sendPerformance, 10000);
};
timeoutId = setTimeout(sendPerformance, 10000);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') sendPerformance();
});
window.addEventListener('pagehide', sendPerformance);
};
/* Start */
if (!window.umami) {
window.umami = {
track,
identify,
getSession: () => ({ cache, website }),
};
}
@@ -226,6 +403,7 @@
let disabled = false;
let cache;
let identity;
let flushPerformance;
if (autoTrack && !trackingDisabled()) {
if (document.readyState === 'complete') {