-
Notifications
You must be signed in to change notification settings - Fork 412
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(analytics): add Umami tracker to
/login
, /cadastro
and `/pub…
…licar` pages
- Loading branch information
1 parent
1574915
commit 354287d
Showing
5 changed files
with
309 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Analytics as VercelAnalytics } from '@vercel/analytics/react'; | ||
import Script from 'next/script'; | ||
|
||
export default function Analytics() { | ||
return ( | ||
<> | ||
<Script | ||
id="umami-script" | ||
src="/analytics.js" | ||
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID} | ||
data-path-matcher="^(/login|/cadastro|/publicar)$" | ||
data-exclude-search="true" | ||
strategy="lazyOnload" | ||
/> | ||
<VercelAnalytics | ||
beforeSend={(event) => { | ||
const { pathname } = new URL(event.url); | ||
if (['/', '/publicar'].includes(pathname)) { | ||
return null; | ||
} | ||
return event; | ||
}} | ||
/> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
// Original code from: | ||
// https://github.com/umami-software/umami/blob/bce70c1034c6668255261c26093a35900b869566/src/tracker/index.js | ||
// With a modification by @aprendendofelipe to filter tracked paths | ||
|
||
((window) => { | ||
const { | ||
screen: { width, height }, | ||
navigator: { language }, | ||
location, | ||
document, | ||
history, | ||
} = window; | ||
const { hostname, href, origin } = location; | ||
const { currentScript, referrer } = document; | ||
const localStorage = href.startsWith('data:') ? undefined : window.localStorage; | ||
|
||
if (!currentScript) return; | ||
|
||
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 tag = attr(_data + 'tag'); | ||
const autoTrack = attr(_data + 'auto-track') !== _false; | ||
const excludeSearch = attr(_data + 'exclude-search') === _true; | ||
const pathMatcher = attr(_data + 'path-matcher'); | ||
const pathRegex = pathMatcher ? new RegExp(pathMatcher) : undefined; | ||
const domain = attr(_data + 'domains') || ''; | ||
const domains = domain.split(',').map((n) => n.trim()); | ||
const host = hostUrl || currentScript.src.split('/').slice(0, -1).join('/'); | ||
const endpoint = `${host.replace(/\/$/, '')}/api/v1/analytics`; | ||
const screen = `${width}x${height}`; | ||
const eventRegex = /data-umami-event-([\w-_]+)/; | ||
const eventNameAttribute = _data + 'umami-event'; | ||
const delayDuration = 300; | ||
|
||
/* Helper functions */ | ||
|
||
const encode = (str) => { | ||
if (!str) { | ||
return undefined; | ||
} | ||
|
||
try { | ||
const result = decodeURI(str); | ||
|
||
if (result !== str) { | ||
return result; | ||
} | ||
} catch (e) { | ||
return str; | ||
} | ||
|
||
return encodeURI(str); | ||
}; | ||
|
||
const parseURL = (url) => { | ||
try { | ||
// use location.origin as the base to handle cases where the url is a relative path | ||
const { pathname, search, hash } = new URL(url, location.href); | ||
url = pathname + search + hash; | ||
} catch (e) { | ||
/* empty */ | ||
} | ||
return excludeSearch ? url.split('?')[0] : url; | ||
}; | ||
|
||
const getPayload = () => ({ | ||
website, | ||
hostname, | ||
screen, | ||
language, | ||
title: encode(title), | ||
url: encode(currentUrl), | ||
referrer: encode(currentRef), | ||
tag: tag ? tag : undefined, | ||
}); | ||
|
||
/* Event handlers */ | ||
|
||
const handlePush = (state, title, url) => { | ||
if (!url) return; | ||
|
||
currentRef = currentUrl; | ||
currentUrl = parseURL(url.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 handleTitleChanges = () => { | ||
const observer = new MutationObserver(([entry]) => { | ||
title = entry && entry.target ? entry.target.text : undefined; | ||
}); | ||
|
||
const node = document.querySelector('head > title'); | ||
|
||
if (node) { | ||
observer.observe(node, { | ||
subtree: true, | ||
characterData: true, | ||
childList: true, | ||
}); | ||
} | ||
}; | ||
|
||
const handleClicks = () => { | ||
document.addEventListener( | ||
'click', | ||
async (e) => { | ||
const isSpecialTag = (tagName) => ['BUTTON', 'A'].includes(tagName); | ||
|
||
const trackElement = async (el) => { | ||
const attr = el.getAttribute.bind(el); | ||
const eventName = attr(eventNameAttribute); | ||
|
||
if (eventName) { | ||
const eventData = {}; | ||
|
||
el.getAttributeNames().forEach((name) => { | ||
const match = name.match(eventRegex); | ||
|
||
if (match) { | ||
eventData[match[1]] = attr(name); | ||
} | ||
}); | ||
|
||
return track(eventName, eventData); | ||
} | ||
}; | ||
|
||
const findParentTag = (rootElem, maxSearchDepth) => { | ||
let currentElement = rootElem; | ||
for (let i = 0; i < maxSearchDepth; i++) { | ||
if (isSpecialTag(currentElement.tagName)) { | ||
return currentElement; | ||
} | ||
currentElement = currentElement.parentElement; | ||
if (!currentElement) { | ||
return null; | ||
} | ||
} | ||
}; | ||
|
||
const el = e.target; | ||
const parentElement = isSpecialTag(el.tagName) ? el : findParentTag(el, 10); | ||
|
||
if (parentElement) { | ||
const { href, target } = parentElement; | ||
const eventName = parentElement.getAttribute(eventNameAttribute); | ||
|
||
if (eventName) { | ||
if (parentElement.tagName === 'A') { | ||
const external = | ||
target === '_blank' || e.ctrlKey || e.shiftKey || e.metaKey || (e.button && e.button === 1); | ||
|
||
if (eventName && href) { | ||
if (!external) { | ||
e.preventDefault(); | ||
} | ||
return trackElement(parentElement).then(() => { | ||
if (!external) location.href = href; | ||
}); | ||
} | ||
} else if (parentElement.tagName === 'BUTTON') { | ||
return trackElement(parentElement); | ||
} | ||
} | ||
} else { | ||
return trackElement(el); | ||
} | ||
}, | ||
true, | ||
); | ||
}; | ||
|
||
/* Tracking functions */ | ||
|
||
const trackingDisabled = () => | ||
!website || (localStorage && localStorage.getItem('umami.disabled')) || (domain && !domains.includes(hostname)); | ||
|
||
const send = async (payload, type = 'event') => { | ||
if (trackingDisabled()) return; | ||
|
||
if (pathRegex && !pathRegex.test(payload.url)) return; | ||
|
||
const headers = { | ||
'Content-Type': 'application/json', | ||
}; | ||
|
||
if (typeof cache !== 'undefined') { | ||
headers['x-umami-cache'] = cache; | ||
} | ||
|
||
try { | ||
const res = await fetch(endpoint, { | ||
method: 'POST', | ||
body: JSON.stringify({ type, payload }), | ||
headers, | ||
}); | ||
const text = await res.text(); | ||
|
||
return (cache = text); | ||
} catch (e) { | ||
/* empty */ | ||
} | ||
}; | ||
|
||
const init = () => { | ||
if (!initialized) { | ||
track(); | ||
handlePathChanges(); | ||
handleTitleChanges(); | ||
handleClicks(); | ||
initialized = true; | ||
} | ||
}; | ||
|
||
const track = (obj, data) => { | ||
if (typeof obj === 'string') { | ||
return send({ | ||
...getPayload(), | ||
name: obj, | ||
data: typeof data === 'object' ? data : undefined, | ||
}); | ||
} else if (typeof obj === 'object') { | ||
return send(obj); | ||
} else if (typeof obj === 'function') { | ||
return send(obj(getPayload())); | ||
} | ||
return send(getPayload()); | ||
}; | ||
|
||
const identify = (data) => send({ ...getPayload(), data }, 'identify'); | ||
|
||
/* Start */ | ||
|
||
if (!window.umami) { | ||
window.umami = { | ||
track, | ||
identify, | ||
}; | ||
} | ||
|
||
let currentUrl = parseURL(href); | ||
let currentRef = referrer.startsWith(origin) ? '' : referrer; | ||
let title = document.title; | ||
let cache; | ||
let initialized; | ||
|
||
if (autoTrack && !trackingDisabled()) { | ||
if (document.readyState === 'complete') { | ||
init(); | ||
} else { | ||
document.addEventListener('readystatechange', init, true); | ||
} | ||
} | ||
})(window); |