Improve device traffic dashboard and domain attribution metrics
This commit is contained in:
@@ -431,6 +431,11 @@ export function createDeviceInventoryService({
|
||||
observedAt: null,
|
||||
source: { error: null },
|
||||
overflowConnections: '0',
|
||||
attributionEvents: {
|
||||
unresolved_host: '0',
|
||||
unknown_device: '0',
|
||||
unsupported_source: '0',
|
||||
},
|
||||
series: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import { deviceId } from './deviceInventoryService.js';
|
||||
|
||||
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
||||
const DEFAULT_MAX_SERIES = 4096;
|
||||
const UNKNOWN_DOMAIN = { domain: '_unknown', service: 'Не распознано' };
|
||||
const ATTRIBUTION_OUTCOMES = ['unresolved_host', 'unknown_device', 'unsupported_source'];
|
||||
const SERVICE_DOMAINS = [
|
||||
['YouTube', ['youtube.com', 'youtube-nocookie.com', 'youtu.be', 'googlevideo.com', 'ytimg.com']],
|
||||
['OpenAI / ChatGPT', ['chatgpt.com', 'openai.com', 'oaistatic.com', 'oaiusercontent.com']],
|
||||
@@ -35,23 +37,29 @@ function sourceFor(type) {
|
||||
function parseConnection(connection, devicesByIp) {
|
||||
const id = String(connection?.id || '');
|
||||
const metadata = connection?.metadata;
|
||||
const source = sourceFor(String(metadata?.type || ''));
|
||||
const domain = classifyDomain(metadata?.host);
|
||||
const currentDeviceId = devicesByIp.get(String(metadata?.sourceIP || ''));
|
||||
const upload = connection?.upload;
|
||||
const download = connection?.download;
|
||||
if (!id || !Number.isSafeInteger(upload) || upload < 0
|
||||
|| !Number.isSafeInteger(download) || download < 0) {
|
||||
throw new Error('Sing-box вернул невалидный domain traffic counter');
|
||||
}
|
||||
if (!source || !domain || !currentDeviceId) return null;
|
||||
return {
|
||||
const parsed = {
|
||||
id,
|
||||
upload: BigInt(upload),
|
||||
download: BigInt(download),
|
||||
};
|
||||
const source = sourceFor(String(metadata?.type || ''));
|
||||
if (!source) return { ...parsed, outcome: 'unsupported_source' };
|
||||
const currentDeviceId = devicesByIp.get(String(metadata?.sourceIP || ''));
|
||||
if (!currentDeviceId) return { ...parsed, outcome: 'unknown_device' };
|
||||
const classifiedDomain = classifyDomain(metadata?.host);
|
||||
const domain = classifiedDomain || UNKNOWN_DOMAIN;
|
||||
return {
|
||||
...parsed,
|
||||
outcome: classifiedDomain ? 'classified' : 'unresolved_host',
|
||||
deviceId: currentDeviceId,
|
||||
...domain,
|
||||
source,
|
||||
upload: BigInt(upload),
|
||||
download: BigInt(download),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -102,12 +110,14 @@ export function createDomainTrafficService({
|
||||
let normalSeries = 0;
|
||||
let previousConnections = new Map();
|
||||
let overflowConnections = 0n;
|
||||
const attributionEvents = Object.fromEntries(ATTRIBUTION_OUTCOMES.map((outcome) => [outcome, 0n]));
|
||||
let refreshPromise = null;
|
||||
let current = {
|
||||
epoch,
|
||||
observedAt: null,
|
||||
source: { error: null },
|
||||
overflowConnections: '0',
|
||||
attributionEvents: Object.fromEntries(ATTRIBUTION_OUTCOMES.map((outcome) => [outcome, '0'])),
|
||||
series: [],
|
||||
};
|
||||
|
||||
@@ -117,6 +127,9 @@ export function createDomainTrafficService({
|
||||
observedAt: current.observedAt,
|
||||
source: { error },
|
||||
overflowConnections: overflowConnections.toString(),
|
||||
attributionEvents: Object.fromEntries(
|
||||
ATTRIBUTION_OUTCOMES.map((outcome) => [outcome, attributionEvents[outcome].toString()]),
|
||||
),
|
||||
series: [...totals.values()]
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
@@ -144,13 +157,22 @@ export function createDomainTrafficService({
|
||||
if (!net.isIPv4(ip) || !id) continue;
|
||||
devicesByIp.set(ip, devicesByIp.has(ip) ? null : id);
|
||||
}
|
||||
const parsed = response.connections
|
||||
.map((connection) => parseConnection(connection, devicesByIp))
|
||||
.filter(Boolean);
|
||||
const activeConnections = new Map();
|
||||
for (const connection of parsed) {
|
||||
const requestedKey = `${connection.deviceId}\0${connection.domain}\0${connection.source}`;
|
||||
for (const rawConnection of response.connections) {
|
||||
const connection = parseConnection(rawConnection, devicesByIp);
|
||||
const previous = previousConnections.get(connection.id);
|
||||
if (connection.outcome !== 'classified' && previous?.outcome !== connection.outcome) {
|
||||
attributionEvents[connection.outcome] += 1n;
|
||||
}
|
||||
if (connection.outcome === 'unknown_device' || connection.outcome === 'unsupported_source') {
|
||||
activeConnections.set(connection.id, {
|
||||
outcome: connection.outcome,
|
||||
countedUpload: previous?.countedUpload ?? null,
|
||||
countedDownload: previous?.countedDownload ?? null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const requestedKey = `${connection.deviceId}\0${connection.domain}\0${connection.source}`;
|
||||
let key = previous?.requestedKey === requestedKey ? previous.key : requestedKey;
|
||||
let domain = connection.domain;
|
||||
let service = connection.service;
|
||||
@@ -165,12 +187,11 @@ export function createDomainTrafficService({
|
||||
} else if (!totals.has(key)) {
|
||||
normalSeries += 1;
|
||||
}
|
||||
const sameSeries = previous?.key === key;
|
||||
const uploadDelta = sameSeries && connection.upload >= previous.upload
|
||||
? connection.upload - previous.upload
|
||||
const uploadDelta = previous?.countedUpload != null && connection.upload >= previous.countedUpload
|
||||
? connection.upload - previous.countedUpload
|
||||
: connection.upload;
|
||||
const downloadDelta = sameSeries && connection.download >= previous.download
|
||||
? connection.download - previous.download
|
||||
const downloadDelta = previous?.countedDownload != null && connection.download >= previous.countedDownload
|
||||
? connection.download - previous.countedDownload
|
||||
: connection.download;
|
||||
const total = totals.get(key) || {
|
||||
deviceId: key === requestedKey ? connection.deviceId : '_other',
|
||||
@@ -184,10 +205,11 @@ export function createDomainTrafficService({
|
||||
total.downloadBytes += downloadDelta;
|
||||
totals.set(key, total);
|
||||
activeConnections.set(connection.id, {
|
||||
outcome: connection.outcome,
|
||||
key,
|
||||
requestedKey,
|
||||
upload: connection.upload,
|
||||
download: connection.download,
|
||||
countedUpload: connection.upload,
|
||||
countedDownload: connection.download,
|
||||
});
|
||||
}
|
||||
previousConnections = activeConnections;
|
||||
|
||||
Reference in New Issue
Block a user