@xiboplayer/stats 0.6.3 → 0.6.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/README.md +168 -11
- package/package.json +2 -2
- package/src/log-reporter.js +7 -3
- package/src/stats-collector.js +7 -3
package/README.md
CHANGED
|
@@ -1,15 +1,50 @@
|
|
|
1
1
|
# @xiboplayer/stats
|
|
2
2
|
|
|
3
|
-
**Proof of play tracking,
|
|
3
|
+
**Proof of play tracking, log reporting, and fault alerts for Xibo CMS.**
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
7
|
Collects and reports display analytics to the Xibo CMS:
|
|
8
8
|
|
|
9
|
-
- **Proof of play**
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
9
|
+
- **Proof of play** -- per-layout and per-widget duration tracking with IndexedDB persistence
|
|
10
|
+
- **Hour-boundary splitting** -- stats crossing hour boundaries are split for correct CMS aggregation
|
|
11
|
+
- **Aggregation modes** -- individual or aggregated submission (configurable from CMS)
|
|
12
|
+
- **Event stats** -- point-in-time engagement data (touch, webhook, interactive triggers)
|
|
13
|
+
- **Log reporting** -- display logs batched and submitted to CMS (max 50 per batch)
|
|
14
|
+
- **Fault alerts** -- error deduplication with 5-minute cooldown, triggers CMS dashboard alerts
|
|
15
|
+
- **enableStat** -- per-layout/per-widget stat suppression via XLF flags
|
|
16
|
+
- **Quota resilience** -- auto-cleans oldest 100 submitted records on IndexedDB quota exceeded
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Renderer events StatsCollector CMS
|
|
22
|
+
(widgetStart/widgetEnd) (IndexedDB) (XMDS)
|
|
23
|
+
|
|
|
24
|
+
widgetStart(id, layout) -----> startWidget() -----> [in-progress Map]
|
|
25
|
+
|
|
|
26
|
+
widgetEnd(id, layout) -------> endWidget() -------> [split at hour]
|
|
27
|
+
| |
|
|
28
|
+
v v
|
|
29
|
+
IndexedDB getStatsForSubmission(50)
|
|
30
|
+
(xibo-player-stats) |
|
|
31
|
+
| formatStats() -> XML
|
|
32
|
+
| |
|
|
33
|
+
v submitStats() -----> CMS
|
|
34
|
+
clearSubmittedStats()
|
|
35
|
+
|
|
36
|
+
Renderer events LogReporter CMS
|
|
37
|
+
(errors, status) (IndexedDB) (XMDS)
|
|
38
|
+
|
|
|
39
|
+
log('error', msg) -----------> _saveLog() -------> IndexedDB
|
|
40
|
+
reportFault(code, reason) ---> [dedup 5min] -----> [alertType field]
|
|
41
|
+
|
|
|
42
|
+
getLogsForSubmission(50)
|
|
43
|
+
|
|
|
44
|
+
formatLogs() -> XML
|
|
45
|
+
|
|
|
46
|
+
submitLog() ---------> CMS
|
|
47
|
+
```
|
|
13
48
|
|
|
14
49
|
## Installation
|
|
15
50
|
|
|
@@ -19,19 +54,141 @@ npm install @xiboplayer/stats
|
|
|
19
54
|
|
|
20
55
|
## Usage
|
|
21
56
|
|
|
57
|
+
### StatsCollector -- proof of play
|
|
58
|
+
|
|
22
59
|
```javascript
|
|
23
|
-
import { StatsCollector } from '@xiboplayer/stats';
|
|
60
|
+
import { StatsCollector, formatStats } from '@xiboplayer/stats';
|
|
61
|
+
|
|
62
|
+
const stats = new StatsCollector();
|
|
63
|
+
await stats.init();
|
|
64
|
+
|
|
65
|
+
// Track layout playback
|
|
66
|
+
await stats.startLayout(123, 456); // layoutId, scheduleId
|
|
67
|
+
// ... layout plays for 30 seconds ...
|
|
68
|
+
await stats.endLayout(123, 456);
|
|
24
69
|
|
|
25
|
-
|
|
26
|
-
stats.
|
|
70
|
+
// Track widget playback
|
|
71
|
+
await stats.startWidget(789, 123, 456); // mediaId, layoutId, scheduleId
|
|
72
|
+
// ... widget plays ...
|
|
73
|
+
await stats.endWidget(789, 123, 456);
|
|
27
74
|
|
|
28
|
-
//
|
|
29
|
-
|
|
75
|
+
// Record interactive event
|
|
76
|
+
await stats.recordEvent('touch', 123, 789, 456); // tag, layoutId, widgetId, scheduleId
|
|
77
|
+
|
|
78
|
+
// Submit to CMS
|
|
79
|
+
const pending = await stats.getStatsForSubmission(50);
|
|
80
|
+
if (pending.length > 0) {
|
|
81
|
+
const xml = formatStats(pending);
|
|
82
|
+
await xmds.submitStats(xml);
|
|
83
|
+
await stats.clearSubmittedStats(pending);
|
|
84
|
+
}
|
|
30
85
|
```
|
|
31
86
|
|
|
87
|
+
### Aggregated submission
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// When CMS aggregationLevel is 'Aggregate':
|
|
91
|
+
const aggregated = await stats.getAggregatedStatsForSubmission(50);
|
|
92
|
+
// Groups by (type, layoutId, mediaId, scheduleId, hour) and sums durations
|
|
93
|
+
const xml = formatStats(aggregated);
|
|
94
|
+
await xmds.submitStats(xml);
|
|
95
|
+
// Clear using _rawIds from aggregated records
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### LogReporter -- CMS logging
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
import { LogReporter, formatLogs, formatFaults } from '@xiboplayer/stats';
|
|
102
|
+
|
|
103
|
+
const reporter = new LogReporter();
|
|
104
|
+
await reporter.init();
|
|
105
|
+
|
|
106
|
+
// Log messages (stored in IndexedDB, submitted in batches)
|
|
107
|
+
await reporter.error('Failed to load layout', 'PLAYER');
|
|
108
|
+
await reporter.info('Layout loaded successfully', 'PLAYER');
|
|
109
|
+
await reporter.debug('Rendering widget 42', 'RENDERER');
|
|
110
|
+
|
|
111
|
+
// Report fault (triggers CMS dashboard alert)
|
|
112
|
+
await reporter.reportFault('LAYOUT_LOAD_FAILED', 'Layout 123 failed to render');
|
|
113
|
+
// Same code won't be reported again within 5 minutes (deduplication)
|
|
114
|
+
|
|
115
|
+
// Submit logs to CMS
|
|
116
|
+
const logs = await reporter.getLogsForSubmission(50);
|
|
117
|
+
if (logs.length > 0) {
|
|
118
|
+
const xml = formatLogs(logs);
|
|
119
|
+
await xmds.submitLog(xml);
|
|
120
|
+
await reporter.clearSubmittedLogs(logs);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Submit faults (faster cycle, ~60s)
|
|
124
|
+
const faults = await reporter.getFaultsForSubmission(10);
|
|
125
|
+
if (faults.length > 0) {
|
|
126
|
+
const json = formatFaults(faults);
|
|
127
|
+
await xmds.reportFaults(json);
|
|
128
|
+
await reporter.clearSubmittedLogs(faults);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Stat Types
|
|
133
|
+
|
|
134
|
+
| Type | Tracked by | Fields |
|
|
135
|
+
|------|------------|--------|
|
|
136
|
+
| `layout` | startLayout/endLayout | layoutId, scheduleId, start, end, duration, count |
|
|
137
|
+
| `media` | startWidget/endWidget | mediaId, widgetId, layoutId, scheduleId, start, end, duration |
|
|
138
|
+
| `event` | recordEvent | tag, layoutId, widgetId, scheduleId, start (point-in-time) |
|
|
139
|
+
|
|
140
|
+
## API Reference
|
|
141
|
+
|
|
142
|
+
### StatsCollector
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
new StatsCollector(cmsId?)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Method | Returns | Description |
|
|
149
|
+
|--------|---------|-------------|
|
|
150
|
+
| `init()` | Promise | Initialize IndexedDB (idempotent) |
|
|
151
|
+
| `startLayout(layoutId, scheduleId, opts?)` | Promise | Start tracking layout |
|
|
152
|
+
| `endLayout(layoutId, scheduleId)` | Promise | End layout, save to DB |
|
|
153
|
+
| `startWidget(mediaId, layoutId, scheduleId, widgetId?, opts?)` | Promise | Start tracking widget |
|
|
154
|
+
| `endWidget(mediaId, layoutId, scheduleId)` | Promise | End widget, save to DB |
|
|
155
|
+
| `recordEvent(tag, layoutId, widgetId, scheduleId)` | Promise | Record instant event |
|
|
156
|
+
| `getStatsForSubmission(limit?)` | Promise<Array> | Get unsubmitted stats (default 50) |
|
|
157
|
+
| `getAggregatedStatsForSubmission(limit?)` | Promise<Array> | Get grouped stats |
|
|
158
|
+
| `clearSubmittedStats(stats)` | Promise | Delete submitted records |
|
|
159
|
+
| `getAllStats()` | Promise<Array> | All stats (debugging) |
|
|
160
|
+
| `clearAllStats()` | Promise | Clear everything (testing) |
|
|
161
|
+
|
|
162
|
+
### LogReporter
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
new LogReporter(cmsId?)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Method | Returns | Description |
|
|
169
|
+
|--------|---------|-------------|
|
|
170
|
+
| `init()` | Promise | Initialize IndexedDB |
|
|
171
|
+
| `log(level, message, category?, extra?)` | Promise | Store log entry |
|
|
172
|
+
| `error(message, category?)` | Promise | Shorthand for log('error', ...) |
|
|
173
|
+
| `audit(message, category?)` | Promise | Shorthand for log('audit', ...) |
|
|
174
|
+
| `info(message, category?)` | Promise | Shorthand for log('info', ...) |
|
|
175
|
+
| `debug(message, category?)` | Promise | Shorthand for log('debug', ...) |
|
|
176
|
+
| `reportFault(code, reason, cooldownMs?)` | Promise | Report fault with dedup (default 5min cooldown) |
|
|
177
|
+
| `getLogsForSubmission(limit?)` | Promise<Array> | Get unsubmitted logs (default 50) |
|
|
178
|
+
| `getFaultsForSubmission(limit?)` | Promise<Array> | Get unsubmitted faults (default 10) |
|
|
179
|
+
| `clearSubmittedLogs(logs)` | Promise | Delete submitted records |
|
|
180
|
+
|
|
181
|
+
### Formatters
|
|
182
|
+
|
|
183
|
+
| Function | Returns | Description |
|
|
184
|
+
|----------|---------|-------------|
|
|
185
|
+
| `formatStats(stats)` | string | Format stats as XML for XMDS SubmitStats |
|
|
186
|
+
| `formatLogs(logs)` | string | Format logs as XML for XMDS SubmitLog |
|
|
187
|
+
| `formatFaults(faults)` | string | Format faults as JSON for XMDS ReportFaults |
|
|
188
|
+
|
|
32
189
|
## Dependencies
|
|
33
190
|
|
|
34
|
-
- `@xiboplayer/utils`
|
|
191
|
+
- `@xiboplayer/utils` -- logger
|
|
35
192
|
|
|
36
193
|
---
|
|
37
194
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/stats",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Proof of play tracking, stats reporting, and CMS logging",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"./collector": "./src/stats-collector.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@xiboplayer/utils": "0.6.
|
|
13
|
+
"@xiboplayer/utils": "0.6.4"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"vitest": "^2.0.0",
|
package/src/log-reporter.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createLogger } from '@xiboplayer/utils';
|
|
|
12
12
|
const log = createLogger('@xiboplayer/stats');
|
|
13
13
|
|
|
14
14
|
// IndexedDB configuration
|
|
15
|
-
const
|
|
15
|
+
const DB_BASE = 'xibo-player-logs';
|
|
16
16
|
const DB_VERSION = 1;
|
|
17
17
|
const LOGS_STORE = 'logs';
|
|
18
18
|
|
|
@@ -37,8 +37,12 @@ const LOGS_STORE = 'logs';
|
|
|
37
37
|
* await reporter.clearSubmittedLogs(logs);
|
|
38
38
|
*/
|
|
39
39
|
export class LogReporter {
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} [cmsId] - Optional CMS ID for namespaced IndexedDB
|
|
42
|
+
*/
|
|
43
|
+
constructor(cmsId) {
|
|
41
44
|
this.db = null;
|
|
45
|
+
this._dbName = cmsId ? `${DB_BASE}-${cmsId}` : DB_BASE;
|
|
42
46
|
this._reportedFaults = new Map(); // code -> timestamp (deduplication)
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -66,7 +70,7 @@ export class LogReporter {
|
|
|
66
70
|
return;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
const request = indexedDB.open(
|
|
73
|
+
const request = indexedDB.open(this._dbName, DB_VERSION);
|
|
70
74
|
|
|
71
75
|
request.onerror = () => {
|
|
72
76
|
const error = new Error(`Failed to open IndexedDB: ${request.error}`);
|
package/src/stats-collector.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createLogger } from '@xiboplayer/utils';
|
|
|
12
12
|
const log = createLogger('@xiboplayer/stats');
|
|
13
13
|
|
|
14
14
|
// IndexedDB configuration
|
|
15
|
-
const
|
|
15
|
+
const DB_BASE = 'xibo-player-stats';
|
|
16
16
|
const DB_VERSION = 1;
|
|
17
17
|
const STATS_STORE = 'stats';
|
|
18
18
|
|
|
@@ -38,8 +38,12 @@ const STATS_STORE = 'stats';
|
|
|
38
38
|
* await collector.clearSubmittedStats(stats);
|
|
39
39
|
*/
|
|
40
40
|
export class StatsCollector {
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} [cmsId] - Optional CMS ID for namespaced IndexedDB
|
|
43
|
+
*/
|
|
44
|
+
constructor(cmsId) {
|
|
42
45
|
this.db = null;
|
|
46
|
+
this._dbName = cmsId ? `${DB_BASE}-${cmsId}` : DB_BASE;
|
|
43
47
|
this.inProgressStats = new Map(); // Track in-progress stats by key
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -67,7 +71,7 @@ export class StatsCollector {
|
|
|
67
71
|
return;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
const request = indexedDB.open(
|
|
74
|
+
const request = indexedDB.open(this._dbName, DB_VERSION);
|
|
71
75
|
|
|
72
76
|
request.onerror = () => {
|
|
73
77
|
const error = new Error(`Failed to open IndexedDB: ${request.error}`);
|