@newrelic/video-core 3.2.0-beta-1 → 4.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/video-core",
3
- "version": "3.2.0-beta-1",
3
+ "version": "4.0.0",
4
4
  "description": "New Relic video tracking core library",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -21,15 +21,17 @@
21
21
  },
22
22
  "author": "Jordi Aguilar",
23
23
  "contributors": [
24
- "Andreu Santarén Llop"
24
+ "Andreu Santarén Llop",
25
+ "Malay Chandan",
26
+ "Bharat Sarsawat"
25
27
  ],
26
28
  "license": "Apache-2.0",
27
29
  "devDependencies": {
28
30
  "@babel/core": "^7.24.5",
29
31
  "@babel/plugin-transform-modules-commonjs": "^7.24.1",
32
+ "@newrelic/newrelic-oss-cli": "^0.1.2",
30
33
  "@babel/preset-env": "^7.24.5",
31
34
  "@babel/register": "^7.24.6",
32
- "@newrelic/newrelic-oss-cli": "^0.1.2",
33
35
  "aws-sdk": "^2.920.0",
34
36
  "babel-loader": "^9.1.3",
35
37
  "chai": "^4.3.4",
@@ -48,6 +50,7 @@
48
50
  "CHANGELOG.md",
49
51
  "LICENSE",
50
52
  "README.md",
53
+ "src",
51
54
  "!test"
52
55
  ],
