@newrelic/video-core 4.1.3 → 4.1.4

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.3",
3
+ "version": "4.1.4",
4
4
  "description": "New Relic video tracking core library",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",
package/src/agent.js CHANGED
@@ -50,8 +50,16 @@ class VideoAnalyticsAgent {
50
50
 
51
51
  try {
52
52
  if(eventObject.actionName && eventObject.actionName === Tracker.Events.QOE_AGGREGATE) {
53
- // Ensure only one QOE_AGGREGATE event exists in buffer per harvest cycle.
53
+ // Ensure only one QOE_AGGREGATE event per (actionName + viewId) in buffer per harvest cycle.
54
+ // Each player has a unique viewId, so multi-player scenarios are handled correctly.
54
55
  // Dirty-check dedup happens at drain time in HarvestScheduler._qoeKpisUnchanged().
56
+ if (eventObject.viewId) {
57
+ return this.eventBuffer.addOrReplaceByActionNameAndViewId(
58
+ Tracker.Events.QOE_AGGREGATE,
59
+ eventObject.viewId,
60
+ eventObject
61
+ );
62
+ }
55
63
  return this.eventBuffer.addOrReplaceByActionName(Tracker.Events.QOE_AGGREGATE, eventObject);
56
64
  }
57
65
  return this.eventBuffer.add(eventObject);
@@ -96,12 +104,15 @@ class VideoAnalyticsAgent {
96
104
 
97
105
  /**
98
106
  * Updates QoE KPI fields on the existing QOE_AGGREGATE event in the buffer.
99
- * Uses addOrReplaceByActionName to keep payload size tracking accurate.
107
+ * Scoped to a specific viewId to support multiple players on the same page.
100
108
  * @param {object} freshKpis - Object with latest KPI values
109
+ * @param {string} [viewId] - The viewId of the player whose QoE event to update
101
110
  */
102
- refreshQoeKpis(freshKpis) {
111
+ refreshQoeKpis(freshKpis, viewId) {
103
112
  if (!this.eventBuffer || !freshKpis) return;
104
- const existing = this.eventBuffer.findByActionName(Tracker.Events.QOE_AGGREGATE);
113
+ const existing = viewId
114
+ ? this.eventBuffer.findByActionNameAndViewId(Tracker.Events.QOE_AGGREGATE, viewId)
115
+ : this.eventBuffer.findByActionName(Tracker.Events.QOE_AGGREGATE);
105
116
  if (existing) {
106
117
  const updated = { ...existing };
107
118
  for (const key of Constants.QOE_KPI_KEYS) {
@@ -109,7 +120,11 @@ class VideoAnalyticsAgent {
109
120
  updated[key] = freshKpis[key];
110
121
  }
111
122
  }
112
- this.eventBuffer.addOrReplaceByActionName(Tracker.Events.QOE_AGGREGATE, updated);
123
+ if (viewId) {
124
+ this.eventBuffer.addOrReplaceByActionNameAndViewId(Tracker.Events.QOE_AGGREGATE, viewId, updated);
125
+ } else {
126
+ this.eventBuffer.addOrReplaceByActionName(Tracker.Events.QOE_AGGREGATE, updated);
127
+ }
113
128
  }
114
129
  }
115
130
  }
@@ -55,6 +55,44 @@ export class NrVideoEventAggregator {
55
55
  }
56
56
  }
57
57
 
58
+ /**
59
+ * If an event with the specified actionName and viewId already exists in the buffer, it will be replaced.
60
+ * Otherwise, the event will be added as a new entry.
61
+ * @param {string} actionName - The actionName to search for in the buffer
62
+ * @param {string} viewId - The viewId to scope the lookup to
63
+ * @param {object} eventObject - The event object to add or use as replacement.
64
+ * @returns {boolean} True if the operation succeeded, false if an error occurred
65
+ */
66
+ addOrReplaceByActionNameAndViewId(actionName, viewId, eventObject) {
67
+ const i = this.buffer.findIndex(
68
+ e => e.actionName === actionName && e.viewId === viewId
69
+ );
70
+ try {
71
+ if (i === -1) {
72
+ this.add(eventObject);
73
+ } else {
74
+ this.add(eventObject, i);
75
+ }
76
+ return true;
77
+ } catch (error) {
78
+ Log.error("Failed to set or replace the event to buffer:", error.message);
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Returns the existing event in buffer matching the given actionName and viewId, or null.
85
+ * @param {string} actionName
86
+ * @param {string} viewId
87
+ * @returns {object|null}
88
+ */
89
+ findByActionNameAndViewId(actionName, viewId) {
90
+ const event = this.buffer.find(
91
+ e => e.actionName === actionName && e.viewId === viewId
92
+ );
93
+ return event || null;
94
+ }
95
+
58
96
  /**
59
97
  * Returns the existing event in buffer matching the given actionName, or null.
60
98
  * @param {string} actionName
@@ -35,7 +35,7 @@ export class HarvestScheduler {
35
35
  this.qoeCycleCount = 1;
36
36
  this.forceNextQoeCycle = false;
37
37
  this.beforeDrainCallback = null;
38
- this._lastSentQoeKpis = null;
38
+ this._lastSentQoeKpis = {};
39
39
 
40
40
  // Page lifecycle handling
41
41
  this.setupPageLifecycleHandlers();
@@ -476,22 +476,24 @@ export class HarvestScheduler {
476
476
  * @private
477
477
  */
478
478
  _qoeKpisUnchanged(event) {
479
- if (!this._lastSentQoeKpis) return false;
479
+ const snapshot = this._lastSentQoeKpis[event.viewId];
480
+ if (!snapshot) return false;
480
481
  for (const key of Constants.QOE_KPI_KEYS) {
481
- if (event[key] !== this._lastSentQoeKpis[key]) return false;
482
+ if (event[key] !== snapshot[key]) return false;
482
483
  }
483
484
  return true;
484
485
  }
485
486
 
486
487
  /**
487
- * Saves QoE KPI values after sending.
488
+ * Saves QoE KPI values after sending, keyed by viewId to support multiple players.
488
489
  * @param {object} event - QoE event that was sent
489
490
  * @private
490
491
  */
491
492
  _saveQoeKpis(event) {
492
- this._lastSentQoeKpis = {};
493
+ const snapshot = {};
493
494
  for (const key of Constants.QOE_KPI_KEYS) {
494
- this._lastSentQoeKpis[key] = event[key];
495
+ snapshot[key] = event[key];
495
496
  }
497
+ this._lastSentQoeKpis[event.viewId] = snapshot;
496
498
  }
497
499
  }
@@ -0,0 +1,33 @@
1
+ import Log from "./log";
2
+
3
+ /**
4
+ * Applies obfuscation rules to a JSON string before sending to the collector.
5
+ * Each rule replaces matches of `regex` with `replacement` in the string.
6
+ *
7
+ * @param {string} jsonString - Serialized JSON payload
8
+ * @param {Array<{regex: string|RegExp, replacement: string}>} rules - Obfuscation rules
9
+ * @returns {string} Obfuscated string
10
+ */
11
+ export function applyObfuscationRules(jsonString, rules) {
12
+ if (!rules || rules.length === 0) return jsonString;
13
+
14
+ return rules.reduce((str, rule) => {
15
+ let pattern;
16
+ try {
17
+ if (rule.regex instanceof RegExp) {
18
+ // Ensure global flag so all occurrences are replaced
19
+ const flags = rule.regex.flags.includes("g")
20
+ ? rule.regex.flags
21
+ : rule.regex.flags + "g";
22
+ pattern = new RegExp(rule.regex.source, flags);
23
+ } else {
24
+ pattern = new RegExp(rule.regex, "g");
25
+ }
26
+ } catch (e) {
27
+ Log.warn("applyObfuscationRules: invalid regex, skipping rule:", rule.regex, e.message);
28
+ return str;
29
+ }
30
+ let s = str.replace(pattern, rule.replacement);
31
+ return s;
32
+ }, jsonString);
33
+ }
@@ -1,5 +1,6 @@
1
- import { dataSize, shouldRetry } from "./utils";
1
+ import { shouldRetry } from "./utils";
2
2
  import Log from "./log";
3
+ import { applyObfuscationRules } from "./obfuscate";
3
4
 
4
5
  /**
5
6
  * Optimized HTTP client for video analytics data transmission with
@@ -50,7 +51,10 @@ export class OptimizedHttpClient {
50
51
  const startTime = Date.now();
51
52
 
52
53
  try {
53
- const requestBody = JSON.stringify(payload.body);
54
+ const requestBody = applyObfuscationRules(
55
+ JSON.stringify(payload.body),
56
+ window.NRVIDEO?.config?.obfuscate
57
+ );
54
58
 
55
59
  // Handle final harvest with sendBeacon
56
60
  if (options.isFinalHarvest && navigator.sendBeacon) {
@@ -92,16 +92,39 @@ class VideoConfiguration {
92
92
  return false;
93
93
  }
94
94
 
95
- const { qoeAggregate } = config;
95
+ const { qoeAggregate, obfuscate } = config;
96
96
 
97
97
  if (qoeAggregate !== undefined && typeof qoeAggregate !== "boolean") {
98
98
  Log.error("qoeAggregate must be a boolean");
99
99
  return false;
100
100
  }
101
101
 
102
+ if (obfuscate !== undefined && !Array.isArray(obfuscate)) {
103
+ Log.error("obfuscate must be an array");
104
+ return false;
105
+ }
106
+
102
107
  return true;
103
108
  }
104
109
 
110
+ /**
111
+ * Filters obfuscation rules, warning about and removing invalid ones.
112
+ * @param {Array} rules - Raw obfuscation rules
113
+ * @returns {Array} Valid rules only
114
+ */
115
+ filterObfuscateRules(rules) {
116
+ if (!rules) return [];
117
+ return rules.filter((rule) => {
118
+ const hasRegex = rule.regex !== undefined && (typeof rule.regex === "string" || rule.regex instanceof RegExp);
119
+ const hasReplacement = rule.replacement !== undefined && typeof rule.replacement === "string";
120
+ if (!hasRegex || !hasReplacement) {
121
+ Log.warn("obfuscate rule missing required 'regex' (string|RegExp) and/or 'replacement' (string), skipping:", rule);
122
+ return false;
123
+ }
124
+ return true;
125
+ });
126
+ }
127
+
105
128
  /**
106
129
  * Sanitizes qoeIntervalFactor, defaulting to 1 if the value is not a positive integer.
107
130
  * @param {*} value
@@ -141,6 +164,7 @@ class VideoConfiguration {
141
164
  config: {
142
165
  qoeAggregate: config?.qoeAggregate ?? false,
143
166
  qoeIntervalFactor: this.sanitizeQoeIntervalFactor(config?.qoeIntervalFactor),
167
+ obfuscate: this.filterObfuscateRules(config?.obfuscate),
144
168
  }
145
169
  };
146
170
  }
@@ -647,7 +647,7 @@ class VideoTracker extends Tracker {
647
647
  videoAnalyticsHarvester.setBeforeDrainCallback(() => {
648
648
  if (this.state) {
649
649
  const freshKpis = this.state.getQoeAttributes({}).qoe;
650
- videoAnalyticsHarvester.refreshQoeKpis(freshKpis);
650
+ videoAnalyticsHarvester.refreshQoeKpis(freshKpis, this.getViewId());
651
651
  }
652
652
  });
653
653
  }