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 { getOverallDownloads, getPythonMajorDownloads, getPythonMinorDownloads, getSystemDownloads, getInstallerDownloads, getVersionDownloads } from '$lib/api.js';
|
||||
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
|
||||
import { trackApiEvent } from '$lib/analytics.js';
|
||||
|
||||
const cache = new CacheManager();
|
||||
|
||||
const width = 1200;
|
||||
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 type = params.type || 'overall';
|
||||
const chartType = (url.searchParams.get('chart') || 'line').toLowerCase(); // 'line' | 'bar'
|
||||
@@ -102,6 +103,13 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
labels,
|
||||
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' } });
|
||||
}
|
||||
|
||||
@@ -140,9 +148,22 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
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' } });
|
||||
} catch (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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,8 +2,9 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { prisma } from '$lib/prisma.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, '-') || '';
|
||||
if (!packageName || packageName === '__all__') {
|
||||
return json({ error: 'Invalid package name' }, { status: 400 });
|
||||
@@ -23,9 +24,17 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
type: 'installer_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);
|
||||
} catch (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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
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 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);
|
||||
} catch (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 });
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
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 version = url.searchParams.get('version');
|
||||
|
||||
@@ -26,10 +27,19 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
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);
|
||||
} catch (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 });
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
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 version = url.searchParams.get('version');
|
||||
|
||||
@@ -26,10 +27,19 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
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);
|
||||
} catch (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 });
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { getRecentDownloads } from '$lib/api.js';
|
||||
import { RECENT_CATEGORIES } from '$lib/database.js';
|
||||
import { RateLimiter } from '$lib/redis.js';
|
||||
import { DataProcessor } from '$lib/data-processor.js';
|
||||
import { trackApiEvent } from '$lib/analytics.js';
|
||||
|
||||
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()
|
||||
};
|
||||
|
||||
trackApiEvent('api_recent', `/api/packages/${encodeURIComponent(packageName)}/recent`, {
|
||||
package: packageName,
|
||||
period: String(category ?? ''),
|
||||
ok: true
|
||||
}, request.headers);
|
||||
return json(response, { headers });
|
||||
} catch (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 });
|
||||
}
|
||||
};
|
||||
@@ -2,8 +2,9 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { prisma } from '$lib/prisma.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, '-') || '';
|
||||
if (!packageName || packageName === '__all__') {
|
||||
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 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({
|
||||
package: packageName,
|
||||
type: 'summary',
|
||||
@@ -53,6 +58,10 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
});
|
||||
} catch (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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
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 os = url.searchParams.get('os');
|
||||
|
||||
@@ -26,10 +27,19 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
downloads: r.downloads
|
||||
}))
|
||||
};
|
||||
|
||||
trackApiEvent('api_system', `/api/packages/${encodeURIComponent(packageName)}/system`, {
|
||||
package: packageName,
|
||||
os: String(os ?? ''),
|
||||
ok: true
|
||||
}, request.headers);
|
||||
return json(response);
|
||||
} catch (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 });
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
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 { MetaTags } from 'svelte-meta-tags';
|
||||
const { data }: { data: PageData } = $props();
|
||||
@@ -12,6 +14,37 @@
|
||||
let charts: any[] = [];
|
||||
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 compactFormatter = new Intl.NumberFormat(undefined, {
|
||||
notation: 'compact',
|
||||
@@ -138,6 +171,9 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Track initial view
|
||||
if (data?.packageName) trackPackageView(data.packageName);
|
||||
|
||||
loadAndRenderChart(overallCanvas, 'overall');
|
||||
requestAnimationFrame(() => {
|
||||
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(() => {
|
||||
for (const c of charts) {
|
||||
try {
|
||||
@@ -241,14 +290,14 @@
|
||||
href={meta.pypiUrl}
|
||||
rel="noopener"
|
||||
target="_blank">View on PyPI</a
|
||||
>
|
||||
>
|
||||
{#if meta.homePage}
|
||||
<a
|
||||
class="inline-flex items-center rounded-full border border-gray-700 bg-gray-900 px-2.5 py-1 text-gray-300 hover:bg-gray-800"
|
||||
href={meta.homePage}
|
||||
rel="noopener"
|
||||
target="_blank">Homepage</a
|
||||
>
|
||||
>
|
||||
{/if}
|
||||
{#if meta.projectUrls}
|
||||
{#each Object.entries(meta.projectUrls).filter(([label, url]) => !['homepage'].includes(label.toLowerCase())) as [label, url]}
|
||||
@@ -258,7 +307,7 @@
|
||||
href={url}
|
||||
rel="noopener"
|
||||
target="_blank">{label}</a
|
||||
>
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -284,11 +333,11 @@
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase"
|
||||
>Period</th
|
||||
>
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-400 uppercase"
|
||||
>Downloads</th
|
||||
>
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800 bg-gray-950">
|
||||
@@ -322,11 +371,11 @@
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase"
|
||||
>Category</th
|
||||
>
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-400 uppercase"
|
||||
>Downloads</th
|
||||
>
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800 bg-gray-950">
|
||||
@@ -362,11 +411,11 @@
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase"
|
||||
>System</th
|
||||
>
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-400 uppercase"
|
||||
>Downloads</th
|
||||
>
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800 bg-gray-950">
|
||||
@@ -402,11 +451,11 @@
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase"
|
||||
>Version</th
|
||||
>
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-400 uppercase"
|
||||
>Downloads</th
|
||||
>
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800 bg-gray-950">
|
||||
@@ -424,7 +473,7 @@
|
||||
</td>
|
||||
<td class="px-6 py-3 text-right text-sm font-semibold text-gray-100"
|
||||
>{formatNumber(row.downloads)}</td
|
||||
>
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
{:catch _}
|
||||
@@ -545,7 +594,7 @@
|
||||
href={endpointUrl(ep.path)}
|
||||
rel="noopener"
|
||||
target="_blank">{endpointUrl(ep.path)}</a
|
||||
>
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="ml-3 shrink-0 rounded-md border border-blue-700 bg-blue-800 px-2 py-1 text-xs text-white hover:bg-blue-700"
|
||||
|
||||
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