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