@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 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';