@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/CHANGELOG.md +7 -0
- package/README.md +67 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.LICENSE.txt +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.LICENSE.txt +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/umd/nrvideo.min.js +1 -1
- package/dist/umd/nrvideo.min.js.LICENSE.txt +1 -1
- package/dist/umd/nrvideo.min.js.map +1 -1
- package/package.json +1 -1
- package/src/agent.js +20 -5
- package/src/eventAggregator.js +38 -0
- package/src/harvestScheduler.js +8 -6
- package/src/obfuscate.js +33 -0
- package/src/optimizedHttpClient.js +6 -2
- package/src/videoConfiguration.js +25 -1
- package/src/videotracker.js +1 -1
package/package.json
CHANGED
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
|
|
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
|
-
*
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|
package/src/eventAggregator.js
CHANGED
|
@@ -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
|
package/src/harvestScheduler.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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] !==
|
|
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
|
-
|
|
493
|
+
const snapshot = {};
|
|
493
494
|
for (const key of Constants.QOE_KPI_KEYS) {
|
|
494
|
-
|
|
495
|
+
snapshot[key] = event[key];
|
|
495
496
|
}
|
|
497
|
+
this._lastSentQoeKpis[event.viewId] = snapshot;
|
|
496
498
|
}
|
|
497
499
|
}
|
package/src/obfuscate.js
ADDED
|
@@ -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 {
|
|
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 =
|
|
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
|
}
|
package/src/videotracker.js
CHANGED
|
@@ -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
|
}
|