@xiboplayer/stats 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/format-helpers.js +46 -0
- package/src/log-reporter.js +6 -90
- package/src/stats-collector.js +6 -83
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/stats",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Proof of play tracking, stats reporting, and CMS logging",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
"./collector": "./src/stats-collector.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@xiboplayer/utils": "0.7.
|
|
13
|
+
"@xiboplayer/utils": "0.7.4"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
16
|
+
"fake-indexeddb": "^5.0.2",
|
|
17
|
+
"vitest": "^2.1.9"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"xibo",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* Shared formatting helpers for stats and log XML serialization.
|
|
5
|
+
*
|
|
6
|
+
* @module @xiboplayer/stats/format-helpers
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format Date object as "YYYY-MM-DD HH:MM:SS"
|
|
11
|
+
* @param {Date|string|number} date
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function formatDateTime(date) {
|
|
15
|
+
if (!(date instanceof Date)) {
|
|
16
|
+
date = new Date(date);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const year = date.getFullYear();
|
|
20
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
21
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
22
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
23
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
24
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
25
|
+
|
|
26
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Escape XML special characters in a string.
|
|
31
|
+
* Returns non-string values unchanged.
|
|
32
|
+
* @param {string|*} str
|
|
33
|
+
* @returns {string|*}
|
|
34
|
+
*/
|
|
35
|
+
export function escapeXml(str) {
|
|
36
|
+
if (typeof str !== 'string') {
|
|
37
|
+
return str;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return str
|
|
41
|
+
.replace(/&/g, '&')
|
|
42
|
+
.replace(/</g, '<')
|
|
43
|
+
.replace(/>/g, '>')
|
|
44
|
+
.replace(/"/g, '"')
|
|
45
|
+
.replace(/'/g, ''');
|
|
46
|
+
}
|
package/src/log-reporter.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* @module @xiboplayer/stats/logger
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { createLogger, openIDB } from '@xiboplayer/utils';
|
|
12
|
+
import { createLogger, openIDB, queryByIndex, deleteByIds } from '@xiboplayer/utils';
|
|
13
|
+
import { formatDateTime, escapeXml } from './format-helpers.js';
|
|
13
14
|
|
|
14
15
|
const log = createLogger('@xiboplayer/stats');
|
|
15
16
|
|
|
@@ -248,45 +249,14 @@ export class LogReporter {
|
|
|
248
249
|
return this.log('debug', message, category);
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
/**
|
|
252
|
+
/** @see queryByIndex in @xiboplayer/utils/idb */
|
|
252
253
|
_queryByIndex(storeName, indexName, keyValue, limit) {
|
|
253
|
-
return
|
|
254
|
-
const tx = this.db.transaction([storeName], 'readonly');
|
|
255
|
-
const index = tx.objectStore(storeName).index(indexName);
|
|
256
|
-
const request = index.openCursor(keyValue);
|
|
257
|
-
const results = [];
|
|
258
|
-
|
|
259
|
-
request.onsuccess = (event) => {
|
|
260
|
-
const cursor = event.target.result;
|
|
261
|
-
if (cursor && results.length < limit) {
|
|
262
|
-
results.push(cursor.value);
|
|
263
|
-
cursor.continue();
|
|
264
|
-
} else {
|
|
265
|
-
resolve(results);
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
request.onerror = () => reject(new Error(`Index query failed: ${request.error}`));
|
|
269
|
-
});
|
|
254
|
+
return queryByIndex(this.db, storeName, indexName, keyValue, limit);
|
|
270
255
|
}
|
|
271
256
|
|
|
272
|
-
/**
|
|
257
|
+
/** @see deleteByIds in @xiboplayer/utils/idb */
|
|
273
258
|
_deleteByIds(storeName, records) {
|
|
274
|
-
return
|
|
275
|
-
const tx = this.db.transaction([storeName], 'readwrite');
|
|
276
|
-
const store = tx.objectStore(storeName);
|
|
277
|
-
let deleted = 0;
|
|
278
|
-
|
|
279
|
-
for (const record of records) {
|
|
280
|
-
if (record.id) {
|
|
281
|
-
const req = store.delete(record.id);
|
|
282
|
-
req.onsuccess = () => { deleted++; };
|
|
283
|
-
req.onerror = () => { log.error(`Failed to delete ${record.id}:`, req.error); };
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
tx.oncomplete = () => resolve(deleted);
|
|
288
|
-
tx.onerror = () => reject(new Error(`Delete failed: ${tx.error}`));
|
|
289
|
-
});
|
|
259
|
+
return deleteByIds(this.db, storeName, records.map(r => r.id));
|
|
290
260
|
}
|
|
291
261
|
|
|
292
262
|
/**
|
|
@@ -308,25 +278,6 @@ export class LogReporter {
|
|
|
308
278
|
return logs;
|
|
309
279
|
}
|
|
310
280
|
|
|
311
|
-
/**
|
|
312
|
-
* Count unsubmitted logs in the database.
|
|
313
|
-
* @returns {Promise<number>}
|
|
314
|
-
*/
|
|
315
|
-
async _countUnsubmitted() {
|
|
316
|
-
return new Promise((resolve) => {
|
|
317
|
-
try {
|
|
318
|
-
const transaction = this.db.transaction([LOGS_STORE], 'readonly');
|
|
319
|
-
const store = transaction.objectStore(LOGS_STORE);
|
|
320
|
-
const index = store.index('submitted');
|
|
321
|
-
const request = index.count(IDBKeyRange.only(0));
|
|
322
|
-
request.onsuccess = () => resolve(request.result);
|
|
323
|
-
request.onerror = () => resolve(0);
|
|
324
|
-
} catch (_) {
|
|
325
|
-
resolve(0);
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
281
|
/**
|
|
331
282
|
* Clear submitted logs from database
|
|
332
283
|
*
|
|
@@ -562,38 +513,3 @@ export function formatFaults(faults) {
|
|
|
562
513
|
})));
|
|
563
514
|
}
|
|
564
515
|
|
|
565
|
-
/**
|
|
566
|
-
* Format Date object as "YYYY-MM-DD HH:MM:SS"
|
|
567
|
-
* @private
|
|
568
|
-
*/
|
|
569
|
-
function formatDateTime(date) {
|
|
570
|
-
if (!(date instanceof Date)) {
|
|
571
|
-
date = new Date(date);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const year = date.getFullYear();
|
|
575
|
-
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
576
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
577
|
-
const hours = String(date.getHours()).padStart(2, '0');
|
|
578
|
-
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
579
|
-
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
580
|
-
|
|
581
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Escape XML special characters
|
|
586
|
-
* @private
|
|
587
|
-
*/
|
|
588
|
-
function escapeXml(str) {
|
|
589
|
-
if (typeof str !== 'string') {
|
|
590
|
-
return str;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
return str
|
|
594
|
-
.replace(/&/g, '&')
|
|
595
|
-
.replace(/</g, '<')
|
|
596
|
-
.replace(/>/g, '>')
|
|
597
|
-
.replace(/"/g, '"')
|
|
598
|
-
.replace(/'/g, ''');
|
|
599
|
-
}
|
package/src/stats-collector.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* @module @xiboplayer/stats/collector
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { createLogger, openIDB } from '@xiboplayer/utils';
|
|
12
|
+
import { createLogger, openIDB, queryByIndex, deleteByIds } from '@xiboplayer/utils';
|
|
13
|
+
import { formatDateTime, escapeXml } from './format-helpers.js';
|
|
13
14
|
|
|
14
15
|
const log = createLogger('@xiboplayer/stats');
|
|
15
16
|
|
|
@@ -309,57 +310,14 @@ export class StatsCollector {
|
|
|
309
310
|
}
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
/**
|
|
313
|
-
* Query records from an IndexedDB index with a cursor, up to a limit.
|
|
314
|
-
* @param {string} storeName - Object store name
|
|
315
|
-
* @param {string} indexName - Index name
|
|
316
|
-
* @param {any} keyValue - Key to query (passed to openCursor)
|
|
317
|
-
* @param {number} limit - Maximum records to return
|
|
318
|
-
* @returns {Promise<Array>}
|
|
319
|
-
*/
|
|
313
|
+
/** @see queryByIndex in @xiboplayer/utils/idb */
|
|
320
314
|
_queryByIndex(storeName, indexName, keyValue, limit) {
|
|
321
|
-
return
|
|
322
|
-
const tx = this.db.transaction([storeName], 'readonly');
|
|
323
|
-
const index = tx.objectStore(storeName).index(indexName);
|
|
324
|
-
const request = index.openCursor(keyValue);
|
|
325
|
-
const results = [];
|
|
326
|
-
|
|
327
|
-
request.onsuccess = (event) => {
|
|
328
|
-
const cursor = event.target.result;
|
|
329
|
-
if (cursor && results.length < limit) {
|
|
330
|
-
results.push(cursor.value);
|
|
331
|
-
cursor.continue();
|
|
332
|
-
} else {
|
|
333
|
-
resolve(results);
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
request.onerror = () => reject(new Error(`Index query failed: ${request.error}`));
|
|
337
|
-
});
|
|
315
|
+
return queryByIndex(this.db, storeName, indexName, keyValue, limit);
|
|
338
316
|
}
|
|
339
317
|
|
|
340
|
-
/**
|
|
341
|
-
* Delete records by ID from an IndexedDB object store.
|
|
342
|
-
* @param {string} storeName - Object store name
|
|
343
|
-
* @param {Array} records - Records with .id property
|
|
344
|
-
* @returns {Promise<number>} Number of deleted records
|
|
345
|
-
*/
|
|
318
|
+
/** @see deleteByIds in @xiboplayer/utils/idb */
|
|
346
319
|
_deleteByIds(storeName, records) {
|
|
347
|
-
return
|
|
348
|
-
const tx = this.db.transaction([storeName], 'readwrite');
|
|
349
|
-
const store = tx.objectStore(storeName);
|
|
350
|
-
let deleted = 0;
|
|
351
|
-
|
|
352
|
-
for (const record of records) {
|
|
353
|
-
if (record.id) {
|
|
354
|
-
const req = store.delete(record.id);
|
|
355
|
-
req.onsuccess = () => { deleted++; };
|
|
356
|
-
req.onerror = () => { log.error(`Failed to delete ${record.id}:`, req.error); };
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
tx.oncomplete = () => resolve(deleted);
|
|
361
|
-
tx.onerror = () => reject(new Error(`Delete failed: ${tx.error}`));
|
|
362
|
-
});
|
|
320
|
+
return deleteByIds(this.db, storeName, records.map(r => r.id));
|
|
363
321
|
}
|
|
364
322
|
|
|
365
323
|
/**
|
|
@@ -704,38 +662,3 @@ export function formatStats(stats) {
|
|
|
704
662
|
return `<stats>\n${statElements.join('\n')}\n</stats>`;
|
|
705
663
|
}
|
|
706
664
|
|
|
707
|
-
/**
|
|
708
|
-
* Format Date object as "YYYY-MM-DD HH:MM:SS"
|
|
709
|
-
* @private
|
|
710
|
-
*/
|
|
711
|
-
function formatDateTime(date) {
|
|
712
|
-
if (!(date instanceof Date)) {
|
|
713
|
-
date = new Date(date);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const year = date.getFullYear();
|
|
717
|
-
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
718
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
719
|
-
const hours = String(date.getHours()).padStart(2, '0');
|
|
720
|
-
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
721
|
-
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
722
|
-
|
|
723
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* Escape XML special characters
|
|
728
|
-
* @private
|
|
729
|
-
*/
|
|
730
|
-
function escapeXml(str) {
|
|
731
|
-
if (typeof str !== 'string') {
|
|
732
|
-
return str;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
return str
|
|
736
|
-
.replace(/&/g, '&')
|
|
737
|
-
.replace(/</g, '<')
|
|
738
|
-
.replace(/>/g, '>')
|
|
739
|
-
.replace(/"/g, '"')
|
|
740
|
-
.replace(/'/g, ''');
|
|
741
|
-
}
|