diff --git a/docker/middleware.ts b/docker/middleware.ts index 584da8d1..1ab60048 100644 --- a/docker/middleware.ts +++ b/docker/middleware.ts @@ -4,6 +4,18 @@ export const config = { matcher: '/:path*', }; +const apiHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Methods': 'GET, DELETE, POST, PUT', + 'Access-Control-Max-Age': process.env.CORS_MAX_AGE || '86400', +}; + +const trackerHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'public, max-age=86400, must-revalidate', +}; + function customCollectEndpoint(req) { const collectEndpoint = process.env.COLLECT_API_ENDPOINT; @@ -13,7 +25,7 @@ function customCollectEndpoint(req) { if (pathname.endsWith(collectEndpoint)) { url.pathname = '/api/send'; - return NextResponse.rewrite(url); + return NextResponse.rewrite(url, { headers: apiHeaders }); } } } @@ -28,7 +40,7 @@ function customScriptName(req) { if (names.find(name => pathname.endsWith(name))) { url.pathname = '/script.js'; - return NextResponse.rewrite(url); + return NextResponse.rewrite(url, { headers: trackerHeaders }); } } } diff --git a/jest.config.ts b/jest.config.ts index 73738651..d06ac09a 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,4 +4,7 @@ export default { transform: { '^.+\\.(ts|tsx)$': 'ts-jest', }, + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, }; diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 5875dc5b..00000000 --- a/jsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": "src" - }, - "include": ["src"] -} diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx index a39ed919..f1a71be0 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx @@ -27,19 +27,19 @@ export function SessionActivity({ return (
- {data.map(({ eventId, createdAt, urlPath, eventName, visitId }) => { + {data.map(({ id, createdAt, urlPath, eventName, visitId }) => { const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt)); lastDay = createdAt; return ( - + {showHeader && (
{formatTimezoneDate(createdAt, 'PPPP')}
)} -
+
- {formatTimezoneDate(createdAt, 'h:mm:ss aaa')} + {formatTimezoneDate(createdAt, 'pp')}
{eventName ? : } diff --git a/src/app/api/auth/verify/route.ts b/src/app/api/auth/verify/route.ts index 4d98b554..5f8543a5 100644 --- a/src/app/api/auth/verify/route.ts +++ b/src/app/api/auth/verify/route.ts @@ -1,7 +1,7 @@ import { parseRequest } from '@/lib/request'; import { json } from '@/lib/response'; -export async function GET(request: Request) { +export async function POST(request: Request) { const { auth, error } = await parseRequest(request); if (error) { diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index bd255eaf..482aad5c 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -34,11 +34,6 @@ const schema = z.object({ export async function POST(request: Request) { try { - // Bot check - if (!process.env.DISABLE_BOT_CHECK && isbot(request.headers.get('user-agent'))) { - return json({ beep: 'boop' }); - } - const { body, error } = await parseRequest(request, schema, { skipAuth: true }); if (error) { @@ -86,6 +81,11 @@ export async function POST(request: Request) { const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } = await getClientInfo(request, payload); + // Bot check + if (!process.env.DISABLE_BOT_CHECK && isbot(userAgent)) { + return json({ beep: 'boop' }); + } + // IP block if (hasBlockedIp(ip)) { return forbidden(); diff --git a/src/components/charts/BarChartTooltip.tsx b/src/components/charts/BarChartTooltip.tsx index 7235d464..49aa7e0b 100644 --- a/src/components/charts/BarChartTooltip.tsx +++ b/src/components/charts/BarChartTooltip.tsx @@ -7,7 +7,7 @@ const formats = { millisecond: 'T', second: 'pp', minute: 'p', - hour: 'h:mm aaa - PP', + hour: 'p - PP', day: 'PPPP', week: 'PPPP', month: 'LLLL yyyy', diff --git a/src/components/metrics/ReferrersTable.tsx b/src/components/metrics/ReferrersTable.tsx index baf92231..c7642ee2 100644 --- a/src/components/metrics/ReferrersTable.tsx +++ b/src/components/metrics/ReferrersTable.tsx @@ -60,19 +60,24 @@ export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) { ); }; + const getDomain = (x: string) => { + for (const { domain, match } of GROUPED_DOMAINS) { + if (Array.isArray(match) ? match.some(str => x.includes(str)) : x.includes(match)) { + return domain; + } + } + return '_other'; + }; + const groupedFilter = (data: any[]) => { const groups = { _other: 0 }; for (const { x, y } of data) { - for (const { domain, match } of GROUPED_DOMAINS) { - if (Array.isArray(match) ? match.some(str => x.includes(str)) : x.includes(match)) { - if (!groups[domain]) { - groups[domain] = 0; - } - groups[domain] += +y; - } + const domain = getDomain(x); + if (!groups[domain]) { + groups[domain] = 0; } - groups._other += +y; + groups[domain] += +y; } return Object.keys(groups) diff --git a/src/lib/__tests__/charts.test.ts b/src/lib/__tests__/charts.test.ts new file mode 100644 index 00000000..e330fadd --- /dev/null +++ b/src/lib/__tests__/charts.test.ts @@ -0,0 +1,81 @@ +import { renderNumberLabels, renderDateLabels } from '../charts'; +import { formatDate } from '../date'; + +// test for renderNumberLabels + +describe('renderNumberLabels', () => { + test.each([ + ['1000000', '1.0m'], + ['2500000', '2.5m'], + ])("formats numbers ≥ 1 million as 'Xm' (%s → %s)", (input, expected) => { + expect(renderNumberLabels(input)).toBe(expected); + }); + + test.each([['150000', '150k']])("formats numbers ≥ 100K as 'Xk' (%s → %s)", (input, expected) => { + expect(renderNumberLabels(input)).toBe(expected); + }); + + test.each([['12500', '12.5k']])( + "formats numbers ≥ 10K as 'X.Xk' (%s → %s)", + (input, expected) => { + expect(renderNumberLabels(input)).toBe(expected); + }, + ); + + test.each([['1500', '1.50k']])("formats numbers ≥ 1K as 'X.XXk' (%s → %s)", (input, expected) => { + expect(renderNumberLabels(input)).toBe(expected); + }); + + test.each([['999', '999']])( + 'calls formatNumber for values < 1000 (%s → %s)', + (input, expected) => { + expect(renderNumberLabels(input)).toBe(expected); + }, + ); + + test.each([ + ['0', '0'], + ['-5000', '-5000'], + ])('handles edge cases correctly (%s → %s)', (input, expected) => { + expect(renderNumberLabels(input)).toBe(expected); + }); +}); + +describe('renderDateLabels', () => { + const mockValues = [{ value: '2024-03-23T10:00:00Z' }, { value: '2024-03-24T15:30:00Z' }]; + + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + jest.spyOn(require('@/lib/date'), 'formatDate'); + }); + + afterEach(() => { + jest.restoreAllMocks(); // Reset spy to prevent interference + }); + + test.each([ + ['minute', 'h:mm', 'en-US'], + ['hour', 'p', 'en-US'], + ['day', 'MMM d', 'en-US'], + ['month', 'MMM', 'en-US'], + ['year', 'yyyy', 'en-US'], + ])('formats date correctly for unit: %s', (unit, expectedFormat, locale) => { + const formatLabel = renderDateLabels(unit, locale); + const formatted = formatLabel('label', 0, mockValues); + + expect(formatDate).toHaveBeenCalledWith(new Date(mockValues[0].value), expectedFormat, locale); + expect(formatted).toBe(formatDate(new Date(mockValues[0].value), expectedFormat, locale)); + }); + + test('returns label for unknown unit', () => { + const formatLabel = renderDateLabels('unknown', 'en-US'); + expect(formatLabel('original-label', 0, mockValues)).toBe('original-label'); + }); + + test('throws error for invalid date input', () => { + const invalidValues = [{ value: 'invalid-date' }]; + const formatLabel = renderDateLabels('day', 'en-US'); + + expect(() => formatLabel('label', 0, invalidValues)).toThrow(); + }); +}); diff --git a/src/lib/charts.ts b/src/lib/charts.ts index d805eefe..957d4962 100644 --- a/src/lib/charts.ts +++ b/src/lib/charts.ts @@ -11,15 +11,15 @@ export function renderDateLabels(unit: string, locale: string) { switch (unit) { case 'minute': - return formatDate(d, 'h:mm', locale); + return formatDate(d, 'p', locale).split(' ')[0]; case 'hour': return formatDate(d, 'p', locale); case 'day': - return formatDate(d, 'MMM d', locale); + return formatDate(d, 'PP', locale).replace(/\W*20\d{2}\W*/, ''); // Remove year case 'month': return formatDate(d, 'MMM', locale); case 'year': - return formatDate(d, 'YYY', locale); + return formatDate(d, 'yyyy', locale); default: return label; } diff --git a/tsconfig.json b/tsconfig.json index efe4861d..1e4f0ae5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,7 @@ "incremental": false, "types": ["jest"], "typeRoots": ["node_modules/@types"], + "baseUrl": ".", "paths": { "@/*": ["./src/*"] },