@newrelic/video-core 3.1.1 → 3.2.0-beta-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/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.2.1] - 2025/07/30
6
+
7
+ ### Breaking Change
8
+
9
+ - Decoupled from browser agent, now uses its own harvesting system for independent data collection.
5
10
 
6
11
  ## [3.1.1] - 2025/06/09
7
12
 
@@ -13,11 +18,11 @@ All notable changes to this project will be documented in this file.
13
18
 
14
19
  ### Enhancements
15
20
 
16
- * **Publishing to npm:** The package can now be published to npm, making it easily accessible.
21
+ - **Publishing to npm:** The package can now be published to npm, making it easily accessible.
17
22
 
18
23
  ### Build
19
24
 
20
- * **Distribution Formats:** Added `cjs`, `esm`, and `umd` builds to the `dist` folder, ensuring compatibility with CommonJS, ES Modules, and UMD module formats.
25
+ - **Distribution Formats:** Added `cjs`, `esm`, and `umd` builds to the `dist` folder, ensuring compatibility with CommonJS, ES Modules, and UMD module formats.
21
26
 
22
27
  ## [3.0.0] - 2025/02/20
23
28
 
package/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  # New Relic Video Core - JavaScript
4
4
 
5
+ > **⚠️ BETA VERSION NOTICE**
6
+ > This version is currently in **beta phase** and is under active development. Features and APIs may change without notice. Use with caution in production environments and expect potential breaking changes in future releases.
7
+
5
8
  The New Relic video tracking core library is the base for all video trackers in the browser platform. It contains the classes and core mechanisms used by the player specific trackers.
6
9
  It segregates the events into different event types based on action, such as video-related events going to `VideoAction`, ad-related events to `VideoAdAction`, errors to `VideoErrorAction`, and custom actions to `VideoCustomAction`.
7
10
 
@@ -36,10 +39,20 @@ Add **scripts** inside `dist` folder to your page.
36
39
  `nrvideo` provides a class called `VideoTracker` that will serve as an interface with
37
40
  _Browser Agent_, allowing you to manage and send events to New Relic.
38
41
 
39
- First of all, you have to add a tracker in the core class:
42
+ First, configure the authentication options and initialize a tracker:
40
43
 
41
44
  ```javascript
42
- var tracker = new VideoTracker(player);
45
+ const options = {
46
+ info: {
47
+ beacon: "your-beacon-url.nr-data.net",
48
+ errorBeacon: "your-beacon-url.nr-data.net",
49
+ licenseKey: "your-license-key",
50
+ applicationID: "your-application-id",
51
+ sa: 1,
52
+ },
53
+ };
54
+
55
+ const tracker = new VideoTracker(player, options);
43
56
  ```
44
57
 
45
58
  Once the tracker is added, any event it emits will be sent to New Relic and processed by the following functions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/video-core",
3
- "version": "3.1.1",
3
+ "version": "3.2.0-beta-0",
4
4
  "description": "New Relic video tracking core library",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -45,12 +45,13 @@
45
45
  "files": [
46
46
  "THIRD_PARTY_NOTICES.md",
47
47
  "dist",
48
+ "src",
48
49
  "CHANGELOG.md",
49
50
  "LICENSE",
50
51
  "README.md",
51
52
  "!test"
52
53
  ],
53
- "publishConfig": {
54
+ "publishConfig": {
54
55
  "access": "public"
55
56
  }
56
57
  }
