Merge branch 'dev' into jajaja

# Conflicts:
#	package.json
#	yarn.lock
This commit is contained in:
Mike Cao 2025-03-31 23:31:09 -05:00
commit c71e9b5707
11 changed files with 126 additions and 30 deletions

View file

@ -4,6 +4,18 @@ export const config = {
matcher: '/:path*', 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) { function customCollectEndpoint(req) {
const collectEndpoint = process.env.COLLECT_API_ENDPOINT; const collectEndpoint = process.env.COLLECT_API_ENDPOINT;
@ -13,7 +25,7 @@ function customCollectEndpoint(req) {
if (pathname.endsWith(collectEndpoint)) { if (pathname.endsWith(collectEndpoint)) {
url.pathname = '/api/send'; 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))) { if (names.find(name => pathname.endsWith(name))) {
url.pathname = '/script.js'; url.pathname = '/script.js';
return NextResponse.rewrite(url); return NextResponse.rewrite(url, { headers: trackerHeaders });
} }
} }
} }

View file

@ -4,4 +4,7 @@ export default {
transform: { transform: {
'^.+\\.(ts|tsx)$': 'ts-jest', '^.+\\.(ts|tsx)$': 'ts-jest',
}, },
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
}; };

View file

@ -1,6 +0,0 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}

View file

@ -27,19 +27,19 @@ export function SessionActivity({
return ( return (
<div className={styles.timeline}> <div className={styles.timeline}>
{data.map(({ eventId, createdAt, urlPath, eventName, visitId }) => { {data.map(({ id, createdAt, urlPath, eventName, visitId }) => {
const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt)); const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt));
lastDay = createdAt; lastDay = createdAt;
return ( return (
<Fragment key={eventId}> <Fragment key={id}>
{showHeader && ( {showHeader && (
<div className={styles.header}>{formatTimezoneDate(createdAt, 'PPPP')}</div> <div className={styles.header}>{formatTimezoneDate(createdAt, 'PPPP')}</div>
)} )}
<div key={eventId} className={styles.row}> <div className={styles.row}>
<div className={styles.time}> <div className={styles.time}>
<StatusLight color={`#${visitId?.substring(0, 6)}`}> <StatusLight color={`#${visitId?.substring(0, 6)}`}>
{formatTimezoneDate(createdAt, 'h:mm:ss aaa')} {formatTimezoneDate(createdAt, 'pp')}
</StatusLight> </StatusLight>
</div> </div>
<Icon>{eventName ? <Icons.Bolt /> : <Icons.Eye />}</Icon> <Icon>{eventName ? <Icons.Bolt /> : <Icons.Eye />}</Icon>

View file

@ -1,7 +1,7 @@
import { parseRequest } from '@/lib/request'; import { parseRequest } from '@/lib/request';
import { json } from '@/lib/response'; import { json } from '@/lib/response';
export async function GET(request: Request) { export async function POST(request: Request) {
const { auth, error } = await parseRequest(request); const { auth, error } = await parseRequest(request);
if (error) { if (error) {

View file

@ -34,11 +34,6 @@ const schema = z.object({
export async function POST(request: Request) { export async function POST(request: Request) {
try { 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 }); const { body, error } = await parseRequest(request, schema, { skipAuth: true });
if (error) { if (error) {
@ -86,6 +81,11 @@ export async function POST(request: Request) {
const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } = const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } =
await getClientInfo(request, payload); await getClientInfo(request, payload);
// Bot check
if (!process.env.DISABLE_BOT_CHECK && isbot(userAgent)) {
return json({ beep: 'boop' });
}
// IP block // IP block
if (hasBlockedIp(ip)) { if (hasBlockedIp(ip)) {
return forbidden(); return forbidden();

View file

@ -7,7 +7,7 @@ const formats = {
millisecond: 'T', millisecond: 'T',
second: 'pp', second: 'pp',
minute: 'p', minute: 'p',
hour: 'h:mm aaa - PP', hour: 'p - PP',
day: 'PPPP', day: 'PPPP',
week: 'PPPP', week: 'PPPP',
month: 'LLLL yyyy', month: 'LLLL yyyy',

View file

@ -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 groupedFilter = (data: any[]) => {
const groups = { _other: 0 }; const groups = { _other: 0 };
for (const { x, y } of data) { for (const { x, y } of data) {
for (const { domain, match } of GROUPED_DOMAINS) { const domain = getDomain(x);
if (Array.isArray(match) ? match.some(str => x.includes(str)) : x.includes(match)) { if (!groups[domain]) {
if (!groups[domain]) { groups[domain] = 0;
groups[domain] = 0;
}
groups[domain] += +y;
}
} }
groups._other += +y; groups[domain] += +y;
} }
return Object.keys(groups) return Object.keys(groups)

View file

@ -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();
});
});

View file

@ -11,15 +11,15 @@ export function renderDateLabels(unit: string, locale: string) {
switch (unit) { switch (unit) {
case 'minute': case 'minute':
return formatDate(d, 'h:mm', locale); return formatDate(d, 'p', locale).split(' ')[0];
case 'hour': case 'hour':
return formatDate(d, 'p', locale); return formatDate(d, 'p', locale);
case 'day': case 'day':
return formatDate(d, 'MMM d', locale); return formatDate(d, 'PP', locale).replace(/\W*20\d{2}\W*/, ''); // Remove year
case 'month': case 'month':
return formatDate(d, 'MMM', locale); return formatDate(d, 'MMM', locale);
case 'year': case 'year':
return formatDate(d, 'YYY', locale); return formatDate(d, 'yyyy', locale);
default: default:
return label; return label;
} }

View file

@ -23,6 +23,7 @@
"incremental": false, "incremental": false,
"types": ["jest"], "types": ["jest"],
"typeRoots": ["node_modules/@types"], "typeRoots": ["node_modules/@types"],
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },