Merge branch 'dev' into tracker-fetch-priority-low
This commit is contained in:
1360 files changed
+67072
-141449
No files matched your search
Vendored
+1
-1
@@ -148,6 +148,6 @@ export type UmamiTracker = {
|
||||
};
|
||||
};
|
||||
|
||||
interface Window {
|
||||
export interface Window {
|
||||
umami: UmamiTracker;
|
||||
}
|
||||
+194
-16
@@ -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') {
|
||||
|
||||
Reference in new issue
Block a user