Files
umami/tests/api/report-migration.spec.ts
T

191 lines
6.8 KiB
TypeScript

import { serializeAnalyticsQuery } from '../../src/lib/analytics-query';
import { expect, test } from './fixtures';
import { dateRange } from './helpers/dates';
import { uniqueName } from './helpers/entities';
const calculations = [
['journey', 'journeys', { steps: 3 }],
['retention', 'retention', {}],
['breakdown', 'breakdown', { fields: ['path', 'country'] }],
['attribution', 'attribution', { model: 'first-click', type: 'path', step: '/pricing' }],
['heatmap', 'heatmaps', { mode: 'click', urlPath: '/' }],
[
'funnel',
'funnels/stats',
{
window: 60,
steps: [
{ type: 'path', value: '/' },
{ type: 'path', value: '/pricing' },
],
},
],
['goal', 'goals/stats', { type: 'path', value: '/' }],
] as const;
// SQL rankings can have ties, so compare result content independently of tied row order.
function canonical(value: any): any {
if (Array.isArray(value))
return value.map(canonical).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
if (value && typeof value === 'object')
return Object.fromEntries(
Object.keys(value)
.sort()
.map(key => [key, canonical(value[key])]),
);
return value;
}
for (const [type, path, parameters] of calculations) {
test(`GET ${path} matches legacy ${type} results and access`, async ({
admin,
share,
viewer,
seed,
}) => {
const range = { ...dateRange(seed), timezone: 'UTC', unit: 'day' };
const body = {
websiteId: seed.website.id,
type,
filters: range,
parameters: {
startDate: new Date(range.startAt).toISOString(),
endDate: new Date(range.endAt).toISOString(),
timezone: 'UTC',
unit: 'day',
...parameters,
},
};
const url = `/api/websites/${seed.website.id}/${path}`;
const params = serializeAnalyticsQuery({ ...range, ...parameters });
const legacy = await admin.post(`/api/reports/${type}`, body);
const result = await admin.get(url, { params });
expect(legacy.status).toBe(200);
expect(result.status).toBe(200);
expect(canonical(result.body)).toEqual(canonical(legacy.body));
expect((await viewer.get(url, { params })).status).toBe(401);
expect((await (await share()).get(url, { params })).status).toBe(
type === 'heatmap' ? 401 : 200,
);
expect((await admin.get(url)).status).toBe(400);
});
}
for (const [type, parameters] of [
[
'funnel',
{
window: 60,
steps: [
{ type: 'path', value: '/' },
{ type: 'event', value: 'signup' },
],
},
],
['goal', { type: 'path', value: '/' }],
] as const) {
test(`${type} definitions preserve IDs and scope saved stats to the website`, async ({
admin,
viewer,
share,
seed,
}) => {
const base = `/api/websites/${seed.website.id}/${type}s`;
const created = await admin.post(base, { name: uniqueName(type), parameters });
expect(created.status).toBe(200);
const id = created.body.id;
try {
expect((await admin.get(`/api/reports/${id}`)).body.id).toBe(id);
expect((await admin.get(`${base}/${id}`)).body.parameters).toEqual(parameters);
expect((await (await share()).get(base)).status).toBe(200);
const range = dateRange(seed);
const saved = await admin.get(`${base}/${id}/stats`, { params: range });
const preview = await admin.get(`${base}/stats`, {
params: serializeAnalyticsQuery({ ...range, ...parameters }),
});
expect(saved.status).toBe(200);
expect(saved.body).toEqual(preview.body);
expect((await viewer.post(`${base}/${id}`, { name: 'Denied', parameters })).status).toBe(401);
const otherBase = `/api/websites/${seed.website2.id}/${type}s/${id}`;
expect((await admin.get(otherBase)).status).toBe(404);
expect((await admin.post(otherBase, { name: 'Wrong website', parameters })).status).toBe(404);
expect((await admin.del(otherBase)).status).toBe(404);
const otherType = type === 'goal' ? 'funnels' : 'goals';
expect((await admin.get(`/api/websites/${seed.website.id}/${otherType}/${id}`)).status).toBe(
404,
);
const updated = await admin.post(`${base}/${id}`, {
name: 'Updated',
websiteId: seed.website2.id,
type: otherType,
parameters,
});
expect(updated.status).toBe(200);
expect(updated.body).toMatchObject({ id, name: 'Updated', websiteId: seed.website.id, type });
} finally {
expect((await admin.del(`${base}/${id}`)).status).toBe(200);
}
expect((await admin.get(`${base}/${id}`)).status).toBe(404);
});
}
test('performance and UTM datasets reproduce legacy aggregates', async ({
admin,
viewer,
share,
seed,
}) => {
const range = { ...dateRange(seed), timezone: 'UTC', unit: 'day' };
const body = (type: string) => ({
websiteId: seed.website.id,
type,
filters: range,
parameters: {
startDate: new Date(range.startAt).toISOString(),
endDate: new Date(range.endAt).toISOString(),
timezone: 'UTC',
unit: 'day',
metric: 'lcp',
},
});
const base = `/api/websites/${seed.website.id}`;
const performance = await admin.post('/api/reports/performance', body('performance'));
expect(performance.status).toBe(200);
for (const [path, key] of [
['performance/stats', 'summary'],
['performance/chart', 'chart'],
] as const) {
const response = await admin.get(`${base}/${path}`, { params: range });
expect(response.status).toBe(200);
expect(canonical(key === 'chart' ? response.body.chart : response.body)).toEqual(
canonical(performance.body[key]),
);
expect((await viewer.get(`${base}/${path}`, { params: range })).status).toBe(401);
expect((await (await share()).get(`${base}/${path}`, { params: range })).status).toBe(200);
}
for (const [type, key] of [
['path', 'pages'],
['title', 'pageTitles'],
['device', 'devices'],
['browser', 'browsers'],
]) {
const response = await admin.get(`${base}/performance/metrics`, { params: { ...range, type } });
expect(response.status).toBe(200);
expect(canonical(response.body)).toEqual(canonical(performance.body[key]));
}
const utm = await admin.post('/api/reports/utm', body('utm'));
expect(utm.status).toBe(200);
for (const type of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']) {
const response = await admin.get(`${base}/utm/metrics`, { params: { ...range, type } });
expect(response.status).toBe(200);
expect(canonical(response.body)).toEqual(canonical(utm.body[type]));
}
expect(
(await admin.get(`${base}/utm/metrics`, { params: { ...range, type: 'invalid' } })).status,
).toBe(400);
expect(
(await admin.get(`${base}/performance/metrics`, { params: { ...range, type: 'invalid' } }))
.status,
).toBe(400);
});