@newrelic/video-core 4.1.1-beta → 4.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/video-core",
3
- "version": "4.1.1-beta",
3
+ "version": "4.1.1",
4
4
  "description": "New Relic video tracking core library",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -10,7 +10,7 @@
10
10
  "watch": "webpack --mode production --progress --color --watch",
11
11
  "watch:dev": "webpack --progress --color --watch",
12
12
  "clean": "rm -rf dist coverage doc",
13
- "test": "nyc --reporter=html --reporter=text mocha --require @babel/register",
13
+ "test": "jest --coverage",
14
14
  "doc": "jsdoc -c jsdoc.json -d documentation",
15
15
  "deploy": "node scripts/deploy.js",
16
16
  "third-party-updates": "oss third-party manifest --includeOptDeps && oss third-party notices --includeOptDeps && git add THIRD_PARTY_NOTICES.md third_party_manifest.json"
@@ -33,9 +33,11 @@
33
33
  "@babel/preset-env": "^7.24.5",
34
34
  "@babel/register": "^7.24.6",
35
35
  "aws-sdk": "^2.920.0",
36
+ "babel-jest": "^30.2.0",
36
37
  "babel-loader": "^9.1.3",
37
- "chai": "^4.3.4",
38
38
  "diff": "^5.0.0",
39
+ "jest": "^30.2.0",
40
+ "jest-environment-jsdom": "^30.2.0",
39
41
  "jsdom": "^25.0.1",
40
42
  "mocha": "^10.4.0",
41
43
  "nyc": "^15.1.0",
package/src/constants.js CHANGED
@@ -45,7 +45,11 @@ Constants.INTERVAL = 10000; //10 seconds
45
45
  Constants.QOE_AGGREGATE_KEYS = [
46
46
  "coreVersion", "instrumentation.name",
47
47
  "instrumentation.provider", "instrumentation.version", "isBackgroundEvent", "playerName", "playerVersion",
48
- "src", "viewId", "viewSession", "contentIsAutoplayed"
48
+ "src", "viewId", "viewSession", "contentIsAutoplayed", "contentIsMuted", "contentRenditionHeight", "contentRenditionWidth",
49
+ "contentSrc", "numberOfVideos", "pageUrl", "trackerName", "trackerVersion", "contentDuration", "contentPlayrate", "contentPlayhead",
50
+ "contentPreload", "elapsedTime", "contentTitle", "contentId", "contentIsLive", "deviceType", "deviceGroup", "deviceManufacturer",
51
+ "deviceModel", "deviceName", "deviceSize", "deviceUuid", "contentRenditionName", "contentIsFullscreen", "contentCdn",
52
+ "contentFps", "asnOrganization", "asnLongitude", "asnLatitude", "asn", "timeSinceRequested", "timeSinceStarted"
49
53
  ]
50
54
 
51
55
  export default Constants;
