@newrelic/video-core 4.1.8-beta → 5.0.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 +65 -15
- package/README.md +628 -45
- package/dist/cjs/browser/index.js +3 -0
- package/dist/cjs/browser/index.js.LICENSE.txt +6 -0
- package/dist/cjs/browser/index.js.map +1 -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/cjs/vega/index.js +3 -0
- package/dist/cjs/vega/index.js.LICENSE.txt +6 -0
- package/dist/cjs/vega/index.js.map +1 -0
- package/dist/esm/browser/index.js +3 -0
- package/dist/esm/browser/index.js.LICENSE.txt +6 -0
- package/dist/esm/browser/index.js.map +1 -0
- 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/esm/vega/index.js +3 -0
- package/dist/esm/vega/index.js.LICENSE.txt +6 -0
- package/dist/esm/vega/index.js.map +1 -0
- 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 +31 -6
- package/src/{agent.js → browser/agent.js} +43 -8
- package/src/{harvestScheduler.js → browser/harvestScheduler.js} +87 -86
- package/src/browser/index.js +58 -0
- package/src/connectedDevice/connectedDeviceAgent.js +116 -0
- package/src/connectedDevice/connectedDeviceConstants.js +121 -0
- package/src/connectedDevice/connectedDeviceHarvester.js +522 -0
- package/src/connectedDevice/index.js +49 -0
- package/src/constants.js +8 -0
- package/src/core.js +4 -18
- package/src/eventAggregator.js +48 -0
- package/src/index.js +9 -10
- package/src/obfuscate.js +33 -0
- package/src/optimizedHttpClient.js +6 -2
- package/src/recordEvent.js +56 -49
- package/src/tracker.js +14 -4
- package/src/utils/eventBuilder.js +126 -0
- package/src/utils/harvestTimer.js +109 -0
- package/src/{utils.js → utils/index.js} +2 -2
- package/src/utils/qoeFilters.js +149 -0
- package/src/videoConfiguration.js +90 -7
- package/src/videotracker.js +100 -32
- package/src/videotrackerstate.js +156 -31
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// =====================================================================
|
|
2
|
+
// Connected-device pipeline constants (Vega / mobile collector)
|
|
3
|
+
// =====================================================================
|
|
4
|
+
// Endpoint URLs, request payload templates, and harvest defaults for the
|
|
5
|
+
// connected-device pipeline (`/v5/connect` + `/v3/data`).
|
|
6
|
+
// Only `connectedDeviceHarvester.js` imports these — browser builds
|
|
7
|
+
// never reference this file, keeping the browser bundle lean.
|
|
8
|
+
|
|
9
|
+
/** US production mobile collector base URL. */
|
|
10
|
+
export const MOBILE_ENDPOINT_US = "https://mobile-collector.newrelic.com/mobile";
|
|
11
|
+
|
|
12
|
+
/** EU production mobile collector base URL. Routes EU-account events to the EU datacenter. */
|
|
13
|
+
export const MOBILE_ENDPOINT_EU = "https://mobile-collector.eu01.nr-data.net/mobile";
|
|
14
|
+
|
|
15
|
+
/** Staging mobile collector base URL. */
|
|
16
|
+
export const STAGING_MOBILE_ENDPOINT =
|
|
17
|
+
"https://staging-mobile-collector.newrelic.com/mobile";
|
|
18
|
+
|
|
19
|
+
/** FedRAMP/GOV mobile collector base URL. Required for US government accounts. */
|
|
20
|
+
export const MOBILE_ENDPOINT_GOV = "https://gov-mobile-collector.newrelic.com/mobile";
|
|
21
|
+
|
|
22
|
+
/** Endpoint region selector. Each value routes to its regional collector. */
|
|
23
|
+
export const NR_ENDPOINT = {
|
|
24
|
+
US: "US",
|
|
25
|
+
EU: "EU",
|
|
26
|
+
STAGING: "staging",
|
|
27
|
+
GOV: "GOV",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Default harvest cadence in ms. */
|
|
31
|
+
export const DEFAULT_HARVEST_TIME = 60000;
|
|
32
|
+
|
|
33
|
+
/** Default in-memory event-buffer cap (informational; aggregator enforces
|
|
34
|
+
* MAX_EVENTS_PER_BATCH / MAX_PAYLOAD_SIZE). */
|
|
35
|
+
export const DEFAULT_BUFFER_SIZE = 100;
|
|
36
|
+
|
|
37
|
+
/** Maximum number of /v5/connect attempts before giving up. */
|
|
38
|
+
export const CD_CONNECT_MAX_ATTEMPTS = 3;
|
|
39
|
+
|
|
40
|
+
/** Fixed delay between /v5/connect retry attempts in ms.
|
|
41
|
+
* 10s gives the device network stack enough time to recover after
|
|
42
|
+
* wake-from-sleep before the next attempt fires. */
|
|
43
|
+
export const CD_CONNECT_RETRY_DELAY_MS = 10000;
|
|
44
|
+
|
|
45
|
+
/** Hard timeout for POST /v5/connect. A hung connect holds _isFetchingToken=true
|
|
46
|
+
* forever, blocking all future reconnect attempts. */
|
|
47
|
+
export const CD_CONNECT_TIMEOUT_MS = 10000;
|
|
48
|
+
|
|
49
|
+
/** Hard timeout for POST /v3/data. A hung data send holds isHarvesting=true,
|
|
50
|
+
* freezing the chained harvest timer until the fetch resolves. */
|
|
51
|
+
export const CD_DATA_TIMEOUT_MS = 30000;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Positional 2-tuple sent as the body of `POST /v5/connect`.
|
|
55
|
+
* [
|
|
56
|
+
* appInfo[3] = [appName, appVersion, bundleId],
|
|
57
|
+
* deviceInfo[10] = [osName, osVersion, deviceModel, agentName,
|
|
58
|
+
* agentVersion, deviceUuid, "", "",
|
|
59
|
+
* manufacturer, sizeMeta]
|
|
60
|
+
* ]
|
|
61
|
+
*
|
|
62
|
+
* DATA_TOKENS_PAYLOAD shape — empty strings are rejected with a 401,
|
|
63
|
+
* every slot must carry a non-empty token. Uses `osName='Android'` +
|
|
64
|
+
* `agentName='AndroidAgent'` for collector auth; the real device identity
|
|
65
|
+
* is recorded in slot [1] / slot [8] of `/v3/data`.
|
|
66
|
+
*/
|
|
67
|
+
export const CD_DATA_TOKENS_PAYLOAD = [
|
|
68
|
+
// appInfo
|
|
69
|
+
[
|
|
70
|
+
"newrelic_mobile_example", // appName
|
|
71
|
+
"1.1", // appVersion
|
|
72
|
+
"com.newrelic.newrelic_mobile_example", // bundleId
|
|
73
|
+
],
|
|
74
|
+
// deviceInfo
|
|
75
|
+
[
|
|
76
|
+
"Android", // osName (collector expects a recognized osName)
|
|
77
|
+
"14", // osVersion
|
|
78
|
+
"sdk_gphone64_arm64", // deviceModel
|
|
79
|
+
"AndroidAgent", // agentName (collector expects a recognized agentName)
|
|
80
|
+
"7.4.0-alpha01", // agentVersion
|
|
81
|
+
"b797aee6-aa69-4879-9ba3-1f4aed1a7777", // deviceUuid
|
|
82
|
+
"",
|
|
83
|
+
"",
|
|
84
|
+
"Google", // manufacturer
|
|
85
|
+
{
|
|
86
|
+
size: "normal",
|
|
87
|
+
platform: "Flutter",
|
|
88
|
+
platformVersion: "1.0.8",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Slot [1] of the `POST /v3/data` 10-tuple. Real device identity tuple
|
|
95
|
+
* (Vega + Amazon by default). Customer-supplied `info.deviceInfo` overrides
|
|
96
|
+
* the runtime fields; static slots (osName, agentName, etc.) stay fixed.
|
|
97
|
+
*/
|
|
98
|
+
export const CD_DEVICE_INFO = [
|
|
99
|
+
"Vega", // osName
|
|
100
|
+
"1.0", // osVersion
|
|
101
|
+
"VegaDevice", // deviceModel
|
|
102
|
+
"VegaAgent", // agentName
|
|
103
|
+
"1.0.0", // agentVersion
|
|
104
|
+
"00000000-0000-0000-0000-000000000000", // deviceUuid
|
|
105
|
+
"",
|
|
106
|
+
"",
|
|
107
|
+
"Amazon", // manufacturer
|
|
108
|
+
{
|
|
109
|
+
size: "normal",
|
|
110
|
+
platform: "Native",
|
|
111
|
+
platformVersion: "1.0.0",
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export const CD_METADATA = {
|
|
116
|
+
osBuild: "1",
|
|
117
|
+
osName: "Vega",
|
|
118
|
+
platform: "Native",
|
|
119
|
+
appBuild: "1",
|
|
120
|
+
architecture: "aarch64",
|
|
121
|
+
};
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { NrVideoEventAggregator } from "../eventAggregator";
|
|
2
|
+
import {
|
|
3
|
+
MOBILE_ENDPOINT_US,
|
|
4
|
+
MOBILE_ENDPOINT_EU,
|
|
5
|
+
MOBILE_ENDPOINT_GOV,
|
|
6
|
+
STAGING_MOBILE_ENDPOINT,
|
|
7
|
+
NR_ENDPOINT,
|
|
8
|
+
DEFAULT_HARVEST_TIME,
|
|
9
|
+
DEFAULT_BUFFER_SIZE,
|
|
10
|
+
CD_DATA_TOKENS_PAYLOAD,
|
|
11
|
+
CD_DEVICE_INFO,
|
|
12
|
+
CD_METADATA,
|
|
13
|
+
CD_CONNECT_MAX_ATTEMPTS,
|
|
14
|
+
CD_CONNECT_RETRY_DELAY_MS,
|
|
15
|
+
CD_CONNECT_TIMEOUT_MS,
|
|
16
|
+
CD_DATA_TIMEOUT_MS,
|
|
17
|
+
} from "./connectedDeviceConstants";
|
|
18
|
+
import {
|
|
19
|
+
bufferEventWithQoeDedup,
|
|
20
|
+
refreshQoeKpisInBuffer,
|
|
21
|
+
partitionByQoeCycle,
|
|
22
|
+
applyQoeDirtyFilter,
|
|
23
|
+
} from "../utils/qoeFilters";
|
|
24
|
+
import { createHarvestTimer } from "../utils/harvestTimer";
|
|
25
|
+
import { applyObfuscationRules } from "../obfuscate";
|
|
26
|
+
import Log from "../log";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generic NR mobile-collector harvester for the connected-device pipeline
|
|
30
|
+
* (`/v5/connect` + `/v3/data`).
|
|
31
|
+
*
|
|
32
|
+
* The class is **not Vega-specific** — it owns the HTTP client, dataToken,
|
|
33
|
+
* harvest setInterval, and event buffer for any consumer that needs to ship
|
|
34
|
+
* to NR's mobile collector. Vega-specific routing/lifecycle is provided by
|
|
35
|
+
* the wrapper `ConnectedDeviceAnalyticsAgent` in `connectedDeviceAgent.js`.
|
|
36
|
+
*
|
|
37
|
+
*/
|
|
38
|
+
export default class ConnectedDeviceHarvester {
|
|
39
|
+
/**
|
|
40
|
+
* @param {object} opts
|
|
41
|
+
* @param {string} [opts.accountId] Captured for parity with CAF; not transmitted.
|
|
42
|
+
* @param {string} opts.applicationToken Sent as `X-App-License-Key` header.
|
|
43
|
+
* @param {string} opts.endpoint One of `US`, `EU`, `staging`.
|
|
44
|
+
* @param {object} [opts.deviceInfo] Customer-collected device identity. Any of:
|
|
45
|
+
* uuid, osVersion, deviceModel, deviceManufacturer, osBuild, appBuild, architecture.
|
|
46
|
+
* Each field optional — missing values fall back to placeholders from
|
|
47
|
+
* `connectedDeviceConstants.js`. Extra fields are ignored. The customer is
|
|
48
|
+
* expected to source these from a platform device-info library (e.g.
|
|
49
|
+
* `@amazon-devices/react-native-device-info` on Kepler) and pass the
|
|
50
|
+
* resulting object once at construction. `osBuild` is the OS image build
|
|
51
|
+
* (e.g., `getBuildIdSync()`); `appBuild` is the consumer app's build
|
|
52
|
+
* number (e.g., `getBuildNumber()`).
|
|
53
|
+
*/
|
|
54
|
+
constructor({
|
|
55
|
+
accountId,
|
|
56
|
+
applicationToken,
|
|
57
|
+
endpoint,
|
|
58
|
+
deviceInfo,
|
|
59
|
+
} = {}) {
|
|
60
|
+
if (!applicationToken) {
|
|
61
|
+
throw new Error("applicationToken is required");
|
|
62
|
+
}
|
|
63
|
+
if (!Object.values(NR_ENDPOINT).includes(endpoint)) {
|
|
64
|
+
throw new Error("Invalid endpoint");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.accountId = accountId;
|
|
68
|
+
this.applicationToken = applicationToken;
|
|
69
|
+
this.endpoint = endpoint;
|
|
70
|
+
this.harvestInterval = DEFAULT_HARVEST_TIME;
|
|
71
|
+
this.maxBufferSize = DEFAULT_BUFFER_SIZE;
|
|
72
|
+
|
|
73
|
+
// Destructure customer-supplied device identity.
|
|
74
|
+
if (!deviceInfo) {
|
|
75
|
+
Log.warn("ConnectedDeviceHarvester: no deviceInfo provided — using placeholder defaults. Pass info.deviceInfo for accurate device data.");
|
|
76
|
+
}
|
|
77
|
+
const {
|
|
78
|
+
uuid,
|
|
79
|
+
osVersion,
|
|
80
|
+
deviceModel,
|
|
81
|
+
deviceManufacturer,
|
|
82
|
+
osBuild,
|
|
83
|
+
appBuild,
|
|
84
|
+
architecture,
|
|
85
|
+
} = deviceInfo ?? {};
|
|
86
|
+
this.deviceInfo = {
|
|
87
|
+
uuid: uuid || CD_DEVICE_INFO[5], // slot [5] = deviceUuid placeholder
|
|
88
|
+
osVersion: osVersion || CD_DEVICE_INFO[1],
|
|
89
|
+
deviceModel: deviceModel || CD_DEVICE_INFO[2],
|
|
90
|
+
deviceManufacturer: deviceManufacturer || CD_DEVICE_INFO[8],
|
|
91
|
+
osBuild: osBuild || CD_METADATA.osBuild,
|
|
92
|
+
appBuild: appBuild || CD_METADATA.appBuild,
|
|
93
|
+
architecture: architecture || CD_METADATA.architecture,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
this.eventBuffer = new NrVideoEventAggregator();
|
|
97
|
+
|
|
98
|
+
// Smart-harvest wiring: buffer triggers an early drain at 60% (smart) and
|
|
99
|
+
// 90% (overflow) capacity, before makeRoom()'s drop-oldest FIFO eviction
|
|
100
|
+
// would silently lose events.
|
|
101
|
+
this.eventBuffer.setSmartHarvestCallback((type, threshold) =>
|
|
102
|
+
this.triggerSmartHarvest(type, threshold)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
this.dataToken = null;
|
|
106
|
+
this.isHarvesting = false;
|
|
107
|
+
this.isDisposed = false;
|
|
108
|
+
this._isFetchingToken = false; // guard: prevents parallel /v5/connect sequences
|
|
109
|
+
|
|
110
|
+
// Periodic harvest timer. Chained-setTimeout under the hood guarantees no overlapping ticks even if the
|
|
111
|
+
// drain takes longer than the interval.
|
|
112
|
+
this.timer = createHarvestTimer({
|
|
113
|
+
interval: this.harvestInterval,
|
|
114
|
+
onTick: () => this.sendBufferedEvents(),
|
|
115
|
+
errorLabel: "ConnectedDeviceHarvester",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// QoE state
|
|
119
|
+
this.qoeCycleCount = 1;
|
|
120
|
+
this.forceQoeNextCycle = false;
|
|
121
|
+
this.beforeDrainCallback = null;
|
|
122
|
+
this._lastSentQoeKpis = {};
|
|
123
|
+
|
|
124
|
+
// Connect retry state
|
|
125
|
+
this._connectAttempt = 0;
|
|
126
|
+
|
|
127
|
+
// fire-and-forget initialise.
|
|
128
|
+
this.initialise();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns the regional mobile collector base URL for the configured endpoint.
|
|
133
|
+
* Each region routes to its own datacenter — EU events terminate on EU
|
|
134
|
+
* infrastructure rather than transiting through the US host.
|
|
135
|
+
*
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
getEndpointBaseUrl() {
|
|
139
|
+
switch (this.endpoint) {
|
|
140
|
+
case NR_ENDPOINT.EU: return MOBILE_ENDPOINT_EU;
|
|
141
|
+
case NR_ENDPOINT.STAGING: return STAGING_MOBILE_ENDPOINT;
|
|
142
|
+
case NR_ENDPOINT.GOV: return MOBILE_ENDPOINT_GOV;
|
|
143
|
+
default: return MOBILE_ENDPOINT_US;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Two-phase init: fetch dataToken, then start harvest interval.
|
|
149
|
+
* @returns {Promise<void>}
|
|
150
|
+
*/
|
|
151
|
+
async initialise() {
|
|
152
|
+
await this.fetchDataTokens();
|
|
153
|
+
// Defensive guard: if dispose() ran while fetchDataTokens was awaiting,
|
|
154
|
+
// do not start an interval that will outlive the harvester.
|
|
155
|
+
if (this.isDisposed) return;
|
|
156
|
+
if (this.dataToken) {
|
|
157
|
+
this.startHarvestInterval();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* POST `/v5/connect` to obtain a dataToken. Retries up to
|
|
163
|
+
* CD_CONNECT_MAX_ATTEMPTS times with a fixed CD_CONNECT_RETRY_DELAY_MS
|
|
164
|
+
* wait between attempts.
|
|
165
|
+
*
|
|
166
|
+
* Iterative loop keeps the call stack flat across all attempts.
|
|
167
|
+
* `_isFetchingToken` guard ensures only one connect sequence runs at a
|
|
168
|
+
* time — if `initialise()` (startup) and the 401-refresh path in
|
|
169
|
+
* `sendBufferedEvents` both call this concurrently, the second call
|
|
170
|
+
* returns immediately without issuing a duplicate POST.
|
|
171
|
+
*
|
|
172
|
+
* @returns {Promise<void>}
|
|
173
|
+
*/
|
|
174
|
+
async fetchDataTokens() {
|
|
175
|
+
if (this._isFetchingToken) return;
|
|
176
|
+
this._isFetchingToken = true;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
for (let attempt = 0; attempt < CD_CONNECT_MAX_ATTEMPTS; attempt++) {
|
|
180
|
+
if (this.isDisposed) return;
|
|
181
|
+
|
|
182
|
+
const url = `${this.getEndpointBaseUrl()}/v5/connect`;
|
|
183
|
+
try {
|
|
184
|
+
Log.notice(
|
|
185
|
+
`ConnectedDeviceHarvester: POST ${url} (attempt ${attempt + 1}/${CD_CONNECT_MAX_ATTEMPTS})`
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const res = await this._fetchWithTimeout(
|
|
189
|
+
url,
|
|
190
|
+
{
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: {
|
|
193
|
+
"Content-Type": "application/json",
|
|
194
|
+
"X-App-License-Key": this.applicationToken,
|
|
195
|
+
},
|
|
196
|
+
body: JSON.stringify(CD_DATA_TOKENS_PAYLOAD),
|
|
197
|
+
},
|
|
198
|
+
CD_CONNECT_TIMEOUT_MS
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
let errBody = "";
|
|
203
|
+
try { errBody = await res.text(); } catch (_) { /* ignore */ }
|
|
204
|
+
throw new Error(`HTTP ${res.status} body=${errBody}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const body = await res.json();
|
|
208
|
+
if (!body || !body.data_token) {
|
|
209
|
+
throw new Error("Missing data_token in connect response");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.dataToken = body.data_token;
|
|
213
|
+
this._connectAttempt = attempt + 1;
|
|
214
|
+
Log.notice("ConnectedDeviceHarvester: dataToken acquired");
|
|
215
|
+
return;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
this._connectAttempt = attempt + 1;
|
|
218
|
+
Log.error(
|
|
219
|
+
`ConnectedDeviceHarvester: /v5/connect failed (attempt ${this._connectAttempt}/${CD_CONNECT_MAX_ATTEMPTS}):`,
|
|
220
|
+
err.message
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (attempt >= CD_CONNECT_MAX_ATTEMPTS - 1) {
|
|
224
|
+
Log.error("ConnectedDeviceHarvester: Max retries reached — dataToken stays null");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Fixed delay gives the device network stack time to recover
|
|
229
|
+
// (e.g. after wake-from-sleep) before the next attempt.
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, CD_CONNECT_RETRY_DELAY_MS));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} finally {
|
|
234
|
+
this._isFetchingToken = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Starts the periodic harvest timer.
|
|
240
|
+
* Idempotent — safe to call repeatedly.
|
|
241
|
+
*/
|
|
242
|
+
startHarvestInterval() {
|
|
243
|
+
if (this.isDisposed) return;
|
|
244
|
+
this.timer.start();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Smart-harvest handler. Invoked by `NrVideoEventAggregator` when the buffer
|
|
249
|
+
* crosses 60% (`type='smart'`) or 90% (`type='overflow'`) of capacity, before
|
|
250
|
+
* `makeRoom()` would start FIFO-evicting events. Drains the buffer immediately
|
|
251
|
+
* and resets the periodic clock so the next scheduled tick fires
|
|
252
|
+
* `harvestInterval` after this drain completes.
|
|
253
|
+
*
|
|
254
|
+
* @param {'smart'|'overflow'} type
|
|
255
|
+
* @param {number} threshold - The threshold percentage that triggered the harvest (60 or 90).
|
|
256
|
+
* @returns {Promise<void>}
|
|
257
|
+
*/
|
|
258
|
+
async triggerSmartHarvest(type, threshold) {
|
|
259
|
+
Log.notice(
|
|
260
|
+
`ConnectedDeviceHarvester: smart-harvest trigger (${type} at ${threshold}%)`
|
|
261
|
+
);
|
|
262
|
+
if (!this.eventBuffer || this.eventBuffer.isEmpty()) return;
|
|
263
|
+
try {
|
|
264
|
+
await this.sendBufferedEvents();
|
|
265
|
+
} catch (error) {
|
|
266
|
+
Log.error(`${type} smart-harvest failed:`, error.message);
|
|
267
|
+
} finally {
|
|
268
|
+
// Reset the periodic clock — next periodic tick happens `harvestInterval`
|
|
269
|
+
// after this smart drain, not from the originally-scheduled time.
|
|
270
|
+
this.timer.cancelAndReschedule();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Buffers an event for the next harvest cycle.
|
|
276
|
+
*
|
|
277
|
+
* @param {object} eventObject
|
|
278
|
+
* @returns {boolean}
|
|
279
|
+
*/
|
|
280
|
+
addEvent(eventObject) {
|
|
281
|
+
try {
|
|
282
|
+
// Shared QOE_AGGREGATE dedup + non-QoE append. Timestamp
|
|
283
|
+
// is preserved from `recordEvent.js` (emit-time), matching the Browser
|
|
284
|
+
// pipeline. Cross-pipeline analytics produce consistent timestamps.
|
|
285
|
+
return bufferEventWithQoeDedup(this.eventBuffer, eventObject);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
Log.error("ConnectedDeviceHarvester.addEvent failed:", err.message);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Forces the next harvest cycle to ship QOE_AGGREGATE regardless of cycle
|
|
294
|
+
* multiplier or dirty check. Used at CONTENT_END for final QoE flush.
|
|
295
|
+
*/
|
|
296
|
+
forceQoeNextHarvest() {
|
|
297
|
+
this.forceQoeNextCycle = true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Registers a callback invoked at the start of every send cycle, before
|
|
302
|
+
* the buffer is drained. Lets the tracker refresh QoE KPIs in the buffer
|
|
303
|
+
* before they ship.
|
|
304
|
+
* @param {Function|null} cb
|
|
305
|
+
*/
|
|
306
|
+
setBeforeDrainCallback(cb) {
|
|
307
|
+
if (typeof cb === "function" || cb === null) {
|
|
308
|
+
this.beforeDrainCallback = cb;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Updates QoE KPI fields on the existing QOE_AGGREGATE event in the buffer.
|
|
314
|
+
* Mirror of `VideoAnalyticsAgent#refreshQoeKpis` (`agent.js`) so VegaTracker's
|
|
315
|
+
* QoE drain wiring can route here through `getHarvester()`.
|
|
316
|
+
*
|
|
317
|
+
* @param {object} freshKpis - Object with latest KPI values
|
|
318
|
+
* @param {string} [viewId] - The viewId of the player whose QoE event to update
|
|
319
|
+
*/
|
|
320
|
+
refreshQoeKpis(freshKpis, viewId) {
|
|
321
|
+
refreshQoeKpisInBuffer(this.eventBuffer, freshKpis, viewId);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Updates the harvest cadence at runtime. If the periodic timer is already
|
|
326
|
+
* running, it is cleared and restarted with the new interval. Mirror of
|
|
327
|
+
* `VideoAnalyticsAgent#setHarvestInterval` (`agent.js`).
|
|
328
|
+
*
|
|
329
|
+
* @param {number} interval - New cadence in ms.
|
|
330
|
+
*/
|
|
331
|
+
setHarvestInterval(interval) {
|
|
332
|
+
if (typeof interval !== "number" || interval <= 0) return;
|
|
333
|
+
this.harvestInterval = interval;
|
|
334
|
+
this.timer.updateInterval(interval);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Drains the buffer and POSTs to `/v3/data`. Applies the QoE cycle filter
|
|
339
|
+
* and cross-cycle dirty check before send.
|
|
340
|
+
*
|
|
341
|
+
* Mirror of `harvestScheduler.js` drain + send logic, adapted for the CAF
|
|
342
|
+
* 10-tuple wire format and plain `fetch` transport.
|
|
343
|
+
*
|
|
344
|
+
*
|
|
345
|
+
* @returns {Promise<void>}
|
|
346
|
+
*/
|
|
347
|
+
async sendBufferedEvents() {
|
|
348
|
+
if (this.isHarvesting) return;
|
|
349
|
+
if (!this.dataToken) return;
|
|
350
|
+
if (this.eventBuffer.isEmpty()) return;
|
|
351
|
+
|
|
352
|
+
// Refresh QoE KPIs before drain.
|
|
353
|
+
if (typeof this.beforeDrainCallback === "function") {
|
|
354
|
+
try {
|
|
355
|
+
this.beforeDrainCallback();
|
|
356
|
+
} catch (e) {
|
|
357
|
+
Log.error("ConnectedDeviceHarvester beforeDrainCallback failed:", e.message);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.isHarvesting = true;
|
|
362
|
+
const drained = this.eventBuffer.drain();
|
|
363
|
+
|
|
364
|
+
// QoE cycle filter
|
|
365
|
+
const multiplier =
|
|
366
|
+
globalThis.__NRVIDEO_CD__?.config?.qoeIntervalFactor ?? 1;
|
|
367
|
+
const isForced = this.forceQoeNextCycle;
|
|
368
|
+
const isQoeCycle =
|
|
369
|
+
(this.qoeCycleCount - 1) % multiplier === 0 || isForced;
|
|
370
|
+
if (this.forceQoeNextCycle) this.forceQoeNextCycle = false;
|
|
371
|
+
|
|
372
|
+
const filtered = partitionByQoeCycle(drained, isQoeCycle, this.eventBuffer);
|
|
373
|
+
this.qoeCycleCount++;
|
|
374
|
+
|
|
375
|
+
// Cross-cycle dirty check
|
|
376
|
+
applyQoeDirtyFilter(filtered, this._lastSentQoeKpis, isForced);
|
|
377
|
+
|
|
378
|
+
if (filtered.length === 0) {
|
|
379
|
+
this.isHarvesting = false;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const url = `${this.getEndpointBaseUrl()}/v3/data`;
|
|
384
|
+
const headers = {
|
|
385
|
+
"Content-Type": "application/json",
|
|
386
|
+
"X-App-License-Key": this.applicationToken,
|
|
387
|
+
};
|
|
388
|
+
// Build 10-tuple body. Retry can
|
|
389
|
+
// rebuild with a refreshed dataToken in slot [0]. Slots [1] and [8] are
|
|
390
|
+
// built from this.deviceInfo (customer-supplied where present, defaults
|
|
391
|
+
// from CD_DEVICE_INFO / CD_METADATA otherwise). Obfuscation rules (if any)
|
|
392
|
+
// are applied to the serialised payload before sending — same behaviour as
|
|
393
|
+
// the Browser pipeline's OptimizedHttpClient.
|
|
394
|
+
const buildBody = () => {
|
|
395
|
+
const raw = JSON.stringify([
|
|
396
|
+
this.dataToken, // [0] from /v5/connect
|
|
397
|
+
this._buildDeviceInfo(), // [1] device tuple
|
|
398
|
+
0, // [2] reserved
|
|
399
|
+
[], // [3] reserved
|
|
400
|
+
[], // [4] reserved
|
|
401
|
+
[], // [5] reserved
|
|
402
|
+
[], // [6] reserved
|
|
403
|
+
[], // [7] reserved
|
|
404
|
+
this._buildMetadata(), // [8] session metadata
|
|
405
|
+
filtered, // [9] events
|
|
406
|
+
]);
|
|
407
|
+
return applyObfuscationRules(raw, globalThis.__NRVIDEO_CD__?.config?.obfuscate);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
let res = await this._fetchWithTimeout(
|
|
412
|
+
url,
|
|
413
|
+
{ method: "POST", headers, body: buildBody() },
|
|
414
|
+
CD_DATA_TIMEOUT_MS
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// DataToken expiry. Re-queue the drained events and kick
|
|
418
|
+
// off a background reconnect rather than awaiting fetchDataTokens()
|
|
419
|
+
// inline. Awaiting inline would hold isHarvesting = true for up to
|
|
420
|
+
// CD_CONNECT_MAX_ATTEMPTS × CD_CONNECT_RETRY_DELAY_MS (20s), freezing
|
|
421
|
+
// all subsequent harvest ticks. Fire-and-forget releases the lock
|
|
422
|
+
// immediately; next ticks short-circuit on !dataToken until the new
|
|
423
|
+
// token arrives.
|
|
424
|
+
if (res.status === 401) {
|
|
425
|
+
Log.notice(
|
|
426
|
+
"ConnectedDeviceHarvester: /v3/data returned 401 — re-queuing events and reconnecting"
|
|
427
|
+
);
|
|
428
|
+
this.dataToken = null;
|
|
429
|
+
this._connectAttempt = 0;
|
|
430
|
+
for (const e of filtered) this.eventBuffer.add(e);
|
|
431
|
+
this.initialise(); // fire-and-forget background reconnect
|
|
432
|
+
return; // isHarvesting released in finally block
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
436
|
+
Log.notice(`ConnectedDeviceHarvester: sent ${filtered.length} events`);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
Log.error("ConnectedDeviceHarvester: /v3/data send failed:", err.message);
|
|
439
|
+
// Re-queue drained events into buffer; next tick retries.
|
|
440
|
+
for (const e of filtered) this.eventBuffer.add(e);
|
|
441
|
+
} finally {
|
|
442
|
+
this.isHarvesting = false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Build slot [1] of /v3/data — the 10-element device-info tuple. Customer-
|
|
448
|
+
* supplied fields override the static defaults from CD_DEVICE_INFO; missing
|
|
449
|
+
* or empty fields fall through to the placeholders that ship in
|
|
450
|
+
* connectedDeviceConstants.js.
|
|
451
|
+
* @returns {Array}
|
|
452
|
+
* @private
|
|
453
|
+
*/
|
|
454
|
+
_buildDeviceInfo() {
|
|
455
|
+
const d = this.deviceInfo;
|
|
456
|
+
return [
|
|
457
|
+
CD_DEVICE_INFO[0], // osName — fixed ("Vega")
|
|
458
|
+
d.osVersion, // [1] osVersion
|
|
459
|
+
d.deviceModel, // [2] deviceModel
|
|
460
|
+
CD_DEVICE_INFO[3], // agentName — fixed ("VegaAgent")
|
|
461
|
+
CD_DEVICE_INFO[4], // agentVersion — fixed
|
|
462
|
+
d.uuid, // [5] deviceUuid
|
|
463
|
+
"", // [6] reserved
|
|
464
|
+
"", // [7] reserved
|
|
465
|
+
d.deviceManufacturer, // [8] manufacturer
|
|
466
|
+
CD_DEVICE_INFO[9], // [9] sizeMeta — fixed
|
|
467
|
+
];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Build slot [8] of /v3/data — the session-metadata object. Spreads the
|
|
472
|
+
* static `CD_METADATA` defaults (`osName`, `platform`, `appBuild`) and
|
|
473
|
+
* overrides only `osBuild` and `architecture` from the customer-supplied
|
|
474
|
+
* deviceInfo. Device-identity fields (osVersion, deviceModel,
|
|
475
|
+
* deviceManufacturer, osMajorVersion, platformVersion) live in slot [1]
|
|
476
|
+
* (`CD_DEVICE_INFO`) and are intentionally not duplicated here.
|
|
477
|
+
* @returns {object}
|
|
478
|
+
* @private
|
|
479
|
+
*/
|
|
480
|
+
_buildMetadata() {
|
|
481
|
+
const d = this.deviceInfo;
|
|
482
|
+
return {
|
|
483
|
+
...CD_METADATA,
|
|
484
|
+
osBuild: d.osBuild,
|
|
485
|
+
appBuild: d.appBuild,
|
|
486
|
+
architecture: d.architecture,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Wraps `fetch` with an AbortController deadline. If no response arrives
|
|
492
|
+
* within `timeoutMs`, the controller aborts the request and the returned
|
|
493
|
+
* Promise rejects with an AbortError. The caller's existing `catch` block
|
|
494
|
+
* handles it identically to any other network failure — connect retries for
|
|
495
|
+
* /v5/connect, event re-queue for /v3/data.
|
|
496
|
+
*
|
|
497
|
+
* `clearTimeout` in `.finally()` cancels the pending abort when the fetch
|
|
498
|
+
* completes before the deadline, so the abort never fires spuriously.
|
|
499
|
+
*
|
|
500
|
+
* @param {string} url
|
|
501
|
+
* @param {RequestInit} options
|
|
502
|
+
* @param {number} timeoutMs
|
|
503
|
+
* @returns {Promise<Response>}
|
|
504
|
+
* @private
|
|
505
|
+
*/
|
|
506
|
+
_fetchWithTimeout(url, options, timeoutMs) {
|
|
507
|
+
const controller = new AbortController();
|
|
508
|
+
const timerId = setTimeout(() => controller.abort(), timeoutMs);
|
|
509
|
+
return fetch(url, { ...options, signal: controller.signal })
|
|
510
|
+
.finally(() => clearTimeout(timerId));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Stops the harvest interval and attempts one final best-effort send.
|
|
515
|
+
* @returns {Promise<void>}
|
|
516
|
+
*/
|
|
517
|
+
async dispose() {
|
|
518
|
+
this.isDisposed = true;
|
|
519
|
+
this.timer.stop();
|
|
520
|
+
return this.sendBufferedEvents();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Core from "../core";
|
|
2
|
+
import Constants from "../constants";
|
|
3
|
+
import Chrono from "../chrono";
|
|
4
|
+
import Log from "../log";
|
|
5
|
+
import Emitter from "../emitter";
|
|
6
|
+
import Tracker from "../tracker";
|
|
7
|
+
import VideoTracker from "../videotracker";
|
|
8
|
+
import VideoTrackerState from "../videotrackerstate";
|
|
9
|
+
import { NrVideoEventAggregator } from "../eventAggregator";
|
|
10
|
+
import ConnectedDeviceHarvester from "./connectedDeviceHarvester";
|
|
11
|
+
import { connectedDeviceAnalyticsHarvester } from "./connectedDeviceAgent";
|
|
12
|
+
import { getRegisteredHarvester } from "../recordEvent";
|
|
13
|
+
import { recordEvent } from "../recordEvent";
|
|
14
|
+
import { version } from "../../package.json";
|
|
15
|
+
|
|
16
|
+
const nrvideo = {
|
|
17
|
+
Constants,
|
|
18
|
+
Chrono,
|
|
19
|
+
Log,
|
|
20
|
+
Emitter,
|
|
21
|
+
Tracker,
|
|
22
|
+
VideoTracker,
|
|
23
|
+
VideoTrackerState,
|
|
24
|
+
Core,
|
|
25
|
+
version,
|
|
26
|
+
|
|
27
|
+
NrVideoEventAggregator,
|
|
28
|
+
ConnectedDeviceHarvester,
|
|
29
|
+
recordEvent,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
Core,
|
|
34
|
+
Constants,
|
|
35
|
+
Chrono,
|
|
36
|
+
Log,
|
|
37
|
+
Emitter,
|
|
38
|
+
Tracker,
|
|
39
|
+
VideoTracker,
|
|
40
|
+
VideoTrackerState,
|
|
41
|
+
NrVideoEventAggregator,
|
|
42
|
+
ConnectedDeviceHarvester,
|
|
43
|
+
connectedDeviceAnalyticsHarvester,
|
|
44
|
+
getRegisteredHarvester,
|
|
45
|
+
recordEvent,
|
|
46
|
+
version,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default nrvideo;
|