openvidu/openvidu-dashboard/dashboard-ce/e2e/fixtures/analytics.ts

181 lines
5.3 KiB
TypeScript

import type {
AnalyticsBreakdownsResponse,
AnalyticsDimension,
AnalyticsSeriesResponse,
AnalyticsSummary
} from '@openvidu-dashboard/typings';
const MINUTE = 60_000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const SIZES: Record<string, number> = { '5m': 5 * MINUTE, '15m': 15 * MINUTE, '1h': HOUR, '6h': 6 * HOUR, '1d': DAY };
export const ANALYTICS_SUMMARY: AnalyticsSummary = {
from: '2026-10-12T12:00:00.000Z',
to: '2026-10-13T12:00:00.000Z',
scope: 'global',
roomsCreated: 1203,
roomsEnded: 1100,
participantSessions: 9000,
uniqueIdentities: { value: 4120, basis: 'utcDays', days: 7 },
connectionSuccessRate: 97.8,
avgRoomSize: 4,
avgRoomDurationMs: 360_000,
bytesUp: 1_200_000_000_000,
bytesDown: 300_000_000_000,
egressStarted: 12,
egressDurationMs: 7_200_000,
ingressSessions: 30,
ingressDurationMs: 5_400_000,
apiCalls: 5000,
apiErrorRate: 1
};
/** A breakdown key with its count and, for a dimension that has them, its errors. */
export type BreakdownKey = [key: string, count: number, errors?: number];
/** Series values and breakdown keys a suite adds to the CE ones, such as those of the PRO metrics and dimensions. */
export interface AnalyticsFixtures {
series?: Record<string, (index: number) => number | null>;
breakdowns?: Partial<Record<AnalyticsDimension, BreakdownKey[]>>;
}
const BREAKDOWN_KEYS: Partial<Record<AnalyticsDimension, BreakdownKey[]>> = {
sdk: [
['JS', 600],
['SWIFT', 300],
['ANDROID', 100]
],
os: [
['windows', 500],
['macos', 300],
['android', 200]
],
browser: [
['chrome', 700],
['safari', 200],
['firefox', 100]
],
connectionType: [
['udp', 900],
['tcp', 80],
['turn', 20]
],
kind: [
['STANDARD', 950],
['EGRESS', 30],
['AGENT', 20]
],
egressType: [
['room_composite', 10],
['track', 2]
],
egressStatus: [['EGRESS_COMPLETE', 12]],
ingressInputType: [
['rtmp', 20],
['whip', 8],
['url', 2]
]
};
/** What the API resolves `auto` to for a range. */
function resolveGranularity(granularity: string, span: number): string {
if (granularity !== 'auto') {
return granularity;
}
return span <= DAY ? '5m' : span <= 7 * DAY ? '1h' : span <= 90 * DAY ? '6h' : '1d';
}
/** Dense series of `metrics` over the range, with values that never leave a card empty. */
export function analyticsSeries(params: URLSearchParams, fixtures: AnalyticsFixtures = {}): AnalyticsSeriesResponse {
const from = Date.parse(params.get('from') ?? '');
const to = Date.parse(params.get('to') ?? '');
const granularity = resolveGranularity(params.get('granularity') ?? 'auto', to - from);
const size = SIZES[granularity] ?? HOUR;
const starts: number[] = [];
for (let start = Math.floor(from / size) * size; start < to; start += size) {
starts.push(start);
}
const points = (value: (index: number) => number | null) =>
starts.map((start, index): [string, number | null] => [new Date(start).toISOString(), value(index)]);
const series = (params.get('metrics') ?? '').split(',').flatMap((name) => {
const values = fixtures.series?.[name];
if (values !== undefined) {
return [{ name, points: points(values) }];
}
switch (name) {
case 'egressEnded':
return [
{ name: 'egressEnded.complete', points: points((index) => (index % 3) + 1) },
{ name: 'egressEnded.failed', points: points((index) => index % 2) }
];
case 'connectionSuccessRate':
return [{ name, points: points((index) => 95 + (index % 5)) }];
case 'bytesUp':
case 'bytesDown':
return [{ name, points: points((index) => ((index % 6) + 1) * 1_000_000) }];
case 'uniqueIdentities':
return [{ name, points: points((index) => 500 + index * 10), basis: 'utcDay' as const }];
case 'egressDurationMs':
case 'ingressDurationMs':
return [{ name, points: points((index) => ((index % 4) + 1) * 600_000) }];
default:
return [{ name, points: points((index) => (index % 7) + 1) }];
}
});
return {
granularity: granularity as AnalyticsSeriesResponse['granularity'],
from: new Date(from).toISOString(),
to: new Date(to).toISOString(),
scope: (params.get('scope') ?? 'global') as AnalyticsSeriesResponse['scope'],
series
};
}
export function analyticsBreakdowns(
params: URLSearchParams,
fixtures: AnalyticsFixtures = {}
): AnalyticsBreakdownsResponse {
const dimensions = (params.get('dimensions') ?? '').split(',') as AnalyticsDimension[];
return {
from: params.get('from') ?? '',
to: params.get('to') ?? '',
scope: (params.get('scope') ?? 'global') as AnalyticsBreakdownsResponse['scope'],
breakdowns: dimensions.map((dimension) => {
const keys = fixtures.breakdowns?.[dimension] ?? BREAKDOWN_KEYS[dimension] ?? [];
const total = keys.reduce((sum, [, count]) => sum + count, 0);
return {
dimension,
total,
items: keys.map(([key, count, errors]) => ({
key,
count,
percent: Math.round((count / total) * 1000) / 10,
...(errors === undefined ? {} : { errors })
}))
};
})
};
}
/** CSV of an export and the attachment name the server gives it. */
export function analyticsCsv(params: URLSearchParams): { body: string; fileName: string } {
const kind = params.get('kind') ?? 'series';
return {
body:
kind === 'series'
? 'bucket,bytesUp\r\n2026-10-13T10:00:00.000Z,1000\r\n'
: 'dimension,key,count,percent\r\n',
fileName: `analytics-${kind}-${params.get('from') ?? ''}-${params.get('to') ?? ''}.csv`.replaceAll(':', '')
};
}