Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ VITE_REMOTE_SERVER_URL=http://localhost:4014

# Controls the initial visibility of the Sample Data section.
VITE_SHOW_SAMPLE_DATA=true

# Optional Netlify Edge Function analytics. Set these in Netlify's Functions
# scope for the deployment contexts to track. Never use a VITE_ prefix for tokens.
GOATCOUNTER_SITE=
GOATCOUNTER_API_TOKEN=
89 changes: 62 additions & 27 deletions netlify/edge-functions/visits.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,73 @@
// This file runs in the Deno runtime
/* global Netlify */
import type { Context } from '@netlify/edge-functions';

// @ts-ignore
// eslint-disable-next-line import/no-unresolved
import { Context } from 'https://edge.netlify.com';

// @ts-ignore
const GOATCOUNTER_SITE = Deno.env.get('GOATCOUNTER_SITE');

export default async (request: Request, context: Context) => {
if (!GOATCOUNTER_SITE) return;

const headers = { 'X-Forwarded-For': context.ip };
const url = new URL(GOATCOUNTER_SITE);
const { searchParams } = url;

url.pathname = '/count';
function getReferrer(request: Request) {
const referrer = request.headers.get('Referer');
if (!referrer) return undefined;
try {
const url = new URL(referrer);
return `${url.protocol}//${url.host}${url.pathname}`;
} catch {
return undefined;
}
}

const requestUrl = new URL(request.url);
searchParams.set('p', `${requestUrl.host}${requestUrl.pathname}`);
searchParams.set('t', 'VolView');
async function countVisit(
request: Request,
ip: string,
site: string,
token: string
) {
try {
const requestUrl = new URL(request.url);
const response = await fetch(new URL('/api/v0/count', site), {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
redirect: 'error',
signal: AbortSignal.timeout(10_000),
body: JSON.stringify({
hits: [
{
path: `${requestUrl.host}${requestUrl.pathname}`,
title: 'VolView',
ref: getReferrer(request),
user_agent: request.headers.get('User-Agent') || 'Unknown',
ip,
},
],
}),
});
await response.body?.cancel();

if (request.headers.has('Referer')) {
searchParams.set(
'r',
encodeURIComponent(request.headers.get('Referer') ?? '')
if (response.status !== 202) {
console.error(`GoatCounter request rejected (HTTP ${response.status}).`);
} else if (response.headers.has('X-Goatcounter-Filter')) {
console.info('GoatCounter visit filtered (HTTP 202).');
}
} catch {
// Errors and response bodies may contain visitor data or credentials.
console.error(
'GoatCounter request failed. Check configuration and network.'
);
}
}

export default (request: Request, context: Context) => {
if (request.method !== 'GET') return;

const site = Netlify.env.get('GOATCOUNTER_SITE');
if (!site) return;

if (request.headers.has('User-Agent')) {
headers['User-Agent'] = request.headers.get('User-Agent') ?? 'Unknown';
const token = Netlify.env.get('GOATCOUNTER_API_TOKEN');
if (!token) {
console.warn('GoatCounter disabled: GOATCOUNTER_API_TOKEN is not set.');
return;
}

// don't block the request
fetch(url.toString(), { headers });
context.waitUntil(countVisit(request, context.ip, site, token));
};

export const config = { path: '/' };
Loading