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/analytics
|
12
TODO.md
12
TODO.md
|
@ -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
|
||||
|
||||
|
|
|
@ -6,5 +6,6 @@ declare namespace App {
|
|||
currentLocale: string;
|
||||
currentTheme: "dark" | "auto" | "light";
|
||||
currentCurrency: string;
|
||||
notFound: boolean;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
import AppLayout from "components/AppLayout/AppLayout.astro";
|
||||
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
|
||||
|
||||
Astro.locals.notFound = true;
|
||||
---
|
||||
|
||||
{/* ------------------------------------------- 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