package/src/agent.js ADDED
@@ -0,0 +1,6 @@
1
+ import { NRVideoEventAggregator } from "./eventAggregator.js";
2
+ import { NRVideoHarvester } from "./harvester.js";
3
+
4
+ export const customEventAggregator = new NRVideoEventAggregator();
5
+ const harvester = new NRVideoHarvester(customEventAggregator);
6
+ harvester.startTimer();
@@ -0,0 +1,138 @@
1
+ import Constants from "./constants";
2
+
3
+ const { COLLECTOR } = Constants;
4
+
5
+ /**
6
+ * Validates and initializes New Relic video tracking information.
7
+ * @param {object} info - The options object containing authentication information.
8
+ * @param {string} info.licenseKey - The New Relic license key.
9
+ * @param {string} [info.appName] - The name of the application (required if no applicationID).
10
+ * @param {string} [info.region] - The region for the New Relic collector (required if no beacon).
11
+ * @param {string} [info.beacon] - Custom beacon URL (optional, overrides region-based beacon).
12
+ * @param {string} [info.sa] - Security attributes (optional).
13
+ * @param {string} [info.applicationID] - Application ID for beacon-based configuration (optional).
14
+ * @returns {boolean} True if configuration was set successfully, false otherwise.
15
+ * @throws {Error} Throws error for invalid configuration parameters.
16
+ */
17
+ export function setAuthConfig(info) {
18
+ try {
19
+ // Input validation
20
+ if (!info || typeof info !== "object" || Array.isArray(info)) {
21
+ throw new Error("setAuthConfig: info parameter must be a valid object");
22
+ }
23
+
24
+ if (isAuthorised(info)) {
25
+ const { licenseKey, appName, region, beacon, sa, applicationID } = info;
26
+
27
+ // Initialize NRVIDEO global object
28
+ window.NRVIDEO = window.NRVIDEO || {};
29
+
30
+ // Determine beacon URL with fallback
31
+ let beaconUrl = beacon;
32
+ if (!beaconUrl && region) {
33
+ if (!COLLECTOR[region]) {
34
+ throw new Error(
35
+ `setAuthConfig: Invalid region '${region}'. Valid regions: ${Object.keys(
36
+ COLLECTOR
37
+ ).join(", ")}`
38
+ );
39
+ }
40
+ beaconUrl = COLLECTOR[region];
41
+ }
42
+
43
+ window.NRVIDEO.info = {
44
+ beacon: beaconUrl,
45
+ licenseKey,
46
+ applicationID: applicationID || null,
47
+ appName: appName || null,
48
+ sa: sa || 0,
49
+ };
50
+
51
+ return true;
52
+ } else {
53
+ const validationError = getValidationError(info);
54
+ throw new Error(`setAuthConfig: ${validationError}`);
55
+ }
56
+ } catch (error) {
57
+ console.error("setAuthConfig:", error.message);
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Checks if the provided information contains valid authentication parameters.
64
+ * @param {object} info - The options object.
65
+ * @returns {boolean} True if authorized, false otherwise.
66
+ */
67
+ function isAuthorised(info) {
68
+ if (!info || typeof info !== "object") {
69
+ return false;
70
+ }
71
+
72
+ const { licenseKey, appName, region, applicationID, beacon } = info;
73
+
74
+ // License key is always required
75
+ if (
76
+ !licenseKey ||
77
+ typeof licenseKey !== "string" ||
78
+ licenseKey.trim().length === 0
79
+ ) {
80
+ return false;
81
+ }
82
+
83
+ // Two valid configuration modes:
84
+ // 1. applicationID + beacon (for direct beacon configuration)
85
+ // 2. appName + region (for region-based beacon resolution)
86
+ if (applicationID) {
87
+ return !!(beacon && typeof beacon === "string" && beacon.trim().length > 0);
88
+ }
89
+
90
+ return !!(
91
+ appName &&
92
+ typeof appName === "string" &&
93
+ appName.trim().length > 0 &&
94
+ region &&
95
+ typeof region === "string" &&
96
+ region.trim().length > 0
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Provides detailed validation error message for debugging.
102
+ * @param {object} info - The options object.
103
+ * @returns {string} Detailed error message.
104
+ */
105
+ function getValidationError(info) {
106
+ if (!info || typeof info !== "object") {
107
+ return "info parameter must be a valid object";
108
+ }
109
+
110
+ const { licenseKey, appName, region, applicationID, beacon } = info;
111
+
112
+ if (
113
+ !licenseKey ||
114
+ typeof licenseKey !== "string" ||
115
+ licenseKey.trim().length === 0
116
+ ) {
117
+ return "licenseKey is required and must be a non-empty string";
118
+ }
119
+
120
+ if (applicationID) {
121
+ if (!beacon || typeof beacon !== "string" || beacon.trim().length === 0) {
122
+ return "beacon URL is required when using applicationID";
123
+ }
124
+ } else {
125
+ if (
126
+ !appName ||
127
+ typeof appName !== "string" ||
128
+ appName.trim().length === 0
129
+ ) {
130
+ return "appName is required when not using applicationID";
131
+ }
132
+ if (!region || typeof region !== "string" || region.trim().length === 0) {
133
+ return "region is required when not using applicationID";
134
+ }
135
+ }
136
+
137
+ return "configuration validation failed";
138
+ }
package/src/chrono.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * This class calculates time lapses between two points on time.
3
+ */
4
+ class Chrono {
5
+ /**
6
+ * Constructor
7
+ */
8
+ constructor() {
9
+ this.reset();
10
+ }
11
+
12
+ /** Reset chrono values. */
13
+ reset() {
14
+ /** Start time */
15
+ this.startTime = 0;
16
+
17
+ /** Stop time */
18
+ this.stopTime = 0;
19
+
20
+ /**
21
+ * If you set an offset in a chrono, its value will be added getDeltaTime and stop.
22
+ *
23
+ * @example
24
+ * let chrono = new Chrono()
25
+ * chrono.offset = 500
26
+ * chrono.start()
27
+ * process.sleep(500)
28
+ * chrono.stop() // Will return 1000
29
+ *
30
+ * @type {number}
31
+ */
32
+ this.offset = 0;
33
+ }
34
+
35
+ /**
36
+ * Returns the time between start() and the last stop() in ms. Returns null if start wasn't
37
+ * called.
38
+ * @return {(number|null)} Time lapse in ms.
39
+ */
40
+ getDeltaTime() {
41
+ if (this.startTime) {
42
+ return this.offset + (new Date().getTime() - this.startTime);
43
+ } else {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Starts the chrono.
50
+ */
51
+ start() {
52
+ this.startTime = new Date().getTime();
53
+ this.stopTime = 0;
54
+ }
55
+
56
+ /**
57
+ * Stops the timer and returns delta time.
58
+ * @return {(number|null)} Returns the delta time
59
+ */
60
+ stop() {
61
+ this.stopTime = new Date().getTime();
62
+ return this.getDeltaTime();
63
+ }
64
+
65
+ /**
66
+ * Creates a copy of the chrono.
67
+ * @returns {Chrono} Cloned chrono
68
+ */
69
+ clone() {
70
+ var chrono = new Chrono();
71
+ chrono.startTime = this.startTime;
72
+ chrono.stopTime = this.stopTime;
73
+ chrono.offset = this.offset;
74
+ return chrono;
75
+ }
76
+ }
77
+
78
+ export default Chrono;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Constants for the library.
3
+ * @class Constants
4
+ * @static
5
+ */
6
+ class Constants {}
7
+
8
+ /**
9
+ * Enum for types/positions of ads.
10
+ * @example var type = Constants.AdPositions.PRE
11
+ * @enum {String}
12
+ */
13
+ Constants.AdPositions = {
14
+ /** For ads shown before the content. */
15
+ PRE: "pre",
16
+ /** For ads shown during the content. */
17
+ MID: "mid",
18
+ /** For ads shown after the content. */
19
+ POST: "post",
20
+ };
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
27
+ Constants.VALID_EVENT_TYPES = [
28
+ "VideoAction",
29
+ "VideoAdAction",
30
+ "VideoErrorAction",
31
+ "VideoCustomAction",
32
+ ];
33
+
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",
42
+
43
+ export default Constants;
package/src/core.js ADDED
@@ -0,0 +1,100 @@
1
+ import Log from "./log";
2
+ import { recordEvent } from "./recordEvent";
3
+ import { setAuthConfig } from "./authConfiguration";
4
+
5
+ /**
6
+ * Static class that sums up core functionalities of the library.
7
+ * @static
8
+ */
9
+ class Core {
10
+ /**
11
+ * Add a tracker to the system. Trackers added will start reporting its events to NR's backend.
12
+ *
13
+ * @param {(Emitter|Tracker)} tracker Tracker instance to add.
14
+ */
15
+ static addTracker(tracker, options) {
16
+ setAuthConfig(options.info);
17
+ if (tracker.on && tracker.emit) {
18
+ trackers.push(tracker);
19
+ tracker.on("*", eventHandler);
20
+ if (typeof tracker.trackerInit == "function") {
21
+ tracker.trackerInit();
22
+ }
23
+ } else {
24
+ Log.error("Tried to load a non-tracker.", tracker);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Disposes and remove given tracker. Removes its listeners.
30
+ *
31
+ * @param {Tracker} tracker Tracker to remove.
32
+ */
33
+ static removeTracker(tracker) {
34
+ tracker.off("*", eventHandler);
35
+ tracker.dispose();
36
+ let index = trackers.indexOf(tracker);
37
+ if (index !== -1) trackers.splice(index, 1);
38
+ }
39
+
40
+ /**
41
+ * Returns the array of trackers.
42
+ *
43
+ * @returns {Tracker[]} Array of trackers.
44
+ */
45
+ static getTrackers() {
46
+ return trackers;
47
+ }
48
+
49
+ static send(eventType, actionName, data) {
50
+ data["timeSinceLoad"] = window.performance.now() / 1000;
51
+ recordEvent(eventType, { actionName, ...data });
52
+ }
53
+
54
+ /**
55
+ * Sends an error event. This may be used for external errors launched by the app, the network or
56
+ * any external factor. Note that errors within the player are normally reported with
57
+ * tracker.sendError, so this method should not be used to report those.
58
+ *
59
+ * @param {object} att attributes to be sent along the error.
60
+ */
61
+ static sendError(att) {
62
+ Core.send("ERROR", att);
63
+ }
64
+ }
65
+
66
+ let trackers = [];
67
+ let isErrorShown = false;
68
+
69
+ /**
70
+ * Logs and sends given event.
71
+ *
72
+ * @private
73
+ * @param {Event} e Event
74
+ */
75
+ 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
+ }
82
+
83
+ Core.send(e.eventType, e.type, data);
84
+ }
85
+
86
+ /**
87
+ * Cleans given object, removing all items with value === null.
88
+ * @private
89
+ * @param {Object} data Data to clean
90
+ * @returns {Object} Cleaned object
91
+ */
92
+ function cleanData(data) {
93
+ let ret = {};
94
+ for (let i in data) {
95
+ if (data[i] !== null && typeof data[i] !== "undefined") ret[i] = data[i];
96
+ }
97
+ return ret;
98
+ }
99
+
100
+ export default Core;
package/src/emitter.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * This base class implements a basic behavior of listeners and events. Extend this object to have
3
+ * this feature built-in inside your classes.
4
+ *
5
+ * @class Emitter
6
+ */
7
+ class Emitter {
8
+ /**
9
+ * Sets a listener to a given event. Use {@link emit} to trigger those events.
10
+ * Pass '*' to listen ALL events.
11
+ *
12
+ * @param {string} event Name of the event.
13
+ * @param {function} callback Callback of the event. Receives event and data.
14
+ * @return this
15
+ */
16
+ on(event, callback) {
17
+ this._listeners = this._listeners || {};
18
+ if (typeof callback === "function") {
19
+ this._listeners[event] = this._listeners[event] || [];
20
+ this._listeners[event].push(callback);
21
+ return this;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Removes given callback from the listeners of this object.
27
+ *
28
+ * @param {string} event Name of the event.
29
+ * @param {function} callback Callback of the event.
30
+ * @return this
31
+ */
32
+ off(event, callback) {
33
+ this._listeners = this._listeners || {};
34
+
35
+ if (this._listeners[event]) {
36
+ var index = this._listeners[event].indexOf(callback);
37
+ if (index !== -1) {
38
+ this._listeners[event].splice(index, 1);
39
+ }
40
+ }
41
+ return this;
42
+ }
43
+
44
+ /**
45
+ * Emits given event, triggering all the associated callbacks.
46
+ *
47
+ * @param {string} event Name of the event.
48
+ * @param {object} [data] Custom data to be sent to the callbacks.
49
+ * @return this
50
+ */
51
+ emit(eventType, event, data) {
52
+ this._listeners = this._listeners || {};
53
+ data = data || {};
54
+
55
+ if (Array.isArray(this._listeners[event])) {
56
+ this._listeners[event].forEach((callback) => {
57
+ callback.call(this, {
58
+ eventType,
59
+ type: event,
60
+ data: data,
61
+ target: this,
62
+ });
63
+ });
64
+ }
65
+
66
+ if (Array.isArray(this._listeners["*"])) {
67
+ this._listeners["*"].forEach((callback) => {
68
+ callback.call(this, {
69
+ eventType,
70
+ type: event,
71
+ data: data,
72
+ target: this,
73
+ });
74
+ });
75
+ }
76
+
77
+ return this;
78
+ }
79
+ }
80
+
81
+ export default Emitter;
@@ -0,0 +1,66 @@
1
+ import { getPayloadSize } from "./utils";
2
+ import Constants from "./constants";
3
+ const { MAX_EVENTS_PER_BATCH, MAX_PAYLOAD_SIZE } = Constants;
4
+
5
+ /**
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
+ */
9
+ export class NRVideoEventAggregator {
10
+ #queue = [];
11
+ #retryQueue = [];
12
+
13
+ /**
14
+ * Checks if the event queue is empty.
15
+ * @returns {boolean}
16
+ */
17
+ isEmpty() {
18
+ return this.#queue.length === 0 && this.#retryQueue.length === 0;
19
+ }
20
+
21
+ /**
22
+ * Drains the entire queue and returns all events.
23
+ * Called by the harvester to begin the chunking process.
24
+ */
25
+ drain() {
26
+ const allEvents = [...this.#retryQueue, ...this.#queue];
27
+ this.#queue = []; // Clear the active queue
28
+ this.#retryQueue = []; // Clear the retry queue
29
+
30
+ return allEvents;
31
+ }
32
+
33
+ /**
34
+ * Adds a complete, enriched event object to the queue.
35
+ * @param {object} eventObject - The event to queue.
36
+ */
37
+ add(eventObject) {
38
+ this.#queue.push(eventObject);
39
+ }
40
+
41
+ // --- Methods for the Harvester ---
42
+
43
+ /**
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.
46
+ */
47
+ postHarvestCleanup(result) {
48
+ if (!result.retry || !result.chunk?.length) {
49
+ this.#retryQueue = [];
50
+ return;
51
+ }
52
+
53
+ 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)
58
+ ) {
59
+ // Removes the oldest item from the retry queue to make space
60
+ this.#retryQueue.shift();
61
+ }
62
+
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
65
+ }
66
+ }