Refactor tracker.

This commit is contained in:
Mike Cao 2020-09-18 13:40:46 -07:00
parent 2e22deb58a
commit 023adafa39
8 changed files with 85 additions and 136 deletions

1
.gitignore vendored
View File

@ -16,7 +16,6 @@
# production # production
/build /build
/public/umami.js /public/umami.js
/public/snippet.js
/lang-compiled /lang-compiled
/lang-formatted /lang-formatted

View File

@ -2,7 +2,7 @@ import React, { useRef } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout'; import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
import CopyButton from '../common/CopyButton'; import CopyButton from 'components/common/CopyButton';
export default function TrackingCodeForm({ values, onClose }) { export default function TrackingCodeForm({ values, onClose }) {
const ref = useRef(); const ref = useRef();
@ -27,9 +27,9 @@ export default function TrackingCodeForm({ values, onClose }) {
/> />
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<CopyButton type="submit" variant="action" element={ref}/> <CopyButton type="submit" variant="action" element={ref} />
<Button onClick={onClose}> <Button onClick={onClose}>
<FormattedMessage id="button.cancel" defaultMessage="Cancel"/> <FormattedMessage id="button.cancel" defaultMessage="Cancel" />
</Button> </Button>
</FormButtons> </FormButtons>
</FormLayout> </FormLayout>

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "0.38.0", "version": "0.39.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ", "description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",
@ -15,7 +15,6 @@
"start": "next start", "start": "next start",
"build-app": "next build", "build-app": "next build",
"build-tracker": "rollup -c rollup.tracker.config.js", "build-tracker": "rollup -c rollup.tracker.config.js",
"build-snippet": "rollup -c rollup.snippet.config.js",
"copy-db-schema": "node scripts/copy-db-schema.js", "copy-db-schema": "node scripts/copy-db-schema.js",
"build-db-schema": "dotenv prisma introspect", "build-db-schema": "dotenv prisma introspect",
"build-db-client": "dotenv prisma generate", "build-db-client": "dotenv prisma generate",

View File

@ -11,6 +11,12 @@ export default function Test() {
return <h1>No id query specified.</h1>; return <h1>No id query specified.</h1>;
} }
function handleClick() {
window.umami('Custom event');
window.umami.pageView('/fake', 'https://www.google.com');
window.umami.pageEvent('pageEvent', 'custom-type');
}
return ( return (
<> <>
<Head> <Head>
@ -20,7 +26,7 @@ export default function Test() {
</Head> </Head>
<Layout> <Layout>
<p> <p>
Here you can test if your umami installation works. Open the network tab in your browser's Here you can test if your umami installation works. Open the network tab in your browser
developer console and watch for requests to the url <b>collect</b>. The links below should developer console and watch for requests to the url <b>collect</b>. The links below should
trigger page views. Clicking on the button should trigger an event. trigger page views. Clicking on the button should trigger an event.
</p> </p>
@ -40,6 +46,10 @@ export default function Test() {
> >
Button Button
</button> </button>
<h2>Manual trigger</h2>
<button id="manual-button" type="button" onClick={handleClick}>
Button
</button>
</Layout> </Layout>
</> </>
); );

View File

@ -1,19 +0,0 @@
import 'dotenv/config';
import buble from '@rollup/plugin-buble';
import replace from '@rollup/plugin-replace';
import resolve from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';
export default {
input: 'tracker/snippet.js',
output: {
file: 'public/snippet.js',
format: 'iife',
},
plugins: [
replace({ __DNT__: !!process.env.ENABLE_DNT }),
resolve(),
buble(),
terser({ compress: { evaluate: false } }),
],
};

View File

@ -1,6 +1,5 @@
import 'dotenv/config'; import 'dotenv/config';
import buble from '@rollup/plugin-buble'; import buble from '@rollup/plugin-buble';
import replace from '@rollup/plugin-replace';
import resolve from '@rollup/plugin-node-resolve'; import resolve from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser'; import { terser } from 'rollup-plugin-terser';
@ -10,10 +9,5 @@ export default {
file: 'public/umami.js', file: 'public/umami.js',
format: 'iife', format: 'iife',
}, },
plugins: [ plugins: [resolve(), buble(), terser({ compress: { evaluate: false } })],
replace({ __DNT__: !!process.env.ENABLE_DNT }),
resolve(),
buble(),
terser({ compress: { evaluate: false } }),
],
}; };

View File

@ -13,44 +13,25 @@ import { removeTrailingSlash } from '../lib/url';
} = window; } = window;
const script = document.querySelector('script[data-website-id]'); const script = document.querySelector('script[data-website-id]');
const attr = key => script && script.getAttribute(key);
// eslint-disable-next-line no-undef const website = attr('data-website-id');
if (!script || (__DNT__ && doNotTrack())) return; const hostUrl = attr('data-host-url');
const autoTrack = attr('data-auto-track') !== 'false';
const dnt = attr('data-do-not-track') === 'true';
if (!script || (dnt && doNotTrack())) return;
const website = script.getAttribute('data-website-id');
const hostUrl = script.getAttribute('data-host-url');
const skipAuto = script.getAttribute('data-skip-auto');
const root = hostUrl const root = hostUrl
? removeTrailingSlash(hostUrl) ? removeTrailingSlash(hostUrl)
: new URL(script.src).href.split('/').slice(0, -1).join('/'); : new URL(script.src).href.split('/').slice(0, -1).join('/');
const screen = `${width}x${height}`; const screen = `${width}x${height}`;
const listeners = []; const listeners = [];
let currentUrl = `${pathname}${search}`; let currentUrl = `${pathname}${search}`;
let currentRef = document.referrer; let currentRef = document.referrer;
/* Handle events */ /* Collect metrics */
const removeEvents = () => {
listeners.forEach(([element, type, listener]) => {
element && element.removeEventListener(type, listener, true);
});
listeners.length = 0;
};
const loadEvents = () => {
document.querySelectorAll('[class*=\'umami--\']').forEach(element => {
element.className.split(' ').forEach(className => {
if (/^umami--([a-z]+)--([a-z0-9_]+[a-z0-9-_]+)$/.test(className)) {
const [, type, value] = className.split('--');
const listener = () => collectEvent(type, value);
listeners.push([element, type, listener]);
element.addEventListener(type, listener, true);
}
});
});
};
const collect = (type, params, uuid) => { const collect = (type, params, uuid) => {
const payload = { const payload = {
website: uuid, website: uuid,
@ -70,17 +51,56 @@ import { removeTrailingSlash } from '../lib/url';
payload, payload,
}); });
}; };
const pageView = (url = currentUrl, referrer = currentRef, uuid = website) => collect('pageview', {
url,
referrer,
}, uuid);
/* Collect metrics */ const pageView = (url = currentUrl, referrer = currentRef, uuid = website) =>
const pageViewWithAutoEvents = (url, referrer) => pageView(url, referrer).then(() => setTimeout(loadEvents, 300)); collect(
'pageview',
{
url,
referrer,
},
uuid,
);
const pageEvent = (event_value, event_type = 'custom', url = currentUrl, uuid = website) =>
collect(
'event',
{
event_type,
event_value,
url,
},
uuid,
);
/* Handle events */
const loadEvents = () => {
document.querySelectorAll("[class*='umami--']").forEach(element => {
element.className.split(' ').forEach(className => {
if (/^umami--([a-z]+)--([a-z0-9_]+[a-z0-9-_]+)$/.test(className)) {
const [, type, value] = className.split('--');
const listener = () => pageEvent(value, type);
listeners.push([element, type, listener]);
element.addEventListener(type, listener, true);
}
});
});
};
const removeEvents = () => {
listeners.forEach(([element, type, listener]) => {
element && element.removeEventListener(type, listener, true);
});
listeners.length = 0;
};
/* Handle history changes */
/* Handle history */
const handlePush = (state, title, url) => { const handlePush = (state, title, url) => {
removeEvents(); removeEvents();
currentRef = currentUrl; currentRef = currentUrl;
const newUrl = url.toString(); const newUrl = url.toString();
@ -91,38 +111,27 @@ import { removeTrailingSlash } from '../lib/url';
currentUrl = newUrl; currentUrl = newUrl;
} }
pageViewWithAutoEvents(currentUrl, currentRef); pageView(currentUrl, currentRef);
setTimeout(loadEvents, 300);
}; };
const collectEvent = (event_type, event_value, url = currentUrl, uuid = website) => collect('event', { /* Global */
url,
event_type,
event_value,
}, uuid);
const registerAutoEvents = () => { if (!window.umami) {
history.pushState = hook(history, 'pushState', handlePush); const umami = event_value => pageEvent(event_value);
history.replaceState = hook(history, 'replaceState', handlePush); umami.pageView = pageView;
return pageViewWithAutoEvents(currentUrl, currentRef); umami.pageEvent = pageEvent;
};
window.umami = umami;
const umamiFunctions = { collect, pageView, collectEvent, registerAutoEvents };
const scheduledCalls = window.umami.calls;
window.umami = event_value => collect('event', { event_type: 'custom', event_value });
Object.keys(umamiFunctions).forEach((key) => {
window.umami[key] = umamiFunctions[key];
});
if (scheduledCalls) {
scheduledCalls.forEach(([fnName, ...params]) => {
window.umami[fnName].apply(window.umami, params);
});
} }
/* Start */ /* Start */
if (!skipAuto) {
registerAutoEvents().catch(e => console.error(e)); if (autoTrack) {
history.pushState = hook(history, 'pushState', handlePush);
history.replaceState = hook(history, 'replaceState', handlePush);
pageView(currentUrl, currentRef);
} }
})(window); })(window);

