Skip to content

Instantly share code, notes, and snippets.

@khromov
Last active March 23, 2024 10:08
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save khromov/8f7cfbf2e3255be4a8c4a3f63083ea75 to your computer and use it in GitHub Desktop.
Save khromov/8f7cfbf2e3255be4a8c4a3f63083ea75 to your computer and use it in GitHub Desktop.
Server side analytics using Umami + SvelteKit
// Run on the server side
trackRequest(
'event',
{
url: '/',
event_name: 'user_created'
}
);
// src/hooks.server.ts
import { trackRequest } from '$lib/server/umami';
const track: Handle = async ({ event, resolve }) => {
const userAgent = event.request.headers.get ? event.request.headers.get('user-agent') : null;
// Add user agent to event locals
event.locals.userAgent = userAgent;
const response = await resolve(event);
// Some of these are spammy like web manifest requests that we don't want to track
const untrackablePaths = [
'/manifest.webmanifest',
];
const shouldTrack =
!untrackablePaths.some((path) => event.url.pathname.startsWith(path)) &&
event.request.method !== 'OPTIONS';
if (shouldTrack) {
// This promise just goes out into the ether
trackRequest(
'pageview',
{
url: event.url.pathname
},
userAgent
);
}
return response;
};
export const handle: Handle = sequence(track);
import { env } from '$env/dynamic/private';
/**
* Docs: https://umami.is/docs/sending-stats
*/
type PayloadType = 'event';
interface Payload {
language?: string; // Eg: en-US
referrer?: string; // Full url or "" if empty
screen?: string; // Eg. 414x986
url: string; // Without domain. Eg. /settings
event_name?: string;
title?: string; // Eg. Settings
data?: Record<string, any>
name?: string // Event name
// website: string // UUID for the site
// hostname: string, // Eg. localhost
}
const UMAMI_HOSTNAME = env?.UMAMI_HOSTNAME || 'localhost';
const UMAMI_SERVER = env?.UMAMI_SERVER;
const UMAMI_SITE = env?.UMAMI_SITE;
/**
* This is needed to prevent bot detection check that prevents the request from being tracked in case we don't pass a user agent
* You can also set DISABLE_BOT_CHECK=true in your umami environment to disable the bot check entirely:
* https://github.com/umami-software/umami/blob/7a3443cd06772f3cde37bdbb0bf38eabf4515561/pages/api/collect.js#L13
*/
const DEFAULT_USER_AGENT =
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1';
/**
* Tracks the request in Umami
* Screen and language is not possible to get from the client without JavaScript
*/
export const trackRequest = async (
type: PayloadType = 'event',
payload: Payload,
userAgent: string | null = null
) => {
const { language = 'en-US', screen = '1024x1024', url, referrer = '', event_name } = payload;
if (!url) {
console.warn('💢 Error: Tracking payload sent without url', url);
return null;
}
console.log(`🏹 Tracking ${type} for url: ${url} ${event_name ? `with event name ${event_name}` : ''}`);
const finalPayload = {
website: UMAMI_SITE,
hostname: UMAMI_HOSTNAME,
language,
screen,
url,
title: undefined, // New since Umami 2.0
referrer,
name: event_name ?? undefined // New format since Umami 2.0
};
const response = await sendRequest(
{
type,
payload: finalPayload
},
userAgent
);
return response;
};
export const sendRequest = async (data: object, userAgent: string | null) => {
const controller = new AbortController();
const signal = controller.signal;
const timeoutPromise = setTimeout(() => {
console.error('💣 Request timed out, aborting!');
if (controller) {
controller.abort('The request timed out.');
}
}, 5000);
let response;
try {
response = await fetch(`${UMAMI_SERVER}/api/send`, {
signal,
method: 'POST',
body: JSON.stringify(data),
headers: {
'content-type': 'application/json', // We need a proper user agent to bypass the bot check: https://github.com/umami-software/umami/blob/7a3443cd06772f3cde37bdbb0bf38eabf4515561/pages/api/collect.js#LL13C68-L13C68
'user-agent': userAgent ?? DEFAULT_USER_AGENT
// This is set by umami to keep track of the user, it's a JWT token that contains the sessionUuid and websiteUuid
// 'x-umami-cache': 'NOT-IMPLEMENTED',
}
});
if(!response.ok) {
console.error('💣 Umami request failed', response.status, response.statusText);
}
// Clean up the timeout after we received the data
clearTimeout(timeoutPromise);
const responseData = await response.text();
return {
status: response?.status,
ok: response.ok ? true : false,
data: responseData
};
} catch (e) {
console.log('Error when fetching data', e);
// Clear timeout as there was an error
clearTimeout(timeoutPromise);
return {
status: response?.status,
ok: false,
message: `${e}`
};
}
};
@buhodev
Copy link

buhodev commented Aug 8, 2023

Thanks for this! Is the log function from $lib/log in umami.ts something special or just a console.log?

@khromov
Copy link
Author

khromov commented Aug 8, 2023

@buhodev Hi! It's just a normal console.log() statement, I updated the gist to remove it!

@ohbob
Copy link

ohbob commented Mar 23, 2024

Thanks man, its exactly what i was looking for!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment