@svta/cml-cmcd 2.4.1 → 2.5.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/dist/index.d.ts +263 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +183 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -315,9 +315,12 @@ const CMCD_REQUEST_KEYS = [
|
|
|
315
315
|
|
|
316
316
|
//#endregion
|
|
317
317
|
//#region src/isCmcdCustomKey.ts
|
|
318
|
-
const CUSTOM_KEY_REGEX = /^[a-
|
|
318
|
+
const CUSTOM_KEY_REGEX = /^[a-z][a-z0-9.-]*$/;
|
|
319
319
|
/**
|
|
320
|
-
* Check if a key is a custom key
|
|
320
|
+
* Check if a key is a valid custom key: a lowercase first letter, then
|
|
321
|
+
* characters from `a-z 0-9 . -`, with a hyphen that is neither the first
|
|
322
|
+
* nor the last character. These are the CTA-5004-B custom-key rules
|
|
323
|
+
* restricted to names that survive RFC 8941 key serialization.
|
|
321
324
|
*
|
|
322
325
|
* @param key - The key to check.
|
|
323
326
|
*
|
|
@@ -569,6 +572,9 @@ function downConvertToV1(obj) {
|
|
|
569
572
|
* @param options - Options for encoding.
|
|
570
573
|
*
|
|
571
574
|
* @public
|
|
575
|
+
*
|
|
576
|
+
* @example
|
|
577
|
+
* {@includeCode ../test/prepareCmcdData.test.ts#example}
|
|
572
578
|
*/
|
|
573
579
|
function prepareCmcdData(obj, options = {}) {
|
|
574
580
|
const results = {};
|
|
@@ -587,6 +593,8 @@ function prepareCmcdData(obj, options = {}) {
|
|
|
587
593
|
if (!keys.includes("e") && eventType != null) keys.push("e");
|
|
588
594
|
if (!keys.includes("ts")) keys.push("ts");
|
|
589
595
|
if (!keys.includes("cen") && data["cen"] != null && eventType === CMCD_EVENT_CUSTOM_EVENT) keys.push("cen");
|
|
596
|
+
if (!keys.includes("ec") && data["ec"] != null && eventType === CMCD_EVENT_ERROR) keys.push("ec");
|
|
597
|
+
if (!keys.includes("url") && data["url"] != null && eventType === CMCD_EVENT_RESPONSE_RECEIVED) keys.push("url");
|
|
590
598
|
const requiredField = eventType ? CMCD_STATE_EVENT_FIELDS.get(eventType) : void 0;
|
|
591
599
|
if (requiredField && data[requiredField] != null && !keys.includes(requiredField)) keys.push(requiredField);
|
|
592
600
|
}
|
|
@@ -1473,7 +1481,8 @@ function createEncodingOptions(reportingMode, config, baseUrl) {
|
|
|
1473
1481
|
version: config.version || CMCD_V2,
|
|
1474
1482
|
reportingMode,
|
|
1475
1483
|
filter: (key) => enabledKeySet.has(key),
|
|
1476
|
-
baseUrl
|
|
1484
|
+
baseUrl,
|
|
1485
|
+
customHeaderMap: config.customHeaderMap
|
|
1477
1486
|
};
|
|
1478
1487
|
}
|
|
1479
1488
|
/**
|
|
@@ -1523,6 +1532,68 @@ const STATE_FIELDS = /* @__PURE__ */ Array.from(CMCD_STATE_EVENT_FIELDS, ([event
|
|
|
1523
1532
|
};
|
|
1524
1533
|
});
|
|
1525
1534
|
const STATE_FIELDS_BY_EVENT = /* @__PURE__ */ new Map(/* @__PURE__ */ STATE_FIELDS.map((e) => [e.event, e]));
|
|
1535
|
+
/**
|
|
1536
|
+
* Maps each event type to the key CTA-5004-B requires beyond `e` and `ts`.
|
|
1537
|
+
* Built from the state-change table plus the three event types whose
|
|
1538
|
+
* required key rides the caller's per-event data.
|
|
1539
|
+
*/
|
|
1540
|
+
const CMCD_REQUIRED_EVENT_KEYS = /* @__PURE__ */ new Map([
|
|
1541
|
+
...CMCD_STATE_EVENT_FIELDS,
|
|
1542
|
+
[CMCD_EVENT_CUSTOM_EVENT, "cen"],
|
|
1543
|
+
[CMCD_EVENT_ERROR, "ec"],
|
|
1544
|
+
[CMCD_EVENT_RESPONSE_RECEIVED, "url"]
|
|
1545
|
+
]);
|
|
1546
|
+
/**
|
|
1547
|
+
* Whether a required key's value will survive report preparation.
|
|
1548
|
+
*
|
|
1549
|
+
* This is `isValid` minus its `false` exclusion, plus an empty-array check.
|
|
1550
|
+
* `false` must count as usable because `bg: false` is a legitimate value on a
|
|
1551
|
+
* backgrounded-mode event, which the encoder emits as `?0`; treating it as
|
|
1552
|
+
* unusable would let restoration silently revert a transform that cleared it.
|
|
1553
|
+
* Empty strings, empty lists and non-finite numbers are dropped downstream, so
|
|
1554
|
+
* a transform substituting one leaves the report short a required key.
|
|
1555
|
+
*/
|
|
1556
|
+
function isUsableRequiredValue(value) {
|
|
1557
|
+
if (value == null || value === "") return false;
|
|
1558
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
1559
|
+
return !Array.isArray(value) || value.length > 0;
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Copies an `SfItem`-shaped value and its `params` record.
|
|
1563
|
+
*
|
|
1564
|
+
* The prototype is preserved because `prepareCmcdData`, the formatter map,
|
|
1565
|
+
* validation, and the structured-field encoder all branch on
|
|
1566
|
+
* `instanceof SfItem`. A plain spread (and `structuredClone`) would return a
|
|
1567
|
+
* prototype-less object and silently change what goes on the wire.
|
|
1568
|
+
*/
|
|
1569
|
+
function copyItemValue(value) {
|
|
1570
|
+
if (value === null || typeof value !== "object") return value;
|
|
1571
|
+
const copy = Object.assign(Object.create(Object.getPrototypeOf(value)), value);
|
|
1572
|
+
if (copy.params !== null && typeof copy.params === "object") copy.params = { ...copy.params };
|
|
1573
|
+
return copy;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Copies the nested values of a report in place so a transform cannot mutate
|
|
1577
|
+
* the reporter's persistent data, or another target's report for the same
|
|
1578
|
+
* event, by mutating an array or an `SfItem` it was handed.
|
|
1579
|
+
*
|
|
1580
|
+
* Complete for the CMCD value space rather than best-effort: `CmcdValue` and
|
|
1581
|
+
* `CmcdCustomValue` admit only primitives, `SfItem<primitive>`, and arrays of
|
|
1582
|
+
* those, and `SfItem.params` is a flat record. Only called where a transform
|
|
1583
|
+
* is configured.
|
|
1584
|
+
*/
|
|
1585
|
+
function copyReportValues(data) {
|
|
1586
|
+
const record = data;
|
|
1587
|
+
for (const key in record) {
|
|
1588
|
+
const value = record[key];
|
|
1589
|
+
if (Array.isArray(value)) {
|
|
1590
|
+
const copy = new Array(value.length);
|
|
1591
|
+
for (let i = 0; i < value.length; i++) copy[i] = copyItemValue(value[i]);
|
|
1592
|
+
record[key] = copy;
|
|
1593
|
+
} else if (value !== null && typeof value === "object") record[key] = copyItemValue(value);
|
|
1594
|
+
}
|
|
1595
|
+
return data;
|
|
1596
|
+
}
|
|
1526
1597
|
function defaultRequester(request) {
|
|
1527
1598
|
const { url,...init } = request;
|
|
1528
1599
|
return fetch(url, init);
|
|
@@ -1541,7 +1612,8 @@ function createCmcdReporterConfig(config) {
|
|
|
1541
1612
|
url: target.url,
|
|
1542
1613
|
events: target.events.slice(),
|
|
1543
1614
|
interval: target.interval ?? CMCD_DEFAULT_TIME_INTERVAL,
|
|
1544
|
-
batchSize: target.batchSize || 1
|
|
1615
|
+
batchSize: target.batchSize || 1,
|
|
1616
|
+
transform: target.transform
|
|
1545
1617
|
});
|
|
1546
1618
|
return acc;
|
|
1547
1619
|
}, [])
|
|
@@ -1550,6 +1622,14 @@ function createCmcdReporterConfig(config) {
|
|
|
1550
1622
|
/**
|
|
1551
1623
|
* The CMCD reporter.
|
|
1552
1624
|
*
|
|
1625
|
+
* `C` describes the player's own `customData`, which the reporter passes
|
|
1626
|
+
* through to each configured `transform`. It is inferred from the
|
|
1627
|
+
* configuration, so annotating a single `transform` types the request in every
|
|
1628
|
+
* other one; the default leaves `customData` values `unknown`.
|
|
1629
|
+
*
|
|
1630
|
+
* @typeParam C - The shape of the player's `customData`. Defaults to
|
|
1631
|
+
* `Record<string, unknown>`.
|
|
1632
|
+
*
|
|
1553
1633
|
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#reporting-modes-when-we-send-data | CTA-5004-B Reporting Modes}
|
|
1554
1634
|
*
|
|
1555
1635
|
* @public
|
|
@@ -1595,6 +1675,7 @@ var CmcdReporter = class {
|
|
|
1595
1675
|
* populated before calling start().
|
|
1596
1676
|
*/
|
|
1597
1677
|
start() {
|
|
1678
|
+
let failure;
|
|
1598
1679
|
this.eventTargets.forEach((target, config) => {
|
|
1599
1680
|
this.disarmInterval(target);
|
|
1600
1681
|
if (config.interval === 0 || !config.events.includes(CMCD_EVENT_TIME_INTERVAL)) return;
|
|
@@ -1603,8 +1684,13 @@ var CmcdReporter = class {
|
|
|
1603
1684
|
this.processEventTargets();
|
|
1604
1685
|
};
|
|
1605
1686
|
target.intervalId = setInterval(timeIntervalEvent, config.interval * 1e3);
|
|
1606
|
-
|
|
1687
|
+
try {
|
|
1688
|
+
timeIntervalEvent();
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
failure ??= { error };
|
|
1691
|
+
}
|
|
1607
1692
|
});
|
|
1693
|
+
if (failure) throw failure.error;
|
|
1608
1694
|
}
|
|
1609
1695
|
/**
|
|
1610
1696
|
* Stops the CMCD reporter. Called by the player when the reporter is disabled.
|
|
@@ -1701,6 +1787,20 @@ var CmcdReporter = class {
|
|
|
1701
1787
|
* into the reporter's data store.
|
|
1702
1788
|
*/
|
|
1703
1789
|
recordEvent(type, data = {}) {
|
|
1790
|
+
this.emitEvent(type, data);
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Records an event across every configured target, applying
|
|
1794
|
+
* state-change dedup once before per-target fan-out.
|
|
1795
|
+
*
|
|
1796
|
+
* @param type - The type of event to record.
|
|
1797
|
+
* @param data - Additional data to record with the event.
|
|
1798
|
+
* @param request - The media request that triggered the event, when
|
|
1799
|
+
* one exists. Only `recordResponseReceived()`
|
|
1800
|
+
* populates this; it is threaded through to each
|
|
1801
|
+
* target's `transform`.
|
|
1802
|
+
*/
|
|
1803
|
+
emitEvent(type, data, request) {
|
|
1704
1804
|
const entry = STATE_FIELDS_BY_EVENT.get(type);
|
|
1705
1805
|
if (entry) {
|
|
1706
1806
|
const field = entry.field;
|
|
@@ -1711,10 +1811,16 @@ var CmcdReporter = class {
|
|
|
1711
1811
|
if (entry.equal(current, this.lastEmitted[field])) return;
|
|
1712
1812
|
Object.assign(this.lastEmitted, { [field]: entry.snapshot(current) });
|
|
1713
1813
|
}
|
|
1814
|
+
let failure;
|
|
1714
1815
|
this.eventTargets.forEach((target, config) => {
|
|
1715
|
-
|
|
1816
|
+
try {
|
|
1817
|
+
this.recordTargetEvent(target, config, type, data, request);
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
failure ??= { error };
|
|
1820
|
+
}
|
|
1716
1821
|
});
|
|
1717
1822
|
this.processEventTargets();
|
|
1823
|
+
if (failure) throw failure.error;
|
|
1718
1824
|
}
|
|
1719
1825
|
/**
|
|
1720
1826
|
* Records an event for a target. Called by the reporter when an event occurs.
|
|
@@ -1725,21 +1831,51 @@ var CmcdReporter = class {
|
|
|
1725
1831
|
* @param data - Additional data to record with the event. This data
|
|
1726
1832
|
* only applies to this event report. Persistent data should
|
|
1727
1833
|
* be updated using `update()`.
|
|
1834
|
+
* @param request - The media request that triggered the event, when
|
|
1835
|
+
* one exists. Passed to the target's `transform`.
|
|
1728
1836
|
*/
|
|
1729
|
-
recordTargetEvent(target, config, type, data = {}) {
|
|
1837
|
+
recordTargetEvent(target, config, type, data = {}, request) {
|
|
1730
1838
|
if (!config.events.includes(type)) return;
|
|
1731
1839
|
const item = {
|
|
1732
1840
|
...this.data,
|
|
1733
1841
|
...data,
|
|
1734
1842
|
e: type,
|
|
1735
|
-
ts: data.ts ?? Date.now()
|
|
1736
|
-
sn: target.sn++
|
|
1843
|
+
ts: data.ts ?? Date.now()
|
|
1737
1844
|
};
|
|
1845
|
+
const { transform } = config;
|
|
1846
|
+
if (!transform) {
|
|
1847
|
+
this.queueTargetEvent(target, item, type);
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
copyReportValues(item);
|
|
1851
|
+
const requiredKey = CMCD_REQUIRED_EVENT_KEYS.get(type);
|
|
1852
|
+
const requiredValue = requiredKey ? item[requiredKey] : void 0;
|
|
1853
|
+
const ts = item.ts;
|
|
1854
|
+
const report = transform(item, request);
|
|
1855
|
+
if (report == null) return;
|
|
1856
|
+
if (!isUsableRequiredValue(report.ts)) report.ts = ts;
|
|
1857
|
+
if (requiredKey && isUsableRequiredValue(requiredValue) && !isUsableRequiredValue(report[requiredKey])) Object.assign(report, { [requiredKey]: requiredValue });
|
|
1858
|
+
this.queueTargetEvent(target, report, type);
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Stamps the reporter-owned fields on a finished event report and pushes it
|
|
1862
|
+
* to the target's queue.
|
|
1863
|
+
*
|
|
1864
|
+
* Called after any transform has run, so a transform cannot bypass the
|
|
1865
|
+
* target's `events` filter via `e` or break `sn` continuity.
|
|
1866
|
+
*
|
|
1867
|
+
* @param target - The target to queue the report for.
|
|
1868
|
+
* @param report - The finished report data.
|
|
1869
|
+
* @param type - The type of event being reported.
|
|
1870
|
+
*/
|
|
1871
|
+
queueTargetEvent(target, report, type) {
|
|
1872
|
+
report.e = type;
|
|
1873
|
+
report.sn = target.sn++;
|
|
1738
1874
|
if (!isNaN(this.msd) && !target.msdSent) {
|
|
1739
|
-
|
|
1875
|
+
report.msd = this.msd;
|
|
1740
1876
|
target.msdSent = true;
|
|
1741
1877
|
}
|
|
1742
|
-
target.queue.push(
|
|
1878
|
+
target.queue.push(report);
|
|
1743
1879
|
}
|
|
1744
1880
|
/**
|
|
1745
1881
|
* Records a response-received event. Called by the player when a media
|
|
@@ -1756,6 +1892,20 @@ var CmcdReporter = class {
|
|
|
1756
1892
|
* Additional keys like `ttfbb`, `cmsdd`, `cmsds`, and `smrt` can be
|
|
1757
1893
|
* supplied via the `data` parameter if the player has access to them.
|
|
1758
1894
|
*
|
|
1895
|
+
* The request's `customData` is generic, so a player can pass a request
|
|
1896
|
+
* carrying its own taxonomy (e.g. `{ requestType: 'segment' }`) without a
|
|
1897
|
+
* cast. The reporter only reads the optional `cmcd` key that
|
|
1898
|
+
* {@link CmcdReporter.createRequestReport} writes there; every other key is
|
|
1899
|
+
* left untouched and stays visible to an event target's `transform` via
|
|
1900
|
+
* its `request` argument.
|
|
1901
|
+
*
|
|
1902
|
+
* A reporter given a concrete `C` requires the request to satisfy it, so a
|
|
1903
|
+
* request the configured transforms could not read is rejected here rather
|
|
1904
|
+
* than reaching them; see {@link CmcdReporterCustomData}.
|
|
1905
|
+
*
|
|
1906
|
+
* @typeParam RD - The `customData` this request carries. Defaults to the
|
|
1907
|
+
* reporter's own `C`.
|
|
1908
|
+
*
|
|
1759
1909
|
* @param response - The HTTP response received.
|
|
1760
1910
|
* @param data - Additional CMCD data to include with the event.
|
|
1761
1911
|
* Values provided here override any auto-derived values.
|
|
@@ -1779,11 +1929,11 @@ var CmcdReporter = class {
|
|
|
1779
1929
|
if (timing.duration != null) derived.ttlb = Math.round(timing.duration);
|
|
1780
1930
|
}
|
|
1781
1931
|
const cmcd = request.customData?.cmcd ?? {};
|
|
1782
|
-
this.
|
|
1932
|
+
this.emitEvent(CMCD_EVENT_RESPONSE_RECEIVED, {
|
|
1783
1933
|
...cmcd,
|
|
1784
1934
|
...derived,
|
|
1785
1935
|
...data
|
|
1786
|
-
});
|
|
1936
|
+
}, request);
|
|
1787
1937
|
}
|
|
1788
1938
|
/**
|
|
1789
1939
|
* Applies the CMCD request report data to the request. Called by the player
|
|
@@ -1809,6 +1959,14 @@ var CmcdReporter = class {
|
|
|
1809
1959
|
* Creates a new request with the CMCD request report data applied. Called by the player
|
|
1810
1960
|
* before sending the request.
|
|
1811
1961
|
*
|
|
1962
|
+
* A reporter given a concrete `C` requires the request's `customData` to
|
|
1963
|
+
* satisfy it, so a request the configured transform could not read is
|
|
1964
|
+
* rejected here rather than reaching it; see
|
|
1965
|
+
* {@link CmcdReporterCustomData}.
|
|
1966
|
+
*
|
|
1967
|
+
* @typeParam R - The request being decorated. Its `customData` must satisfy
|
|
1968
|
+
* the reporter's `C`.
|
|
1969
|
+
*
|
|
1812
1970
|
* @param request - The request to apply the CMCD request report to.
|
|
1813
1971
|
* @param data - The data to apply to the request. This data only
|
|
1814
1972
|
* applies to this request report. Persistent data
|
|
@@ -1826,17 +1984,24 @@ var CmcdReporter = class {
|
|
|
1826
1984
|
}
|
|
1827
1985
|
};
|
|
1828
1986
|
if (!this.config.enabledKeys?.length || !report.url) return report;
|
|
1829
|
-
const
|
|
1830
|
-
const cmcdData = {
|
|
1987
|
+
const merged = {
|
|
1831
1988
|
...this.data,
|
|
1832
|
-
...data
|
|
1833
|
-
sn: this.requestTarget.sn++
|
|
1989
|
+
...data
|
|
1834
1990
|
};
|
|
1835
|
-
const
|
|
1991
|
+
const { transform } = this.config;
|
|
1992
|
+
let cmcdData = merged;
|
|
1993
|
+
if (transform) {
|
|
1994
|
+
copyReportValues(merged);
|
|
1995
|
+
cmcdData = transform(merged, request);
|
|
1996
|
+
if (cmcdData == null) return report;
|
|
1997
|
+
}
|
|
1998
|
+
cmcdData.sn = this.requestTarget.sn++;
|
|
1836
1999
|
if (!isNaN(this.msd) && !this.requestTarget.msdSent) {
|
|
1837
2000
|
cmcdData.msd = this.msd;
|
|
1838
2001
|
this.requestTarget.msdSent = true;
|
|
1839
2002
|
}
|
|
2003
|
+
const url = new URL(report.url);
|
|
2004
|
+
const options = createEncodingOptions(CMCD_REQUEST_MODE, this.config, report.url);
|
|
1840
2005
|
const cmcd = report.customData.cmcd = prepareCmcdData(cmcdData, options);
|
|
1841
2006
|
switch (this.config.transmissionMode) {
|
|
1842
2007
|
case CMCD_QUERY:
|