@newrelic/video-core 3.1.0 → 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/src/tracker.js ADDED
@@ -0,0 +1,281 @@
1
+ import pkg from "../package.json";
2
+ import Emitter from "./emitter";
3
+ import Chrono from "./chrono";
4
+ import Constants from "./constants";
5
+
6
+ /**
7
+ * Tracker class provides the basic logic to extend Newrelic's Browser Agent capabilities.
8
+ * Trackers are designed to listen third party elements (like video tags, banners, etc.) and send
9
+ * information over to Browser Agent. Extend this class to create your own tracker, override
10
+ * registerListeners and unregisterListeners for full coverage!
11
+ *
12
+ * @example
13
+ * Tracker instances should be added to Core library to start sending data:
14
+ * nrvideo.Core.addTracker(new Tracker())
15
+ *
16
+ * @extends Emitter
17
+ */
18
+ class Tracker extends Emitter {
19
+ /**
20
+ * Constructor, receives options. You should call {@see registerListeners} after this.
21
+ *
22
+ * @param {Object} [options] Options for the tracker. See {@link setOptions}.
23
+ */
24
+ constructor(options) {
25
+ super();
26
+
27
+ /**
28
+ * If you add something to this custom dictionary it will be added to every action. If you set
29
+ * any value, it will always override the values returned by the getters.
30
+ *
31
+ * @example
32
+ * If you define tracker.customData.contentTitle = 'a' and tracker.getTitle() returns 'b'.
33
+ * 'a' will prevail.
34
+ */
35
+ this.customData = {};
36
+
37
+ /**
38
+ * Set time between hearbeats, in ms.
39
+ */
40
+ this.heartbeat = null;
41
+
42
+ /**
43
+ * Another Tracker instance. Useful to relate ad Trackers to their parent content Trackers.
44
+ * @type Tracker
45
+ */
46
+ this.parentTracker = null;
47
+
48
+ /**
49
+ * Chrono that counts time since this class was instantiated.
50
+ * @private
51
+ */
52
+ this._trackerReadyChrono = new Chrono();
53
+ this._trackerReadyChrono.start();
54
+
55
+ /**
56
+ * Store the initial table of actions with time 0 ms
57
+ */
58
+ this._actionTable = Constants.ACTION_TABLE;
59
+ this._actionAdTable = Constants.ACTION_AD_TABLE;
60
+
61
+ options = options || {};
62
+ this.setOptions(options);
63
+ }
64
+
65
+ /**
66
+ * Set options for the Tracker.
67
+ *
68
+ * @param {Object} [options] Options for the tracker.
69
+ * @param {number} [options.heartbeat] Set time between heartbeats. See {@link heartbeat}.
70
+ * @param {Object} [options.customData] Set custom data. See {@link customData}.
71
+ * @param {Tracker} [options.parentTracker] Set parent tracker. See {@link parentTracker}.
72
+ */
73
+ setOptions(options) {
74
+ if (options) {
75
+ if (options.parentTracker) this.parentTracker = options.parentTracker;
76
+ if (options.customData) this.customData = options.customData;
77
+ if (options.heartbeat) this.heartbeat = options.heartbeat;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Prepares tracker to dispose. Calls {@see unregisterListeners} and drops references.
83
+ */
84
+ dispose() {
85
+ this.unregisterListeners();
86
+ }
87
+
88
+ /**
89
+ * Override this method to register listeners to third party elements.
90
+ *
91
+ * @example
92
+ * class SpecificTracker extends Tracker {
93
+ * registerListeners() {
94
+ * this.player.on('play', () => this.playHandler)
95
+ * }
96
+ *
97
+ * playHandler() {
98
+ * this.emit(Tracker.Events.REQUESTED)
99
+ * }
100
+ * }
101
+ */
102
+ registerListeners() {}
103
+
104
+ /**
105
+ * Override this method to unregister listeners to third party elements created with
106
+ * {@see registerListeners}.
107
+ *
108
+ * @example
109
+ * class SpecificTracker extends Tracker {
110
+ * registerListeners() {
111
+ * this.player.on('play', () => this.playHandler)
112
+ * }
113
+ *
114
+ * unregisterListeners() {
115
+ * this.player.off('play', () => this.playHandler)
116
+ * }
117
+ *
118
+ * playHandler() {
119
+ * this.emit(Tracker.Events.REQUESTED)
120
+ * }
121
+ * }
122
+ */
123
+ unregisterListeners() {}
124
+
125
+ /**
126
+ * Returns heartbeat time interval. 30000 (30s) if not set. See {@link setOptions}.
127
+ * @return {number} Heartbeat interval in ms.
128
+ * @final
129
+ */
130
+ getHeartbeat() {
131
+ if (this.state._isAd) {
132
+ // modifying heartbeat for Ad Tracker
133
+ return 2000;
134
+ } else {
135
+ if (this.heartbeat) {
136
+ return this.heartbeat;
137
+ } else if (this.parentTracker && this.parentTracker.heartbeat) {
138
+ return this.parentTracker.heartbeat;
139
+ } else {
140
+ return 30000;
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Starts heartbeating. Interval period set by options.heartbeat. Min 2000 ms.
147
+ * This method is automaticaly called by the tracker once sendRequest is called.
148
+ */
149
+ startHeartbeat() {
150
+ this._heartbeatInterval = setInterval(
151
+ this.sendHeartbeat.bind(this),
152
+ Math.max(this.getHeartbeat(), 2000)
153
+ );
154
+ }
155
+
156
+ /**
157
+ * Stops heartbeating. This method is automaticaly called by the tracker.
158
+ */
159
+ stopHeartbeat() {
160
+ if (this._heartbeatInterval) {
161
+ clearInterval(this._heartbeatInterval);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Heartbeating allows you to call this function each X milliseconds, defined by
167
+ * {@link getHeartbeat}. This is useful to send regular events to track changes.
168
+ *
169
+ * By default it will send {@link Tracker.Events.HEARTBEAT}.
170
+ * To start heartbeating use {@link startHeartbeat} and to stop them use {@link stopHeartbeat}.
171
+ *
172
+ * @example
173
+ * Override this method to define your own Heartbeat reporting.
174
+ *
175
+ * class TrackerChild extends Tracker {
176
+ * sendHeartbeat (att) {
177
+ * this.send('MY_HEARBEAT_EVENT')
178
+ * }
179
+ * }
180
+ *
181
+ * @param {Object} [att] Collection of key:value attributes to send with the request.
182
+ */
183
+ sendHeartbeat(att) {
184
+ this.sendVideoAction(Tracker.Events.HEARTBEAT, att);
185
+ }
186
+
187
+ /**
188
+ * Override this method to return attributes for actions.
189
+ *
190
+ * @example
191
+ * class SpecificTracker extends Tracker {
192
+ * getAttributes(att) {
193
+ * att = att || {}
194
+ * att.information = 'something'
195
+ * return att
196
+ * }
197
+ * }
198
+ *
199
+ * @param {object} [att] Collection of key value attributes
200
+ * @return {object} Filled attributes
201
+ * @final
202
+ */
203
+ getAttributes(att, eventType) {
204
+ att = att || {};
205
+ att.trackerName = this.getTrackerName();
206
+ att.trackerVersion = this.getTrackerVersion();
207
+ att.coreVersion = pkg.version;
208
+ att.timeSinceTrackerReady = this._trackerReadyChrono.getDeltaTime();
209
+
210
+ for (let key in this.customData) {
211
+ att[key] = this.customData[key];
212
+ }
213
+
214
+ if (document.hidden != undefined) {
215
+ att.isBackgroundEvent = document.hidden;
216
+ }
217
+
218
+ return att;
219
+ }
220
+
221
+ /** Override to change of the Version of tracker. ie: '1.0.1' */
222
+ getTrackerVersion() {
223
+ return pkg.version;
224
+ }
225
+
226
+ /** Override to change of the Name of the tracker. ie: 'custom-html5' */
227
+ getTrackerName() {
228
+ return "base-tracker";
229
+ }
230
+
231
+ /**
232
+ * Send given event. Will automatically call {@see getAttributes} to fill information.
233
+ * Internally, this will call {@see Emitter#emit}, so you could listen any event fired.
234
+ *
235
+ * @example
236
+ * tracker.send('BANNER_CLICK', { url: 'http....' })
237
+ *
238
+ * @param {string} event Event name
239
+ * @param {object} [att] Key:value dictionary filled with attributes.
240
+ */
241
+
242
+ /**
243
+ * getElapsedTime: Calculate the time elapsed between two same actions
244
+ *
245
+ */
246
+
247
+ sendVideoAction(event, att) {
248
+ this.emit("VideoAction", event, this.getAttributes(att));
249
+ }
250
+
251
+ sendVideoAdAction(event, att) {
252
+ this.emit("VideoAdAction", event, this.getAttributes(att));
253
+ }
254
+
255
+ sendVideoErrorAction(event, att) {
256
+ let ev = this.isAd() ? "adError" : "videoError";
257
+ this.emit("VideoErrorAction", event, this.getAttributes(att, ev));
258
+ }
259
+
260
+ sendVideoCustomAction(event, att) {
261
+ this.emit(
262
+ "VideoCustomAction",
263
+ event,
264
+ this.getAttributes(att, "customAction")
265
+ );
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Enumeration of events fired by this class.
271
+ *
272
+ * @static
273
+ * @memberof Tracker
274
+ * @enum {string}
275
+ */
276
+ Tracker.Events = {
277
+ /** The heartbeat event is sent once every 30 seconds while the video is playing. */
278
+ HEARTBEAT: "HEARTBEAT",
279
+ };
280
+
281
+ export default Tracker;
package/src/utils.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Makes an API call with retry logic and fallback to sendBeacon for final harvests
3
+ * @param {Object} params - Request parameters
4
+ * @param {string} params.url - The URL to send the request to
5
+ * @param {Object} params.payload - The payload object containing body data
6
+ * @param {Object} params.options - Request options
7
+ * @param {boolean} params.options.isFinalHarvest - Whether this is a final harvest on page unload
8
+ * @param {Function} callback - Callback function to handle the response
9
+ */
10
+ export function callApi({ url, payload, options = {} }, callback) {
11
+ // Input validation
12
+ if (!url || !payload || !callback) {
13
+ console.error("callApi: Missing required parameters");
14
+ if (callback) callback({ retry: false, status: 0 });
15
+ return;
16
+ }
17
+
18
+ // The Browser Agent sends the 'body' part of the payload object as the actual request body.
19
+ let body;
20
+ try {
21
+ body = JSON.stringify(payload.body);
22
+ } catch (error) {
23
+ console.error("callApi: Error serializing payload", error);
24
+ callback({ retry: false, status: 0 });
25
+ return;
26
+ }
27
+
28
+ // For final harvests on page unload, use sendBeacon for reliability.
29
+ if (options.isFinalHarvest && navigator.sendBeacon) {
30
+ try {
31
+ const success = navigator.sendBeacon(url, body);
32
+ // sendBeacon returns true if the request was successfully queued
33
+ callback({ retry: !success, status: success ? 200 : 0 });
34
+ } catch (e) {
35
+ // sendBeacon can fail if the payload is too large.
36
+ callback({ retry: true, status: 0 });
37
+ }
38
+ return;
39
+ }
40
+
41
+ fetch(url, {
42
+ method: "POST",
43
+ body: body,
44
+ headers: {
45
+ "Content-Type": "application/json", // More accurate content type
46
+ },
47
+ keepalive: options.isFinalHarvest, // Important for final harvest fallback
48
+ })
49
+ .then((response) => {
50
+ // Check for statuses that indicate a retry is needed.
51
+ const isRetry = shouldRetry(response.status);
52
+ callback({
53
+ retry: isRetry,
54
+ status: response.status,
55
+ ok: response.ok,
56
+ });
57
+ })
58
+ .catch(() => {
59
+ // Any network failure (e.g., no internet) should also trigger a retry.
60
+ callback({ retry: true, status: 0 });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Determines if a request should be retried based on HTTP status code
66
+ * @param {number} status - HTTP status code
67
+ * @returns {boolean} - True if request should be retried
68
+ */
69
+ function shouldRetry(status) {
70
+ switch (status) {
71
+ case 408: // Request Timeout
72
+ case 429: // Too Many Requests
73
+ case 500: // Internal Server Error
74
+ return true;
75
+ case 401: // Unauthorized - don't retry
76
+ case 403: // Forbidden - don't retry
77
+ case 404: // Not Found - don't retry
78
+ return false;
79
+ }
80
+ // Retry for 5xx server errors and specific ranges
81
+ return (status >= 502 && status <= 504) || (status >= 512 && status <= 530);
82
+ }
83
+
84
+ /**
85
+ * Calculates the size of a payload object in megabytes
86
+ * @param {Object} obj - The object to calculate size for
87
+ * @returns {number} - Size in megabytes, or 0 if calculation fails
88
+ */
89
+ export function getPayloadSize(obj) {
90
+ if (!obj || typeof obj !== "object") {
91
+ return 0;
92
+ }
93
+
94
+ try {
95
+ const json = JSON.stringify(obj);
96
+ return new TextEncoder().encode(json).length / (1024 * 1024);
97
+ } catch (error) {
98
+ console.error("getPayloadSize: Error calculating payload size", error);
99
+ return 0;
100
+ }
101
+ }