aboutsummaryrefslogtreecommitdiff
path: root/src/tracker/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/tracker/index.js')
-rw-r--r--src/tracker/index.js240
1 files changed, 240 insertions, 0 deletions
diff --git a/src/tracker/index.js b/src/tracker/index.js
new file mode 100644
index 0000000..ad3648a
--- /dev/null
+++ b/src/tracker/index.js
@@ -0,0 +1,240 @@
+(window => {
+ const {
+ screen: { width, height },
+ navigator: { language, doNotTrack: ndnt, msDoNotTrack: msdnt },
+ location,
+ document,
+ history,
+ top,
+ doNotTrack,
+ } = window;
+ const { currentScript, referrer } = document;
+ if (!currentScript) return;
+
+ const { hostname, href, origin } = location;
+ const localStorage = href.startsWith('data:') ? undefined : window.localStorage;
+
+ 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 credentials = attr(`${_data}fetch-credentials`) || 'omit';
+
+ 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 delayDuration = 300;
+
+ /* Helper functions */
+
+ const normalize = raw => {
+ if (!raw) return raw;
+ try {
+ const u = new URL(raw, location.href);
+ if (excludeSearch) u.search = '';
+ if (excludeHash) u.hash = '';
+ return u.toString();
+ } catch {
+ return raw;
+ }
+ };
+
+ const getPayload = () => ({
+ website,
+ screen,
+ language,
+ title: document.title,
+ hostname,
+ url: currentUrl,
+ referrer: currentRef,
+ tag,
+ id: identity ? identity : undefined,
+ });
+
+ const hasDoNotTrack = () => {
+ const dnt = doNotTrack || ndnt || msdnt;
+ return dnt === 1 || dnt === '1' || dnt === 'yes';
+ };
+
+ /* Event handlers */
+
+ const handlePush = (_state, _title, url) => {
+ if (!url) return;
+
+ currentRef = currentUrl;
+ currentUrl = normalize(new URL(url, location.href).toString());
+
+ if (currentUrl !== currentRef) {
+ setTimeout(track, delayDuration);
+ }
+ };
+
+ const handlePathChanges = () => {
+ const hook = (_this, method, callback) => {
+ const orig = _this[method];
+ return (...args) => {
+ callback.apply(null, args);
+ return orig.apply(_this, args);
+ };
+ };
+
+ history.pushState = hook(history, 'pushState', handlePush);
+ history.replaceState = hook(history, 'replaceState', handlePush);
+ };
+
+ const handleClicks = () => {
+ const trackElement = async el => {
+ const eventName = el.getAttribute(eventNameAttribute);
+ if (eventName) {
+ const eventData = {};
+
+ el.getAttributeNames().forEach(name => {
+ const match = name.match(eventRegex);
+ if (match) eventData[match[1]] = el.getAttribute(name);
+ });
+
+ return track(eventName, eventData);
+ }
+ };
+ const onClick = async e => {
+ const el = e.target;
+ const parentElement = el.closest('a,button');
+ if (!parentElement) return trackElement(el);
+
+ const { href, target } = parentElement;
+ if (!parentElement.getAttribute(eventNameAttribute)) return;
+
+ if (parentElement.tagName === 'BUTTON') {
+ return trackElement(parentElement);
+ }
+ if (parentElement.tagName === 'A' && href) {
+ const external =
+ target === '_blank' ||
+ e.ctrlKey ||
+ e.shiftKey ||
+ e.metaKey ||
+ (e.button && e.button === 1);
+ if (!external) e.preventDefault();
+ return trackElement(parentElement).then(() => {
+ if (!external) {
+ (target === '_top' ? top.location : location).href = href;
+ }
+ });
+ }
+ };
+ document.addEventListener('click', onClick, true);
+ };
+
+ /* Tracking functions */
+
+ const trackingDisabled = () =>
+ disabled ||
+ !website ||
+ localStorage?.getItem('umami.disabled') ||
+ (domain && !domains.includes(hostname)) ||
+ (dnt && hasDoNotTrack());
+
+ const send = async (payload, type = 'event') => {
+ if (trackingDisabled()) return;
+
+ const callback = window[beforeSend];
+
+ if (typeof callback === 'function') {
+ payload = await Promise.resolve(callback(type, payload));
+ }
+
+ if (!payload) return;
+
+ try {
+ const res = await fetch(endpoint, {
+ keepalive: true,
+ method: 'POST',
+ body: JSON.stringify({ type, payload }),
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(typeof cache !== 'undefined' && { 'x-umami-cache': cache }),
+ },
+ credentials,
+ });
+
+ const data = await res.json();
+ if (data) {
+ disabled = !!data.disabled;
+ cache = data.cache;
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (_e) {
+ /* no-op */
+ }
+ };
+
+ const init = () => {
+ if (!initialized) {
+ initialized = true;
+ track();
+ handlePathChanges();
+ handleClicks();
+ }
+ };
+
+ const track = (name, data) => {
+ if (typeof name === 'string') return send({ ...getPayload(), name, data });
+ if (typeof name === 'object') return send({ ...name });
+ if (typeof name === 'function') return send(name(getPayload()));
+ return send(getPayload());
+ };
+
+ const identify = (id, data) => {
+ if (typeof id === 'string') {
+ identity = id;
+ }
+
+ cache = '';
+ return send(
+ {
+ ...getPayload(),
+ data: typeof id === 'object' ? id : data,
+ },
+ 'identify',
+ );
+ };
+
+ /* Start */
+
+ if (!window.umami) {
+ window.umami = {
+ track,
+ identify,
+ };
+ }
+
+ let currentUrl = normalize(href);
+ let currentRef = normalize(referrer.startsWith(origin) ? '' : referrer);
+
+ let initialized = false;
+ let disabled = false;
+ let cache;
+ let identity;
+
+ if (autoTrack && !trackingDisabled()) {
+ if (document.readyState === 'complete') {
+ init();
+ } else {
+ document.addEventListener('readystatechange', init, true);
+ }
+ }
+})(window);