package/src/core.js CHANGED
@@ -16,7 +16,7 @@ class Core {
16
16
  static addTracker(tracker, options) {
17
17
  // Set video analytics configuration
18
18
  if (options?.info) {
19
- setVideoConfig(options.info);
19
+ setVideoConfig(options.info, options?.config);
20
20
  }
21
21
 
22
22
  if (tracker.on && tracker.emit) {
@@ -53,7 +53,6 @@ export class NrVideoEventAggregator {
53
53
  Log.error("Failed to set or replace the event to buffer:", error.message);
54
54
  return false;
55
55
  }
56
- return false;
57
56
  }
58
57
 
59
58
  /**
@@ -31,6 +31,7 @@ export class HarvestScheduler {
31
31
  this.currentTimerId = null;
32
32
  this.harvestCycle = Constants.INTERVAL;
33
33
  this.isHarvesting = false;
34
+ this.qoeCycleCount = 1;
34
35
 
35
36
  // Page lifecycle handling
36
37
  this.setupPageLifecycleHandlers();
@@ -237,15 +238,29 @@ export class HarvestScheduler {
237
238
  /**
238
239
  * Drains events from the event buffer and optionally includes retry queue data.
239
240
  * Uses fresh-events-first approach with payload limits.
241
+ * Filters out QOE_AGGREGATE events based on the harvest interval multiplier,
242
+ * always including them on the first and final harvest cycles.
240
243
  * @param {object} options - Harvest options
241
244
  * @returns {Array} Drained events
242
245
  * @private
243
246
  */
244
- drainEvents() {
247
+ drainEvents(options = {}) {
248
+ // Determine if this cycle should include the QOE_AGGREGATE event
249
+ const multiplier = window.NRVIDEO?.config?.qoeIntervalFactor ?? 1;
250
+ const isQoeCycle =
251
+ (this.qoeCycleCount - 1) % multiplier === 0 ||
252
+ !!options.isFinalHarvest;
253
+
245
254
  // Always drain fresh events first (priority approach)
246
255
  const freshEvents = this.eventBuffer.drain();
247
- let events = [...freshEvents];
248
- let currentPayloadSize = dataSize(freshEvents);
256
+ const filteredFreshEvents = isQoeCycle
257
+ ? freshEvents
258
+ : freshEvents.filter((e) => e.actionName !== "QOE_AGGREGATE");
259
+
260
+ this.qoeCycleCount++;
261
+
262
+ let events = [...filteredFreshEvents];
263
+ let currentPayloadSize = dataSize(filteredFreshEvents);
249
264
 
250
265
  // Always check retry queue if it has data - no flags needed
251
266
  if (this.retryQueueHandler && this.retryQueueHandler.getQueueSize() > 0) {
@@ -57,7 +57,7 @@ export function recordEvent(eventType, attributes = {}) {
57
57
  // Send to video analytics harvester
58
58
  const success = videoAnalyticsHarvester.addEvent(eventObject);
59
59
 
60
- if(qoeEventObject) {
60
+ if(qoeEventObject && window?.NRVIDEO?.config?.qoeAggregate) {
61
61
  const successQoe = videoAnalyticsHarvester.addEvent(qoeEventObject);
62
62
  return success && successQoe;
63
63
  }
@@ -10,12 +10,19 @@ const { COLLECTOR } = Constants;
10
10
  class VideoConfiguration {
11
11
  /**
12
12
  * Validates and sets the video analytics configuration.
13
- * @param {object} userConfig - User provided configuration
13
+ * @param {object} userInfo - User provided configuration
14
+ * @param {object} [config] - Optional configuration object
14
15
  * @returns {boolean} True if configuration is valid and set
15
16
  */
16
17
 
17
- setConfiguration(userInfo) {
18
- this.initializeGlobalConfig(userInfo);
18
+ setConfiguration(userInfo, config) {
19
+ if (!this.validateRequiredFields(userInfo)) {
20
+ return false;
21
+ }
22
+ if (!this.validateConfigFields(config)) {
23
+ return false;
24
+ }
25
+ this.initializeGlobalConfig(userInfo, config);
19
26
  Log.notice("Video analytics configuration initialized successfully");
20
27
  return true;
21
28
  }
@@ -70,11 +77,49 @@ class VideoConfiguration {
70
77
  return true;
71
78
  }
72
79
 
80
+ /**
81
+ * Validates optional config fields.
82
+ * @param {object} config - Config to validate
83
+ * @returns {boolean} True if valid
84
+ */
85
+ validateConfigFields(config) {
86
+ if (config === null || config === undefined) {
87
+ return true;
88
+ }
89
+
90
+ if (typeof config !== "object" || Array.isArray(config)) {
91
+ Log.error("config must be an object");
92
+ return false;
93
+ }
94
+
95
+ const { qoeAggregate } = config;
96
+
97
+ if (qoeAggregate !== undefined && typeof qoeAggregate !== "boolean") {
98
+ Log.error("qoeAggregate must be a boolean");
99
+ return false;
100
+ }
101
+
102
+ return true;
103
+ }
104
+
105
+ /**
106
+ * Sanitizes qoeIntervalFactor, defaulting to 1 if the value is not a positive integer.
107
+ * @param {*} value
108
+ * @returns {number}
109
+ */
110
+ sanitizeQoeIntervalFactor(value) {
111
+ if (value === undefined || value === null) return 1;
112
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
113
+ console.warn(`[nrvideo] Invalid qoeIntervalFactor "${value}" — must be a positive integer. Defaulting to 1.`);
114
+ return 1;
115
+ }
116
+
73
117
  /**
74
118
  * Initializes the global NRVIDEO configuration object.
119
+ * @param {object} userInfo - User provided configuration
120
+ * @param {object} [config] - Optional configuration object
75
121
  */
76
- initializeGlobalConfig(userInfo) {
77
- if (!this.validateRequiredFields(userInfo)) return;
122
+ initializeGlobalConfig(userInfo, config) {
78
123
 
79
124
  let { licenseKey, appName, region, beacon, applicationID } = userInfo;
80
125
 
@@ -93,6 +138,10 @@ class VideoConfiguration {
93
138
  applicationID,
94
139
  ...(applicationID ? {} : { appName }), // Only include appName when no applicationID
95
140
  },
141
+ config: {
142
+ qoeAggregate: config?.qoeAggregate ?? false,
143
+ qoeIntervalFactor: this.sanitizeQoeIntervalFactor(config?.qoeIntervalFactor),
144
+ }
96
145
  };
97
146
  }
98
147
  }
@@ -102,11 +151,12 @@ const videoConfiguration = new VideoConfiguration();
102
151
 
103
152
  /**
104
153
  * Sets the video analytics configuration.
105
- * @param {object} config - Configuration object
154
+ * @param {object} info - Info configuration object
155
+ * @param {object} [config] - Optional configuration object
106
156
  * @returns {boolean} True if configuration was set successfully
107
157
  */
108
- export function setVideoConfig(info) {
109
- return videoConfiguration.setConfiguration(info);
158
+ export function setVideoConfig(info, config) {
159
+ return videoConfiguration.setConfiguration(info, config);
110
160
  }
111
161
 
112
162
  export { videoConfiguration };
@@ -239,6 +239,21 @@ class VideoTracker extends Tracker {
239
239
  return null;
240
240
  }
241
241
 
242
+ /** Override to return the manifest-declared bitrate in bps (Indicated Bitrate). */
243
+ getManifestBitrate() {
244
+ return null;
245
+ }
246
+
247
+ /** Override to return the measured network bitrate in bps (Observed Bitrate). */
248
+ getMeasuredBitrate() {
249
+ return null;
250
+ }
251
+
252
+ /** Override to return the download throughput in bps. */
253
+ getDownloadBitrate() {
254
+ return null;
255
+ }
256
+
242
257
  /** Calculates consumed bitrate using webkitVideoDecodedByteCount. */
243
258
  getWebkitBitrate() {
244
259
  if (this.tag && this.tag.webkitVideoDecodedByteCount) {
@@ -465,12 +480,17 @@ class VideoTracker extends Tracker {
465
480
  att.adTitle = this.getTitle();
466
481
  att.adSrc = this.getSrc();
467
482
  att.adCdn = this.getCdn();
468
- att.adBitrate =
469
- this.getBitrate() ||
470
- this.getWebkitBitrate() ||
471
- this.getRenditionBitrate();
483
+
484
+ // Only add bitrate attributes after ad has started
485
+ if (this.state.isStarted) {
486
+ att.adBitrate =
487
+ this.getBitrate() ||
488
+ this.getWebkitBitrate() ||
489
+ this.getRenditionBitrate();
490
+ att.adRenditionBitrate = this.getRenditionBitrate();
491
+ }
492
+
472
493
  att.adRenditionName = this.getRenditionName();
473
- att.adRenditionBitrate = this.getRenditionBitrate();
474
494
  att.adRenditionHeight = this.getRenditionHeight();
475
495
  att.adRenditionWidth = this.getRenditionWidth();
476
496
  att.adDuration = this.getDuration();
@@ -492,13 +512,20 @@ class VideoTracker extends Tracker {
492
512
  att.contentPlayhead = this.getPlayhead();
493
513
 
494
514
  att.contentIsLive = this.isLive();
495
- att.contentBitrate =
496
- this.getBitrate() ||
497
- this.getWebkitBitrate() ||
498
- this.getRenditionBitrate();
515
+
516
+ // Only add bitrate attributes after content has started
517
+ if (this.state.isStarted) {
518
+ att.contentBitrate =
519
+ this.getBitrate() ||
520
+ this.getWebkitBitrate() ||
521
+ this.getRenditionBitrate();
522
+ att.contentRenditionBitrate = this.getRenditionBitrate();
523
+ att.contentManifestBitrate = this.getManifestBitrate();
524
+ att.contentMeasuredBitrate = this.getMeasuredBitrate();
525
+ att.contentDownloadBitrate = this.getDownloadBitrate();
526
+ }
499
527
 
500
528
  att.contentRenditionName = this.getRenditionName();
501
- att.contentRenditionBitrate = this.getRenditionBitrate();
502
529
  att.contentRenditionHeight = this.getRenditionHeight();
503
530
  att.contentRenditionWidth = this.getRenditionWidth();
504
531
  att.contentDuration = this.getDuration();
@@ -363,6 +363,7 @@ class VideoTrackerState {
363
363
  : 0;
364
364
  kpi["totalPlaytime"] = this.totalPlaytime;
365
365
  kpi["averageBitrate"] = this.weightedBitrate;
366
+ kpi["numberOfErrors"] = this.numberOfErrors;
366
367
  } catch (error) {
367
368
  Log.error("Failed to add attributes for QOE KPIs", error.message);
368
369
  }