View File

@ -1,43 +0,0 @@
(window => {
const umami = window.umami = window.umami || [];
if (!umami.registerAutoEvents) {
if (umami.invoked) {
window.console && console.error && console.error('Umami snippet included twice.');
} else {
umami.invoked = true;
umami.calls = [];
umami.methods = ['registerAutoEvents', 'event', 'pageView'];
umami.factory = t => {
return function() {
const e = Array.prototype.slice.call(arguments);
e.unshift(t);
umami.calls.push(e);
return umami;
};
};
for (let t = 0; t < umami.methods.length; t++) {
let e = umami.methods[t];
umami[e] = umami.factory(e);
}
umami.load = function(umamiScript, umamiUUID, skipAuto) {
const scriptElement = document.createElement('script');
scriptElement.type = 'text/javascript';
scriptElement.defer = true;
scriptElement.async = true;
scriptElement.setAttribute('data-website-id', umamiUUID);
if (skipAuto) {
scriptElement.setAttribute('data-skip-auto', 'true');
}
scriptElement.src = umamiScript;
const otherScript = document.getElementsByTagName('script')[0];
otherScript.parentNode.insertBefore(scriptElement, otherScript);
};
umami.load('[HOST]/umami.js', '[UMAMI_UUID]', false);
}
}
})(window);
// This snippet is for more advanced use case of Umami. If you want to track custom events,
// and not worry about having blocking script in the header,
// use this snippet (compiled version available in /public/snippet.js).
// Just remember to replace [HOST] and [UMAMI_UUID] when pasting it.