Add plausible

This commit is contained in:
Andras Schmelczer 2023-10-01 18:57:07 +01:00
parent 1098b2ddd5
commit 207b846322
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
3 changed files with 417 additions and 0 deletions

View file

@ -15,6 +15,16 @@ import {
} from './helper/accessibility';
import { scrollToFragment } from './helper/scroll-to-fragment';
import './index.scss';
import Plausible from './plausible/tracker';
const plausible = Plausible({
hashMode: true,
trackLocalhost: true,
apiURI: 'https://stats.schmelczer.dev/status',
});
plausible.enableAutoPageviews();
plausible.enableAutoOutboundTracking();
addSupportForTabNavigation();
removeUnnecessaryOutlines();

73
src/plausible/request.ts Normal file
View file

@ -0,0 +1,73 @@
import type { PlausibleOptions } from './tracker';
type EventPayload = {
readonly n: string;
readonly u: Location['href'];
readonly d: Location['hostname'];
readonly r: Document['referrer'] | null;
readonly w: Window['innerWidth'];
readonly h: 1 | 0;
readonly p?: string;
};
export type EventOptions = {
/**
* Callback called when the event is successfully sent.
*/
readonly callback?: () => void;
/**
* Properties to be bound to the event.
*/
readonly props?: { readonly [propName: string]: string };
};
/**
* @internal
* Sends an event to Plausible's API
*
* @param data - Event data to send
* @param options - Event options
*/
export function sendEvent(
eventName: string,
data: Required<PlausibleOptions>,
options?: EventOptions,
): void {
const isLocalhost =
/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*:)*?:?0*1$/.test(
window.location.hostname,
) || window.location.protocol === 'file:';
if (!data.trackLocalhost && isLocalhost) {
return console.warn('[Plausible] Ignoring event because website is running locally');
}
const shouldIgnoreCurrentBrowser = localStorage.getItem('plausible_ignore') === 'true';
if (shouldIgnoreCurrentBrowser) {
return console.warn(
'[Plausible] Ignoring event because "plausible_ignore" is set to "true" in localStorage',
);
}
const payload: EventPayload = {
n: eventName,
u: data.url,
d: data.domain,
r: data.referrer,
w: data.deviceWidth,
h: data.hashMode ? 1 : 0,
p: options && options.props ? JSON.stringify(options.props) : undefined,
};
const req = new XMLHttpRequest();
req.open('POST', data.apiURI, true);
req.setRequestHeader('Content-Type', 'text/plain');
req.send(JSON.stringify(payload));
req.onreadystatechange = () => {
if (req.readyState !== 4) return;
if (options && options.callback) {
options.callback();
}
};
}

334
src/plausible/tracker.ts Normal file
View file

