unified-video-framework 1.4.157 → 1.4.159
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/package.json +12 -2
- package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.d.ts +18 -0
- package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.d.ts.map +1 -0
- package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.js +117 -0
- package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.js.map +1 -0
- package/packages/core/dist/analytics/core/AnalyticsProvider.d.ts +18 -0
- package/packages/core/dist/analytics/core/AnalyticsProvider.d.ts.map +1 -0
- package/packages/core/dist/analytics/core/AnalyticsProvider.js +99 -0
- package/packages/core/dist/analytics/core/AnalyticsProvider.js.map +1 -0
- package/packages/core/dist/analytics/core/DynamicAnalyticsManager.d.ts +20 -0
- package/packages/core/dist/analytics/core/DynamicAnalyticsManager.d.ts.map +1 -0
- package/packages/core/dist/analytics/core/DynamicAnalyticsManager.js +161 -0
- package/packages/core/dist/analytics/core/DynamicAnalyticsManager.js.map +1 -0
- package/packages/core/dist/analytics/core/EventBatcher.d.ts +32 -0
- package/packages/core/dist/analytics/core/EventBatcher.d.ts.map +1 -0
- package/packages/core/dist/analytics/core/EventBatcher.js +98 -0
- package/packages/core/dist/analytics/core/EventBatcher.js.map +1 -0
- package/packages/core/dist/analytics/core/PlayerAnalytics.d.ts +19 -0
- package/packages/core/dist/analytics/core/PlayerAnalytics.d.ts.map +1 -0
- package/packages/core/dist/analytics/core/PlayerAnalytics.js +80 -0
- package/packages/core/dist/analytics/core/PlayerAnalytics.js.map +1 -0
- package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.d.ts +32 -0
- package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.d.ts.map +1 -0
- package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.js +220 -0
- package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.js.map +1 -0
- package/packages/core/dist/analytics/index.d.ts +13 -0
- package/packages/core/dist/analytics/index.d.ts.map +1 -0
- package/packages/core/dist/analytics/index.js +13 -0
- package/packages/core/dist/analytics/index.js.map +1 -0
- package/packages/core/dist/analytics/types/AnalyticsTypes.d.ts +239 -0
- package/packages/core/dist/analytics/types/AnalyticsTypes.d.ts.map +1 -0
- package/packages/core/dist/analytics/types/AnalyticsTypes.js +8 -0
- package/packages/core/dist/analytics/types/AnalyticsTypes.js.map +1 -0
- package/packages/core/dist/analytics/utils/DeviceDetection.d.ts +27 -0
- package/packages/core/dist/analytics/utils/DeviceDetection.d.ts.map +1 -0
- package/packages/core/dist/analytics/utils/DeviceDetection.js +184 -0
- package/packages/core/dist/analytics/utils/DeviceDetection.js.map +1 -0
- package/packages/core/dist/chapter-manager.d.ts +39 -0
- package/packages/core/dist/index.d.ts +1 -0
- package/packages/core/dist/index.d.ts.map +1 -1
- package/packages/core/dist/index.js +1 -0
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/src/analytics/README.md +902 -0
- package/packages/core/src/analytics/adapters/PlayerAnalyticsAdapter.ts +156 -0
- package/packages/core/src/analytics/core/AnalyticsProvider.ts +169 -0
- package/packages/core/src/analytics/core/DynamicAnalyticsManager.ts +199 -0
- package/packages/core/src/analytics/core/EventBatcher.ts +160 -0
- package/packages/core/src/analytics/core/PlayerAnalytics.ts +147 -0
- package/packages/core/src/analytics/index.ts +51 -0
- package/packages/core/src/analytics/types/AnalyticsTypes.ts +315 -0
- package/packages/core/src/analytics/utils/DeviceDetection.ts +220 -0
- package/packages/core/src/index.ts +3 -0
- package/packages/ios/README.md +84 -0
- package/packages/web/dist/WebPlayer.d.ts +6 -0
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +273 -67
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/epg/EPGController.d.ts +78 -0
- package/packages/web/dist/epg/EPGController.d.ts.map +1 -0
- package/packages/web/dist/epg/EPGController.js +476 -0
- package/packages/web/dist/epg/EPGController.js.map +1 -0
- package/packages/web/src/WebPlayer.ts +336 -85
- package/src/analytics/README.md +902 -0
- package/src/analytics/adapters/PlayerAnalyticsAdapter.ts +572 -0
- package/src/analytics/core/DynamicAnalyticsManager.ts +526 -0
- package/src/analytics/examples/DynamicAnalyticsExample.ts +324 -0
- package/src/analytics/index.ts +60 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlayerAnalyticsAdapter - Adapter for integrating with player analytics API
|
|
3
|
+
* Maps internal analytics events to the player analytics system format
|
|
4
|
+
* Implements BaseAnalyticsProvider interface
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
AnalyticsEventData,
|
|
9
|
+
PlayerSessionInfo,
|
|
10
|
+
BaseAnalyticsProvider,
|
|
11
|
+
PlayerAnalyticsConfig
|
|
12
|
+
} from '../types/AnalyticsTypes';
|
|
13
|
+
import { deviceDetection } from '../utils/DeviceDetection';
|
|
14
|
+
|
|
15
|
+
export class PlayerAnalyticsAdapter implements BaseAnalyticsProvider {
|
|
16
|
+
public name: string;
|
|
17
|
+
public enabled: boolean;
|
|
18
|
+
private config: PlayerAnalyticsConfig;
|
|
19
|
+
private currentSessionId: string | null = null;
|
|
20
|
+
private eventQueue: AnalyticsEventData[] = [];
|
|
21
|
+
private flushTimer: any = null;
|
|
22
|
+
|
|
23
|
+
constructor(name: string, config: PlayerAnalyticsConfig) {
|
|
24
|
+
this.name = name;
|
|
25
|
+
this.enabled = true;
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async initialize(): Promise<void> {
|
|
30
|
+
// No initialization needed
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async startSession(sessionInfo: PlayerSessionInfo): Promise<string> {
|
|
34
|
+
const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
35
|
+
this.currentSessionId = sessionId;
|
|
36
|
+
|
|
37
|
+
const event: AnalyticsEventData = {
|
|
38
|
+
eventType: 'session_start',
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
video: sessionInfo.initialVideo,
|
|
41
|
+
device: deviceDetection.getDeviceInfo(),
|
|
42
|
+
metadata: {
|
|
43
|
+
sessionId,
|
|
44
|
+
playerId: this.config.playerId,
|
|
45
|
+
userId: sessionInfo.userId,
|
|
46
|
+
customData: sessionInfo.customData
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
await this.trackEvent(event);
|
|
51
|
+
return sessionId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async trackEvent(event: AnalyticsEventData): Promise<void> {
|
|
55
|
+
if (!this.enabled) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.eventQueue.push(event);
|
|
60
|
+
|
|
61
|
+
// Auto-flush if batch size reached
|
|
62
|
+
if (this.eventQueue.length >= (this.config.batchSize || 10)) {
|
|
63
|
+
await this.flush();
|
|
64
|
+
} else {
|
|
65
|
+
// Schedule flush
|
|
66
|
+
this.scheduleFlush();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async endSession(): Promise<void> {
|
|
71
|
+
if (this.currentSessionId) {
|
|
72
|
+
const event: AnalyticsEventData = {
|
|
73
|
+
eventType: 'session_end',
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
metadata: {
|
|
76
|
+
sessionId: this.currentSessionId,
|
|
77
|
+
playerId: this.config.playerId
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
await this.trackEvent(event);
|
|
82
|
+
await this.flush();
|
|
83
|
+
this.currentSessionId = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async flush(): Promise<void> {
|
|
88
|
+
if (this.eventQueue.length === 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const events = [...this.eventQueue];
|
|
93
|
+
this.eventQueue = [];
|
|
94
|
+
|
|
95
|
+
if (this.flushTimer) {
|
|
96
|
+
clearTimeout(this.flushTimer);
|
|
97
|
+
this.flushTimer = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const payload = {
|
|
102
|
+
session: {
|
|
103
|
+
sessionId: this.currentSessionId || 'unknown',
|
|
104
|
+
playerId: this.config.playerId,
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
customData: {}
|
|
107
|
+
},
|
|
108
|
+
events
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const response = await fetch(`${this.config.baseUrl}/analytics/player/ingest`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
116
|
+
...(this.config.tenantId && { 'X-Tenant-ID': this.config.tenantId })
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify(payload)
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new Error(`Analytics API error: ${response.status}`);
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error('Failed to send analytics events:', error);
|
|
126
|
+
// Re-queue events on failure
|
|
127
|
+
this.eventQueue = [...events, ...this.eventQueue];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async destroy(): Promise<void> {
|
|
132
|
+
if (this.currentSessionId) {
|
|
133
|
+
await this.endSession();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (this.flushTimer) {
|
|
137
|
+
clearTimeout(this.flushTimer);
|
|
138
|
+
this.flushTimer = null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Final flush attempt
|
|
142
|
+
if (this.eventQueue.length > 0) {
|
|
143
|
+
await this.flush();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private scheduleFlush(): void {
|
|
148
|
+
if (this.flushTimer) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.flushTimer = setTimeout(() => {
|
|
153
|
+
this.flush().catch(console.error);
|
|
154
|
+
}, (this.config.flushInterval || 30) * 1000);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnalyticsProvider - Base provider and factory functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BaseAnalyticsProvider,
|
|
7
|
+
AnalyticsEventData,
|
|
8
|
+
PlayerSessionInfo,
|
|
9
|
+
AnalyticsProviderType,
|
|
10
|
+
DynamicAnalyticsConfig,
|
|
11
|
+
PlayerAnalyticsConfig
|
|
12
|
+
} from '../types/AnalyticsTypes';
|
|
13
|
+
import { PlayerAnalyticsAdapter } from '../adapters/PlayerAnalyticsAdapter';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Abstract base class for analytics providers
|
|
17
|
+
*/
|
|
18
|
+
export abstract class AnalyticsProvider implements BaseAnalyticsProvider {
|
|
19
|
+
public name: string;
|
|
20
|
+
public enabled: boolean;
|
|
21
|
+
|
|
22
|
+
constructor(name: string, enabled = true) {
|
|
23
|
+
this.name = name;
|
|
24
|
+
this.enabled = enabled;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
abstract initialize(): Promise<void>;
|
|
28
|
+
abstract trackEvent(event: AnalyticsEventData): Promise<void>;
|
|
29
|
+
abstract startSession(sessionInfo: PlayerSessionInfo): Promise<string>;
|
|
30
|
+
abstract endSession(): Promise<void>;
|
|
31
|
+
abstract flush(): Promise<void>;
|
|
32
|
+
abstract destroy(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create analytics provider based on type and configuration
|
|
37
|
+
*/
|
|
38
|
+
export function createAnalyticsProvider(
|
|
39
|
+
type: AnalyticsProviderType,
|
|
40
|
+
name: string,
|
|
41
|
+
config: any
|
|
42
|
+
): BaseAnalyticsProvider {
|
|
43
|
+
switch (type) {
|
|
44
|
+
case AnalyticsProviderType.PLAYER_ANALYTICS:
|
|
45
|
+
return new PlayerAnalyticsAdapter(name, config);
|
|
46
|
+
|
|
47
|
+
case AnalyticsProviderType.CUSTOM:
|
|
48
|
+
if (config.factory) {
|
|
49
|
+
return config.factory(config);
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Custom provider "${name}" requires a factory function`);
|
|
52
|
+
|
|
53
|
+
case AnalyticsProviderType.GOOGLE_ANALYTICS:
|
|
54
|
+
case AnalyticsProviderType.ADOBE_ANALYTICS:
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Analytics provider type "${type}" is not supported yet`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get default analytics configuration
|
|
62
|
+
*/
|
|
63
|
+
export function getDefaultAnalyticsConfig(): DynamicAnalyticsConfig {
|
|
64
|
+
return {
|
|
65
|
+
enabled: true,
|
|
66
|
+
providers: [],
|
|
67
|
+
globalSettings: {
|
|
68
|
+
enableConsoleLogging: false,
|
|
69
|
+
enableErrorReporting: true,
|
|
70
|
+
sessionTimeout: 60, // 60 minutes
|
|
71
|
+
defaultBatchSize: 10,
|
|
72
|
+
defaultFlushInterval: 30, // 30 seconds
|
|
73
|
+
retryAttempts: 3,
|
|
74
|
+
retryDelay: 1000 // 1 second
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Helper function to create Player Analytics provider config
|
|
81
|
+
*/
|
|
82
|
+
export function createPlayerAnalyticsProviderConfig(
|
|
83
|
+
baseUrl: string,
|
|
84
|
+
apiKey: string,
|
|
85
|
+
playerId: string,
|
|
86
|
+
options: Partial<PlayerAnalyticsConfig> = {}
|
|
87
|
+
): PlayerAnalyticsConfig {
|
|
88
|
+
return {
|
|
89
|
+
baseUrl,
|
|
90
|
+
apiKey,
|
|
91
|
+
playerId,
|
|
92
|
+
tenantId: options.tenantId,
|
|
93
|
+
heartbeatInterval: options.heartbeatInterval || 10,
|
|
94
|
+
batchSize: options.batchSize || 10,
|
|
95
|
+
flushInterval: options.flushInterval || 30,
|
|
96
|
+
enableOfflineStorage: options.enableOfflineStorage !== false,
|
|
97
|
+
maxRetries: options.maxRetries || 3,
|
|
98
|
+
retryDelay: options.retryDelay || 1000
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate analytics configuration
|
|
104
|
+
*/
|
|
105
|
+
export function validateAnalyticsConfig(config: DynamicAnalyticsConfig): void {
|
|
106
|
+
if (!config) {
|
|
107
|
+
throw new Error('Analytics configuration is required');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (typeof config.enabled !== 'boolean') {
|
|
111
|
+
throw new Error('Analytics configuration must specify enabled state');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!Array.isArray(config.providers)) {
|
|
115
|
+
throw new Error('Analytics configuration must include providers array');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
config.providers.forEach((provider, index) => {
|
|
119
|
+
if (!provider.name) {
|
|
120
|
+
throw new Error(`Provider at index ${index} must have a name`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!provider.type) {
|
|
124
|
+
throw new Error(`Provider "${provider.name}" must have a type`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof provider.enabled !== 'boolean') {
|
|
128
|
+
throw new Error(`Provider "${provider.name}" must specify enabled state`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (provider.type === AnalyticsProviderType.PLAYER_ANALYTICS) {
|
|
132
|
+
validatePlayerAnalyticsConfig(provider.config);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Validate Player Analytics specific configuration
|
|
139
|
+
*/
|
|
140
|
+
function validatePlayerAnalyticsConfig(config: PlayerAnalyticsConfig): void {
|
|
141
|
+
if (!config.baseUrl) {
|
|
142
|
+
throw new Error('Player Analytics provider requires baseUrl');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!config.apiKey) {
|
|
146
|
+
throw new Error('Player Analytics provider requires apiKey');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!config.playerId) {
|
|
150
|
+
throw new Error('Player Analytics provider requires playerId');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Merge analytics configurations
|
|
156
|
+
*/
|
|
157
|
+
export function mergeAnalyticsConfigs(
|
|
158
|
+
defaultConfig: DynamicAnalyticsConfig,
|
|
159
|
+
userConfig: Partial<DynamicAnalyticsConfig>
|
|
160
|
+
): DynamicAnalyticsConfig {
|
|
161
|
+
return {
|
|
162
|
+
enabled: userConfig.enabled !== undefined ? userConfig.enabled : defaultConfig.enabled,
|
|
163
|
+
providers: userConfig.providers || defaultConfig.providers,
|
|
164
|
+
globalSettings: {
|
|
165
|
+
...defaultConfig.globalSettings,
|
|
166
|
+
...userConfig.globalSettings
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DynamicAnalyticsManager - Main analytics system
|
|
3
|
+
* Manages multiple analytics providers dynamically
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DynamicAnalyticsConfig,
|
|
8
|
+
AnalyticsProviderType,
|
|
9
|
+
BaseAnalyticsProvider,
|
|
10
|
+
AnalyticsEventData,
|
|
11
|
+
VideoInfo,
|
|
12
|
+
PlayerState,
|
|
13
|
+
PlayerSessionInfo
|
|
14
|
+
} from '../types/AnalyticsTypes';
|
|
15
|
+
import { createAnalyticsProvider } from './AnalyticsProvider';
|
|
16
|
+
|
|
17
|
+
export class DynamicAnalyticsManager {
|
|
18
|
+
private config: DynamicAnalyticsConfig;
|
|
19
|
+
private providers: Map<string, BaseAnalyticsProvider> = new Map();
|
|
20
|
+
private currentSessionId: string | null = null;
|
|
21
|
+
private enabled: boolean;
|
|
22
|
+
|
|
23
|
+
constructor(config: DynamicAnalyticsConfig) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.enabled = config.enabled;
|
|
26
|
+
this.initializeProviders();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private async initializeProviders(): Promise<void> {
|
|
30
|
+
for (const providerConfig of this.config.providers) {
|
|
31
|
+
if (providerConfig.enabled) {
|
|
32
|
+
try {
|
|
33
|
+
const provider = createAnalyticsProvider(
|
|
34
|
+
providerConfig.type,
|
|
35
|
+
providerConfig.name,
|
|
36
|
+
providerConfig.config
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await provider.initialize();
|
|
40
|
+
this.providers.set(providerConfig.name, provider);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`Failed to initialize analytics provider "${providerConfig.name}":`, error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
startSession(videoInfo: VideoInfo, userInfo: any = {}): string {
|
|
49
|
+
if (!this.enabled) {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
54
|
+
this.currentSessionId = sessionId;
|
|
55
|
+
|
|
56
|
+
const sessionInfo: PlayerSessionInfo = {
|
|
57
|
+
sessionId,
|
|
58
|
+
playerId: 'default',
|
|
59
|
+
startTime: Date.now(),
|
|
60
|
+
userId: userInfo.userId,
|
|
61
|
+
userType: userInfo.userType,
|
|
62
|
+
customData: userInfo,
|
|
63
|
+
initialVideo: videoInfo
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.providers.forEach(async (provider) => {
|
|
67
|
+
if (provider.enabled) {
|
|
68
|
+
try {
|
|
69
|
+
await provider.startSession(sessionInfo);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(`Provider "${provider.name}" failed to start session:`, error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return sessionId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async endSession(): Promise<void> {
|
|
80
|
+
if (!this.currentSessionId || !this.enabled) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const promises = Array.from(this.providers.values()).map(async (provider) => {
|
|
85
|
+
if (provider.enabled) {
|
|
86
|
+
try {
|
|
87
|
+
await provider.endSession();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`Provider "${provider.name}" failed to end session:`, error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await Promise.allSettled(promises);
|
|
95
|
+
this.currentSessionId = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
trackEvent(eventType: string, playerState?: PlayerState, customData?: any): void {
|
|
99
|
+
if (!this.enabled || !this.currentSessionId) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const event: AnalyticsEventData = {
|
|
104
|
+
eventType,
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
currentTime: playerState?.currentTime,
|
|
107
|
+
player: playerState,
|
|
108
|
+
custom: customData
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
this.providers.forEach(async (provider) => {
|
|
112
|
+
if (provider.enabled) {
|
|
113
|
+
try {
|
|
114
|
+
await provider.trackEvent(event);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`Provider "${provider.name}" failed to track event:`, error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
trackCustomEvent(eventType: string, data: any): void {
|
|
123
|
+
if (!this.enabled) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const event: AnalyticsEventData = {
|
|
128
|
+
eventType: 'custom',
|
|
129
|
+
timestamp: Date.now(),
|
|
130
|
+
custom: { type: eventType, ...data }
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
this.providers.forEach(async (provider) => {
|
|
134
|
+
if (provider.enabled) {
|
|
135
|
+
try {
|
|
136
|
+
await provider.trackEvent(event);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`Provider "${provider.name}" failed to track custom event:`, error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
addProvider(name: string, type: AnalyticsProviderType, config: any): void {
|
|
145
|
+
try {
|
|
146
|
+
const provider = createAnalyticsProvider(type, name, config);
|
|
147
|
+
provider.initialize().then(() => {
|
|
148
|
+
this.providers.set(name, provider);
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(`Failed to add provider "${name}":`, error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async removeProvider(name: string): Promise<void> {
|
|
156
|
+
const provider = this.providers.get(name);
|
|
157
|
+
if (provider) {
|
|
158
|
+
try {
|
|
159
|
+
await provider.destroy();
|
|
160
|
+
this.providers.delete(name);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`Failed to remove provider "${name}":`, error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
toggleProvider(name: string, enabled: boolean): void {
|
|
168
|
+
const provider = this.providers.get(name);
|
|
169
|
+
if (provider) {
|
|
170
|
+
provider.enabled = enabled;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async initialize(): Promise<void> {
|
|
175
|
+
// Already done in constructor, but this method is needed for interface compatibility
|
|
176
|
+
return Promise.resolve();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
isEnabled(): boolean {
|
|
180
|
+
return this.enabled;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async destroy(): Promise<void> {
|
|
184
|
+
if (this.currentSessionId) {
|
|
185
|
+
await this.endSession();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const promises = Array.from(this.providers.values()).map(async (provider) => {
|
|
189
|
+
try {
|
|
190
|
+
await provider.destroy();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(`Provider "${provider.name}" failed to destroy:`, error);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await Promise.allSettled(promises);
|
|
197
|
+
this.providers.clear();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventBatcher - Batches analytics events for efficient network usage
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AnalyticsEventData } from '../types/AnalyticsTypes';
|
|
6
|
+
|
|
7
|
+
export interface BatcherConfig {
|
|
8
|
+
maxSize?: number;
|
|
9
|
+
flushInterval?: number; // in milliseconds
|
|
10
|
+
maxWaitTime?: number; // in milliseconds
|
|
11
|
+
onFlush: (events: AnalyticsEventData[]) => Promise<void>;
|
|
12
|
+
onError?: (error: Error, events: AnalyticsEventData[]) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class EventBatcher {
|
|
16
|
+
private events: AnalyticsEventData[] = [];
|
|
17
|
+
private flushTimer: any = null;
|
|
18
|
+
private maxWaitTimer: any = null;
|
|
19
|
+
private destroyed = false;
|
|
20
|
+
|
|
21
|
+
private readonly maxSize: number;
|
|
22
|
+
private readonly flushInterval: number;
|
|
23
|
+
private readonly maxWaitTime: number;
|
|
24
|
+
private readonly onFlush: (events: AnalyticsEventData[]) => Promise<void>;
|
|
25
|
+
private readonly onError?: (error: Error, events: AnalyticsEventData[]) => void;
|
|
26
|
+
|
|
27
|
+
constructor(config: BatcherConfig) {
|
|
28
|
+
this.maxSize = config.maxSize || 10;
|
|
29
|
+
this.flushInterval = config.flushInterval || 30000; // 30 seconds
|
|
30
|
+
this.maxWaitTime = config.maxWaitTime || 60000; // 60 seconds
|
|
31
|
+
this.onFlush = config.onFlush;
|
|
32
|
+
this.onError = config.onError;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Add an event to the batch
|
|
37
|
+
*/
|
|
38
|
+
addEvent(event: AnalyticsEventData): void {
|
|
39
|
+
if (this.destroyed) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.events.push(event);
|
|
44
|
+
|
|
45
|
+
// Start max wait timer if this is the first event
|
|
46
|
+
if (this.events.length === 1) {
|
|
47
|
+
this.startMaxWaitTimer();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Flush if batch is full
|
|
51
|
+
if (this.events.length >= this.maxSize) {
|
|
52
|
+
this.flush();
|
|
53
|
+
} else {
|
|
54
|
+
// Reset flush timer
|
|
55
|
+
this.resetFlushTimer();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Manually flush all events
|
|
61
|
+
*/
|
|
62
|
+
async flush(): Promise<void> {
|
|
63
|
+
if (this.destroyed || this.events.length === 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const eventsToFlush = [...this.events];
|
|
68
|
+
this.events = [];
|
|
69
|
+
this.clearTimers();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await this.onFlush(eventsToFlush);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (this.onError) {
|
|
75
|
+
this.onError(error as Error, eventsToFlush);
|
|
76
|
+
} else {
|
|
77
|
+
console.error('EventBatcher flush error:', error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get current batch size
|
|
84
|
+
*/
|
|
85
|
+
getBatchSize(): number {
|
|
86
|
+
return this.events.length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if batcher has pending events
|
|
91
|
+
*/
|
|
92
|
+
hasPendingEvents(): boolean {
|
|
93
|
+
return this.events.length > 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Destroy the batcher and flush remaining events
|
|
98
|
+
*/
|
|
99
|
+
async destroy(): Promise<void> {
|
|
100
|
+
if (this.destroyed) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.destroyed = true;
|
|
105
|
+
this.clearTimers();
|
|
106
|
+
|
|
107
|
+
// Flush remaining events
|
|
108
|
+
if (this.events.length > 0) {
|
|
109
|
+
await this.flush();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private resetFlushTimer(): void {
|
|
114
|
+
this.clearFlushTimer();
|
|
115
|
+
|
|
116
|
+
this.flushTimer = setTimeout(() => {
|
|
117
|
+
this.flush();
|
|
118
|
+
}, this.flushInterval);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private startMaxWaitTimer(): void {
|
|
122
|
+
this.clearMaxWaitTimer();
|
|
123
|
+
|
|
124
|
+
this.maxWaitTimer = setTimeout(() => {
|
|
125
|
+
this.flush();
|
|
126
|
+
}, this.maxWaitTime);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private clearFlushTimer(): void {
|
|
130
|
+
if (this.flushTimer) {
|
|
131
|
+
clearTimeout(this.flushTimer);
|
|
132
|
+
this.flushTimer = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private clearMaxWaitTimer(): void {
|
|
137
|
+
if (this.maxWaitTimer) {
|
|
138
|
+
clearTimeout(this.maxWaitTimer);
|
|
139
|
+
this.maxWaitTimer = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private clearTimers(): void {
|
|
144
|
+
this.clearFlushTimer();
|
|
145
|
+
this.clearMaxWaitTimer();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create an EventBatcher with default configuration
|
|
151
|
+
*/
|
|
152
|
+
export function createEventBatcher(
|
|
153
|
+
onFlush: (events: AnalyticsEventData[]) => Promise<void>,
|
|
154
|
+
options: Partial<BatcherConfig> = {}
|
|
155
|
+
): EventBatcher {
|
|
156
|
+
return new EventBatcher({
|
|
157
|
+
onFlush,
|
|
158
|
+
...options
|
|
159
|
+
});
|
|
160
|
+
}
|