Added basic analytics
This commit is contained in:
parent
95af61a412
commit
ef2d182ce4
|
@ -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
|
|
@ -1 +1,2 @@
|
||||||
src/shared/*
|
src/shared/*
|
||||||
|
!src/shared/analytics
|
12
TODO.md
12
TODO.md
|
@ -4,19 +4,21 @@
|
||||||
|
|
||||||
- Number of audio players seems limited (on Chrome and Firefox)
|
- Number of audio players seems limited (on Chrome and Firefox)
|
||||||
- Automatically generate different sizes of images
|
- Automatically generate different sizes of images
|
||||||
- Handle relationship in RichText Content
|
- [RichTextContent] Handle relationship
|
||||||
|
- [RichTextContent] Add autolink block support
|
||||||
|
|
||||||
## Mid term
|
## Mid term
|
||||||
|
|
||||||
- [RichTextContent] Add autolink block support
|
|
||||||
- Save cookies for longer than just the session
|
- Save cookies for longer than just the session
|
||||||
- Support for nameless section
|
- [Folders] Support for nameless section
|
||||||
- [Timeline] Error if collectible not published?
|
- [Timeline] Error if collectible not published?
|
||||||
- [Timeline] display source language
|
- [Timeline] display source language
|
||||||
- [Timeline] Add details button in footer with credits + last updated / created
|
- [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] 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
|
- [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
|
## Long term
|
||||||
|
|
||||||
|
@ -30,7 +32,6 @@
|
||||||
- The Changelog
|
- The Changelog
|
||||||
- Grid view (all files)
|
- Grid view (all files)
|
||||||
- Web archives
|
- Web archives
|
||||||
- Videos
|
|
||||||
- Contact page
|
- Contact page
|
||||||
- About us page
|
- About us page
|
||||||
- Global search function
|
- Global search function
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
- [Images] add images group (which could be named or not)
|
- [Images] add images group (which could be named or not)
|
||||||
- [Recorders] add list of contributions on recorders' pages
|
- [Recorders] add list of contributions on recorders' pages
|
||||||
- Check if cache and prefetching can be used / should be used
|
- Check if cache and prefetching can be used / should be used
|
||||||
|
- Detect unused wording keys
|
||||||
|
|
||||||
## Bonus
|
## Bonus
|
||||||
|
|
||||||
|
|
|
@ -6,5 +6,6 @@ declare namespace App {
|
||||||
currentLocale: string;
|
currentLocale: string;
|
||||||
currentTheme: "dark" | "auto" | "light";
|
currentTheme: "dark" | "auto" | "light";
|
||||||
currentCurrency: string;
|
currentCurrency: string;
|
||||||
|
notFound: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { cache } from "src/utils/payload";
|
||||||
import acceptLanguage from "accept-language";
|
import acceptLanguage from "accept-language";
|
||||||
import type { AstroCookies } from "astro";
|
import type { AstroCookies } from "astro";
|
||||||
import { z } from "astro:content";
|
import { z } from "astro:content";
|
||||||
|
import { trackRequest, trackEvent } from "src/shared/analytics/analytics";
|
||||||
import { defaultLocale } from "src/i18n/i18n";
|
import { defaultLocale } from "src/i18n/i18n";
|
||||||
|
|
||||||
const getAbsoluteLocaleUrl = (locale: string, url: string) => `/${locale}${url}`;
|
const getAbsoluteLocaleUrl = (locale: string, url: string) => `/${locale}${url}`;
|
||||||
|
@ -29,12 +30,14 @@ const localeNegotiator = defineMiddleware(({ cookies, url, request }, next) => {
|
||||||
|
|
||||||
if (!currentLocale) {
|
if (!currentLocale) {
|
||||||
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname);
|
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, url.pathname);
|
||||||
|
trackEvent("locale-redirect");
|
||||||
return redirect(redirectURL);
|
return redirect(redirectURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentLocale !== bestMatchingLocale) {
|
if (currentLocale !== bestMatchingLocale) {
|
||||||
const pathnameWithoutLocale = url.pathname.substring(currentLocale.length + 1);
|
const pathnameWithoutLocale = url.pathname.substring(currentLocale.length + 1);
|
||||||
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, pathnameWithoutLocale);
|
const redirectURL = getAbsoluteLocaleUrl(bestMatchingLocale, pathnameWithoutLocale);
|
||||||
|
trackEvent("locale-redirect");
|
||||||
return redirect(redirectURL);
|
return redirect(redirectURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +52,7 @@ const handleActionsSearchParams = defineMiddleware(async ({ url }, next) => {
|
||||||
? url.pathname.substring(currentLocale.length + 1)
|
? url.pathname.substring(currentLocale.length + 1)
|
||||||
: url.pathname;
|
: url.pathname;
|
||||||
const redirectURL = getAbsoluteLocaleUrl(actionLang, pathnameWithoutLocale);
|
const redirectURL = getAbsoluteLocaleUrl(actionLang, pathnameWithoutLocale);
|
||||||
|
trackEvent("action-lang");
|
||||||
return redirect(redirectURL, {
|
return redirect(redirectURL, {
|
||||||
"Set-Cookie": `${CookieKeys.Languages}=${JSON.stringify([actionLang])}; Path=/`,
|
"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");
|
const actionCurrency = url.searchParams.get("action-currency");
|
||||||
if (isValidCurrency(actionCurrency)) {
|
if (isValidCurrency(actionCurrency)) {
|
||||||
|
trackEvent("action-currency");
|
||||||
return redirect(url.pathname, {
|
return redirect(url.pathname, {
|
||||||
"Set-Cookie": `${CookieKeys.Currency}=${JSON.stringify(actionCurrency)}; Path=/`,
|
"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 actionTheme = url.searchParams.get("action-theme");
|
||||||
const verifiedActionTheme = themeSchema.safeParse(actionTheme);
|
const verifiedActionTheme = themeSchema.safeParse(actionTheme);
|
||||||
if (verifiedActionTheme.success) {
|
if (verifiedActionTheme.success) {
|
||||||
url.searchParams.delete("action-theme");
|
trackEvent("action-theme");
|
||||||
if (verifiedActionTheme.data === "auto") {
|
if (verifiedActionTheme.data === "auto") {
|
||||||
return redirect(url.pathname, {
|
return redirect(url.pathname, {
|
||||||
"Set-Cookie": `${CookieKeys.Theme}=; Path=/; Expires=${new Date(0).toUTCString()}`,
|
"Set-Cookie": `${CookieKeys.Theme}=; Path=/; Expires=${new Date(0).toUTCString()}`,
|
||||||
|
@ -95,18 +100,26 @@ const provideLocalsToRequest = defineMiddleware(async ({ url, locals, cookies },
|
||||||
return next();
|
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(
|
export const onRequest = sequence(
|
||||||
addContentLanguageResponseHeader,
|
addContentLanguageResponseHeader,
|
||||||
handleActionsSearchParams,
|
handleActionsSearchParams,
|
||||||
localeNegotiator,
|
localeNegotiator,
|
||||||
provideLocalsToRequest
|
provideLocalsToRequest,
|
||||||
|
analytics
|
||||||
);
|
);
|
||||||
|
|
||||||
/* LOCALE */
|
/* LOCALE */
|
||||||
|
|
||||||
const getCurrentLocale = (pathname: string): string | undefined => {
|
const getCurrentLocale = (pathname: string): string | undefined => {
|
||||||
for (const locale of cache.locales) {
|
for (const locale of cache.locales) {
|
||||||
if (pathname.startsWith(`/${locale.id}`)) {
|
if (pathname.split("/")[1] === locale.id) {
|
||||||
return locale.id;
|
return locale.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
---
|
---
|
||||||
import AppLayout from "components/AppLayout/AppLayout.astro";
|
import AppLayout from "components/AppLayout/AppLayout.astro";
|
||||||
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
|
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
|
||||||
|
|
||||||
|
Astro.locals.notFound = true;
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue