@newrelic/video-core 3.2.0-beta-1 → 4.0.1
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/CHANGELOG.md +20 -3
- package/README.md +17 -39
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.LICENSE.txt +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.LICENSE.txt +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/umd/nrvideo.min.js +1 -1
- package/dist/umd/nrvideo.min.js.LICENSE.txt +3 -1
- package/dist/umd/nrvideo.min.js.map +1 -1
- package/package.json +6 -3
- package/src/agent.js +74 -5
- package/src/constants.js +15 -13
- package/src/core.js +59 -15
- package/src/eventAggregator.js +189 -37
- package/src/harvestScheduler.js +416 -0
- package/src/index.js +21 -0
- package/src/optimizedHttpClient.js +165 -0
- package/src/recordEvent.js +23 -50
- package/src/retryQueueHandler.js +124 -0
- package/src/tracker.js +23 -6
- package/src/utils.js +124 -63
- package/src/videoConfiguration.js +113 -0
- package/src/videotrackerstate.js +25 -1
- package/src/authConfiguration.js +0 -138
- package/src/harvester.js +0 -171
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { NrVideoEventAggregator } from "./eventAggregator";
|
|
2
|
+
import { RetryQueueHandler } from "./retryQueueHandler";
|
|
3
|
+
import { OptimizedHttpClient } from "./optimizedHttpClient";
|
|
4
|
+
import { buildUrl, dataSize } from "./utils";
|
|
5
|
+
import Constants from "./constants";
|
|
6
|
+
import Log from "./log";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Enhanced harvest scheduler that orchestrates the video analytics data collection,
|
|
10
|
+
* processing, and transmission with smart harvesting and performance monitoring.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class HarvestScheduler {
|
|
14
|
+
constructor(eventAggregator) {
|
|
15
|
+
// Core components
|
|
16
|
+
this.eventBuffer = eventAggregator;
|
|
17
|
+
this.retryQueueHandler = new RetryQueueHandler();
|
|
18
|
+
this.httpClient = new OptimizedHttpClient();
|
|
19
|
+
this.fallBackUrl = "";
|
|
20
|
+
this.retryCount = 0;
|
|
21
|
+
|
|
22
|
+
// Set up smart harvest callback
|
|
23
|
+
if (this.eventBuffer instanceof NrVideoEventAggregator) {
|
|
24
|
+
this.eventBuffer.setSmartHarvestCallback((type, threshold) =>
|
|
25
|
+
this.triggerSmartHarvest(type, threshold)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Scheduler state
|
|
30
|
+
this.isStarted = false;
|
|
31
|
+
this.currentTimerId = null;
|
|
32
|
+
this.harvestCycle = Constants.INTERVAL;
|
|
33
|
+
this.isHarvesting = false;
|
|
34
|
+
|
|
35
|
+
// Page lifecycle handling
|
|
36
|
+
this.setupPageLifecycleHandlers();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Starts the harvest scheduler.
|
|
41
|
+
*/
|
|
42
|
+
startScheduler() {
|
|
43
|
+
if (this.isStarted) {
|
|
44
|
+
Log.warn("Harvest scheduler is already started");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.isStarted = true;
|
|
49
|
+
|
|
50
|
+
Log.notice("Starting harvest scheduler", {
|
|
51
|
+
harvestCycle: this.harvestCycle,
|
|
52
|
+
eventBufferSize: this.eventBuffer ? this.eventBuffer.size() : 0,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
this.scheduleNextHarvest();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Stops the harvest scheduler.
|
|
60
|
+
*/
|
|
61
|
+
stopScheduler() {
|
|
62
|
+
if (!this.isStarted) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.isStarted = false;
|
|
67
|
+
|
|
68
|
+
if (this.currentTimerId) {
|
|
69
|
+
clearTimeout(this.currentTimerId);
|
|
70
|
+
this.currentTimerId = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Log.notice("Harvest scheduler stopped");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Triggers a smart harvest when buffer reaches threshold capacity.
|
|
78
|
+
* @param {string} type - Type of harvest trigger ('smart' or 'overflow')
|
|
79
|
+
* @param {number} threshold - Threshold percentage that triggered the harvest (60 or 90)
|
|
80
|
+
*/
|
|
81
|
+
async triggerSmartHarvest(type, threshold) {
|
|
82
|
+
Log.notice(`${type} harvest triggered at ${threshold}% threshold`, {
|
|
83
|
+
type,
|
|
84
|
+
threshold,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// If buffer is empty, abort harvest
|
|
88
|
+
if (!this.eventBuffer || this.eventBuffer.isEmpty()) return;
|
|
89
|
+
|
|
90
|
+
// Clear existing timer to prevent redundant harvests
|
|
91
|
+
if (this.currentTimerId) {
|
|
92
|
+
clearTimeout(this.currentTimerId);
|
|
93
|
+
this.currentTimerId = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await this.triggerHarvest({});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
Log.error(`${type} harvest failed:`, error.message);
|
|
100
|
+
} finally {
|
|
101
|
+
// Schedule next harvest after smart harvest completes
|
|
102
|
+
if (this.isStarted) {
|
|
103
|
+
this.scheduleNextHarvest();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Schedules the next harvest based on current conditions.
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
scheduleNextHarvest() {
|
|
113
|
+
if (!this.isStarted) return;
|
|
114
|
+
|
|
115
|
+
const interval = this.harvestCycle;
|
|
116
|
+
this.currentTimerId = setTimeout(() => this.onHarvestInterval(), interval);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handles the harvest interval timer.
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
async onHarvestInterval() {
|
|
124
|
+
try {
|
|
125
|
+
// Check if there's any data to harvest (buffer or retry queue) before starting the harvest process
|
|
126
|
+
const hasBufferData = this.eventBuffer && !this.eventBuffer.isEmpty();
|
|
127
|
+
const hasRetryData =
|
|
128
|
+
this.retryQueueHandler && this.retryQueueHandler.getQueueSize() > 0;
|
|
129
|
+
|
|
130
|
+
if (!hasBufferData && !hasRetryData) return;
|
|
131
|
+
await this.triggerHarvest({});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
Log.error("Error during scheduled harvest:", error.message);
|
|
134
|
+
} finally {
|
|
135
|
+
this.scheduleNextHarvest();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Triggers a harvest cycle with comprehensive error handling and monitoring.
|
|
141
|
+
* @param {object} options - Harvest options
|
|
142
|
+
* @param {boolean} options.isFinalHarvest - Whether this is a final harvest on page unload
|
|
143
|
+
* @param {boolean} options.force - Force harvest even if buffer is empty
|
|
144
|
+
* @returns {Promise<object>} Harvest result
|
|
145
|
+
*/
|
|
146
|
+
async triggerHarvest(options = {}) {
|
|
147
|
+
if (this.isHarvesting) {
|
|
148
|
+
return { success: false, reason: "harvest_in_progress" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.isHarvesting = true;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// Drain events from buffer
|
|
155
|
+
let events = this.drainEvents(options);
|
|
156
|
+
|
|
157
|
+
// For beacon harvests, trim events to fit beacon size if necessary
|
|
158
|
+
if (options.isFinalHarvest) {
|
|
159
|
+
const maxBeaconSize = Constants.MAX_BEACON_SIZE;
|
|
160
|
+
const payloadSize = dataSize(events);
|
|
161
|
+
|
|
162
|
+
if (payloadSize > maxBeaconSize) {
|
|
163
|
+
// Trim events to fit beacon size (keep most recent events)
|
|
164
|
+
events = this.trimEventsToFit(events, maxBeaconSize);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Send single payload - buffer limits guarantee it fits API constraints
|
|
169
|
+
const result = await this.sendChunk(events, options, true);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
success: result.success,
|
|
173
|
+
totalChunks: 1,
|
|
174
|
+
results: [result],
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
Log.error("Harvest cycle failed:", error.message);
|
|
178
|
+
this.handleHarvestFailure(error);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
error: error.message,
|
|
183
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
184
|
+
};
|
|
185
|
+
} finally {
|
|
186
|
+
this.isHarvesting = false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Trims events to fit within a specified size limit for beacon harvests.
|
|
192
|
+
* Keeps the most recent events and discards older ones.
|
|
193
|
+
* @param {Array} events - Events to trim
|
|
194
|
+
* @param {number} maxSize - Maximum payload size in bytes
|
|
195
|
+
* @returns {Array} Trimmed events that fit within size limit
|
|
196
|
+
* @private
|
|
197
|
+
*/
|
|
198
|
+
trimEventsToFit(events, maxSize) {
|
|
199
|
+
if (events.length === 0) return events;
|
|
200
|
+
|
|
201
|
+
// Start from the most recent events (end of array) and work backwards
|
|
202
|
+
const trimmedEvents = [];
|
|
203
|
+
let currentSize = 0;
|
|
204
|
+
|
|
205
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
206
|
+
const event = events[i];
|
|
207
|
+
|
|
208
|
+
// Check if adding this event would exceed the limit
|
|
209
|
+
const testPayloadSize = dataSize({ ins: [event, ...trimmedEvents] });
|
|
210
|
+
|
|
211
|
+
if (testPayloadSize > maxSize) continue;
|
|
212
|
+
|
|
213
|
+
// Add event to the beginning to maintain chronological order
|
|
214
|
+
trimmedEvents.unshift(event);
|
|
215
|
+
currentSize = testPayloadSize;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const discardedCount = events.length - trimmedEvents.length;
|
|
219
|
+
if (discardedCount > 0) {
|
|
220
|
+
const discardedEvents = events.slice(0, discardedCount);
|
|
221
|
+
Log.warn(`Discarded ${discardedCount} events to fit beacon size limit`, {
|
|
222
|
+
originalCount: events.length,
|
|
223
|
+
trimmedCount: trimmedEvents.length,
|
|
224
|
+
finalSize: currentSize,
|
|
225
|
+
maxSize,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// send discarded events to retry queue
|
|
229
|
+
if (this.retryQueueHandler) {
|
|
230
|
+
this.retryQueueHandler.addFailedEvents(discardedEvents);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return trimmedEvents;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Drains events from the event buffer and optionally includes retry queue data.
|
|
239
|
+
* Uses fresh-events-first approach with payload limits.
|
|
240
|
+
* @param {object} options - Harvest options
|
|
241
|
+
* @returns {Array} Drained events
|
|
242
|
+
* @private
|
|
243
|
+
*/
|
|
244
|
+
drainEvents() {
|
|
245
|
+
// Always drain fresh events first (priority approach)
|
|
246
|
+
const freshEvents = this.eventBuffer.drain();
|
|
247
|
+
let events = [...freshEvents];
|
|
248
|
+
let currentPayloadSize = dataSize(freshEvents);
|
|
249
|
+
|
|
250
|
+
// Always check retry queue if it has data - no flags needed
|
|
251
|
+
if (this.retryQueueHandler && this.retryQueueHandler.getQueueSize() > 0) {
|
|
252
|
+
//const retryQueueSize = this.retryQueueHandler.getQueueSize();
|
|
253
|
+
|
|
254
|
+
// Calculate available space for retry events
|
|
255
|
+
const availableSpace = Constants.MAX_PAYLOAD_SIZE - currentPayloadSize;
|
|
256
|
+
const availableEventCount =
|
|
257
|
+
Constants.MAX_EVENTS_PER_BATCH - events.length;
|
|
258
|
+
|
|
259
|
+
if (availableSpace > 0 && availableEventCount > 0) {
|
|
260
|
+
const retryEvents = this.retryQueueHandler.getRetryEventsToFit(
|
|
261
|
+
availableSpace,
|
|
262
|
+
availableEventCount
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (retryEvents.length > 0) {
|
|
266
|
+
events = [...retryEvents, ...events]; // Append retry events before fresh events for maintaining chronoligical order
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return events;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Sends a chunk of events using the optimized HTTP client.
|
|
275
|
+
* @param {Array} chunk - Events to send
|
|
276
|
+
* @param {object} options - Harvest options
|
|
277
|
+
* @param {boolean} isLastChunk - Whether this is the last chunk
|
|
278
|
+
* @returns {Promise<object>} Send result
|
|
279
|
+
* @private
|
|
280
|
+
*/
|
|
281
|
+
async sendChunk(chunk, options, isLastChunk) {
|
|
282
|
+
const url = buildUrl(this.fallBackUrl); //
|
|
283
|
+
const payload = { body: { ins: chunk } };
|
|
284
|
+
const requestOptions = {
|
|
285
|
+
url,
|
|
286
|
+
payload,
|
|
287
|
+
options: {
|
|
288
|
+
...options,
|
|
289
|
+
isLastChunk,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
return new Promise((resolve) => {
|
|
293
|
+
this.httpClient.send(requestOptions, (result) => {
|
|
294
|
+
if (result.retry) {
|
|
295
|
+
this.handleRequestFailure(chunk);
|
|
296
|
+
} else {
|
|
297
|
+
// Inline reset logic for successful request
|
|
298
|
+
this.retryCount = 0;
|
|
299
|
+
this.fallBackUrl = "";
|
|
300
|
+
}
|
|
301
|
+
resolve({
|
|
302
|
+
success: !result.retry,
|
|
303
|
+
status: result.status,
|
|
304
|
+
error: result.error,
|
|
305
|
+
chunk,
|
|
306
|
+
eventCount: chunk.length,
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Handles request failure and implements failover logic for US region.
|
|
314
|
+
* @param {Array} chunk - Failed chunk to add to retry queue
|
|
315
|
+
* @private
|
|
316
|
+
*/
|
|
317
|
+
handleRequestFailure(chunk) {
|
|
318
|
+
// Add failed events to retry queue
|
|
319
|
+
this.retryQueueHandler.addFailedEvents(chunk);
|
|
320
|
+
// Only apply failover logic for US region
|
|
321
|
+
if (window.NRVIDEO?.info?.region !== "US") return;
|
|
322
|
+
this.retryCount++;
|
|
323
|
+
if (this.retryCount > 5) {
|
|
324
|
+
// Reset to primary endpoint after too many failures
|
|
325
|
+
this.retryCount = 0;
|
|
326
|
+
this.fallBackUrl = "";
|
|
327
|
+
} else if (this.retryCount >= 2) {
|
|
328
|
+
// Switch to fallback after 2 consecutive failures
|
|
329
|
+
this.fallBackUrl = Constants.COLLECTOR["US"][1];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Handles harvest failure scenarios.
|
|
335
|
+
* @param {Error} error - Harvest error
|
|
336
|
+
* @private
|
|
337
|
+
*/
|
|
338
|
+
handleHarvestFailure(error) {
|
|
339
|
+
this.consecutiveFailures++;
|
|
340
|
+
|
|
341
|
+
Log.warn("Harvest failure handled", {
|
|
342
|
+
error: error.message,
|
|
343
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Updates the harvest interval and restarts the scheduler to apply the new interval.
|
|
349
|
+
* @param {number} newInterval - The new harvest interval in milliseconds
|
|
350
|
+
* @returns {boolean} - True if interval was updated successfully, false otherwise
|
|
351
|
+
*/
|
|
352
|
+
|
|
353
|
+
updateHarvestInterval(newInterval) {
|
|
354
|
+
if (typeof newInterval !== "number" && isNaN(newInterval)) {
|
|
355
|
+
Log.warn("Invalid newInterval provided to updateHarvestInterval");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (newInterval < 1000 || newInterval > 300000) {
|
|
360
|
+
Log.warn("newInterval out of bounds (1000-300000), ignoring");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check if the interval has actually changed to avoid unnecessary actions
|
|
365
|
+
if (this.harvestCycle === newInterval) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 1. Update the harvestCycle property with the new interval
|
|
370
|
+
this.harvestCycle = newInterval;
|
|
371
|
+
Log.notice("Updated harvestCycle:", this.harvestCycle);
|
|
372
|
+
|
|
373
|
+
// 2. Clear the existing timer
|
|
374
|
+
if (this.currentTimerId) {
|
|
375
|
+
clearTimeout(this.currentTimerId);
|
|
376
|
+
this.currentTimerId = null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 3. Schedule a new timer with the updated interval
|
|
380
|
+
if (this.isStarted) {
|
|
381
|
+
this.scheduleNextHarvest();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Sets up page lifecycle event handlers.
|
|
389
|
+
* @private
|
|
390
|
+
*/
|
|
391
|
+
setupPageLifecycleHandlers() {
|
|
392
|
+
let finalHarvestTriggered = false;
|
|
393
|
+
|
|
394
|
+
const triggerFinalHarvest = () => {
|
|
395
|
+
if (finalHarvestTriggered) return;
|
|
396
|
+
finalHarvestTriggered = true;
|
|
397
|
+
|
|
398
|
+
this.triggerHarvest({ isFinalHarvest: true, force: true });
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Handle page visibility changes
|
|
402
|
+
document.addEventListener("visibilitychange", () => {
|
|
403
|
+
if (document.hidden) triggerFinalHarvest();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Handle page unload
|
|
407
|
+
window.addEventListener("pagehide", () => {
|
|
408
|
+
triggerFinalHarvest();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Handle beforeunload as backup
|
|
412
|
+
window.addEventListener("beforeunload", () => {
|
|
413
|
+
triggerFinalHarvest();
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,9 +6,15 @@ import Emitter from "./emitter";
|
|
|
6
6
|
import Tracker from "./tracker";
|
|
7
7
|
import VideoTracker from "./videotracker";
|
|
8
8
|
import VideoTrackerState from "./videotrackerstate";
|
|
9
|
+
import { NrVideoEventAggregator } from "./eventAggregator";
|
|
10
|
+
import { RetryQueueHandler } from "./retryQueueHandler";
|
|
11
|
+
import { OptimizedHttpClient } from "./optimizedHttpClient";
|
|
12
|
+
import { HarvestScheduler } from "./harvestScheduler";
|
|
13
|
+
import { recordEvent } from "./recordEvent";
|
|
9
14
|
import { version } from "../package.json";
|
|
10
15
|
|
|
11
16
|
const nrvideo = {
|
|
17
|
+
// Core components (existing)
|
|
12
18
|
Constants,
|
|
13
19
|
Chrono,
|
|
14
20
|
Log,
|
|
@@ -18,5 +24,20 @@ const nrvideo = {
|
|
|
18
24
|
VideoTrackerState,
|
|
19
25
|
Core,
|
|
20
26
|
version,
|
|
27
|
+
|
|
28
|
+
// Enhanced video analytics components (new)
|
|
29
|
+
|
|
30
|
+
NrVideoEventAggregator,
|
|
31
|
+
RetryQueueHandler,
|
|
32
|
+
OptimizedHttpClient,
|
|
33
|
+
HarvestScheduler,
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
// Enhanced event recording
|
|
38
|
+
recordEvent,
|
|
39
|
+
|
|
40
|
+
|
|
21
41
|
};
|
|
42
|
+
|
|
22
43
|
export default nrvideo;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { dataSize, shouldRetry } from "./utils";
|
|
2
|
+
import Log from "./log";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Optimized HTTP client for video analytics data transmission with
|
|
6
|
+
* performance monitoring and efficient request handling.
|
|
7
|
+
*/
|
|
8
|
+
export class OptimizedHttpClient {
|
|
9
|
+
/**
|
|
10
|
+
* Sends data to the specified URL with performance monitoring.
|
|
11
|
+
* @param {object} requestOptions - Request configuration
|
|
12
|
+
* @param {string} requestOptions.url - Target URL
|
|
13
|
+
* @param {object} requestOptions.payload - Request payload
|
|
14
|
+
* @param {object} requestOptions.options - Additional options
|
|
15
|
+
* @param {Function} callback - Callback function for handling response
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
async send(requestOptions, callback) {
|
|
19
|
+
const { url, payload, options = {} } = requestOptions;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Validate input
|
|
23
|
+
if (!url || !payload) {
|
|
24
|
+
throw new Error("URL and payload are required");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Create request object
|
|
28
|
+
const request = {
|
|
29
|
+
url,
|
|
30
|
+
payload,
|
|
31
|
+
options,
|
|
32
|
+
callback,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Execute request immediately
|
|
36
|
+
await this.executeRequest(request);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
Log.error("Failed to send request:", error.message);
|
|
39
|
+
callback({ retry: false, status: 0, error: error.message });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Executes an HTTP request with timeout and error handling.
|
|
45
|
+
* @param {object} request - Request object
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
async executeRequest(request) {
|
|
49
|
+
const { url, payload, options, callback } = request;
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const requestBody = JSON.stringify(payload.body);
|
|
54
|
+
|
|
55
|
+
// Handle final harvest with sendBeacon
|
|
56
|
+
if (options.isFinalHarvest && navigator.sendBeacon) {
|
|
57
|
+
const success = await this.sendWithBeacon(url, requestBody);
|
|
58
|
+
const result = { success, status: success ? 204 : 0 };
|
|
59
|
+
this.handleRequestComplete(request, result, startTime);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Use fetch with timeout
|
|
64
|
+
const response = await this.fetchWithTimeout(
|
|
65
|
+
url,
|
|
66
|
+
{
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: requestBody,
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
},
|
|
72
|
+
keepalive: options.isFinalHarvest,
|
|
73
|
+
},
|
|
74
|
+
10000
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const result = {
|
|
78
|
+
success: response.ok,
|
|
79
|
+
status: response.status,
|
|
80
|
+
statusText: response.statusText,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this.handleRequestComplete(request, result, startTime);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const result = {
|
|
86
|
+
success: false,
|
|
87
|
+
status: 0,
|
|
88
|
+
error: error.message,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.handleRequestComplete(request, result, startTime);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handles request completion.
|
|
97
|
+
* @param {object} request - Request object
|
|
98
|
+
* @param {object} result - Request result
|
|
99
|
+
* @param {number} startTime - Request start timestamp
|
|
100
|
+
* @param {string} endpoint - The endpoint that was used for the request
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
handleRequestComplete(request, result) {
|
|
104
|
+
const { callback } = request;
|
|
105
|
+
|
|
106
|
+
// Use smart retry logic based on HTTP status codes
|
|
107
|
+
const shouldRetryRequest =
|
|
108
|
+
!result.success &&
|
|
109
|
+
(result.status === 0 || // Network/timeout errors
|
|
110
|
+
shouldRetry(result.status)); // Smart status code-based retry
|
|
111
|
+
|
|
112
|
+
callback({
|
|
113
|
+
retry: shouldRetryRequest,
|
|
114
|
+
status: result.status,
|
|
115
|
+
error: result.error,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Sends data using navigator.sendBeacon for final harvests.
|
|
121
|
+
* @param {string} url - Target URL
|
|
122
|
+
* @param {string} body - Request body
|
|
123
|
+
* @returns {Promise<boolean>} True if successful
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
async sendWithBeacon(url, body) {
|
|
127
|
+
try {
|
|
128
|
+
return navigator.sendBeacon(url, body);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
Log.warn("sendBeacon failed, falling back to fetch:", error.message);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fetch with timeout implementation.
|
|
137
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortController|MDN AbortController}
|
|
138
|
+
* @param {string} url - Target URL
|
|
139
|
+
* @param {object} options - Fetch options
|
|
140
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
141
|
+
* @returns {Promise<Response>} Fetch response
|
|
142
|
+
* @private
|
|
143
|
+
*/
|
|
144
|
+
async fetchWithTimeout(url, options, timeout) {
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(url, {
|
|
150
|
+
...options,
|
|
151
|
+
signal: controller.signal,
|
|
152
|
+
});
|
|
153
|
+
clearTimeout(timeoutId);
|
|
154
|
+
return response;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
clearTimeout(timeoutId);
|
|
157
|
+
if (error.name === "AbortError") {
|
|
158
|
+
throw new Error(`Request timeout after ${timeout}ms`);
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export default OptimizedHttpClient;
|
package/src/recordEvent.js
CHANGED
|
@@ -1,68 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
const { VALID_EVENT_TYPES, MAX_EVENT_SIZE } = Constants;
|
|
1
|
+
import { videoAnalyticsHarvester } from "./agent.js";
|
|
2
|
+
import Constants from "./constants.js";
|
|
3
|
+
import Log from "./log.js";
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
|
-
*
|
|
9
|
-
* @param {string} eventType -
|
|
10
|
-
* @param {
|
|
11
|
-
* @returns {boolean} - True if event was recorded successfully, false otherwise
|
|
6
|
+
* Enhanced record event function with validation, enrichment, and unified handling.
|
|
7
|
+
* @param {string} eventType - Type of event to record
|
|
8
|
+
* @param {object} attributes - Event attributes
|
|
12
9
|
*/
|
|
13
10
|
export function recordEvent(eventType, attributes = {}) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
console.warn("recordEvent: Invalid eventType provided:", eventType);
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Validate attributes parameter
|
|
25
|
-
if (
|
|
26
|
-
attributes !== null &&
|
|
27
|
-
(typeof attributes !== "object" || Array.isArray(attributes))
|
|
28
|
-
) {
|
|
29
|
-
console.warn("recordEvent: attributes must be a plain object");
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
11
|
+
try {
|
|
12
|
+
// Validate event type
|
|
13
|
+
if (!Constants.VALID_EVENT_TYPES.includes(eventType)) {
|
|
14
|
+
Log.warn("Invalid event type provided to recordEvent", { eventType });
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
32
17
|
|
|
33
|
-
|
|
34
|
-
attributes = attributes || {};
|
|
18
|
+
// Get app configuration
|
|
35
19
|
|
|
36
|
-
|
|
37
|
-
if (!window.NRVIDEO || !window.NRVIDEO.info) {
|
|
38
|
-
console.error("recordEvent: NRVIDEO not properly initialized");
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
20
|
+
if (!window?.NRVIDEO?.info) return;
|
|
41
21
|
|
|
42
|
-
|
|
43
|
-
const { appName } = window.NRVIDEO.info;
|
|
22
|
+
const { appName, applicationID } = window.NRVIDEO.info;
|
|
44
23
|
|
|
45
24
|
const eventObject = {
|
|
46
25
|
...attributes,
|
|
47
26
|
eventType,
|
|
48
|
-
appName,
|
|
27
|
+
...(applicationID ? {} : { appName }), // Only include appName when no applicationID
|
|
49
28
|
timestamp: Date.now(),
|
|
29
|
+
timeSinceLoad: window.performance
|
|
30
|
+
? window.performance.now() / 1000
|
|
31
|
+
: null,
|
|
50
32
|
};
|
|
51
33
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
if (eventSize > MAX_EVENT_SIZE) {
|
|
56
|
-
console.warn(
|
|
57
|
-
`recordEvent: Event size (${eventSize} bytes) exceeds maximum (${MAX_EVENT_SIZE} bytes)`
|
|
58
|
-
);
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
customEventAggregator.add(eventObject);
|
|
63
|
-
return true;
|
|
34
|
+
// Send to video analytics harvester
|
|
35
|
+
const success = videoAnalyticsHarvester.addEvent(eventObject);
|
|
36
|
+
return success;
|
|
64
37
|
} catch (error) {
|
|
65
|
-
|
|
38
|
+
Log.error("Failed to record event:", error.message);
|
|
66
39
|
return false;
|
|
67
40
|
}
|
|
68
41
|
}
|