mirror of
https://github.com/LukeHagar/pypistats.dev.git
synced 2025-12-06 12:47:48 +00:00
Add analytics tracking to API endpoints for improved event monitoring. Updated GET handlers across various routes to include tracking of successful and failed requests, enhancing data collection for package usage insights.
This commit is contained in:
56
src/lib/analytics.ts
Normal file
56
src/lib/analytics.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { PLAUSIBLE_DOMAIN as PUBLIC_DOMAIN, PLAUSIBLE_EVENT_ENDPOINT } from '$lib/plausible-config.js';
|
||||||
|
|
||||||
|
|
||||||
|
type TrackProps = Record<string, string | number | boolean | null | undefined>;
|
||||||
|
|
||||||
|
function getEnv(name: string, fallback: string = ''): string {
|
||||||
|
const v = process.env[name];
|
||||||
|
return typeof v === 'string' && v.trim() !== '' ? v : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEventUrl(pathname: string): string {
|
||||||
|
const siteBase = 'https://pypistats.dev';
|
||||||
|
return `${siteBase}${pathname}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackApiEvent(
|
||||||
|
name: string,
|
||||||
|
pathname: string,
|
||||||
|
props: TrackProps = {},
|
||||||
|
requestHeaders?: Headers
|
||||||
|
): void {
|
||||||
|
const endpoint = PLAUSIBLE_EVENT_ENDPOINT;
|
||||||
|
const domain = PUBLIC_DOMAIN;
|
||||||
|
if (!domain) return;
|
||||||
|
|
||||||
|
const url = endpoint;
|
||||||
|
const body = {
|
||||||
|
name,
|
||||||
|
url: buildEventUrl(pathname),
|
||||||
|
domain,
|
||||||
|
props
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiKey = getEnv('PLAUSIBLE_API_KEY');
|
||||||
|
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
||||||
|
|
||||||
|
const ua = requestHeaders?.get('user-agent');
|
||||||
|
if (ua) headers['User-Agent'] = ua;
|
||||||
|
|
||||||
|
const ip =
|
||||||
|
requestHeaders?.get('x-forwarded-for') ||
|
||||||
|
requestHeaders?.get('x-real-ip') ||
|
||||||
|
'';
|
||||||
|
if (ip) headers['X-Forwarded-For'] = ip;
|
||||||
|
|
||||||
|
// Fire-and-forget; never throw or await
|
||||||
|
void fetch(url, { method: 'POST', headers, body: JSON.stringify(body) })
|
||||||
|
.then(() => {})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
15
src/lib/plausible-config.ts
Normal file
15
src/lib/plausible-config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Hardcoded Plausible configuration for public usage
|
||||||
|
|
||||||
|
// Self-hosted Plausible server base
|
||||||
|
export const PLAUSIBLE_BASE = 'https://events.plygrnd.org';
|
||||||
|
|
||||||
|
// Standard event endpoint derived from the unified base
|
||||||
|
export const PLAUSIBLE_EVENT_ENDPOINT = `${PLAUSIBLE_BASE}/api/event`;
|
||||||
|
|
||||||
|
// Public domain/site ID for this app
|
||||||
|
export const PLAUSIBLE_DOMAIN = 'pypistats.dev';
|
||||||
|
|
||||||
|
// Dev capture toggle (public) — default false; override via Vite define if needed
|
||||||
|
export const PLAUSIBLE_CAPTURE_LOCALHOST = false;
|
||||||
|
|
||||||
|
|
||||||
@@ -3,13 +3,14 @@ import { dev } from '$app/environment';
|
|||||||
import { CacheManager } from '$lib/redis.js';
|
import { CacheManager } from '$lib/redis.js';
|
||||||
import { getOverallDownloads, getPythonMajorDownloads, getPythonMinorDownloads, getSystemDownloads, getInstallerDownloads, getVersionDownloads } from '$lib/api.js';
|
import { getOverallDownloads, getPythonMajorDownloads, getPythonMinorDownloads, getSystemDownloads, getInstallerDownloads, getVersionDownloads } from '$lib/api.js';
|
||||||
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
|
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
|
||||||
|
import { trackApiEvent } from '$lib/analytics.js';
|
||||||
|
|
||||||
const cache = new CacheManager();
|
const cache = new CacheManager();
|
||||||
|
|
||||||
const width = 1200;
|
const width = 1200;
|
||||||
const height = 600;
|
const height = 600;
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params, url }) => {
|
export const GET: RequestHandler = async ({ params, url, request }) => {
|
||||||
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
||||||
const type = params.type || 'overall';
|
const type = params.type || 'overall';
|
||||||
const chartType = (url.searchParams.get('chart') || 'line').toLowerCase(); // 'line' | 'bar'
|
const chartType = (url.searchParams.get('chart') || 'line').toLowerCase(); // 'line' | 'bar'
|
||||||
@@ -102,6 +103,13 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
|||||||
labels,
|
labels,
|
||||||
datasets
|
datasets
|
||||||
};
|
};
|
||||||
|
trackApiEvent('api_chart', `/api/packages/${encodeURIComponent(packageName)}/chart/${type}`, {
|
||||||
|
package: packageName,
|
||||||
|
type,
|
||||||
|
chart: chartType,
|
||||||
|
format: 'json',
|
||||||
|
ok: true
|
||||||
|
}, request.headers);
|
||||||
return new Response(JSON.stringify(body), { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' } });
|
return new Response(JSON.stringify(body), { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,9 +148,22 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
|||||||
await cache.set(cacheKey, image.toString('base64'), 3600);
|
await cache.set(cacheKey, image.toString('base64'), 3600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackApiEvent('api_chart', `/api/packages/${encodeURIComponent(packageName)}/chart/${type}`, {
|
||||||
|
package: packageName,
|
||||||
|
type,
|
||||||
|
chart: chartType,
|
||||||
|
format: 'png',
|
||||||
|
ok: true
|
||||||
|
}, request.headers);
|
||||||
return new Response(image, { headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=3600' } });
|
return new Response(image, { headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=3600' } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error rendering chart:', error);
|
console.error('Error rendering chart:', error);
|
||||||
|
trackApiEvent('api_chart', `/api/packages/${encodeURIComponent(packageName)}/chart/${type}`, {
|
||||||
|
package: packageName,
|
||||||
|
type,
|
||||||
|
chart: chartType,
|
||||||
|
ok: false
|
||||||
|
}, request.headers);
|
||||||
return new Response('Internal server error', { status: 500 });
|
return new Response('Internal server error', { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { json } from '@sveltejs/kit';
|
|||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { prisma } from '$lib/prisma.js';
|
import { prisma } from '$lib/prisma.js';
|
||||||
import { DataProcessor } from '$lib/data-processor.js';
|
import { DataProcessor } from '$lib/data-processor.js';
|
||||||
|
import { trackApiEvent } from '$lib/analytics.js';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params, request }) => {
|
||||||
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
||||||
if (!packageName || packageName === '__all__') {
|
if (!packageName || packageName === '__all__') {
|
||||||
return json({ error: 'Invalid package name' }, { status: 400 });
|
return json({ error: 'Invalid package name' }, { status: 400 });
|
||||||
@@ -23,9 +24,17 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
type: 'installer_downloads',
|
type: 'installer_downloads',
|
||||||
data: rows.map(r => ({ date: r.date, category: r.category, downloads: r.downloads }))
|
data: rows.map(r => ({ date: r.date, category: r.category, downloads: r.downloads }))
|
||||||
};
|
};
|
||||||
|
trackApiEvent('api_installer', `/api/packages/${encodeURIComponent(packageName)}/installer`, {
|
||||||
|
package: packageName,
|
||||||
|
ok: true
|
||||||
|
}, request.headers);
|
||||||
return json(response);
|
return json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching installer downloads:', error);
|
console.error('Error fetching installer downloads:', error);
|
||||||
|
trackApiEvent('api_installer', `/api/packages/${encodeURIComponent(packageName)}/installer`, {
|
||||||
|
package: packageName,
|
||||||
|
ok: false
|
||||||
|
}, request.headers);
|
||||||
return json({ error: 'Internal server error' }, { status: 500 });
|
return json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { getOverallDownloads } from '$lib/api.js';
|
import { getOverallDownloads } from '$lib/api.js';
|
||||||
|
import { trackApiEvent } from '$lib/analytics.js';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params, url }) => {
|
export const GET: RequestHandler = async ({ params, url, request }) => {
|
||||||
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
||||||
const mirrors = url.searchParams.get('mirrors');
|
const mirrors = url.searchParams.get('mirrors');
|
||||||
|
|
||||||
@@ -27,9 +28,19 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
|||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
trackApiEvent('api_overall', `/api/packages/${encodeURIComponent(packageName)}/overall`, {
|
||||||
|
package: packageName,
|
||||||
|
mirrors: String(mirrors ?? ''),
|
||||||
|
ok: true
|
||||||
|
}, request.headers);
|
||||||
return json(response);
|
return json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching overall downloads:', error);
|
console.error('Error fetching overall downloads:', error);
|
||||||
|
trackApiEvent('api_overall', `/api/packages/${encodeURIComponent(packageName)}/overall`, {
|
||||||
|
package: packageName,
|
||||||
|
mirrors: String(mirrors ?? ''),
|
||||||
|
ok: false
|
||||||
|
}, request.headers);
|
||||||
return json({ error: 'Internal server error' }, { status: 500 });
|
return json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { getPythonMajorDownloads } from '$lib/api.js';
|
import { getPythonMajorDownloads } from '$lib/api.js';
|
||||||
|
import { trackApiEvent } from '$lib/analytics.js';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params, url }) => {
|
export const GET: RequestHandler = async ({ params, url, request }) => {
|
||||||
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
||||||
const version = url.searchParams.get('version');
|
const version = url.searchParams.get('version');
|
||||||
|
|
||||||
@@ -26,10 +27,19 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
|||||||
downloads: r.downloads
|
downloads: r.downloads
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
trackApiEvent('api_python_major', `/api/packages/${encodeURIComponent(packageName)}/python_major`, {
|
||||||
|
package: packageName,
|
||||||
|
version: String(version ?? ''),
|
||||||
|
ok: true
|
||||||
|
}, request.headers);
|
||||||
return json(response);
|
return json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching Python major downloads:', error);
|
console.error('Error fetching Python major downloads:', error);
|
||||||
|
trackApiEvent('api_python_major', `/api/packages/${encodeURIComponent(packageName)}/python_major`, {
|
||||||
|
package: packageName,
|
||||||
|
version: String(version ?? ''),
|
||||||
|
ok: false
|
||||||
|
}, request.headers);
|
||||||
return json({ error: 'Internal server error' }, { status: 500 });
|
return json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { getPythonMinorDownloads } from '$lib/api.js';
|
import { getPythonMinorDownloads } from '$lib/api.js';
|
||||||
|
import { trackApiEvent } from '$lib/analytics.js';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params, url }) => {
|
export const GET: RequestHandler = async ({ params, url, request }) => {
|
||||||
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
||||||
const version = url.searchParams.get('version');
|
const version = url.searchParams.get('version');
|
||||||
|
|
||||||
@@ -26,10 +27,19 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
|||||||
downloads: r.downloads
|
downloads: r.downloads
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
trackApiEvent('api_python_minor', `/api/packages/${encodeURIComponent(packageName)}/python_minor`, {
|
||||||
|
package: packageName,
|
||||||
|
version: String(version ?? ''),
|
||||||
|
ok: true
|
||||||
|
}, request.headers);
|
||||||
return json(response);
|
return json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching Python minor downloads:', error);
|
console.error('Error fetching Python minor downloads:', error);
|
||||||
|
trackApiEvent('api_python_minor', `/api/packages/${encodeURIComponent(packageName)}/python_minor`, {
|
||||||
|
package: packageName,
|
||||||
|
version: String(version ?? ''),
|
||||||
|
ok: false
|
||||||
|
}, request.headers);
|
||||||
return json({ error: 'Internal server error' }, { status: 500 });
|
return json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -4,6 +4,7 @@ import { getRecentDownloads } from '$lib/api.js';
|
|||||||
import { RECENT_CATEGORIES } from '$lib/database.js';
|
import { RECENT_CATEGORIES } from '$lib/database.js';
|
||||||
import { RateLimiter } from '$lib/redis.js';
|
import { RateLimiter } from '$lib/redis.js';
|
||||||
import { DataProcessor } from '$lib/data-processor.js';
|
import { DataProcessor } from '$lib/data-processor.js';
|
||||||
|
import { trackApiEvent } from '$lib/analytics.js';
|
||||||
|
|
||||||
const rateLimiter = new RateLimiter();
|
const rateLimiter = new RateLimiter();
|
||||||
|
|
||||||
@@ -62,9 +63,19 @@ export const GET: RequestHandler = async ({ params, url, request }) => {
|
|||||||
'X-RateLimit-Reset': (Math.floor(Date.now() / 1000) + 3600).toString()
|
'X-RateLimit-Reset': (Math.floor(Date.now() / 1000) + 3600).toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
trackApiEvent('api_recent', `/api/packages/${encodeURIComponent(packageName)}/recent`, {
|
||||||
|
package: packageName,
|
||||||
|
period: String(category ?? ''),
|
||||||
|
ok: true
|
||||||
|
}, request.headers);
|
||||||
return json(response, { headers });
|
return json(response, { headers });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching recent downloads:', error);
|
console.error('Error fetching recent downloads:', error);
|
||||||
|
trackApiEvent('api_recent', `/api/packages/${encodeURIComponent(packageName)}/recent`, {
|
||||||
|
package: packageName,
|
||||||
|
period: String(category ?? ''),
|
||||||
|
ok: false
|
||||||
|
}, request.headers);
|
||||||
return json({ error: 'Internal server error' }, { status: 500 });
|
return json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2,8 +2,9 @@ import { json } from '@sveltejs/kit';
|
|||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { prisma } from '$lib/prisma.js';
|
import { prisma } from '$lib/prisma.js';
|
||||||
import { DataProcessor } from '$lib/data-processor.js';
|
import { DataProcessor } from '$lib/data-processor.js';
|
||||||
|
import { trackApiEvent } from '$lib/analytics.js';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params, request }) => {
|
||||||
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
||||||
if (!packageName || packageName === '__all__') {
|
if (!packageName || packageName === '__all__') {
|
||||||
return json({ error: 'Invalid package name' }, { status: 400 });
|
return json({ error: 'Invalid package name' }, { status: 400 });
|
||||||
@@ -41,6 +42,10 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
const pythonMajorTotals = Object.fromEntries(pyMajorAll.map(r => [r.category, Number(r._sum.downloads || 0)]));
|
const pythonMajorTotals = Object.fromEntries(pyMajorAll.map(r => [r.category, Number(r._sum.downloads || 0)]));
|
||||||
const pythonMinorTotals = Object.fromEntries(pyMinorAll.map(r => [r.category, Number(r._sum.downloads || 0)]));
|
const pythonMinorTotals = Object.fromEntries(pyMinorAll.map(r => [r.category, Number(r._sum.downloads || 0)]));
|
||||||
|
|
||||||
|
trackApiEvent('api_summary', `/api/packages/${encodeURIComponent(packageName)}/summary`, {
|
||||||
|
package: packageName,
|
||||||
|
ok: true
|
||||||
|
}, request.headers);
|
||||||
return json({
|
return json({
|
||||||
package: packageName,
|
package: packageName,
|
||||||
type: 'summary',
|
type: 'summary',
|
||||||
@@ -53,6 +58,10 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error building package summary:', error);
|
console.error('Error building package summary:', error);
|
||||||
|
trackApiEvent('api_summary', `/api/packages/${encodeURIComponent(packageName)}/summary`, {
|
||||||
|
package: packageName,
|
||||||
|
ok: false
|
||||||
|
}, request.headers);
|
||||||
return json({ error: 'Internal server error' }, { status: 500 });
|
return json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { getSystemDownloads } from '$lib/api.js';
|
import { getSystemDownloads } from '$lib/api.js';
|
||||||
|
import { trackApiEvent } from '$lib/analytics.js';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params, url }) => {
|
export const GET: RequestHandler = async ({ params, url, request }) => {
|
||||||
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || '';
|
||||||
const os = url.searchParams.get('os');
|
const os = url.searchParams.get('os');
|
||||||
|
|
||||||
@@ -26,10 +27,19 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
|||||||
downloads: r.downloads
|
downloads: r.downloads
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
trackApiEvent('api_system', `/api/packages/${encodeURIComponent(packageName)}/system`, {
|
||||||
|
package: packageName,
|
||||||
|
os: String(os ?? ''),
|
||||||
|
ok: true
|
||||||
|
}, request.headers);
|
||||||
return json(response);
|
return json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching system downloads:', error);
|
console.error('Error fetching system downloads:', error);
|
||||||
|
trackApiEvent('api_system', `/api/packages/${encodeURIComponent(packageName)}/system`, {
|
||||||
|
package: packageName,
|
||||||
|
os: String(os ?? ''),
|
||||||
|
ok: false
|
||||||
|
}, request.headers);
|
||||||
return json({ error: 'Internal server error' }, { status: 500 });
|
return json({ error: 'Internal server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { afterNavigate } from '$app/navigation';
|
||||||
|
import { PLAUSIBLE_DOMAIN, PLAUSIBLE_EVENT_ENDPOINT, PLAUSIBLE_CAPTURE_LOCALHOST } from '$lib/plausible-config.js';
|
||||||
import LoadingSpinner from '$lib/components/LoadingSpinner.svelte';
|
import LoadingSpinner from '$lib/components/LoadingSpinner.svelte';
|
||||||
import { MetaTags } from 'svelte-meta-tags';
|
import { MetaTags } from 'svelte-meta-tags';
|
||||||
const { data }: { data: PageData } = $props();
|
const { data }: { data: PageData } = $props();
|
||||||
@@ -12,6 +14,37 @@
|
|||||||
let charts: any[] = [];
|
let charts: any[] = [];
|
||||||
let installerCanvas: HTMLCanvasElement | null = $state(null);
|
let installerCanvas: HTMLCanvasElement | null = $state(null);
|
||||||
|
|
||||||
|
// Plausible: track package views
|
||||||
|
let lastTrackedPackage: string = '';
|
||||||
|
let plausibleInitDone: boolean = false;
|
||||||
|
let plausibleTrack: ((name: string, options?: any) => void) | null = null;
|
||||||
|
|
||||||
|
async function ensurePlausible() {
|
||||||
|
if (plausibleInitDone && plausibleTrack) return;
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
const mod = await import('@plausible-analytics/tracker');
|
||||||
|
const domain = PLAUSIBLE_DOMAIN || window.location.hostname;
|
||||||
|
const endpoint = PLAUSIBLE_EVENT_ENDPOINT || undefined;
|
||||||
|
mod.init({
|
||||||
|
domain,
|
||||||
|
endpoint,
|
||||||
|
captureOnLocalhost: PLAUSIBLE_CAPTURE_LOCALHOST,
|
||||||
|
logging: false,
|
||||||
|
bindToWindow: false
|
||||||
|
});
|
||||||
|
plausibleTrack = mod.track;
|
||||||
|
plausibleInitDone = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function trackPackageView(pkg: string) {
|
||||||
|
if (!pkg || lastTrackedPackage === pkg) return;
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
await ensurePlausible();
|
||||||
|
if (!plausibleTrack) return;
|
||||||
|
plausibleTrack('package_view', { props: { package: pkg } });
|
||||||
|
lastTrackedPackage = pkg;
|
||||||
|
}
|
||||||
|
|
||||||
const numberFormatter = new Intl.NumberFormat(undefined);
|
const numberFormatter = new Intl.NumberFormat(undefined);
|
||||||
const compactFormatter = new Intl.NumberFormat(undefined, {
|
const compactFormatter = new Intl.NumberFormat(undefined, {
|
||||||
notation: 'compact',
|
notation: 'compact',
|
||||||
@@ -138,6 +171,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
// Track initial view
|
||||||
|
if (data?.packageName) trackPackageView(data.packageName);
|
||||||
|
|
||||||
loadAndRenderChart(overallCanvas, 'overall');
|
loadAndRenderChart(overallCanvas, 'overall');
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
loadAndRenderChart(pyMajorCanvas, 'python_major');
|
loadAndRenderChart(pyMajorCanvas, 'python_major');
|
||||||
@@ -147,6 +183,19 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterNavigate((nav) => {
|
||||||
|
let url: URL;
|
||||||
|
const maybe = (nav as any);
|
||||||
|
if (maybe && maybe.to && maybe.to.url instanceof URL) {
|
||||||
|
url = maybe.to.url as URL;
|
||||||
|
} else {
|
||||||
|
url = new URL(window.location.href);
|
||||||
|
}
|
||||||
|
const match = url.pathname.match(/^\/packages\/([^\/]+)/);
|
||||||
|
const pkg = match?.[1] ? decodeURIComponent(match[1]) : '';
|
||||||
|
if (pkg) void trackPackageView(pkg);
|
||||||
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
for (const c of charts) {
|
for (const c of charts) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
6
src/types/plausible-tracker.d.ts
vendored
Normal file
6
src/types/plausible-tracker.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
declare module '@plausible-analytics/tracker' {
|
||||||
|
export function init(options: any): void;
|
||||||
|
export const track: (name: string, options?: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user