umami tracking
This commit is contained in:
parent
072feaf373
commit
e56aa1aa03
@ -19,6 +19,7 @@ import type {
|
||||
VisaSponsorStatusResponse,
|
||||
VisaSponsor,
|
||||
} from '../../shared/types';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
|
||||
const API_BASE = '/api';
|
||||
|
||||
@ -116,6 +117,12 @@ export async function searchUkVisaJobs(input: {
|
||||
searchTerm?: string;
|
||||
page?: number;
|
||||
}): Promise<UkVisaJobsSearchResponse> {
|
||||
if (input.searchTerm?.trim()) {
|
||||
trackEvent('ukvisajobs_search', {
|
||||
searchTerm: input.searchTerm.trim(),
|
||||
page: input.page ?? 1,
|
||||
});
|
||||
}
|
||||
return fetchApi<UkVisaJobsSearchResponse>('/ukvisajobs/search', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
@ -202,6 +209,13 @@ export async function searchVisaSponsors(input: {
|
||||
limit?: number;
|
||||
minScore?: number;
|
||||
}): Promise<VisaSponsorSearchResponse> {
|
||||
if (input.query?.trim()) {
|
||||
trackEvent('visa_sponsor_search', {
|
||||
query: input.query.trim(),
|
||||
limit: input.limit,
|
||||
minScore: input.minScore,
|
||||
});
|
||||
}
|
||||
return fetchApi<VisaSponsorSearchResponse>('/visa-sponsors/search', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
|
||||
14
orchestrator/src/client/lib/analytics.ts
Normal file
14
orchestrator/src/client/lib/analytics.ts
Normal file
@ -0,0 +1,14 @@
|
||||
type UmamiTracker = {
|
||||
track: (event: string, data?: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
umami?: UmamiTracker;
|
||||
}
|
||||
}
|
||||
|
||||
export function trackEvent(event: string, data?: Record<string, unknown>) {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.umami?.track(event, data);
|
||||
}
|
||||
@ -6,17 +6,18 @@ import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { apiRouter } from './api/index.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function buildBasicAuthMiddleware() {
|
||||
function createBasicAuthGuard() {
|
||||
const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER || '';
|
||||
const BASIC_AUTH_PASSWORD = process.env.BASIC_AUTH_PASSWORD || '';
|
||||
const basicAuthEnabled = BASIC_AUTH_USER.length > 0 && BASIC_AUTH_PASSWORD.length > 0;
|
||||
|
||||
function isAuthorized(req: express.Request): boolean {
|
||||
if (!basicAuthEnabled) return true;
|
||||
if (!basicAuthEnabled) return false;
|
||||
const authHeader = req.headers.authorization || '';
|
||||
if (!authHeader.startsWith('Basic ')) return false;
|
||||
const encoded = authHeader.slice('Basic '.length).trim();
|
||||
@ -46,16 +47,33 @@ function buildBasicAuthMiddleware() {
|
||||
return !['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase());
|
||||
}
|
||||
|
||||
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const middleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (!basicAuthEnabled || !requiresAuth(req.method, req.path)) return next();
|
||||
if (isAuthorized(req)) return next();
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Job Ops"');
|
||||
res.status(401).send('Authentication required');
|
||||
};
|
||||
|
||||
return {
|
||||
middleware,
|
||||
isAuthorized,
|
||||
basicAuthEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
function injectUmami(html: string): string {
|
||||
const snippet =
|
||||
'<script defer src="https://umami.dakheera47.com/script.js" data-website-id="0dc42ed1-87c3-4ac0-9409-5a9b9588fe66"></script>';
|
||||
if (html.includes(snippet)) return html;
|
||||
if (html.includes('</head>')) {
|
||||
return html.replace('</head>', `${snippet}\n</head>`);
|
||||
}
|
||||
return `${html}\n${snippet}`;
|
||||
}
|
||||
|
||||
export function createApp() {
|
||||
const app = express();
|
||||
const authGuard = createBasicAuthGuard();
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
@ -71,7 +89,7 @@ export function createApp() {
|
||||
});
|
||||
|
||||
// Optional Basic Auth for write access (read-only by default)
|
||||
app.use(buildBasicAuthMiddleware());
|
||||
app.use(authGuard.middleware);
|
||||
|
||||
// API routes
|
||||
app.use('/api', apiRouter);
|
||||
@ -93,8 +111,20 @@ export function createApp() {
|
||||
app.use(express.static(clientDir));
|
||||
|
||||
// SPA fallback
|
||||
app.get('*', (_req, res) => {
|
||||
res.sendFile(join(clientDir, 'index.html'));
|
||||
const indexPath = join(clientDir, 'index.html');
|
||||
let cachedIndexHtml: string | null = null;
|
||||
app.get('*', async (req, res) => {
|
||||
if (!req.accepts('html')) {
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
if (!cachedIndexHtml) {
|
||||
cachedIndexHtml = await readFile(indexPath, 'utf-8');
|
||||
}
|
||||
const isAuthenticated = authGuard.basicAuthEnabled && authGuard.isAuthorized(req);
|
||||
const html = isAuthenticated ? cachedIndexHtml : injectUmami(cachedIndexHtml);
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.send(html);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user