@scarlett-player/analytics 0.2.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/LICENSE +21 -0
- package/README.md +473 -0
- package/dist/index.cjs +609 -0
- package/dist/index.d.cts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +582 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Hackney Enterprises Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# @scarlett-player/analytics
|
|
2
|
+
|
|
3
|
+
Analytics plugin for Scarlett Player that collects Quality of Experience (QoE) metrics and engagement data for live events and VOD content.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Quality of Experience (QoE) Metrics**
|
|
8
|
+
- Startup time tracking
|
|
9
|
+
- Rebuffering detection and measurement
|
|
10
|
+
- Quality level changes
|
|
11
|
+
- Error tracking
|
|
12
|
+
|
|
13
|
+
- **Engagement Analytics**
|
|
14
|
+
- Watch time vs. play time
|
|
15
|
+
- Pause/seek behavior
|
|
16
|
+
- Completion rates
|
|
17
|
+
- Exit type detection
|
|
18
|
+
|
|
19
|
+
- **Automatic Tracking**
|
|
20
|
+
- Periodic heartbeat reporting
|
|
21
|
+
- Page visibility handling
|
|
22
|
+
- Persistent viewer identification
|
|
23
|
+
- Session management
|
|
24
|
+
|
|
25
|
+
- **Custom Events**
|
|
26
|
+
- Track business events (purchases, signups, etc.)
|
|
27
|
+
- Custom dimensions support
|
|
28
|
+
- Flexible data collection
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @scarlett-player/analytics
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or with pnpm:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add @scarlett-player/analytics
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Basic Usage
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { createPlayer } from '@scarlett-player/core';
|
|
46
|
+
import { hlsPlugin } from '@scarlett-player/hls';
|
|
47
|
+
import { createAnalyticsPlugin } from '@scarlett-player/analytics';
|
|
48
|
+
import { uiPlugin } from '@scarlett-player/ui';
|
|
49
|
+
|
|
50
|
+
const player = await createPlayer({
|
|
51
|
+
container: '#player',
|
|
52
|
+
src: 'https://example.com/stream.m3u8',
|
|
53
|
+
plugins: [
|
|
54
|
+
hlsPlugin(),
|
|
55
|
+
createAnalyticsPlugin({
|
|
56
|
+
beaconUrl: 'https://api.example.com/analytics/beacon',
|
|
57
|
+
videoId: 'event-123',
|
|
58
|
+
videoTitle: 'Live Fight Night',
|
|
59
|
+
isLive: true,
|
|
60
|
+
viewerId: user?.id,
|
|
61
|
+
viewerPlan: 'ppv',
|
|
62
|
+
}),
|
|
63
|
+
uiPlugin(),
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
### Required Options
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
{
|
|
74
|
+
beaconUrl: string; // Your analytics API endpoint
|
|
75
|
+
videoId: string; // Unique video identifier
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Optional Options
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
{
|
|
83
|
+
// Video metadata
|
|
84
|
+
videoTitle?: string;
|
|
85
|
+
videoSeries?: string;
|
|
86
|
+
videoDuration?: number;
|
|
87
|
+
isLive?: boolean;
|
|
88
|
+
|
|
89
|
+
// Viewer information
|
|
90
|
+
viewerId?: string; // Auto-generated if not provided
|
|
91
|
+
viewerPlan?: string; // 'free', 'ppv', 'subscriber', etc.
|
|
92
|
+
|
|
93
|
+
// Custom dimensions
|
|
94
|
+
customDimensions?: {
|
|
95
|
+
promoter?: string;
|
|
96
|
+
eventType?: string;
|
|
97
|
+
[key: string]: any;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Behavior
|
|
101
|
+
heartbeatInterval?: number; // Default: 10000ms (10 seconds)
|
|
102
|
+
errorSampleRate?: number; // Default: 1.0 (100%)
|
|
103
|
+
disableInDev?: boolean; // Default: false
|
|
104
|
+
apiKey?: string; // Optional API key for authentication
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Events Tracked
|
|
109
|
+
|
|
110
|
+
### Automatic Events
|
|
111
|
+
|
|
112
|
+
The plugin automatically tracks these events:
|
|
113
|
+
|
|
114
|
+
| Event | Description | Data |
|
|
115
|
+
|-------|-------------|------|
|
|
116
|
+
| `viewStart` | Player initialized | viewId, sessionId, environment |
|
|
117
|
+
| `playRequest` | User clicked play | timestamp |
|
|
118
|
+
| `videoStart` | First frame rendered | startupTime |
|
|
119
|
+
| `heartbeat` | Periodic update (10s default) | watchTime, playTime, QoE score |
|
|
120
|
+
| `pause` | Playback paused | currentTime, pauseCount |
|
|
121
|
+
| `seeking` | User seeked | seekTo, seekCount |
|
|
122
|
+
| `rebufferStart` | Buffering started | rebufferCount |
|
|
123
|
+
| `rebufferEnd` | Buffering ended | duration, totalRebufferTime |
|
|
124
|
+
| `qualityChange` | Quality level changed | bitrate, width, height |
|
|
125
|
+
| `error` | Error occurred | errorType, errorMessage, fatal |
|
|
126
|
+
| `viewEnd` | View session ended | all metrics, exitType, QoE score |
|
|
127
|
+
|
|
128
|
+
### Exit Types
|
|
129
|
+
|
|
130
|
+
- `completed` - Video played to the end
|
|
131
|
+
- `abandoned` - User left before completion
|
|
132
|
+
- `error` - Fatal error stopped playback
|
|
133
|
+
- `background` - Tab/window was backgrounded
|
|
134
|
+
|
|
135
|
+
## Custom Event Tracking
|
|
136
|
+
|
|
137
|
+
Track custom business events:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const analytics = player.plugins.get('analytics');
|
|
141
|
+
|
|
142
|
+
// Track PPV purchase
|
|
143
|
+
analytics.trackEvent('ppv_purchase', {
|
|
144
|
+
price: 49.99,
|
|
145
|
+
currency: 'USD',
|
|
146
|
+
paymentMethod: 'stripe',
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Track user signup
|
|
150
|
+
analytics.trackEvent('user_signup', {
|
|
151
|
+
plan: 'premium',
|
|
152
|
+
referral: 'social',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Track engagement
|
|
156
|
+
analytics.trackEvent('share_clicked', {
|
|
157
|
+
platform: 'twitter',
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Beacon Payload Structure
|
|
162
|
+
|
|
163
|
+
Every beacon sent includes:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
{
|
|
167
|
+
// Event info
|
|
168
|
+
event: string; // Event type
|
|
169
|
+
timestamp: number; // Unix timestamp
|
|
170
|
+
|
|
171
|
+
// View context
|
|
172
|
+
viewId: string; // Unique per playback attempt
|
|
173
|
+
sessionId: string; // Persists across views in session
|
|
174
|
+
viewerId: string; // Persists across sessions
|
|
175
|
+
|
|
176
|
+
// Video context
|
|
177
|
+
videoId: string;
|
|
178
|
+
videoTitle?: string;
|
|
179
|
+
isLive?: boolean;
|
|
180
|
+
|
|
181
|
+
// Player context
|
|
182
|
+
playerVersion: string;
|
|
183
|
+
playerName: string;
|
|
184
|
+
|
|
185
|
+
// Environment
|
|
186
|
+
browser: string; // 'Chrome', 'Safari', etc.
|
|
187
|
+
os: string; // 'Windows', 'macOS', etc.
|
|
188
|
+
deviceType: string; // 'desktop', 'mobile', 'tablet'
|
|
189
|
+
screenSize: string; // '1920x1080'
|
|
190
|
+
playerSize: string; // '1280x720'
|
|
191
|
+
connectionType: string; // '4g', 'wifi', etc.
|
|
192
|
+
|
|
193
|
+
// Custom dimensions
|
|
194
|
+
...customDimensions,
|
|
195
|
+
|
|
196
|
+
// Event-specific data
|
|
197
|
+
...eventData
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Quality of Experience (QoE) Score
|
|
202
|
+
|
|
203
|
+
The plugin calculates a QoE score (0-100) based on:
|
|
204
|
+
|
|
205
|
+
- **Success Score (30%)** - Did playback succeed without errors?
|
|
206
|
+
- **Startup Score (25%)** - How fast did video start?
|
|
207
|
+
- <1s: 100
|
|
208
|
+
- <2s: 85
|
|
209
|
+
- <4s: 70
|
|
210
|
+
- <8s: 50
|
|
211
|
+
- 8s+: 30
|
|
212
|
+
|
|
213
|
+
- **Smoothness Score (30%)** - How much rebuffering?
|
|
214
|
+
- <0.1% rebuffer ratio: 100
|
|
215
|
+
- <1%: 85
|
|
216
|
+
- <2%: 70
|
|
217
|
+
- <5%: 50
|
|
218
|
+
- 5%+: 30
|
|
219
|
+
|
|
220
|
+
- **Quality Score (15%)** - What bitrate was achieved?
|
|
221
|
+
- >4 Mbps (4K): 100
|
|
222
|
+
- >2 Mbps (1080p): 90
|
|
223
|
+
- >1 Mbps (720p): 75
|
|
224
|
+
- >500 Kbps (480p): 60
|
|
225
|
+
- Lower: 40
|
|
226
|
+
|
|
227
|
+
Access the score:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
const analytics = player.plugins.get('analytics');
|
|
231
|
+
const qoeScore = analytics.getQoEScore(); // 0-100
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Metrics API
|
|
235
|
+
|
|
236
|
+
Get current session metrics:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const analytics = player.plugins.get('analytics');
|
|
240
|
+
const metrics = analytics.getMetrics();
|
|
241
|
+
|
|
242
|
+
console.log({
|
|
243
|
+
viewId: metrics.viewId,
|
|
244
|
+
watchTime: metrics.watchTime,
|
|
245
|
+
playTime: metrics.playTime,
|
|
246
|
+
rebufferCount: metrics.rebufferCount,
|
|
247
|
+
startupTime: metrics.startupTime,
|
|
248
|
+
qoeScore: analytics.getQoEScore(),
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Backend Integration
|
|
253
|
+
|
|
254
|
+
### Endpoint Requirements
|
|
255
|
+
|
|
256
|
+
Your beacon endpoint should:
|
|
257
|
+
|
|
258
|
+
1. Accept `POST` requests
|
|
259
|
+
2. Handle `application/json` content type
|
|
260
|
+
3. Support the `navigator.sendBeacon()` API (for reliability)
|
|
261
|
+
4. Return quickly (don't block analytics on slow processing)
|
|
262
|
+
|
|
263
|
+
### Example Express.js Handler
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
app.post('/analytics/beacon', async (req, res) => {
|
|
267
|
+
// Immediately respond
|
|
268
|
+
res.status(204).send();
|
|
269
|
+
|
|
270
|
+
// Process asynchronously
|
|
271
|
+
const event = req.body;
|
|
272
|
+
|
|
273
|
+
// Validate event
|
|
274
|
+
if (!event.viewId || !event.videoId) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Store in database
|
|
279
|
+
await db.analyticsEvents.insert({
|
|
280
|
+
event_type: event.event,
|
|
281
|
+
view_id: event.viewId,
|
|
282
|
+
session_id: event.sessionId,
|
|
283
|
+
viewer_id: event.viewerId,
|
|
284
|
+
video_id: event.videoId,
|
|
285
|
+
timestamp: new Date(event.timestamp),
|
|
286
|
+
payload: event,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Update aggregations if needed
|
|
290
|
+
if (event.event === 'viewEnd') {
|
|
291
|
+
await updateVideoStats(event.videoId, event);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Laravel Example
|
|
297
|
+
|
|
298
|
+
```php
|
|
299
|
+
Route::post('/analytics/beacon', function (Request $request) {
|
|
300
|
+
// Immediately respond
|
|
301
|
+
return response('', 204);
|
|
302
|
+
})->middleware(['throttle:1000,1']); // Rate limit
|
|
303
|
+
|
|
304
|
+
// Queue processing
|
|
305
|
+
Queue::push(new ProcessAnalyticsEvent($request->all()));
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## TSP Integration Example
|
|
309
|
+
|
|
310
|
+
For The Stream Platform (TSP) live events:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
const player = await createPlayer({
|
|
314
|
+
container: '#player',
|
|
315
|
+
src: event.streamUrl,
|
|
316
|
+
plugins: [
|
|
317
|
+
hlsPlugin({
|
|
318
|
+
lowLatencyMode: true,
|
|
319
|
+
}),
|
|
320
|
+
createAnalyticsPlugin({
|
|
321
|
+
beaconUrl: `${import.meta.env.VITE_API_URL}/analytics/beacon`,
|
|
322
|
+
apiKey: import.meta.env.VITE_ANALYTICS_KEY,
|
|
323
|
+
|
|
324
|
+
// Video context
|
|
325
|
+
videoId: event.id,
|
|
326
|
+
videoTitle: event.title,
|
|
327
|
+
isLive: true,
|
|
328
|
+
|
|
329
|
+
// Viewer context
|
|
330
|
+
viewerId: user?.id,
|
|
331
|
+
viewerPlan: user?.subscription ? 'subscriber' : 'ppv',
|
|
332
|
+
|
|
333
|
+
// Custom dimensions for TSP
|
|
334
|
+
customDimensions: {
|
|
335
|
+
eventType: 'fight',
|
|
336
|
+
promoter: event.promoter,
|
|
337
|
+
isPpv: event.isPpv,
|
|
338
|
+
price: event.price,
|
|
339
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
// Behavior
|
|
343
|
+
heartbeatInterval: 15000, // 15s for live
|
|
344
|
+
errorSampleRate: 1.0, // Track all errors
|
|
345
|
+
disableInDev: false, // Track even in dev
|
|
346
|
+
}),
|
|
347
|
+
uiPlugin(),
|
|
348
|
+
],
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Track PPV purchase
|
|
352
|
+
if (purchaseSuccessful) {
|
|
353
|
+
const analytics = player.plugins.get('analytics');
|
|
354
|
+
analytics.trackEvent('ppv_purchase', {
|
|
355
|
+
price: event.price,
|
|
356
|
+
currency: 'USD',
|
|
357
|
+
paymentMethod: paymentData.method,
|
|
358
|
+
promotionCode: paymentData.promoCode,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Privacy Considerations
|
|
364
|
+
|
|
365
|
+
The plugin respects user privacy:
|
|
366
|
+
|
|
367
|
+
- **Anonymous Tracking**: If no `viewerId` is provided, generates anonymous IDs stored in localStorage
|
|
368
|
+
- **No PII**: Doesn't collect personally identifiable information
|
|
369
|
+
- **User Agent Only**: Uses standard browser APIs for environment detection
|
|
370
|
+
- **Opt-out Support**: Can disable with `disableInDev` or custom logic
|
|
371
|
+
|
|
372
|
+
### GDPR Compliance Example
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
const hasAnalyticsConsent = cookieConsent.analytics;
|
|
376
|
+
|
|
377
|
+
const player = await createPlayer({
|
|
378
|
+
container: '#player',
|
|
379
|
+
plugins: [
|
|
380
|
+
hlsPlugin(),
|
|
381
|
+
// Only load analytics if user consented
|
|
382
|
+
...(hasAnalyticsConsent ? [
|
|
383
|
+
createAnalyticsPlugin({
|
|
384
|
+
beaconUrl: API_URL,
|
|
385
|
+
videoId: video.id,
|
|
386
|
+
})
|
|
387
|
+
] : []),
|
|
388
|
+
uiPlugin(),
|
|
389
|
+
],
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Testing
|
|
394
|
+
|
|
395
|
+
The plugin includes comprehensive tests. Run them:
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
npm test
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
For coverage:
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
npm run test:coverage
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Mock Beacon for Testing
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
import { createAnalyticsPlugin } from '@scarlett-player/analytics';
|
|
411
|
+
|
|
412
|
+
const beacons = [];
|
|
413
|
+
const mockBeacon = (url, payload) => {
|
|
414
|
+
beacons.push(payload);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const plugin = createAnalyticsPlugin({
|
|
418
|
+
beaconUrl: 'http://test',
|
|
419
|
+
videoId: 'test-123',
|
|
420
|
+
customBeacon: mockBeacon, // Use mock instead of real beacon
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// ... test your code ...
|
|
424
|
+
|
|
425
|
+
expect(beacons).toHaveLength(1);
|
|
426
|
+
expect(beacons[0].event).toBe('viewStart');
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Performance
|
|
430
|
+
|
|
431
|
+
The plugin is designed for minimal performance impact:
|
|
432
|
+
|
|
433
|
+
- **Async Beacons**: Uses `navigator.sendBeacon()` for non-blocking sends
|
|
434
|
+
- **Efficient Timers**: Single heartbeat interval per player
|
|
435
|
+
- **Lazy Calculation**: QoE score calculated only when needed
|
|
436
|
+
- **Memory Efficient**: Limits error history and bitrate tracking
|
|
437
|
+
|
|
438
|
+
## Troubleshooting
|
|
439
|
+
|
|
440
|
+
### Beacons Not Sending
|
|
441
|
+
|
|
442
|
+
1. Check browser console for CORS errors
|
|
443
|
+
2. Verify `beaconUrl` is correct
|
|
444
|
+
3. Check network tab for beacon requests
|
|
445
|
+
4. Ensure endpoint accepts POST with JSON
|
|
446
|
+
|
|
447
|
+
### Missing Events
|
|
448
|
+
|
|
449
|
+
1. Verify plugin is loaded before playback
|
|
450
|
+
2. Check event subscriptions in browser DevTools
|
|
451
|
+
3. Enable debug logging: `disableInDev: false`
|
|
452
|
+
|
|
453
|
+
### Incorrect Metrics
|
|
454
|
+
|
|
455
|
+
1. Verify player state is correct
|
|
456
|
+
2. Check for multiple plugin instances
|
|
457
|
+
3. Ensure cleanup on destroy
|
|
458
|
+
|
|
459
|
+
## License
|
|
460
|
+
|
|
461
|
+
MIT
|
|
462
|
+
|
|
463
|
+
## Support
|
|
464
|
+
|
|
465
|
+
For issues and questions:
|
|
466
|
+
- GitHub: https://github.com/Hackney-Enterprises-Inc/scarlett-player/issues
|
|
467
|
+
- Docs: https://scarlettplayer.com
|
|
468
|
+
|
|
469
|
+
## Related Packages
|
|
470
|
+
|
|
471
|
+
- [@scarlett-player/core](../core) - Core player
|
|
472
|
+
- [@scarlett-player/hls](../hls) - HLS provider
|
|
473
|
+
- [@scarlett-player/ui](../ui) - UI components
|