@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 CHANGED
@@ -1,15 +1,50 @@
1
1
  # @xiboplayer/stats
2
2
 
3
- **Proof of play tracking, stats reporting, and CMS logging.**
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** per-layout and per-widget duration tracking
10
- - **Aggregation modes** individual or aggregated stat submission (configurable from CMS)
11
- - **Log reporting** display logs batched and submitted to CMS
12
- - **Fault alerts** error deduplication and fault reporting
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
- const stats = new StatsCollector({ transport });
26
- stats.init();
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
- // Stats are collected automatically from player events
29
- // and submitted during each collection cycle
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` logger, events
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",
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.3"
13
+ "@xiboplayer/utils": "0.6.4"
14
14
  },
15
15
  "devDependencies": {
16
16
  "vitest": "^2.0.0",
@@ -12,7 +12,7 @@ import { createLogger } from '@xiboplayer/utils';
12
12
  const log = createLogger('@xiboplayer/stats');
13
13
 
14
14
  // IndexedDB configuration
15
- const DB_NAME = 'xibo-player-logs';
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
- constructor() {
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(DB_NAME, DB_VERSION);
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}`);
@@ -12,7 +12,7 @@ import { createLogger } from '@xiboplayer/utils';
12
12
  const log = createLogger('@xiboplayer/stats');
13
13
 
14
14
  // IndexedDB configuration
15
- const DB_NAME = 'xibo-player-stats';
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
- constructor() {
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(DB_NAME, DB_VERSION);
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}`);