mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Merge branch 'dev' into jajaja
# Conflicts: # package.json # yarn.lock
This commit is contained in:
commit
c71e9b5707
11 changed files with 126 additions and 30 deletions
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,7 @@ export default {
|
|||
transform: {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -27,19 +27,19 @@ export function SessionActivity({
|
|||
|
||||
return (
|
||||
<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));
|
||||
lastDay = createdAt;
|
||||
|
||||
return (
|
||||
<Fragment key={eventId}>
|
||||
<Fragment key={id}>
|
||||
{showHeader && (
|
||||
<div className={styles.header}>{formatTimezoneDate(createdAt, 'PPPP')}</div>
|
||||
)}
|
||||
<div key={eventId} className={styles.row}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.time}>
|
||||
<StatusLight color={`#${visitId?.substring(0, 6)}`}>
|
||||
{formatTimezoneDate(createdAt, 'h:mm:ss aaa')}
|
||||
{formatTimezoneDate(createdAt, 'pp')}
|
||||
</StatusLight>
|
||||
</div>
|
||||
<Icon>{eventName ? <Icons.Bolt /> : <Icons.Eye />}</Icon>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
81
src/lib/__tests__/charts.test.ts
Normal file
81
src/lib/__tests__/charts.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"incremental": false,
|
||||
"types": ["jest"],
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue