Added basic analytics

This commit is contained in:
DrMint 2024-05-22 18:53:20 +02:00
parent 95af61a412
commit ef2d182ce4
7 changed files with 114 additions and 9 deletions

15
.env.example Normal file
View File

@ -0,0 +1,15 @@
## ASTRO
ASTRO_HOST=localhost
ASTRO_PORT=12499
## PAYLOAD
PAYLOAD_API_URL=https://payload.domain.com/api
PAYLOAD_USER=myemail@domain.com
PAYLOAD_PASSWORD=somepassword123
WEB_HOOK_TOKEN=webhookd5e6ea45ef4e66eaa151612bdcb599df
## OPEN EXCHANGE RATE
OER_APP_ID=oerappid5e6ea45ef4e66eaa151612bdcb599df
## ANALYTICS
ANALYTICS_URL=http://analytics.domain.com

View File

@ -1 +1,2 @@
src/shared/*
src/shared/*
!src/shared/analytics

12
TODO.md
View File

@ -4,19 +4,21 @@
- Number of audio players seems limited (on Chrome and Firefox)
- Automatically generate different sizes of images
- Handle relationship in RichText Content
- [RichTextContent] Handle relationship
- [RichTextContent] Add autolink block support
## Mid term
- [RichTextContent] Add autolink block support
- Save cookies for longer than just the session
- Support for nameless section
- [Folders] Support for nameless section
- [Timeline] Error if collectible not published?
- [Timeline] display source language
- [Timeline] Add details button in footer with credits + last updated / created
- [Videos] see why no video on Firefox and no poster on Chrome https://v3.accords-library.com/en/videos/661b672825d380e548dbb8c8
- [Videos] Display platform info + channel page
- [Timeline] Handle no JS for footers
- [JSLess] Provide JS-less alternative for timeline card footers
- [JSLess] Provide JS-less alternative for parent pages
- [Analytics] Add analytics
## Long term
@ -30,7 +32,6 @@
- The Changelog
- Grid view (all files)
- Web archives
- Videos
- Contact page
- About us page
- Global search function
@ -40,6 +41,7 @@
- [Images] add images group (which could be named or not)
- [Recorders] add list of contributions on recorders' pages
- Check if cache and prefetching can be used / should be used
- Detect unused wording keys
## Bonus

1
src/env.d.ts vendored
View File

@ -6,5 +6,6 @@ declare namespace App {
currentLocale: string;
currentTheme: "dark" | "auto" | "light";
currentCurrency: string;
notFound: boolean;
}
}

View File

@ -3,6 +3,7 @@ import { cache } from "src/utils/payload";
import acceptLanguage from "accept-language";
import type { AstroCookies } from "astro";
import { z } from "astro:content";
import { trackRequest, trackEvent } from "src/shared/analytics/analytics";
import { defaultLocale } from "src/i18n/i18n";
const getAbsoluteLocaleUrl = (locale: string, url: string) => `/${locale}${url}`;
@ -29,12 +30,14 @@ const localeNegotiator = defineMiddleware(({ cookies, url, request }, next) => {
if (!currentLocale) {
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname);
trackEvent("locale-redirect");
return redirect(redirectURL);
}
if (currentLocale !== bestMatchingLocale) {
const pathnameWithoutLocale = url.pathname.substring(currentLocale.length + 1);
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, pathnameWithoutLocale);
trackEvent("locale-redirect");
return redirect(redirectURL);
}
@ -49,6 +52,7 @@ const handleActionsSearchParams = defineMiddleware(async ({ url }, next) => {
? url.pathname.substring(currentLocale.length + 1)
: url.pathname;
const redirectURL = getAbsoluteLocaleUrl(actionLang, pathnameWithoutLocale);
trackEvent("action-lang");
return redirect(redirectURL, {
"Set-Cookie": `${CookieKeys.Languages}=${JSON.stringify([actionLang])}; Path=/`,
});
@ -56,6 +60,7 @@ const handleActionsSearchParams = defineMiddleware(async ({ url }, next) => {
const actionCurrency = url.searchParams.get("action-currency");
if (isValidCurrency(actionCurrency)) {
trackEvent("action-currency");
return redirect(url.pathname, {
"Set-Cookie": `${CookieKeys.Currency}=${JSON.stringify(actionCurrency)}; Path=/`,
});
@ -64,7 +69,7 @@ const handleActionsSearchParams = defineMiddleware(async ({ url }, next) => {
const actionTheme = url.searchParams.get("action-theme");
const verifiedActionTheme = themeSchema.safeParse(actionTheme);
if (verifiedActionTheme.success) {
url.searchParams.delete("action-theme");
trackEvent("action-theme");
if (verifiedActionTheme.data === "auto") {
return redirect(url.pathname, {
"Set-Cookie": `${CookieKeys.Theme}=; Path=/; Expires=${new Date(0).toUTCString()}`,
@ -95,18 +100,26 @@ const provideLocalsToRequest = defineMiddleware(async ({ url, locals, cookies },
return next();
});
const analytics = defineMiddleware(async (context, next) => {
const { request, params, locals, clientAddress } = context;
const response = await next();
trackRequest(request, { params, locals, clientAddress });
return response;
});
export const onRequest = sequence(
addContentLanguageResponseHeader,
handleActionsSearchParams,
localeNegotiator,
provideLocalsToRequest
provideLocalsToRequest,
analytics
);
/* LOCALE */
const getCurrentLocale = (pathname: string): string | undefined => {
for (const locale of cache.locales) {
if (pathname.startsWith(`/${locale.id}`)) {
if (pathname.split("/")[1] === locale.id) {
return locale.id;
}
}

View File

@ -1,6 +1,8 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
Astro.locals.notFound = true;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}

View File

@ -0,0 +1,71 @@
import { cache } from "src/utils/payload";
const getUnlocalizedPathname = (pathname: string): string => {
for (const locale of cache.locales) {
if (pathname.startsWith(`/${locale.id}`)) {
return pathname.substring(`/${locale.id}`.length) || "/";
}
}
return pathname;
};
type TrackRequestParams = {
params: Record<string, string | undefined>;
locals: App.Locals;
clientAddress: string;
};
export const trackRequest = (request: Request, { clientAddress, locals }: TrackRequestParams) => {
const userAgent = request.headers.get("User-Agent");
const acceptLanguage = request.headers.get("Accept-Language");
const { method, url: stringUrl, referrer } = request;
const url = new URL(stringUrl);
const body: AnalyticsBody = {
type: "request",
timestamp: Date.now(),
payload: {
user: {
address: clientAddress,
attributes: {
locale: locals.currentLocale,
currency: locals.currentCurrency,
theme: locals.currentTheme,
},
},
request: {
method,
pathname: getUnlocalizedPathname(url.pathname),
referrer,
...(acceptLanguage ? { acceptLanguage } : {}),
...(userAgent ? { userAgent } : {}),
},
response: {
status: locals.notFound ? 404 : 200,
},
},
};
track(body);
};
export const trackEvent = (eventName: string) => {
const body: AnalyticsBody = { type: "event", timestamp: Date.now(), eventName };
track(body);
};
type AnalyticsBody = Record<string, unknown> & {
type: "event" | "request";
timestamp: number;
};
const track = async (body: AnalyticsBody) => {
try {
await fetch(import.meta.env.ANALYTICS_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
} catch (e) {
console.warn("Couldn't send analytics", e);
}
};