@ -0,0 +1,334 @@
import { EventOptions, sendEvent } from './request';
/**
* Options used when initializing the tracker.
*/
export type PlausibleInitOptions = {
/**
* If true, pageviews will be tracked when the URL hash changes.
* Enable this if you are using a frontend that uses hash-based routing.
*/
readonly hashMode?: boolean;
/**
* Set to true if you want events to be tracked when running the site locally.
*/
readonly trackLocalhost?: boolean;
/**
* The domain to bind the event to.
* Defaults to `location.hostname`
*/
readonly domain?: Location['hostname'];
/**
* The API host where the events will be sent.
* Defaults to `'https://plausible.io/api/event'`
*/
readonly apiURI?: string;
};
/**
* Data passed to Plausible as events.
*/
export type PlausibleEventData = {
/**
* The URL to bind the event to.
* Defaults to `location.href`.
*/
readonly url?: Location['href'];
/**
* The referrer to bind the event to.
* Defaults to `document.referrer`
*/
readonly referrer?: Document['referrer'] | null;
/**
* The current device's width.
* Defaults to `window.innerWidth`
*/
readonly deviceWidth?: Window['innerWidth'];
};
/**
* Options used when tracking Plausible events.
*/
export type PlausibleOptions = PlausibleInitOptions & PlausibleEventData;
/**
* Tracks a custom event.
*
* Use it to track your defined goals by providing the goal's name as `eventName`.
*
* ### Example
* ```js
* import Plausible from 'plausible-tracker'
*
* const { trackEvent } = Plausible()
*
* // Tracks the 'signup' goal
* trackEvent('signup')
*
* // Tracks the 'Download' goal passing a 'method' property.
* trackEvent('Download', { props: { method: 'HTTP' } })
* ```
*
* @param eventName - Name of the event to track
* @param options - Event options.
* @param eventData - Optional event data to send. Defaults to the current page's data merged with the default options provided earlier.
*/
type TrackEvent = (
eventName: string,
options?: EventOptions,
eventData?: PlausibleOptions,
) => void;
/**
* Manually tracks a page view.
*
* ### Example
* ```js
* import Plausible from 'plausible-tracker'
*
* const { trackPageview } = Plausible()
*
* // Track a page view
* trackPageview()
* ```
*
* @param eventData - Optional event data to send. Defaults to the current page's data merged with the default options provided earlier.
* @param options - Event options.
*/
type TrackPageview = (eventData?: PlausibleOptions, options?: EventOptions) => void;
/**
* Cleans up all event listeners attached.
*/
type Cleanup = () => void;
/**
* Tracks the current page and all further pages automatically.
*
* Call this if you don't want to manually manage pageview tracking.
*
* ### Example
* ```js
* import Plausible from 'plausible-tracker'
*
* const { enableAutoPageviews } = Plausible()
*
* // This tracks the current page view and all future ones as well
* enableAutoPageviews()
* ```
*
* The returned value is a callback that removes the added event listeners and restores `history.pushState`
* ```js
* import Plausible from 'plausible-tracker'
*
* const { enableAutoPageviews } = Plausible()
*
* const cleanup = enableAutoPageviews()
*
* // Remove event listeners and restore `history.pushState`
* cleanup()
* ```
*/
type EnableAutoPageviews = () => Cleanup;
/**
* Tracks all outbound link clicks automatically
*
* Call this if you don't want to manually manage these links.
*
* It works using a **[MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)** to automagically detect link nodes throughout your application and bind `click` events to them.
*
* Optionally takes the same parameters as [`MutationObserver.observe`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe).
*
* ### Example
* ```js
* import Plausible from 'plausible-tracker'
*
* const { enableAutoOutboundTracking } = Plausible()
*
* // This tracks all the existing and future outbound links on your page.
* enableAutoOutboundTracking()
* ```
*
* The returned value is a callback that removes the added event listeners and disconnects the observer
* ```js
* import Plausible from 'plausible-tracker'
*
* const { enableAutoOutboundTracking } = Plausible()
*
* const cleanup = enableAutoOutboundTracking()
*
* // Remove event listeners and disconnect the observer
* cleanup()
* ```
*/
type EnableAutoOutboundTracking = (
targetNode?: Node & ParentNode,
observerInit?: MutationObserverInit,
) => Cleanup;
/**
* Initializes the tracker with your default values.
*
* ### Example (es module)
* ```js
* import Plausible from 'plausible-tracker'
*
* const { enableAutoPageviews, trackEvent } = Plausible({
* domain: 'my-app-domain.com',
* hashMode: true
* })
*
* enableAutoPageviews()
*
* function onUserRegister() {
* trackEvent('register')
* }
* ```
*
* ### Example (commonjs)
* ```js
* var Plausible = require('plausible-tracker');
*
* var { enableAutoPageviews, trackEvent } = Plausible({
* domain: 'my-app-domain.com',
* hashMode: true
* })
*
* enableAutoPageviews()
*
* function onUserRegister() {
* trackEvent('register')
* }
* ```
*
* @param defaults - Default event parameters that will be applied to all requests.
*/
export default function Plausible(defaults?: PlausibleInitOptions): {
readonly trackEvent: TrackEvent;
readonly trackPageview: TrackPageview;
readonly enableAutoPageviews: EnableAutoPageviews;
readonly enableAutoOutboundTracking: EnableAutoOutboundTracking;
} {
const getConfig = (): Required<PlausibleOptions> => ({
hashMode: false,
trackLocalhost: false,
url: window.location.href,
domain: window.location.hostname,
referrer: document.referrer || null,
deviceWidth: window.innerWidth,
apiURI: 'https://plausible.io/api/event/',
...defaults,
});
const trackEvent: TrackEvent = (eventName, options, eventData) => {
sendEvent(eventName, { ...getConfig(), ...eventData }, options);
};
const trackPageview: TrackPageview = (eventData, options) => {
trackEvent('pageview', options, eventData);
};
const enableAutoPageviews: EnableAutoPageviews = () => {
const page = () => trackPageview();
// Attach pushState and popState listeners
const originalPushState = window.history.pushState;
if (originalPushState) {
window.history.pushState = function (data, title, url) {
originalPushState.apply(this, [data, title, url]);
page();
};
window.addEventListener('popstate', page);
}
// Attach hashchange listener
if (defaults && defaults.hashMode) {
window.addEventListener('hashchange', page);
}
// Trigger first page view
trackPageview();
return function cleanup() {
if (originalPushState) {
window.history.pushState = originalPushState;
window.removeEventListener('popstate', page);
}
if (defaults && defaults.hashMode) {
window.removeEventListener('hashchange', page);
}
};
};
const enableAutoOutboundTracking: EnableAutoOutboundTracking = (
targetNode: Node & ParentNode = document,
observerInit: MutationObserverInit = {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['href'],
},
) => {
function trackClick(this: HTMLAnchorElement, event: MouseEvent) {
trackEvent('Outbound Link: Click', { props: { url: this.href } });
}
const tracked: Set<HTMLAnchorElement> = new Set();
function addNode(node: Node | ParentNode) {
if (node instanceof HTMLAnchorElement) {
if (node.host !== window.location.host) {
node.addEventListener('click', trackClick);
tracked.add(node);
}
} /* istanbul ignore next */ else if ('querySelectorAll' in node) {
node.querySelectorAll('a').forEach(addNode);
}
}
function removeNode(node: Node | ParentNode) {
if (node instanceof HTMLAnchorElement) {
node.removeEventListener('click', trackClick);
tracked.delete(node);
} /* istanbul ignore next */ else if ('querySelectorAll' in node) {
node.querySelectorAll('a').forEach(removeNode);
}
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes') {
// Handle changed href
removeNode(mutation.target);
addNode(mutation.target);
} /* istanbul ignore next */ else if (mutation.type === 'childList') {
// Handle added nodes
mutation.addedNodes.forEach(addNode);
// Handle removed nodes
mutation.removedNodes.forEach(removeNode);
}
});
});
// Track existing nodes
targetNode.querySelectorAll('a').forEach(addNode);
// Observe mutations
observer.observe(targetNode, observerInit);
return function cleanup() {
tracked.forEach((a) => {
a.removeEventListener('click', trackClick);
});
tracked.clear();
observer.disconnect();
};
};
return {
trackEvent,
trackPageview,
enableAutoPageviews,
enableAutoOutboundTracking,
};
}