@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.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-zA-Z0-9.-]+$/;
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
- timeIntervalEvent();
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
- this.recordTargetEvent(target, config, type, data);
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
- item.msd = this.msd;
1875
+ report.msd = this.msd;
1740
1876
  target.msdSent = true;
1741
1877
  }
1742
- target.queue.push(item);
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.recordEvent(CMCD_EVENT_RESPONSE_RECEIVED, {
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 url = new URL(report.url);
1830
- const cmcdData = {
1987
+ const merged = {
1831
1988
  ...this.data,
1832
- ...data,
1833
- sn: this.requestTarget.sn++
1989
+ ...data
1834
1990
  };
1835
- const options = createEncodingOptions(CMCD_REQUEST_MODE, this.config, report.url);
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: