@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.
@@ -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;
@@ -1,68 +1,41 @@
1
- import { customEventAggregator } from "./agent.js";
2
- import { getPayloadSize } from "./utils.js";
3
- import Constants from "./constants";
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
- * Records a video event with the specified type and attributes
9
- * @param {string} eventType - The type of event to record
10
- * @param {Object} attributes - Additional attributes to include with the event
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
- // Input validation
15
- if (
16
- typeof eventType !== "string" ||
17
- eventType.length === 0 ||
18
- !VALID_EVENT_TYPES.includes(eventType)
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
- // Ensure attributes is an object (handle null case)
34
- attributes = attributes || {};
18
+ // Get app configuration
35
19
 
36
- // Check if NRVIDEO is properly initialized
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
- try {
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
- // Check event size to prevent oversized payloads
53
- const eventSize = getPayloadSize(eventObject);
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
- console.error("recordEvent: Error recording event:", error);
38
+ Log.error("Failed to record event:", error.message);
66
39
  return false;
67
40
  }
68
41
  }