@xiboplayer/stats 0.1.0
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 +375 -0
- package/package.json +37 -0
- package/src/index.js +13 -0
- package/src/log-reporter.js +541 -0
- package/src/log-reporter.test.js +484 -0
- package/src/stats-collector.js +633 -0
- package/src/stats-collector.test.js +461 -0
- package/vitest.config.js +9 -0
- package/vitest.setup.js +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# @xiboplayer/stats
|
|
2
|
+
|
|
3
|
+
Proof of play tracking and CMS logging for Xibo Players.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **StatsCollector**: Track layout and widget playback for proof of play reporting
|
|
8
|
+
- **LogReporter**: Collect and submit application logs to CMS
|
|
9
|
+
- **IndexedDB Storage**: Persistent storage across browser sessions
|
|
10
|
+
- **Offline Support**: Stats and logs are queued when offline and submitted when online
|
|
11
|
+
- **Quota Management**: Automatic cleanup of old data when storage quota is exceeded
|
|
12
|
+
- **XMDS Integration**: Format stats and logs as XML for CMS submission
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @xiboplayer/stats
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### StatsCollector - Proof of Play Tracking
|
|
23
|
+
|
|
24
|
+
The StatsCollector tracks when layouts and widgets (media) are played for reporting to the CMS.
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
import { StatsCollector, formatStats } from '@xiboplayer/stats';
|
|
28
|
+
|
|
29
|
+
// Initialize collector
|
|
30
|
+
const collector = new StatsCollector();
|
|
31
|
+
await collector.init();
|
|
32
|
+
|
|
33
|
+
// Track layout playback
|
|
34
|
+
await collector.startLayout(layoutId, scheduleId);
|
|
35
|
+
// ... layout plays for 5 minutes ...
|
|
36
|
+
await collector.endLayout(layoutId, scheduleId);
|
|
37
|
+
|
|
38
|
+
// Track widget playback
|
|
39
|
+
await collector.startWidget(mediaId, layoutId, scheduleId);
|
|
40
|
+
// ... widget plays for 30 seconds ...
|
|
41
|
+
await collector.endWidget(mediaId, layoutId, scheduleId);
|
|
42
|
+
|
|
43
|
+
// Submit stats to CMS
|
|
44
|
+
const stats = await collector.getStatsForSubmission(50); // Get up to 50 stats
|
|
45
|
+
const xml = formatStats(stats);
|
|
46
|
+
const success = await xmds.submitStats(xml);
|
|
47
|
+
|
|
48
|
+
if (success) {
|
|
49
|
+
await collector.clearSubmittedStats(stats);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### API Reference
|
|
54
|
+
|
|
55
|
+
**`new StatsCollector()`**
|
|
56
|
+
|
|
57
|
+
Create a new stats collector instance.
|
|
58
|
+
|
|
59
|
+
**`async init()`**
|
|
60
|
+
|
|
61
|
+
Initialize IndexedDB storage. Must be called before using other methods.
|
|
62
|
+
|
|
63
|
+
**`async startLayout(layoutId, scheduleId)`**
|
|
64
|
+
|
|
65
|
+
Start tracking a layout. Creates a new stat entry and marks it as in-progress.
|
|
66
|
+
|
|
67
|
+
- `layoutId` (number): Layout ID from CMS
|
|
68
|
+
- `scheduleId` (number): Schedule ID that triggered this layout
|
|
69
|
+
|
|
70
|
+
**`async endLayout(layoutId, scheduleId)`**
|
|
71
|
+
|
|
72
|
+
End tracking a layout. Calculates duration and saves to database.
|
|
73
|
+
|
|
74
|
+
- `layoutId` (number): Layout ID from CMS
|
|
75
|
+
- `scheduleId` (number): Schedule ID
|
|
76
|
+
|
|
77
|
+
**`async startWidget(mediaId, layoutId, scheduleId)`**
|
|
78
|
+
|
|
79
|
+
Start tracking a widget/media item.
|
|
80
|
+
|
|
81
|
+
- `mediaId` (number): Media ID from CMS
|
|
82
|
+
- `layoutId` (number): Parent layout ID
|
|
83
|
+
- `scheduleId` (number): Schedule ID
|
|
84
|
+
|
|
85
|
+
**`async endWidget(mediaId, layoutId, scheduleId)`**
|
|
86
|
+
|
|
87
|
+
End tracking a widget. Calculates duration and saves to database.
|
|
88
|
+
|
|
89
|
+
- `mediaId` (number): Media ID from CMS
|
|
90
|
+
- `layoutId` (number): Parent layout ID
|
|
91
|
+
- `scheduleId` (number): Schedule ID
|
|
92
|
+
|
|
93
|
+
**`async getStatsForSubmission(limit = 50)`**
|
|
94
|
+
|
|
95
|
+
Get unsubmitted stats ready for submission to CMS.
|
|
96
|
+
|
|
97
|
+
- `limit` (number): Maximum number of stats to return (default: 50)
|
|
98
|
+
- Returns: Array of stat objects
|
|
99
|
+
|
|
100
|
+
**`async clearSubmittedStats(stats)`**
|
|
101
|
+
|
|
102
|
+
Delete stats that were successfully submitted to CMS.
|
|
103
|
+
|
|
104
|
+
- `stats` (Array): Array of stat objects to delete
|
|
105
|
+
|
|
106
|
+
**`async getAllStats()`**
|
|
107
|
+
|
|
108
|
+
Get all stats from database (for debugging).
|
|
109
|
+
|
|
110
|
+
- Returns: Array of all stat objects
|
|
111
|
+
|
|
112
|
+
**`async clearAllStats()`**
|
|
113
|
+
|
|
114
|
+
Clear all stats from database (for testing).
|
|
115
|
+
|
|
116
|
+
#### Stat Object Structure
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
{
|
|
120
|
+
id: 123, // Auto-generated ID
|
|
121
|
+
type: 'layout', // 'layout' or 'media'
|
|
122
|
+
layoutId: 456, // Layout ID from CMS
|
|
123
|
+
scheduleId: 789, // Schedule ID
|
|
124
|
+
mediaId: 111, // Only for type='media'
|
|
125
|
+
start: Date, // Start time
|
|
126
|
+
end: Date, // End time
|
|
127
|
+
duration: 300, // Duration in seconds
|
|
128
|
+
count: 1, // Number of plays (always 1)
|
|
129
|
+
submitted: 0 // 0 = not submitted, 1 = submitted
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### formatStats - XML Formatting
|
|
134
|
+
|
|
135
|
+
Format stats array as XML for XMDS SubmitStats API.
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
import { formatStats } from '@xiboplayer/stats';
|
|
139
|
+
|
|
140
|
+
const stats = await collector.getStatsForSubmission(50);
|
|
141
|
+
const xml = formatStats(stats);
|
|
142
|
+
|
|
143
|
+
console.log(xml);
|
|
144
|
+
// <stats>
|
|
145
|
+
// <stat type="layout" fromdt="2026-02-10 12:00:00" todt="2026-02-10 12:05:00"
|
|
146
|
+
// scheduleid="123" layoutid="456" count="1" duration="300" />
|
|
147
|
+
// <stat type="media" fromdt="2026-02-10 12:00:00" todt="2026-02-10 12:01:00"
|
|
148
|
+
// scheduleid="123" layoutid="456" mediaid="789" count="1" duration="60" />
|
|
149
|
+
// </stats>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### LogReporter - CMS Logging
|
|
153
|
+
|
|
154
|
+
The LogReporter collects application logs and submits them to the CMS via XMDS.
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
import { LogReporter, formatLogs } from '@xiboplayer/stats';
|
|
158
|
+
|
|
159
|
+
// Initialize reporter
|
|
160
|
+
const reporter = new LogReporter();
|
|
161
|
+
await reporter.init();
|
|
162
|
+
|
|
163
|
+
// Log messages
|
|
164
|
+
await reporter.error('Failed to load layout 123', 'PLAYER');
|
|
165
|
+
await reporter.audit('User logged in', 'AUTH');
|
|
166
|
+
await reporter.info('Layout loaded successfully', 'RENDERER');
|
|
167
|
+
await reporter.debug('Cache hit for file ABC', 'CACHE');
|
|
168
|
+
|
|
169
|
+
// Submit logs to CMS
|
|
170
|
+
const logs = await reporter.getLogsForSubmission(100); // Get up to 100 logs
|
|
171
|
+
const xml = formatLogs(logs);
|
|
172
|
+
const success = await xmds.submitLog(xml);
|
|
173
|
+
|
|
174
|
+
if (success) {
|
|
175
|
+
await reporter.clearSubmittedLogs(logs);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### API Reference
|
|
180
|
+
|
|
181
|
+
**`new LogReporter()`**
|
|
182
|
+
|
|
183
|
+
Create a new log reporter instance.
|
|
184
|
+
|
|
185
|
+
**`async init()`**
|
|
186
|
+
|
|
187
|
+
Initialize IndexedDB storage. Must be called before using other methods.
|
|
188
|
+
|
|
189
|
+
**`async log(level, message, category = 'PLAYER')`**
|
|
190
|
+
|
|
191
|
+
Log a message with specified level.
|
|
192
|
+
|
|
193
|
+
- `level` (string): Log level - 'error', 'audit', 'info', or 'debug'
|
|
194
|
+
- `message` (string): Log message
|
|
195
|
+
- `category` (string): Log category (default: 'PLAYER')
|
|
196
|
+
|
|
197
|
+
**`async error(message, category = 'PLAYER')`**
|
|
198
|
+
|
|
199
|
+
Shorthand for logging an error message.
|
|
200
|
+
|
|
201
|
+
**`async audit(message, category = 'PLAYER')`**
|
|
202
|
+
|
|
203
|
+
Shorthand for logging an audit message.
|
|
204
|
+
|
|
205
|
+
**`async info(message, category = 'PLAYER')`**
|
|
206
|
+
|
|
207
|
+
Shorthand for logging an info message.
|
|
208
|
+
|
|
209
|
+
**`async debug(message, category = 'PLAYER')`**
|
|
210
|
+
|
|
211
|
+
Shorthand for logging a debug message.
|
|
212
|
+
|
|
213
|
+
**`async getLogsForSubmission(limit = 100)`**
|
|
214
|
+
|
|
215
|
+
Get unsubmitted logs ready for submission to CMS.
|
|
216
|
+
|
|
217
|
+
- `limit` (number): Maximum number of logs to return (default: 100)
|
|
218
|
+
- Returns: Array of log objects
|
|
219
|
+
|
|
220
|
+
**`async clearSubmittedLogs(logs)`**
|
|
221
|
+
|
|
222
|
+
Delete logs that were successfully submitted to CMS.
|
|
223
|
+
|
|
224
|
+
- `logs` (Array): Array of log objects to delete
|
|
225
|
+
|
|
226
|
+
**`async getAllLogs()`**
|
|
227
|
+
|
|
228
|
+
Get all logs from database (for debugging).
|
|
229
|
+
|
|
230
|
+
- Returns: Array of all log objects
|
|
231
|
+
|
|
232
|
+
**`async clearAllLogs()`**
|
|
233
|
+
|
|
234
|
+
Clear all logs from database (for testing).
|
|
235
|
+
|
|
236
|
+
#### Log Object Structure
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
{
|
|
240
|
+
id: 123, // Auto-generated ID
|
|
241
|
+
level: 'error', // 'error', 'audit', 'info', or 'debug'
|
|
242
|
+
message: 'Error text', // Log message
|
|
243
|
+
category: 'PLAYER', // Log category
|
|
244
|
+
timestamp: Date, // When log was created
|
|
245
|
+
submitted: 0 // 0 = not submitted, 1 = submitted
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Common Categories
|
|
250
|
+
|
|
251
|
+
- `PLAYER` - Player lifecycle and general operations
|
|
252
|
+
- `RENDERER` - Layout rendering and widget display
|
|
253
|
+
- `CACHE` - File caching and downloads
|
|
254
|
+
- `XMDS` - CMS communication
|
|
255
|
+
- `AUTH` - Authentication and registration
|
|
256
|
+
- `SCHEDULE` - Schedule management
|
|
257
|
+
|
|
258
|
+
### formatLogs - XML Formatting
|
|
259
|
+
|
|
260
|
+
Format logs array as XML for XMDS SubmitLog API.
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
import { formatLogs } from '@xiboplayer/stats';
|
|
264
|
+
|
|
265
|
+
const logs = await reporter.getLogsForSubmission(100);
|
|
266
|
+
const xml = formatLogs(logs);
|
|
267
|
+
|
|
268
|
+
console.log(xml);
|
|
269
|
+
// <logs>
|
|
270
|
+
// <log date="2026-02-10 12:00:00" category="PLAYER" type="error"
|
|
271
|
+
// message="Failed to load layout 123" />
|
|
272
|
+
// <log date="2026-02-10 12:01:00" category="AUTH" type="audit"
|
|
273
|
+
// message="User logged in" />
|
|
274
|
+
// </logs>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Storage
|
|
278
|
+
|
|
279
|
+
Both StatsCollector and LogReporter use IndexedDB for persistent storage:
|
|
280
|
+
|
|
281
|
+
- **Database Names**: `xibo-player-stats` and `xibo-player-logs`
|
|
282
|
+
- **Indexes**: Both use an index on the `submitted` field for fast queries
|
|
283
|
+
- **Quota Management**: Automatically cleans old submitted entries when quota is exceeded
|
|
284
|
+
- **Offline Support**: Data persists across browser sessions and restarts
|
|
285
|
+
|
|
286
|
+
### Storage Limits
|
|
287
|
+
|
|
288
|
+
- **Chrome**: ~60% of available disk space
|
|
289
|
+
- **Firefox**: ~10% of available disk space
|
|
290
|
+
- **Safari**: ~1GB
|
|
291
|
+
|
|
292
|
+
When storage quota is exceeded, the oldest 100 submitted entries are automatically deleted.
|
|
293
|
+
|
|
294
|
+
## Integration with PWA Platform
|
|
295
|
+
|
|
296
|
+
The stats package is integrated into the PWA platform (`platforms/pwa/src/main.ts`):
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// Initialize stats collector
|
|
300
|
+
this.statsCollector = new StatsCollector();
|
|
301
|
+
await this.statsCollector.init();
|
|
302
|
+
|
|
303
|
+
// Track layout events
|
|
304
|
+
this.renderer.on('layoutStart', (layoutId) => {
|
|
305
|
+
this.statsCollector.startLayout(layoutId, this.currentScheduleId);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
this.renderer.on('layoutEnd', (layoutId) => {
|
|
309
|
+
this.statsCollector.endLayout(layoutId, this.currentScheduleId);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Track widget events
|
|
313
|
+
this.renderer.on('widgetStart', ({ widgetId, layoutId, mediaId }) => {
|
|
314
|
+
if (mediaId) {
|
|
315
|
+
this.statsCollector.startWidget(mediaId, layoutId, this.currentScheduleId);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
this.renderer.on('widgetEnd', ({ widgetId, layoutId, mediaId }) => {
|
|
320
|
+
if (mediaId) {
|
|
321
|
+
this.statsCollector.endWidget(mediaId, layoutId, this.currentScheduleId);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Submit stats periodically (every 10 minutes)
|
|
326
|
+
setInterval(async () => {
|
|
327
|
+
const stats = await this.statsCollector.getStatsForSubmission(50);
|
|
328
|
+
if (stats.length > 0) {
|
|
329
|
+
const xml = formatStats(stats);
|
|
330
|
+
const success = await this.xmds.submitStats(xml);
|
|
331
|
+
if (success) {
|
|
332
|
+
await this.statsCollector.clearSubmittedStats(stats);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}, 600000);
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Testing
|
|
339
|
+
|
|
340
|
+
The package includes comprehensive tests using Vitest and fake-indexeddb:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# Run tests
|
|
344
|
+
npm test
|
|
345
|
+
|
|
346
|
+
# Run tests in watch mode
|
|
347
|
+
npm run test:watch
|
|
348
|
+
|
|
349
|
+
# Run tests with coverage
|
|
350
|
+
npm run test:coverage
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Test Coverage
|
|
354
|
+
|
|
355
|
+
- **StatsCollector**: 32 tests covering initialization, layout tracking, widget tracking, submission flow, edge cases, and database operations
|
|
356
|
+
- **LogReporter**: 35 tests covering initialization, log creation, submission flow, edge cases, and database operations
|
|
357
|
+
- **Total**: 67 tests with 100% pass rate
|
|
358
|
+
|
|
359
|
+
## Compatibility
|
|
360
|
+
|
|
361
|
+
- **Node.js**: 18+ (for ESM support)
|
|
362
|
+
- **Browsers**: Chrome 58+, Firefox 52+, Safari 12+, Edge 79+
|
|
363
|
+
- **IndexedDB**: Required for persistent storage
|
|
364
|
+
|
|
365
|
+
## Author
|
|
366
|
+
|
|
367
|
+
Pau Aliagas <linuxnow@gmail.com>
|
|
368
|
+
|
|
369
|
+
## Repository
|
|
370
|
+
|
|
371
|
+
https://github.com/xibo/xibo-players/tree/main/packages/stats
|
|
372
|
+
|
|
373
|
+
## License
|
|
374
|
+
|
|
375
|
+
AGPL-3.0-or-later
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xiboplayer/stats",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Proof of play tracking, stats reporting, and CMS logging",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./collector": "./src/stats-collector.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@xiboplayer/utils": "0.1.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"vitest": "^2.0.0",
|
|
16
|
+
"fake-indexeddb": "^5.0.2"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"xibo",
|
|
20
|
+
"digital-signage",
|
|
21
|
+
"proof-of-play",
|
|
22
|
+
"statistics",
|
|
23
|
+
"reporting"
|
|
24
|
+
],
|
|
25
|
+
"author": "Pau Aliagas <linuxnow@gmail.com>",
|
|
26
|
+
"license": "AGPL-3.0-or-later",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/xibo-players/xiboplayer.git",
|
|
30
|
+
"directory": "packages/stats"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"test:coverage": "vitest run --coverage"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @xiboplayer/stats - Proof of play and statistics reporting
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stats collector for proof of play tracking
|
|
5
|
+
* @module @xiboplayer/stats/collector
|
|
6
|
+
*/
|
|
7
|
+
export { StatsCollector, formatStats } from './stats-collector.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Log reporter for CMS logging
|
|
11
|
+
* @module @xiboplayer/stats/logger
|
|
12
|
+
*/
|
|
13
|
+
export { LogReporter, formatLogs } from './log-reporter.js';
|