@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/log.js ADDED
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Static Log class
3
+ *
4
+ * @class
5
+ * @static
6
+ */
7
+ class Log {
8
+ /**
9
+ * Sends an error console log.
10
+ * @param {...any} [msg] Message to show
11
+ * @static
12
+ */
13
+ static error(...msg) {
14
+ _report(msg, Log.Levels.ERROR, "darkred");
15
+ }
16
+
17
+ /**
18
+ * Sends a warning console log.
19
+ * @method Log.warn
20
+ * @static
21
+ * @param {...any} msg Message to show
22
+ */
23
+ static warn(...msg) {
24
+ _report(msg, Log.Levels.WARNING, "darkorange");
25
+ }
26
+
27
+ /**
28
+ * Sends a notice console log.
29
+ * @method Log.notice
30
+ * @static
31
+ * @param {...any} msg Message to show
32
+ */
33
+ static notice(...msg) {
34
+ _report([].slice.call(arguments), Log.Levels.NOTICE, "darkcyan");
35
+ }
36
+
37
+ /**
38
+ * Sends a debug message to console.
39
+ * @method Log.debug
40
+ * @static
41
+ * @param {...any} msg Message to show
42
+ */
43
+ static debug(...msg) {
44
+ _report(msg, Log.Levels.DEBUG, "indigo");
45
+ }
46
+
47
+ /**
48
+ * This utility method will add most of the HTML5 common event listeners to the player sent.
49
+ * Events will be reported as DEBUG level messages.
50
+ *
51
+ * @example
52
+ * // Already included events:
53
+ * ['canplay', 'buffering', 'waiting', 'ended', 'play', 'playing', 'pause', 'resume', 'error',
54
+ * 'abort', 'seek', 'seeking', 'seeked', 'stalled', 'dispose', 'loadeddata', 'loadstart',
55
+ * 'loadedmetadata']
56
+ *
57
+ * @method Log.debugCommonVideoEvents
58
+ * @static
59
+ * @param {object|function} o Object to attach the events.
60
+ * @param {array} [extraEvents]
61
+ * An array of extra events to watch. ie: ['timeupdate', 'progress'].
62
+ * If the first item is null, no common events will be added.
63
+ * @param {function} [report] Callback function called to report events.
64
+ * Default calls Log.debug()
65
+ */
66
+ static debugCommonVideoEvents(o, extraEvents, report) {
67
+ try {
68
+ if (Log.level <= Log.Levels.DEBUG) {
69
+ report =
70
+ report ||
71
+ function (e) {
72
+ Log.debug("Event: " + e.type);
73
+ };
74
+
75
+ var playerEvents = [
76
+ "canplay",
77
+ "buffering",
78
+ "waiting",
79
+ "ended",
80
+ "play",
81
+ "playing",
82
+ "pause",
83
+ "resume",
84
+ "error",
85
+ "abort",
86
+ "seek",
87
+ "seeking",
88
+ "seeked",
89
+ "stalled",
90
+ "dispose",
91
+ "loadeddata",
92
+ "loadstart",
93
+ "loadedmetadata",
94
+ ];
95
+ if (extraEvents) {
96
+ if (extraEvents[0] === null) {
97
+ extraEvents.shift();
98
+ playerEvents = extraEvents;
99
+ } else {
100
+ playerEvents = playerEvents.concat(extraEvents);
101
+ }
102
+ }
103
+
104
+ for (var i = 0; i < playerEvents.length; i++) {
105
+ if (typeof o === "function") {
106
+ o.call(window, playerEvents[i], report);
107
+ } else if (o.on) {
108
+ o.on(playerEvents[i], report);
109
+ } else if (o.addEventListener) {
110
+ o.addEventListener(playerEvents[i], report);
111
+ } else if (o.addEventHandler) {
112
+ o.addEventHandler(playerEvents[i], report);
113
+ } else {
114
+ Log.warn(
115
+ "debugCommonVideoEvents: No common listener function found for ",
116
+ o
117
+ );
118
+ }
119
+ }
120
+ }
121
+ } catch (err) {
122
+ Log.warn(err);
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Enum for log levels
129
+ * @enum {integer}
130
+ * @static
131
+ * @var
132
+ */
133
+ Log.Levels = {
134
+ /** No console outputs */
135
+ SILENT: 5,
136
+ /** Console will show errors */
137
+ ERROR: 4,
138
+ /** Console will show warnings */
139
+ WARNING: 3,
140
+ /** Console will show notices (ie: life-cyrcle logs) */
141
+ NOTICE: 2,
142
+ /** Console will show debug messages. */
143
+ DEBUG: 1,
144
+ /** Show all messages. */
145
+ ALL: 0,
146
+ };
147
+
148
+ /**
149
+ * Only logs of this imporance or higher will be shown.
150
+ * @example Log.level = Log.Levels.ALL
151
+ * @default Log.Levels.ERROR
152
+ * @static
153
+ */
154
+ Log.level = Log.Levels.ERROR;
155
+
156
+ /**
157
+ * If true, logs will be outputed with colors.
158
+ * @default true
159
+ * @static
160
+ */
161
+ Log.colorful = true;
162
+
163
+ /**
164
+ * If true, logs will include the time mark.
165
+ * @default true
166
+ * @static
167
+ */
168
+ Log.includeTime = true;
169
+
170
+ /**
171
+ * Prefix included at the start of every log.
172
+ * @default '[New Relic]'
173
+ * @static
174
+ */
175
+ Log.prefix = "[nrvideo]";
176
+
177
+ // PRIVATE MEMBERS
178
+
179
+ /**
180
+ * Returns a console message
181
+ *
182
+ * @private
183
+ * @param {array} msg Message array, error object or array of messages.
184
+ * @param {Log.Level} [level=Log.Levels.NOTICE] Defines the level of the error sent.
185
+ * Only errors with higher or equal level than Log.logLevel will be displayed.
186
+ * @param {string} [color='darkgreen'] Color of the header
187
+ * @see {@link Log.level}
188
+ */
189
+ function _report(msg, level, color) {
190
+ level = level || Log.Levels.NOTICE;
191
+ color = color || "darkcyan";
192
+
193
+ var prefix = Log.prefix;
194
+ if (Log.includeTime) prefix += _getCurrentTime() + " ";
195
+ prefix += _level2letter(level) + ":";
196
+
197
+ // Show messages in actual console if level is enought
198
+ if (Log.level <= level && level !== Log.Levels.SILENT) {
199
+ if (
200
+ !Log.colorful ||
201
+ (typeof document !== "undefined" && document.documentMode)
202
+ ) {
203
+ // document.documentMode exits only in IE
204
+ _plainReport(msg, prefix);
205
+ } else {
206
+ // choose log method
207
+ var logMethod;
208
+ if (level === Log.Levels.ERROR && console.error) {
209
+ logMethod = console.error;
210
+ } else if (level === Log.Levels.WARNING && console.warn) {
211
+ logMethod = console.warn;
212
+ } else if (level === Log.Levels.DEBUG && console.debug) {
213
+ // NOTE: for some reason console.debug doesn't work on CAF Receivers.
214
+ if (window.cast == undefined) {
215
+ logMethod = console.debug;
216
+ } else {
217
+ logMethod = console.log;
218
+ }
219
+ } else {
220
+ logMethod = console.log;
221
+ }
222
+
223
+ // print message
224
+ prefix = "%c" + prefix;
225
+ msg.splice(0, 0, prefix, "color: " + color);
226
+ logMethod.apply(console, msg);
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Returns the current time in format hh:mm:ss.mmm (with trailing 0s)
233
+ * @private
234
+ * @return {string} Current time.
235
+ */
236
+ function _getCurrentTime() {
237
+ var d = new Date();
238
+ var hh = ("0" + d.getDate()).slice(-2);
239
+ var mm = ("0" + d.getMinutes()).slice(-2);
240
+ var ss = ("0" + d.getSeconds()).slice(-2);
241
+ var mmm = ("00" + d.getMilliseconds()).slice(-3);
242
+ return "[" + hh + ":" + mm + ":" + ss + "." + mmm + "]";
243
+ }
244
+
245
+ /**
246
+ * Returns a console message without style
247
+ *
248
+ * @private
249
+ * @param {(string|object|array)} msg Message string, object or array of messages.
250
+ * @param {string} prefix Prefix of the message.
251
+ */
252
+ function _plainReport(msg, prefix) {
253
+ if (msg instanceof Array) {
254
+ for (var m in msg) {
255
+ _plainReport(msg[m], prefix);
256
+ }
257
+ } else {
258
+ if (typeof msg === "string") {
259
+ console.log(prefix + " " + msg);
260
+ } else {
261
+ console.log(prefix + "↵");
262
+ console.log(msg);
263
+ }
264
+ }
265
+ }
266
+
267
+ const _letters = {
268
+ 4: "e", // Error
269
+ 3: "w", // Warning
270
+ 2: "n", // Notice
271
+ 1: "d", // Debug
272
+ };
273
+
274
+ /**
275
+ * Transforms a level to a letter to identify every message.
276
+ *
277
+ * @private
278
+ * @param {sLog.Level} level Level of the message
279
+ */
280
+ function _level2letter(level) {
281
+ return _letters[level];
282
+ }
283
+
284
+ /**
285
+ * This function is automatically executed at load.
286
+ * Will search inside window.location.search for attribute 'nrvideo-debug=X'.
287
+ * X can have one of these values, that will modify Log.Levels.
288
+ * 5: SILENT,
289
+ * 4: ERROR,
290
+ * 3: WARNING,
291
+ * 2: NOTICE,
292
+ * 1: DEBUG,
293
+ *
294
+ * If nrvideo-colors=false is present, Log.colorful will be set to false.
295
+ *
296
+ * @private
297
+ */
298
+ function _loadLevelFromUrl() {
299
+ if (
300
+ typeof window !== "undefined" &&
301
+ window.location &&
302
+ window.location.search
303
+ ) {
304
+ var m = /\?.*&*nrvideo-debug=(.+)/i.exec(window.location.search);
305
+ if (m !== null) {
306
+ if (m[1] === "true") {
307
+ Log.level = Log.Levels.ALL;
308
+ } else {
309
+ Log.level = m[1];
310
+ }
311
+ }
312
+
313
+ var m2 = /\?.*&*nrvideo-colors=false/i.exec(window.location.search);
314
+ if (m2 !== null) {
315
+ Log.colorful = false;
316
+ }
317
+ }
318
+ }
319
+
320
+ // Execute load level
321
+ _loadLevelFromUrl();
322
+
323
+ export default Log;
@@ -0,0 +1,68 @@
1
+ import { customEventAggregator } from "./agent.js";
2
+ import { getPayloadSize } from "./utils.js";
3
+ import Constants from "./constants";
4
+
5
+ const { VALID_EVENT_TYPES, MAX_EVENT_SIZE } = Constants;
6
+
7
+ /**
8
+ * Records a video event with the specified type and attributes
9
+ * @param {string} eventType - The type of event to record
10
+ * @param {Object} attributes - Additional attributes to include with the event
11
+ * @returns {boolean} - True if event was recorded successfully, false otherwise
12
+ */
13
+ export function recordEvent(eventType, attributes = {}) {
14
+ // Input validation
15
+ if (
16
+ typeof eventType !== "string" ||
17
+ eventType.length === 0 ||
18
+ !VALID_EVENT_TYPES.includes(eventType)
19
+ ) {
20
+ console.warn("recordEvent: Invalid eventType provided:", eventType);
21
+ return false;
22
+ }
23
+
24
+ // Validate attributes parameter
25
+ if (
26
+ attributes !== null &&
27
+ (typeof attributes !== "object" || Array.isArray(attributes))
28
+ ) {
29
+ console.warn("recordEvent: attributes must be a plain object");
30
+ return false;
31
+ }
32
+
33
+ // Ensure attributes is an object (handle null case)
34
+ attributes = attributes || {};
35
+
36
+ // Check if NRVIDEO is properly initialized
37
+ if (!window.NRVIDEO || !window.NRVIDEO.info) {
38
+ console.error("recordEvent: NRVIDEO not properly initialized");
39
+ return false;
40
+ }
41
+
42
+ try {
43
+ const { appName } = window.NRVIDEO.info;
44
+
45
+ const eventObject = {
46
+ ...attributes,
47
+ eventType,
48
+ appName,
49
+ timestamp: Date.now(),
50
+ };
51
+
52
+ // Check event size to prevent oversized payloads
53
+ const eventSize = getPayloadSize(eventObject);
54
+
55
+ if (eventSize > MAX_EVENT_SIZE) {
56
+ console.warn(
57
+ `recordEvent: Event size (${eventSize} bytes) exceeds maximum (${MAX_EVENT_SIZE} bytes)`
58
+ );
59
+ return false;
60
+ }
61
+
62
+ customEventAggregator.add(eventObject);
63
+ return true;
64
+ } catch (error) {
65
+ console.error("recordEvent: Error recording event:", error);
66
+ return false;
67
+ }
68
+ }
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;