@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/CHANGELOG.md +14 -2
- package/{LICENSE.txt → LICENSE} +2 -2
- package/README.md +22 -16
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/package.json +5 -3
- package/src/agent.js +6 -0
- package/src/authConfiguration.js +138 -0
- package/src/chrono.js +78 -0
- package/src/constants.js +43 -0
- package/src/core.js +100 -0
- package/src/emitter.js +81 -0
- package/src/eventAggregator.js +66 -0
- package/src/harvester.js +171 -0
- package/src/index.js +22 -0
- package/src/log.js +323 -0
- package/src/recordEvent.js +68 -0
- package/src/tracker.js +281 -0
- package/src/utils.js +101 -0
- package/src/videotracker.js +1060 -0
- package/src/videotrackerstate.js +574 -0
- package/dist/cjs/index.js +0 -1
- package/dist/cjs/index.js.LICENSE.txt +0 -6
- package/dist/esm/index.js +0 -1
- package/dist/esm/index.js.LICENSE.txt +0 -6
- package/dist/umd/nrvideo.min.js +0 -1
- package/dist/umd/nrvideo.min.js.LICENSE.txt +0 -6
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
|
+
}
|