53
56
  "publishConfig": {
package/src/agent.js CHANGED
@@ -1,6 +1,75 @@
1
- import { NRVideoEventAggregator } from "./eventAggregator.js";
2
- import { NRVideoHarvester } from "./harvester.js";
1
+ import { HarvestScheduler } from "./harvestScheduler.js";
2
+ import { NrVideoEventAggregator } from "./eventAggregator.js";
3
+ import Log from "./log.js";
3
4
 
4
- export const customEventAggregator = new NRVideoEventAggregator();
5
- const harvester = new NRVideoHarvester(customEventAggregator);
6
- harvester.startTimer();
5
+ /**
6
+ * Enhanced video analytics agent with HarvestScheduler only.
7
+ */
8
+ class VideoAnalyticsAgent {
9
+ constructor() {
10
+ this.isInitialized = false;
11
+ this.harvestScheduler = null;
12
+ this.eventBuffer = null;
13
+ }
14
+
15
+ /**
16
+ * Initializes the video analytics agent with enhanced HarvestScheduler.
17
+ */
18
+ initialize() {
19
+ if (this.isInitialized) {
20
+ Log.warn("Video analytics agent already initialized");
21
+ return;
22
+ }
23
+
24
+ try {
25
+ this.eventBuffer = new NrVideoEventAggregator();
26
+ this.harvestScheduler = new HarvestScheduler(this.eventBuffer);
27
+
28
+ // Start the enhanced harvest scheduler
29
+ this.harvestScheduler.startScheduler();
30
+
31
+ this.isInitialized = true;
32
+ Log.notice("Video analytics agent initialized successfully");
33
+ } catch (error) {
34
+ Log.error("Failed to initialize video analytics agent:", error.message);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Adds an event to the harvesting system.
40
+ * @param {object} eventObject - Event to add
41
+ * @returns {boolean} True if event was added successfully
42
+ */
43
+ addEvent(eventObject) {
44
+ if (!this.isInitialized) {
45
+ Log.warn("Video analytics agent not initialized, initializing now");
46
+ this.initialize();
47
+ }
48
+
49
+ try {
50
+ return this.eventBuffer.add(eventObject);
51
+ } catch (error) {
52
+ Log.error("Failed to add event to harvesting system:", error.message);
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Sets the harvest interval for the scheduler.
59
+ * @param {number} interval - The harvest interval in milliseconds.
60
+ */
61
+
62
+ setHarvestInterval(interval) {
63
+ if (!this.isInitialized) {
64
+ this.initialize();
65
+ }
66
+
67
+ this.harvestScheduler.updateHarvestInterval(interval);
68
+ }
69
+ }
70
+
71
+ // Create singleton instance
72
+ const videoAnalyticsAgent = new VideoAnalyticsAgent();
73
+
74
+ // Enhanced video analytics harvester
75
+ export const videoAnalyticsHarvester = videoAnalyticsAgent;
package/src/constants.js CHANGED
@@ -19,11 +19,17 @@ Constants.AdPositions = {
19
19
  POST: "post",
20
20
  };
21
21
 
22
- Constants.INTERVAL = 10000;
23
- Constants.MAX_EVENTS_PER_BATCH = 1000;
24
- Constants.MAX_PAYLOAD_SIZE = 1; // 1mb
25
- Constants.MAX_BEACON_SIZE = 0.0625; // 64kb
26
- Constants.MAX_EVENT_SIZE = 0.0625; // 64kb
22
+ // bam.nr-data.net
23
+ //bam-cell.nr-data.net
24
+
25
+ Constants.COLLECTOR = {
26
+ US: ["bam.nr-data.net", "bam-cell.nr-data.net"],
27
+ EU: "bam.eu01.nr-data.net",
28
+ Staging: "staging-bam-cell.nr-data.net",
29
+ GOV: "gov-bam.nr-data.net",
30
+ };
31
+
32
+ // ====== VALID EVENT TYPES ======
27
33
  Constants.VALID_EVENT_TYPES = [
28
34
  "VideoAction",
29
35
  "VideoAdAction",
@@ -31,13 +37,9 @@ Constants.VALID_EVENT_TYPES = [
31
37
  "VideoCustomAction",
32
38
  ];
33
39
 
34
- Constants.COLLECTOR = {
35
- US: "bam-cell.nr-data.net",
36
- EU: "bam.eu01.nr-data.net",
37
- Stage: "staging-bam-cell.nr-data.net",
38
- GOV: "gov-bam.nr-data.net",
39
- };
40
-
41
- // "bam.nr-data.net",
40
+ Constants.MAX_PAYLOAD_SIZE = 1048576; // 1MB = 1024 × 1024 bytes
41
+ Constants.MAX_BEACON_SIZE = 61440; // 60KB = 60 × 1024 bytes
42
+ Constants.MAX_EVENTS_PER_BATCH = 1000;
43
+ Constants.INTERVAL = 10000; //10 seconds
42
44
 
43
45
  export default Constants;
package/src/core.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import Log from "./log";
2
2
  import { recordEvent } from "./recordEvent";
3
- import { setAuthConfig } from "./authConfiguration";
3
+ import { setVideoConfig } from "./videoConfiguration";
4
4
 
5
5
  /**
6
6
  * Static class that sums up core functionalities of the library.
@@ -8,12 +8,17 @@ import { setAuthConfig } from "./authConfiguration";
8
8
  */
9
9
  class Core {
10
10
  /**
11
- * Add a tracker to the system. Trackers added will start reporting its events to NR's backend.
11
+ * Add a tracker to the system. Trackers added will start reporting its events to the video analytics backend.
12
12
  *
13
13
  * @param {(Emitter|Tracker)} tracker Tracker instance to add.
14
+ * @param {object} options Configuration options including video analytics settings.
14
15
  */
15
16
  static addTracker(tracker, options) {
16
- setAuthConfig(options.info);
17
+ // Set video analytics configuration
18
+ if (options?.info) {
19
+ setVideoConfig(options.info);
20
+ }
21
+
17
22
  if (tracker.on && tracker.emit) {
18
23
  trackers.push(tracker);
19
24
  tracker.on("*", eventHandler);
@@ -46,20 +51,52 @@ class Core {
46
51
  return trackers;
47
52
  }
48
53
 
54
+ /**
55
+ * Enhanced send method with performance timing.
56
+ * @param {string} eventType - Type of event
57
+ * @param {string} actionName - Action name
58
+ * @param {object} data - Event data
59
+ */
49
60
  static send(eventType, actionName, data) {
50
- data["timeSinceLoad"] = window.performance.now() / 1000;
51
- recordEvent(eventType, { actionName, ...data });
61
+ const enrichedData = {
62
+ actionName,
63
+ ...data,
64
+
65
+ };
66
+
67
+ return recordEvent(eventType, enrichedData);
52
68
  }
53
69
 
54
70
  /**
55
- * Sends an error event. This may be used for external errors launched by the app, the network or
71
+ * Sends an error event.
72
+ * This may be used for external errors launched by the app, the network or
56
73
  * any external factor. Note that errors within the player are normally reported with
57
74
  * tracker.sendError, so this method should not be used to report those.
58
75
  *
59
76
  * @param {object} att attributes to be sent along the error.
60
77
  */
61
78
  static sendError(att) {
62
- Core.send("ERROR", att);
79
+ return recordEvent("VideoErrorAction", {
80
+ actionName: "ERROR",
81
+ ...att
82
+ });
83
+ }
84
+
85
+
86
+
87
+
88
+ /**
89
+ * Forces an immediate harvest of all pending events.
90
+ * @returns {Promise<object>} Harvest result
91
+ */
92
+ static async forceHarvest() {
93
+ try {
94
+ const { videoAnalyticsHarvester } = require("./agent"); // lazy loading for dynamic import
95
+ return await videoAnalyticsHarvester.forceHarvest();
96
+ } catch (error) {
97
+ Log.error("Failed to force harvest:", error.message);
98
+ return { success: false, error: error.message };
99
+ }
63
100
  }
64
101
  }
65
102
 
@@ -67,20 +104,27 @@ let trackers = [];
67
104
  let isErrorShown = false;
68
105
 
69
106
  /**
70
- * Logs and sends given event.
107
+ * Enhanced event handler with error handling and performance monitoring.
71
108
  *
72
109
  * @private
73
110
  * @param {Event} e Event
74
111
  */
75
112
  function eventHandler(e) {
76
- let data = cleanData(e.data);
77
- if (Log.level <= Log.Levels.DEBUG) {
78
- Log.notice("Sent", e.type, data);
79
- } else {
80
- Log.notice("Sent", e.type);
81
- }
113
+ try {
114
+ let data = cleanData(e.data);
115
+
116
+ if (Log.level <= Log.Levels.DEBUG) {
117
+ Log.notice("Sent", e.type, data);
118
+ } else {
119
+ Log.notice("Sent", e.type);
120
+ }
121
+
122
+ // Send event without priority discrimination
123
+ Core.send(e.eventType, e.type, data);
82
124
 
83
- Core.send(e.eventType, e.type, data);
125
+ } catch (error) {
126
+ Log.error("Error in event handler:", error.message);
127
+ }
84
128
  }
85
129
 
86
130
  /**
@@ -1,66 +1,218 @@
1
- import { getPayloadSize } from "./utils";
1
+ import Log from "./log";
2
2
  import Constants from "./constants";
3
- const { MAX_EVENTS_PER_BATCH, MAX_PAYLOAD_SIZE } = Constants;
3
+ import { dataSize } from "./utils";
4
+
5
+ const { MAX_PAYLOAD_SIZE, MAX_EVENTS_PER_BATCH } = Constants;
4
6
 
5
7
  /**
6
- * A simple aggregator that queues raw events without any statistical aggregation.
7
- * It includes the necessary save/reload logic for the harvester's retry mechanism.
8
+ * Enhanced event buffer that manages video events with unified priority handling
9
+ * and automatic size management. All events are treated with equal priority
10
+ * unless explicitly specified otherwise.
8
11
  */
9
- export class NRVideoEventAggregator {
10
- #queue = [];
11
- #retryQueue = [];
12
+ export class NrVideoEventAggregator {
13
+ constructor() {
14
+ // Simplified to single priority queue for equal treatment
15
+
16
+ this.buffer = [];
17
+ this.maxPayloadSize = MAX_PAYLOAD_SIZE;
18
+ this.maxEventsPerBatch = MAX_EVENTS_PER_BATCH;
19
+ this.currentPayloadSize = 0;
20
+ this.totalEvents = 0;
21
+
22
+ // Dual threshold system - whichever is reached first triggers harvest
23
+ // Payload size thresholds
24
+ this.smartHarvestPayloadThreshold = Math.floor(this.maxPayloadSize * 0.6); // 60% of 1MB = 600KB
25
+ this.overflowPayloadThreshold = Math.floor(this.maxPayloadSize * 0.9); // 90% of 1MB = 900KB
26
+
27
+ // Event count thresholds
28
+ this.smartHarvestEventThreshold = Math.floor(this.maxEventsPerBatch * 0.6); // 60% of 1000 = 600 events
29
+ this.overflowEventThreshold = Math.floor(this.maxEventsPerBatch * 0.9); // 90% of 1000 = 900 events
30
+
31
+ // Callback for triggering harvest
32
+ this.onSmartHarvestTrigger = null;
33
+ }
12
34
 
13
35
  /**
14
- * Checks if the event queue is empty.
15
- * @returns {boolean}
36
+ * Adds an event to the unified buffer.
37
+ * All events are treated equally in FIFO order.
38
+ * @param {object} eventObject - The event to add
16
39
  */
17
- isEmpty() {
18
- return this.#queue.length === 0 && this.#retryQueue.length === 0;
40
+ add(eventObject) {
41
+ try {
42
+ // Calculate event payload size
43
+ const eventSize = dataSize(eventObject);
44
+
45
+ // Check if we need to make room based on EITHER payload size OR event count limits
46
+ const wouldExceedPayload =
47
+ this.currentPayloadSize + eventSize >= this.maxPayloadSize;
48
+ const wouldExceedEventCount =
49
+ this.totalEvents + 1 >= this.maxEventsPerBatch;
50
+
51
+ if (wouldExceedPayload || wouldExceedEventCount) {
52
+ this.makeRoom(eventSize);
53
+ }
54
+
55
+ // Add to unified buffer
56
+ this.buffer.push(eventObject);
57
+ this.totalEvents++;
58
+ this.currentPayloadSize += eventSize;
59
+
60
+ // Check if smart harvest should be triggered
61
+ this.checkSmartHarvestTrigger();
62
+
63
+ return true;
64
+ } catch (error) {
65
+ Log.error("Failed to add event to buffer:", error.message);
66
+ return false;
67
+ }
19
68
  }
20
69
 
21
70
  /**
22
- * Drains the entire queue and returns all events.
23
- * Called by the harvester to begin the chunking process.
71
+ * Checks if smart harvest should be triggered based on dual threshold system.
72
+ * Triggers when EITHER condition is met first:
73
+ * - 60% of payload size (600KB) OR 60% of event count (600 events)
74
+ * - 90% of payload size (900KB) OR 90% of event count (900 events)
75
+ * @private
76
+ */
77
+ checkSmartHarvestTrigger() {
78
+ const payloadPercentage = this.currentPayloadSize / this.maxPayloadSize;
79
+ const eventPercentage = this.totalEvents / this.maxEventsPerBatch;
80
+
81
+ // Check 90% emergency thresholds first (either payload OR event count)
82
+ const isPayloadOverflowReached =
83
+ this.currentPayloadSize >= this.overflowPayloadThreshold;
84
+ const isEventOverflowReached =
85
+ this.totalEvents >= this.overflowEventThreshold;
86
+
87
+ if (isPayloadOverflowReached || isEventOverflowReached) {
88
+ const triggerReason = isPayloadOverflowReached
89
+ ? `payload ${this.currentPayloadSize}/${
90
+ this.maxPayloadSize
91
+ } bytes (${Math.round(payloadPercentage * 100)}%)`
92
+ : `events ${this.totalEvents}/${this.maxEventsPerBatch} (${Math.round(
93
+ eventPercentage * 100
94
+ )}%)`;
95
+
96
+ Log.warn(
97
+ `OVERFLOW PREVENTION: ${triggerReason} - Emergency harvest triggered`
98
+ );
99
+
100
+ if (
101
+ this.onSmartHarvestTrigger &&
102
+ typeof this.onSmartHarvestTrigger === "function"
103
+ ) {
104
+ // Trigger immediate emergency harvest
105
+ this.onSmartHarvestTrigger("overflow", 90);
106
+ }
107
+ }
108
+
109
+ // Check 60% smart harvest thresholds (either payload OR event count)
110
+ else {
111
+ const isPayloadSmartReached =
112
+ this.currentPayloadSize >= this.smartHarvestPayloadThreshold;
113
+ const isEventSmartReached =
114
+ this.totalEvents >= this.smartHarvestEventThreshold;
115
+
116
+ if (isPayloadSmartReached || isEventSmartReached) {
117
+ if (
118
+ this.onSmartHarvestTrigger &&
119
+ typeof this.onSmartHarvestTrigger === "function"
120
+ ) {
121
+ // Trigger proactive harvest
122
+ this.onSmartHarvestTrigger("smart", 60);
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Sets the callback function for smart harvest triggers.
130
+ * @param {Function} callback - Function to call when smart harvest is triggered
131
+ */
132
+ setSmartHarvestCallback(callback) {
133
+ this.onSmartHarvestTrigger = callback;
134
+ }
135
+
136
+ /**
137
+ * Drains all events from the buffer in FIFO order (first in, first out).
138
+ * No limits needed since buffer already manages size via makeRoom() and smart harvest triggers.
139
+ * @returns {Array} Array of events in order they were added
24
140
  */
25
141
  drain() {
26
- const allEvents = [...this.#retryQueue, ...this.#queue];
27
- this.#queue = []; // Clear the active queue
28
- this.#retryQueue = []; // Clear the retry queue
142
+ try {
143
+ // Drain ALL events - buffer size is already managed by makeRoom() and smart harvest
144
+ const events = this.buffer.splice(0);
145
+
146
+ // Reset counters since buffer is now empty
147
+ this.totalEvents = 0;
148
+ this.currentPayloadSize = 0;
29
149
 
30
- return allEvents;
150
+ return events;
151
+ } catch (error) {
152
+ Log.error("Failed to drain events from buffer:", error.message);
153
+ return [];
154
+ }
31
155
  }
32
156
 
33
157
  /**
34
- * Adds a complete, enriched event object to the queue.
35
- * @param {object} eventObject - The event to queue.
158
+ * Checks if the buffer is empty.
159
+ * @returns {boolean} True if all buffers are empty
36
160
  */
37
- add(eventObject) {
38
- this.#queue.push(eventObject);
161
+ isEmpty() {
162
+ return this.totalEvents === 0;
39
163
  }
40
164
 
41
- // --- Methods for the Harvester ---
165
+ /**
166
+ * Gets the total number of events across all buffers.
167
+ * @returns {number} Total event count
168
+ */
169
+ size() {
170
+ return this.totalEvents;
171
+ }
42
172
 
43
173
  /**
44
- * Cleans up the queue after a harvest attempt, based on the result.
45
- * @param {object} result - The result from the harvester, containing a 'retry' flag.
174
+ * Clears the entire buffer.
46
175
  */
47
- postHarvestCleanup(result) {
48
- if (!result.retry || !result.chunk?.length) {
49
- this.#retryQueue = [];
50
- return;
176
+ clear() {
177
+ this.buffer = [];
178
+ this.totalEvents = 0;
179
+ }
180
+
181
+ /**
182
+ * Makes room in the buffer by removing the oldest event.
183
+ * Uses FIFO approach - removes the first (oldest) event.
184
+ * @private
185
+ */
186
+ makeRoom(newEventSize) {
187
+ // Before the while loop in makeRoom()
188
+ if (newEventSize > this.maxPayloadSize) {
189
+ Log.error("Event dropped: Event size exceeds maximum payload size.");
190
+ return; // Exit the function to prevent infinite loop
51
191
  }
52
192
 
193
+ // Keep a loop to evict events until we meet ALL conditions for the new event
53
194
  while (
54
- this.#retryQueue.length > 0 &&
55
- (getPayloadSize(this.#retryQueue) + getPayloadSize(result.chunk) >
56
- MAX_PAYLOAD_SIZE ||
57
- this.#retryQueue.length + result.chunk.length > MAX_EVENTS_PER_BATCH)
195
+ // Condition 1: Exceeding max event count
196
+ this.totalEvents >= this.maxEventsPerBatch ||
197
+ // Condition 2: Exceeding max payload size
198
+ this.currentPayloadSize + newEventSize >= this.maxPayloadSize
58
199
  ) {
59
- // Removes the oldest item from the retry queue to make space
60
- this.#retryQueue.shift();
61
- }
200
+ if (this.buffer.length > 0) {
201
+ const removed = this.buffer.shift(); // Remove the oldest event (FIFO)
202
+
203
+ // Recalculate size and count after removal
204
+ const removedSize = dataSize(removed);
205
+ this.totalEvents--;
206
+ this.currentPayloadSize -= removedSize;
62
207
 
63
- // Add the entire failed chunk to the retry queue.
64
- this.#retryQueue.push(...result.chunk); // result.chunk will be never greater than 1mb or 1000
208
+ // Optional: Log a warning for a dropped event
209
+ Log.warn("Event buffer full, oldest event removed.");
210
+ } else {
211
+ // Buffer is somehow empty, but conditions were met. Break the loop.
212
+ break;
213
+ }
214
+ }
65
215
  }
66
216
  }
217
+
218
+ export default NrVideoEventAggregator;