@newrelic/video-core 3.1.1 → 3.2.0-beta-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/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
+ }
@@ -0,0 +1,171 @@
1
+ import Constants from "./constants";
2
+ import pkg from "../package.json";
3
+ import { callApi, getPayloadSize } from "./utils";
4
+
5
+ const { INTERVAL, MAX_EVENTS_PER_BATCH, MAX_PAYLOAD_SIZE, MAX_BEACON_SIZE } =
6
+ Constants;
7
+
8
+ /**
9
+ * A scheduler and dispatcher for sending raw event data to the New Relic 'ins' endpoint.
10
+ * It manages the harvest cycle, URL construction, and retries.
11
+ */
12
+ export class NRVideoHarvester {
13
+ #started = false;
14
+ #aggregate; // EventAggregator instance
15
+ #timerId = null; // Timer ID for cleanup
16
+
17
+ /**
18
+ * @param {object} agentController - The agent's configuration object.
19
+ * @param {object} aggregate - The aggregator instance (e.g., EventAggregator).
20
+ */
21
+ constructor(aggregate) {
22
+ this.#aggregate = aggregate;
23
+ // Ensure any queued data is sent when the user navigates away.
24
+ window.addEventListener("pagehide", () =>
25
+ this.triggerHarvest({ isFinalHarvest: true })
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Starts the periodic harvest timer.
31
+ */
32
+ startTimer() {
33
+ if (this.#started) return;
34
+ this.#started = true;
35
+ const onHarvestInterval = () => {
36
+ this.triggerHarvest({});
37
+ if (this.#started) {
38
+ this.#timerId = setTimeout(onHarvestInterval, INTERVAL);
39
+ }
40
+ };
41
+ this.#timerId = setTimeout(onHarvestInterval, INTERVAL);
42
+ }
43
+
44
+ /**
45
+ * Stops the harvest timer and cleans up resources.
46
+ */
47
+ stopTimer() {
48
+ this.#started = false;
49
+ if (this.#timerId) {
50
+ clearTimeout(this.#timerId);
51
+ this.#timerId = null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Executes a harvest cycle by draining the queue and sending it in chunks.
57
+ */
58
+
59
+ triggerHarvest(options = {}) {
60
+ if (this.#aggregate.isEmpty()) return;
61
+
62
+ try {
63
+ // 1. Drain the entire queue to get all pending events.
64
+ const allEvents = this.#aggregate.drain();
65
+
66
+ // 2. Determine the correct size limit for this harvest.
67
+ const maxChunkSize = options.isFinalHarvest
68
+ ? MAX_BEACON_SIZE
69
+ : MAX_PAYLOAD_SIZE;
70
+
71
+ // 3. Split the events into chunks that respect size and count limits.
72
+ const chunks = this.chunkEvents(allEvents, maxChunkSize);
73
+
74
+ // 4. Send each chunk sequentially.
75
+ chunks.forEach((chunk, index) => {
76
+ const isLastChunk = index === chunks.length - 1;
77
+ this.sendChunk(chunk, options, isLastChunk);
78
+ });
79
+ } catch (error) {
80
+ console.error("Error during harvest:", error);
81
+ // Re-add events to the queue if something went wrong
82
+ // This is a failsafe to prevent data loss
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Splits an array of events into multiple smaller arrays (chunks).
88
+ */
89
+ chunkEvents(events, maxChunkSize) {
90
+ const chunks = [];
91
+ let currentChunk = [];
92
+
93
+ for (const event of events) {
94
+ if (currentChunk.length >= MAX_EVENTS_PER_BATCH) {
95
+ chunks.push(currentChunk);
96
+ currentChunk = [];
97
+ }
98
+
99
+ currentChunk.push(event);
100
+ const payloadSize = getPayloadSize({ ins: currentChunk });
101
+ // Use the maxChunkSize passed into the function
102
+ if (payloadSize > maxChunkSize) {
103
+ const lastEvent = currentChunk.pop();
104
+ if (currentChunk.length > 0) {
105
+ chunks.push(currentChunk);
106
+ }
107
+ currentChunk = [lastEvent];
108
+ }
109
+ }
110
+
111
+ if (currentChunk.length > 0) {
112
+ chunks.push(currentChunk);
113
+ }
114
+
115
+ return chunks;
116
+ }
117
+
118
+ /**
119
+ * Sends a single chunk of events.
120
+ */
121
+ sendChunk(chunk, options, isLastChunk) {
122
+ const url = this.#buildUrl();
123
+ if (!url) {
124
+ // If URL construction failed, treat as a failed request that shouldn't be retried
125
+ this.#aggregate.postHarvestCleanup({ retry: false, status: 0 });
126
+ return;
127
+ }
128
+
129
+ const payload = { body: { ins: chunk } };
130
+
131
+ callApi(
132
+ {
133
+ url: url,
134
+ payload: payload,
135
+ options: options,
136
+ },
137
+ (result) => {
138
+ // Pass the failed chunk back to the aggregator for re-queuing.
139
+ if (result.retry) {
140
+ result.chunk = chunk;
141
+ }
142
+ this.#aggregate.postHarvestCleanup(result);
143
+ }
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Constructs the specific URL for the New Relic 'ins' endpoint with all required parameters.
149
+ * @private
150
+ */
151
+
152
+ #buildUrl() {
153
+ try {
154
+ if (!window.NRVIDEO || !window.NRVIDEO.info) {
155
+ throw new Error("NRVIDEO info is not available.");
156
+ }
157
+
158
+ const { beacon, licenseKey, applicationID, sa } = window.NRVIDEO.info;
159
+
160
+ if (!beacon || !licenseKey || !applicationID)
161
+ throw new Error(
162
+ "Options object provided by New Relic is not correctly initialized"
163
+ );
164
+ const url = `https://${beacon}/ins/1/${licenseKey}?a=${applicationID}&v=${pkg.version}&ref=${window.location.href}&ca=VA`;
165
+ return url;
166
+ } catch (error) {
167
+ console.error(error.message);
168
+ return null; // Return null instead of undefined
169
+ }
170
+ }
171
+ }
package/src/index.js ADDED
@@ -0,0 +1,22 @@
1
+ import Core from "./core";
2
+ import Constants from "./constants";
3
+ import Chrono from "./chrono";
4
+ import Log from "./log";
5
+ import Emitter from "./emitter";
6
+ import Tracker from "./tracker";
7
+ import VideoTracker from "./videotracker";
8
+ import VideoTrackerState from "./videotrackerstate";
9
+ import { version } from "../package.json";
10
+
11
+ const nrvideo = {
12
+ Constants,
13
+ Chrono,
14
+ Log,
15
+ Emitter,
16
+ Tracker,
17
+ VideoTracker,
18
+ VideoTrackerState,
19
+ Core,
20
+ version,
21
+ };
22
+ export default nrvideo;