@svta/cml-cmcd 2.6.1 → 2.8.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/README.md +3 -3
- package/dist/index.d.ts +617 -578
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1543 -939
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,229 +1,325 @@
|
|
|
1
1
|
import { SfItem, SfToken, decodeSfDict, encodeSfDict, symbolToStr } from "@svta/cml-structured-field-values";
|
|
2
2
|
import { getBaseUrl, urlToRelativePath, uuid } from "@svta/cml-utils";
|
|
3
3
|
|
|
4
|
-
//#region src/
|
|
5
|
-
const roundValue = (value) => {
|
|
6
|
-
if (value instanceof SfItem) return new SfItem(Math.round(value.value), value.params);
|
|
7
|
-
return Math.round(value);
|
|
8
|
-
};
|
|
9
|
-
const toRounded = (value) => {
|
|
10
|
-
if (Array.isArray(value)) return value.map(roundValue);
|
|
11
|
-
return roundValue(value);
|
|
12
|
-
};
|
|
13
|
-
const toUrlSafe = (value, options) => {
|
|
14
|
-
if (Array.isArray(value)) return value.map((item) => toUrlSafe(item, options));
|
|
15
|
-
if (value instanceof SfItem && typeof value.value === "string") return new SfItem(toUrlSafe(value.value, options), value.params);
|
|
16
|
-
else {
|
|
17
|
-
if (options.baseUrl) value = urlToRelativePath(value, getBaseUrl(options.baseUrl));
|
|
18
|
-
return options.version === 1 ? encodeURIComponent(value) : value;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
const hundredValue = (value) => {
|
|
22
|
-
if (value instanceof SfItem) return new SfItem(Math.round(value.value / 100) * 100, value.params);
|
|
23
|
-
return Math.round(value / 100) * 100;
|
|
24
|
-
};
|
|
25
|
-
const toHundred = (value) => {
|
|
26
|
-
if (Array.isArray(value)) return value.map(hundredValue);
|
|
27
|
-
return hundredValue(value);
|
|
28
|
-
};
|
|
29
|
-
const nor = (value, options) => {
|
|
30
|
-
let norValue = value;
|
|
31
|
-
if (options.version >= 2) {
|
|
32
|
-
if (value instanceof SfItem && typeof value.value === "string") norValue = new SfItem([value]);
|
|
33
|
-
else if (typeof value === "string") norValue = [value];
|
|
34
|
-
}
|
|
35
|
-
return toUrlSafe(norValue, options);
|
|
36
|
-
};
|
|
4
|
+
//#region src/CMCD_INNER_LIST_KEYS.ts
|
|
37
5
|
/**
|
|
38
|
-
*
|
|
6
|
+
* Inner-list keys in version 2 that are scalars in version 1.
|
|
39
7
|
*
|
|
40
|
-
*
|
|
41
|
-
*/
|
|
42
|
-
const CMCD_FORMATTER_MAP = {
|
|
43
|
-
br: toRounded,
|
|
44
|
-
d: toRounded,
|
|
45
|
-
bl: toHundred,
|
|
46
|
-
dl: toHundred,
|
|
47
|
-
mtp: toHundred,
|
|
48
|
-
nor,
|
|
49
|
-
rtp: toHundred,
|
|
50
|
-
tb: toRounded
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
//#endregion
|
|
54
|
-
//#region src/CMCD_V2.ts
|
|
55
|
-
/**
|
|
56
|
-
* CMCD Version 2
|
|
8
|
+
* Used by both encoding (down-conversion) and decoding (up-conversion).
|
|
57
9
|
*
|
|
58
|
-
* @
|
|
10
|
+
* @internal
|
|
59
11
|
*/
|
|
60
|
-
const
|
|
12
|
+
const CMCD_INNER_LIST_KEYS = new Set([
|
|
13
|
+
"ab",
|
|
14
|
+
"bl",
|
|
15
|
+
"br",
|
|
16
|
+
"bsa",
|
|
17
|
+
"bsd",
|
|
18
|
+
"bsda",
|
|
19
|
+
"lab",
|
|
20
|
+
"lb",
|
|
21
|
+
"mtp",
|
|
22
|
+
"pb",
|
|
23
|
+
"tab",
|
|
24
|
+
"tb",
|
|
25
|
+
"tbl",
|
|
26
|
+
"tpb"
|
|
27
|
+
]);
|
|
61
28
|
|
|
62
29
|
//#endregion
|
|
63
|
-
//#region src/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*
|
|
73
|
-
* @public
|
|
74
|
-
*/
|
|
75
|
-
const CMCD_EVENT_PLAY_STATE = "ps";
|
|
76
|
-
/**
|
|
77
|
-
* CMCD event type for the 'pr' key (playback rate change).
|
|
78
|
-
*
|
|
79
|
-
* @public
|
|
80
|
-
*/
|
|
81
|
-
const CMCD_EVENT_PLAYBACK_RATE = "pr";
|
|
82
|
-
/**
|
|
83
|
-
* CMCD event type for the 'e' key (error).
|
|
84
|
-
*
|
|
85
|
-
* @public
|
|
86
|
-
*/
|
|
87
|
-
const CMCD_EVENT_ERROR = "e";
|
|
88
|
-
/**
|
|
89
|
-
* CMCD event type for the 't' key (time interval).
|
|
90
|
-
*
|
|
91
|
-
* @public
|
|
92
|
-
*/
|
|
93
|
-
const CMCD_EVENT_TIME_INTERVAL = "t";
|
|
94
|
-
/**
|
|
95
|
-
* CMCD event type for the 'c' key (content ID).
|
|
96
|
-
*
|
|
97
|
-
* @public
|
|
98
|
-
*/
|
|
99
|
-
const CMCD_EVENT_CONTENT_ID = "c";
|
|
100
|
-
/**
|
|
101
|
-
* CMCD event type for the 'b' key (backgrounded mode).
|
|
102
|
-
*
|
|
103
|
-
* @public
|
|
104
|
-
*/
|
|
105
|
-
const CMCD_EVENT_BACKGROUNDED_MODE = "b";
|
|
106
|
-
/**
|
|
107
|
-
* CMCD event type for the 'm' key (mute).
|
|
108
|
-
*
|
|
109
|
-
* @public
|
|
110
|
-
*/
|
|
111
|
-
const CMCD_EVENT_MUTE = "m";
|
|
112
|
-
/**
|
|
113
|
-
* CMCD event type for the 'um' key (unmute).
|
|
114
|
-
*
|
|
115
|
-
* @public
|
|
116
|
-
*/
|
|
117
|
-
const CMCD_EVENT_UNMUTE = "um";
|
|
118
|
-
/**
|
|
119
|
-
* CMCD event type for the 'pe' key (player expand).
|
|
120
|
-
*
|
|
121
|
-
* @public
|
|
122
|
-
*/
|
|
123
|
-
const CMCD_EVENT_PLAYER_EXPAND = "pe";
|
|
124
|
-
/**
|
|
125
|
-
* CMCD event type for the 'pc' key (player collapse).
|
|
126
|
-
*
|
|
127
|
-
* @public
|
|
128
|
-
*/
|
|
129
|
-
const CMCD_EVENT_PLAYER_COLLAPSE = "pc";
|
|
130
|
-
/**
|
|
131
|
-
* CMCD event type for the 'rr' key (response received).
|
|
132
|
-
*
|
|
133
|
-
* @public
|
|
134
|
-
*/
|
|
135
|
-
const CMCD_EVENT_RESPONSE_RECEIVED = "rr";
|
|
136
|
-
/**
|
|
137
|
-
* CMCD event type for the 'as' key (ad start).
|
|
138
|
-
*
|
|
139
|
-
* @public
|
|
140
|
-
*/
|
|
141
|
-
const CMCD_EVENT_AD_START = "as";
|
|
142
|
-
/**
|
|
143
|
-
* CMCD event type for the 'ae' key (ad end).
|
|
144
|
-
*
|
|
145
|
-
* @public
|
|
146
|
-
*/
|
|
147
|
-
const CMCD_EVENT_AD_END = "ae";
|
|
148
|
-
/**
|
|
149
|
-
* CMCD event type for the 'abs' key (ad break start).
|
|
150
|
-
*
|
|
151
|
-
* @public
|
|
152
|
-
*/
|
|
153
|
-
const CMCD_EVENT_AD_BREAK_START = "abs";
|
|
154
|
-
/**
|
|
155
|
-
* CMCD event type for the 'abe' key (ad break end).
|
|
156
|
-
*
|
|
157
|
-
* @public
|
|
158
|
-
*/
|
|
159
|
-
const CMCD_EVENT_AD_BREAK_END = "abe";
|
|
160
|
-
/**
|
|
161
|
-
* CMCD event type for the 'sk' key (skip).
|
|
162
|
-
*
|
|
163
|
-
* @public
|
|
164
|
-
*/
|
|
165
|
-
const CMCD_EVENT_SKIP = "sk";
|
|
166
|
-
/**
|
|
167
|
-
* CMCD event type for the 'ce' key (custom event).
|
|
168
|
-
*
|
|
169
|
-
* @public
|
|
170
|
-
*/
|
|
171
|
-
const CMCD_EVENT_CUSTOM_EVENT = "ce";
|
|
30
|
+
//#region src/CMCD_KEY_SPECS.ts
|
|
31
|
+
const MEDIA = [
|
|
32
|
+
"a",
|
|
33
|
+
"v",
|
|
34
|
+
"av",
|
|
35
|
+
"tt",
|
|
36
|
+
"c",
|
|
37
|
+
"o"
|
|
38
|
+
];
|
|
172
39
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* @enum
|
|
176
|
-
*
|
|
177
|
-
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#event | CTA-5004-B Event}
|
|
40
|
+
* The encoder rules of each key. The table has one row for each key of CTA-5004-B Table 1, plus `nrr` of CTA-5004 and `cdn`.
|
|
178
41
|
*
|
|
179
|
-
* @
|
|
42
|
+
* @internal
|
|
180
43
|
*/
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
44
|
+
const CMCD_KEY_SPECS = {
|
|
45
|
+
ab: {
|
|
46
|
+
type: "ot-list",
|
|
47
|
+
round: 1,
|
|
48
|
+
supersededBy: "br",
|
|
49
|
+
version: 2
|
|
50
|
+
},
|
|
51
|
+
bg: {
|
|
52
|
+
type: "boolean",
|
|
53
|
+
omitDefault: false,
|
|
54
|
+
requiredOn: "b",
|
|
55
|
+
version: 2
|
|
56
|
+
},
|
|
57
|
+
bl: {
|
|
58
|
+
type: "ot-list",
|
|
59
|
+
round: 100
|
|
60
|
+
},
|
|
61
|
+
br: {
|
|
62
|
+
type: "ot-list",
|
|
63
|
+
round: 1,
|
|
64
|
+
requiredOn: "bc"
|
|
65
|
+
},
|
|
66
|
+
bs: {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
omitDefault: false
|
|
69
|
+
},
|
|
70
|
+
bsa: {
|
|
71
|
+
type: "ot-list",
|
|
72
|
+
round: 1,
|
|
73
|
+
version: 2
|
|
74
|
+
},
|
|
75
|
+
bsd: {
|
|
76
|
+
type: "ot-list",
|
|
77
|
+
round: 1,
|
|
78
|
+
version: 2
|
|
79
|
+
},
|
|
80
|
+
bsda: {
|
|
81
|
+
type: "ot-list",
|
|
82
|
+
round: 1,
|
|
83
|
+
version: 2
|
|
84
|
+
},
|
|
85
|
+
cdn: {
|
|
86
|
+
type: "string",
|
|
87
|
+
max: 128,
|
|
88
|
+
version: 2
|
|
89
|
+
},
|
|
90
|
+
cen: {
|
|
91
|
+
type: "string",
|
|
92
|
+
max: 64,
|
|
93
|
+
mode: "event",
|
|
94
|
+
onlyOn: "ce",
|
|
95
|
+
requiredOn: "ce",
|
|
96
|
+
version: 2
|
|
97
|
+
},
|
|
98
|
+
cid: {
|
|
99
|
+
type: "string",
|
|
100
|
+
max: 128,
|
|
101
|
+
v1Max: 64,
|
|
102
|
+
requiredOn: "c"
|
|
103
|
+
},
|
|
104
|
+
cmsdd: {
|
|
105
|
+
type: "string",
|
|
106
|
+
mode: "event",
|
|
107
|
+
onlyOn: "rr",
|
|
108
|
+
version: 2
|
|
109
|
+
},
|
|
110
|
+
cmsds: {
|
|
111
|
+
type: "string",
|
|
112
|
+
mode: "event",
|
|
113
|
+
onlyOn: "rr",
|
|
114
|
+
version: 2
|
|
115
|
+
},
|
|
116
|
+
cs: {
|
|
117
|
+
type: "string",
|
|
118
|
+
version: 2
|
|
119
|
+
},
|
|
120
|
+
d: {
|
|
121
|
+
type: "integer",
|
|
122
|
+
round: 1,
|
|
123
|
+
ot: MEDIA
|
|
124
|
+
},
|
|
125
|
+
dfa: {
|
|
126
|
+
type: "integer",
|
|
127
|
+
round: 1,
|
|
128
|
+
version: 2
|
|
129
|
+
},
|
|
130
|
+
dl: {
|
|
131
|
+
type: "integer",
|
|
132
|
+
round: 100
|
|
133
|
+
},
|
|
134
|
+
e: {
|
|
135
|
+
type: "token",
|
|
136
|
+
mode: "event",
|
|
137
|
+
requiredOn: "always",
|
|
138
|
+
version: 2
|
|
139
|
+
},
|
|
140
|
+
ec: {
|
|
141
|
+
type: "string-list",
|
|
142
|
+
requiredOn: "e",
|
|
143
|
+
version: 2
|
|
144
|
+
},
|
|
145
|
+
h: {
|
|
146
|
+
type: "string",
|
|
147
|
+
max: 128,
|
|
148
|
+
mode: "event",
|
|
149
|
+
requiredOn: "h",
|
|
150
|
+
version: 2
|
|
151
|
+
},
|
|
152
|
+
lab: {
|
|
153
|
+
type: "ot-list",
|
|
154
|
+
round: 1,
|
|
155
|
+
supersededBy: "lb",
|
|
156
|
+
version: 2
|
|
157
|
+
},
|
|
158
|
+
lb: {
|
|
159
|
+
type: "ot-list",
|
|
160
|
+
round: 1,
|
|
161
|
+
version: 2
|
|
162
|
+
},
|
|
163
|
+
ltc: {
|
|
164
|
+
type: "integer",
|
|
165
|
+
round: 1,
|
|
166
|
+
version: 2
|
|
167
|
+
},
|
|
168
|
+
msd: {
|
|
169
|
+
type: "integer",
|
|
170
|
+
round: 1,
|
|
171
|
+
version: 2
|
|
172
|
+
},
|
|
173
|
+
mtp: {
|
|
174
|
+
type: "ot-list",
|
|
175
|
+
round: 100
|
|
176
|
+
},
|
|
177
|
+
nor: { type: "nor" },
|
|
178
|
+
nr: {
|
|
179
|
+
type: "boolean",
|
|
180
|
+
omitDefault: false,
|
|
181
|
+
version: 2
|
|
182
|
+
},
|
|
183
|
+
nrr: {
|
|
184
|
+
type: "string",
|
|
185
|
+
version: 1
|
|
186
|
+
},
|
|
187
|
+
ot: { type: "token" },
|
|
188
|
+
pb: {
|
|
189
|
+
type: "ot-list",
|
|
190
|
+
round: 1,
|
|
191
|
+
version: 2
|
|
192
|
+
},
|
|
193
|
+
pr: {
|
|
194
|
+
type: "decimal",
|
|
195
|
+
omitDefault: 1,
|
|
196
|
+
requiredOn: "pr"
|
|
197
|
+
},
|
|
198
|
+
pt: {
|
|
199
|
+
type: "integer",
|
|
200
|
+
round: 1,
|
|
201
|
+
version: 2
|
|
202
|
+
},
|
|
203
|
+
rc: {
|
|
204
|
+
type: "integer",
|
|
205
|
+
round: 1,
|
|
206
|
+
mode: "event",
|
|
207
|
+
onlyOn: "rr",
|
|
208
|
+
version: 2
|
|
209
|
+
},
|
|
210
|
+
rtp: {
|
|
211
|
+
type: "integer",
|
|
212
|
+
round: 100
|
|
213
|
+
},
|
|
214
|
+
sf: {
|
|
215
|
+
type: "token",
|
|
216
|
+
v1Map: { e: "o" }
|
|
217
|
+
},
|
|
218
|
+
sid: {
|
|
219
|
+
type: "string",
|
|
220
|
+
max: 64
|
|
221
|
+
},
|
|
222
|
+
smrt: {
|
|
223
|
+
type: "string",
|
|
224
|
+
mode: "event",
|
|
225
|
+
onlyOn: "rr",
|
|
226
|
+
version: 2
|
|
227
|
+
},
|
|
228
|
+
sn: {
|
|
229
|
+
type: "integer",
|
|
230
|
+
round: 1,
|
|
231
|
+
version: 2
|
|
232
|
+
},
|
|
233
|
+
st: {
|
|
234
|
+
type: "token",
|
|
235
|
+
v1Map: { ll: "l" }
|
|
236
|
+
},
|
|
237
|
+
sta: {
|
|
238
|
+
type: "token",
|
|
239
|
+
requiredOn: "ps",
|
|
240
|
+
version: 2
|
|
241
|
+
},
|
|
242
|
+
su: {
|
|
243
|
+
type: "boolean",
|
|
244
|
+
omitDefault: false
|
|
245
|
+
},
|
|
246
|
+
tab: {
|
|
247
|
+
type: "ot-list",
|
|
248
|
+
round: 1,
|
|
249
|
+
supersededBy: "tb",
|
|
250
|
+
version: 2
|
|
251
|
+
},
|
|
252
|
+
tb: {
|
|
253
|
+
type: "ot-list",
|
|
254
|
+
round: 1
|
|
255
|
+
},
|
|
256
|
+
tbl: {
|
|
257
|
+
type: "ot-list",
|
|
258
|
+
round: 100,
|
|
259
|
+
version: 2
|
|
260
|
+
},
|
|
261
|
+
tpb: {
|
|
262
|
+
type: "ot-list",
|
|
263
|
+
round: 1,
|
|
264
|
+
ot: [
|
|
265
|
+
"a",
|
|
266
|
+
"v",
|
|
267
|
+
"av",
|
|
268
|
+
"c"
|
|
269
|
+
],
|
|
270
|
+
version: 2
|
|
271
|
+
},
|
|
272
|
+
ts: {
|
|
273
|
+
type: "integer",
|
|
274
|
+
round: 1,
|
|
275
|
+
mode: "event",
|
|
276
|
+
requiredOn: "always",
|
|
277
|
+
version: 2
|
|
278
|
+
},
|
|
279
|
+
ttfb: {
|
|
280
|
+
type: "integer",
|
|
281
|
+
round: 1,
|
|
282
|
+
mode: "event",
|
|
283
|
+
onlyOn: "rr",
|
|
284
|
+
version: 2
|
|
285
|
+
},
|
|
286
|
+
ttfbb: {
|
|
287
|
+
type: "integer",
|
|
288
|
+
round: 1,
|
|
289
|
+
mode: "event",
|
|
290
|
+
onlyOn: "rr",
|
|
291
|
+
version: 2
|
|
292
|
+
},
|
|
293
|
+
ttlb: {
|
|
294
|
+
type: "integer",
|
|
295
|
+
round: 1,
|
|
296
|
+
mode: "event",
|
|
297
|
+
onlyOn: "rr",
|
|
298
|
+
version: 2
|
|
299
|
+
},
|
|
300
|
+
url: {
|
|
301
|
+
type: "string",
|
|
302
|
+
mode: "event",
|
|
303
|
+
onlyOn: "rr",
|
|
304
|
+
requiredOn: "rr",
|
|
305
|
+
version: 2
|
|
306
|
+
},
|
|
307
|
+
v: {
|
|
308
|
+
type: "integer",
|
|
309
|
+
round: 1,
|
|
310
|
+
requiredOn: "always",
|
|
311
|
+
version: 2
|
|
312
|
+
}
|
|
200
313
|
};
|
|
201
314
|
|
|
202
315
|
//#endregion
|
|
203
|
-
//#region src/
|
|
316
|
+
//#region src/CMCD_V2.ts
|
|
204
317
|
/**
|
|
205
|
-
*
|
|
206
|
-
* the event signals.
|
|
207
|
-
*
|
|
208
|
-
* Per CTA-5004-B, the state-change events `ps`, `pr`, `c`, `b`, `bc` are
|
|
209
|
-
* state-transition markers and must carry the field whose value they signal.
|
|
210
|
-
* Consumers force-include the field post-filter (`prepareCmcdData`), dedup
|
|
211
|
-
* against its value (`CmcdReporter`), and check its presence in payloads
|
|
212
|
-
* (`validateCmcdStructure`).
|
|
213
|
-
*
|
|
214
|
-
* Iteration order is load-bearing: `CmcdReporter.update()` fires state-change
|
|
215
|
-
* events in map order when multiple tracked fields change in a single call.
|
|
216
|
-
* Do not reorder entries without auditing reporter behavior.
|
|
318
|
+
* CMCD Version 2
|
|
217
319
|
*
|
|
218
|
-
* @
|
|
320
|
+
* @public
|
|
219
321
|
*/
|
|
220
|
-
const
|
|
221
|
-
[CMCD_EVENT_PLAY_STATE, "sta"],
|
|
222
|
-
[CMCD_EVENT_PLAYBACK_RATE, "pr"],
|
|
223
|
-
[CMCD_EVENT_CONTENT_ID, "cid"],
|
|
224
|
-
[CMCD_EVENT_BACKGROUNDED_MODE, "bg"],
|
|
225
|
-
[CMCD_EVENT_BITRATE_CHANGE, "br"]
|
|
226
|
-
]);
|
|
322
|
+
const CMCD_V2 = 2;
|
|
227
323
|
|
|
228
324
|
//#endregion
|
|
229
325
|
//#region src/CmcdReportingMode.ts
|
|
@@ -252,75 +348,68 @@ const CmcdReportingMode = {
|
|
|
252
348
|
};
|
|
253
349
|
|
|
254
350
|
//#endregion
|
|
255
|
-
//#region src/
|
|
351
|
+
//#region src/downConvertToV1.ts
|
|
352
|
+
function hasParams(params) {
|
|
353
|
+
for (const _ in params) return true;
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
256
356
|
/**
|
|
257
|
-
*
|
|
357
|
+
* Unwrap an inner list or SfItem value to a scalar.
|
|
258
358
|
*
|
|
259
|
-
*
|
|
359
|
+
* The scalar is the value of the item with the object type flag of `ot`
|
|
360
|
+
* (CTA-5004-B section 4.1, item 14). Without that item, it is the value
|
|
361
|
+
* of the first item without parameters. Otherwise it is `undefined`.
|
|
260
362
|
*/
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
363
|
+
function unwrapValue(value, ot) {
|
|
364
|
+
if (value instanceof SfItem) value = value.value;
|
|
365
|
+
if (!Array.isArray(value)) return value;
|
|
366
|
+
let fallback;
|
|
367
|
+
for (const item of value) {
|
|
368
|
+
if (!(item instanceof SfItem)) {
|
|
369
|
+
if (fallback === void 0) fallback = item;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (ot && item.params?.[ot] === true) return item.value;
|
|
373
|
+
if (fallback === void 0 && !hasParams(item.params)) fallback = item.value;
|
|
374
|
+
}
|
|
375
|
+
return fallback;
|
|
376
|
+
}
|
|
270
377
|
/**
|
|
271
|
-
*
|
|
378
|
+
* Down-convert version 2 CMCD data to version 1.
|
|
272
379
|
*
|
|
273
|
-
*
|
|
380
|
+
* - Extracts `nrr` from the `nor` SfItem's `r` parameter.
|
|
381
|
+
* - Unwraps inner-list values to scalars for the object type `ot`.
|
|
382
|
+
*
|
|
383
|
+
* @internal
|
|
274
384
|
*/
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
"msd",
|
|
295
|
-
"mtp",
|
|
296
|
-
"nor",
|
|
297
|
-
"nr",
|
|
298
|
-
"ot",
|
|
299
|
-
"pb",
|
|
300
|
-
"pr",
|
|
301
|
-
"pt",
|
|
302
|
-
"rtp",
|
|
303
|
-
"sf",
|
|
304
|
-
"sid",
|
|
305
|
-
"sn",
|
|
306
|
-
"st",
|
|
307
|
-
"sta",
|
|
308
|
-
"su",
|
|
309
|
-
"tab",
|
|
310
|
-
"tb",
|
|
311
|
-
"tbl",
|
|
312
|
-
"tpb",
|
|
313
|
-
"v"
|
|
314
|
-
];
|
|
385
|
+
function downConvertToV1(obj, ot) {
|
|
386
|
+
const result = {};
|
|
387
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
388
|
+
if (value == null) {
|
|
389
|
+
result[key] = value;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (key === "nor") {
|
|
393
|
+
const list = value instanceof SfItem && Array.isArray(value.value) ? value.value : value;
|
|
394
|
+
const first = Array.isArray(list) ? list[0] : list;
|
|
395
|
+
if (first instanceof SfItem) {
|
|
396
|
+
result["nor"] = first.value;
|
|
397
|
+
if (first.params?.r) result["nrr"] = first.params.r;
|
|
398
|
+
} else result["nor"] = first;
|
|
399
|
+
} else if (CMCD_INNER_LIST_KEYS.has(key)) result[key] = unwrapValue(value, ot);
|
|
400
|
+
else result[key] = value;
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
315
404
|
|
|
316
405
|
//#endregion
|
|
317
406
|
//#region src/isCmcdCustomKey.ts
|
|
318
407
|
const CUSTOM_KEY_REGEX = /^[a-z][a-z0-9.-]*$/;
|
|
319
408
|
/**
|
|
320
409
|
* Check if a key is a valid custom key: a lowercase first letter, then
|
|
321
|
-
* characters from `a-z 0-9 .
|
|
322
|
-
*
|
|
323
|
-
*
|
|
410
|
+
* characters from `a-z 0-9 . -`. The key needs a hyphen, not at the start
|
|
411
|
+
* or end. This check applies the CTA-5004-B custom-key rules, restricted
|
|
412
|
+
* to names that RFC 8941 key serialization accepts.
|
|
324
413
|
*
|
|
325
414
|
* @param key - The key to check.
|
|
326
415
|
*
|
|
@@ -337,254 +426,245 @@ function isCmcdCustomKey(key) {
|
|
|
337
426
|
}
|
|
338
427
|
|
|
339
428
|
//#endregion
|
|
340
|
-
//#region src/
|
|
341
|
-
const
|
|
429
|
+
//#region src/getKeySpec.ts
|
|
430
|
+
const CUSTOM_KEY_SPEC = {
|
|
431
|
+
type: "custom",
|
|
432
|
+
omitDefault: false,
|
|
433
|
+
max: 64,
|
|
434
|
+
v1Max: Infinity
|
|
435
|
+
};
|
|
342
436
|
/**
|
|
343
|
-
*
|
|
437
|
+
* Returns the row of a key in the key table.
|
|
344
438
|
*
|
|
345
|
-
* @param key - The key to
|
|
439
|
+
* @param key - The key to look up.
|
|
346
440
|
*
|
|
347
|
-
* @returns
|
|
441
|
+
* @returns The row of a reserved key, the custom row for a valid custom key, or `undefined`.
|
|
348
442
|
*
|
|
349
|
-
* @
|
|
350
|
-
*
|
|
351
|
-
* @example
|
|
352
|
-
* {@includeCode ../test/isCmcdRequestKey.test.ts#example}
|
|
443
|
+
* @internal
|
|
353
444
|
*/
|
|
354
|
-
function
|
|
355
|
-
|
|
445
|
+
function getKeySpec(key) {
|
|
446
|
+
if (Object.prototype.hasOwnProperty.call(CMCD_KEY_SPECS, key)) return CMCD_KEY_SPECS[key];
|
|
447
|
+
return isCmcdCustomKey(key) ? CUSTOM_KEY_SPEC : void 0;
|
|
356
448
|
}
|
|
357
449
|
|
|
358
450
|
//#endregion
|
|
359
|
-
//#region src/
|
|
360
|
-
/**
|
|
361
|
-
* CMCD v2 - Response-only and timing keys.
|
|
362
|
-
*
|
|
363
|
-
* @public
|
|
364
|
-
*/
|
|
365
|
-
const CMCD_RESPONSE_KEYS = [
|
|
366
|
-
"cmsdd",
|
|
367
|
-
"cmsds",
|
|
368
|
-
"rc",
|
|
369
|
-
"smrt",
|
|
370
|
-
"ttfb",
|
|
371
|
-
"ttfbb",
|
|
372
|
-
"ttlb",
|
|
373
|
-
"url"
|
|
374
|
-
];
|
|
375
|
-
|
|
376
|
-
//#endregion
|
|
377
|
-
//#region src/isCmcdResponseReceivedKey.ts
|
|
378
|
-
const CMCD_RESPONSE_KEY_SET = /* @__PURE__ */ new Set(CMCD_RESPONSE_KEYS);
|
|
451
|
+
//#region src/isValid.ts
|
|
379
452
|
/**
|
|
380
|
-
*
|
|
453
|
+
* Checks if the given value is valid to send as a CMCD value.
|
|
381
454
|
*
|
|
382
|
-
*
|
|
455
|
+
* `undefined`, `null`, an empty string, `false`, an empty array, and a
|
|
456
|
+
* number that is not finite are not valid. CTA-5004-B requires omitting
|
|
457
|
+
* the key when its value is unknown.
|
|
383
458
|
*
|
|
384
|
-
* @
|
|
459
|
+
* @param value - The value to check.
|
|
385
460
|
*
|
|
386
|
-
* @
|
|
461
|
+
* @returns `true` if the value is valid.
|
|
387
462
|
*
|
|
388
|
-
* @
|
|
389
|
-
* {@includeCode ../test/isCmcdResponseReceivedKey.test.ts#example}
|
|
463
|
+
* @internal
|
|
390
464
|
*/
|
|
391
|
-
function
|
|
392
|
-
return
|
|
465
|
+
function isValid(value) {
|
|
466
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
467
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
468
|
+
return value != null && value !== "" && value !== false;
|
|
393
469
|
}
|
|
394
470
|
|
|
395
471
|
//#endregion
|
|
396
|
-
//#region src/
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
function
|
|
411
|
-
|
|
472
|
+
//#region src/formatNor.ts
|
|
473
|
+
function toBase(baseUrl) {
|
|
474
|
+
if (!baseUrl) return;
|
|
475
|
+
try {
|
|
476
|
+
return new URL(getBaseUrl(baseUrl)).href;
|
|
477
|
+
} catch {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function formatPath(path, base, version) {
|
|
482
|
+
if (typeof path !== "string" || path === "") return;
|
|
483
|
+
const relative = base ? urlToRelativePath(path, base) : path;
|
|
484
|
+
return version === 1 ? encodeURIComponent(relative) : relative;
|
|
485
|
+
}
|
|
486
|
+
function formatEntry(entry, base, version) {
|
|
487
|
+
if (entry instanceof SfItem) {
|
|
488
|
+
const path = formatPath(entry.value, base, version);
|
|
489
|
+
return path === void 0 ? void 0 : new SfItem(path, entry.params);
|
|
490
|
+
}
|
|
491
|
+
return formatPath(entry, base, version);
|
|
492
|
+
}
|
|
493
|
+
function formatEntries(entries, base, version) {
|
|
494
|
+
const list = [];
|
|
495
|
+
for (const entry of entries) {
|
|
496
|
+
const formatted = formatEntry(entry, base, version);
|
|
497
|
+
if (formatted !== void 0) list.push(formatted);
|
|
498
|
+
}
|
|
499
|
+
return list.length > 0 ? list : void 0;
|
|
412
500
|
}
|
|
413
|
-
|
|
414
|
-
//#endregion
|
|
415
|
-
//#region src/CMCD_INNER_LIST_KEYS.ts
|
|
416
|
-
/**
|
|
417
|
-
* Keys that are inner lists in V2 but plain scalars in V1.
|
|
418
|
-
*
|
|
419
|
-
* Used by both encoding (down-conversion) and decoding (up-conversion).
|
|
420
|
-
*
|
|
421
|
-
* @internal
|
|
422
|
-
*/
|
|
423
|
-
const CMCD_INNER_LIST_KEYS = new Set([
|
|
424
|
-
"ab",
|
|
425
|
-
"bl",
|
|
426
|
-
"br",
|
|
427
|
-
"bsa",
|
|
428
|
-
"bsd",
|
|
429
|
-
"bsda",
|
|
430
|
-
"lab",
|
|
431
|
-
"lb",
|
|
432
|
-
"mtp",
|
|
433
|
-
"pb",
|
|
434
|
-
"tab",
|
|
435
|
-
"tb",
|
|
436
|
-
"tbl",
|
|
437
|
-
"tpb"
|
|
438
|
-
]);
|
|
439
|
-
|
|
440
|
-
//#endregion
|
|
441
|
-
//#region src/CMCD_V1_KEYS.ts
|
|
442
|
-
/**
|
|
443
|
-
* Defines the keys for CMCD (Common Media Client Data) version 1.
|
|
444
|
-
*
|
|
445
|
-
* @public
|
|
446
|
-
*/
|
|
447
|
-
const CMCD_V1_KEYS = [
|
|
448
|
-
"bl",
|
|
449
|
-
"br",
|
|
450
|
-
"bs",
|
|
451
|
-
"cid",
|
|
452
|
-
"d",
|
|
453
|
-
"dl",
|
|
454
|
-
"mtp",
|
|
455
|
-
"nor",
|
|
456
|
-
"nrr",
|
|
457
|
-
"ot",
|
|
458
|
-
"pr",
|
|
459
|
-
"rtp",
|
|
460
|
-
"sf",
|
|
461
|
-
"sid",
|
|
462
|
-
"st",
|
|
463
|
-
"su",
|
|
464
|
-
"tb",
|
|
465
|
-
"v"
|
|
466
|
-
];
|
|
467
|
-
|
|
468
|
-
//#endregion
|
|
469
|
-
//#region src/isCmcdV1Key.ts
|
|
470
|
-
const CMCD_V1_KEY_SET$1 = /* @__PURE__ */ new Set(CMCD_V1_KEYS);
|
|
471
501
|
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
502
|
+
* Formats a `nor` value. Each path becomes relative to `baseUrl` when `baseUrl` is a valid URL.
|
|
503
|
+
* Version 1 encodes the path with `encodeURIComponent`. Version 2 wraps one path in a list.
|
|
504
|
+
* The parameters of an entry and of an inner list stay on the value.
|
|
475
505
|
*
|
|
476
|
-
* @
|
|
506
|
+
* @param value - A path, an `SfItem` of a path, or a list of them. An `SfItem` can also wrap the list.
|
|
507
|
+
* @param options - The version and `baseUrl` of the report.
|
|
477
508
|
*
|
|
478
|
-
* @
|
|
509
|
+
* @returns The formatted value, or `undefined` when no entry is a non-empty string.
|
|
479
510
|
*
|
|
480
|
-
* @
|
|
481
|
-
* {@includeCode ../test/isCmcdV1Key.test.ts#example}
|
|
511
|
+
* @internal
|
|
482
512
|
*/
|
|
483
|
-
function
|
|
484
|
-
|
|
513
|
+
function formatNor(value, options) {
|
|
514
|
+
const base = toBase(options.baseUrl);
|
|
515
|
+
const version = options.version;
|
|
516
|
+
if (Array.isArray(value)) return formatEntries(value, base, version);
|
|
517
|
+
if (value instanceof SfItem && Array.isArray(value.value)) {
|
|
518
|
+
const list = formatEntries(value.value, base, version);
|
|
519
|
+
return list && new SfItem(list, value.params);
|
|
520
|
+
}
|
|
521
|
+
const entry = formatEntry(value, base, version);
|
|
522
|
+
return version === 1 || entry === void 0 ? entry : [entry];
|
|
485
523
|
}
|
|
486
524
|
|
|
487
525
|
//#endregion
|
|
488
|
-
//#region src/
|
|
489
|
-
const TOKEN_FIELDS = /* @__PURE__ */ new Set([
|
|
490
|
-
"ot",
|
|
491
|
-
"sf",
|
|
492
|
-
"st",
|
|
493
|
-
"e",
|
|
494
|
-
"sta"
|
|
495
|
-
]);
|
|
526
|
+
//#region src/toTokenString.ts
|
|
496
527
|
/**
|
|
497
|
-
*
|
|
528
|
+
* Resolve the text of a token value.
|
|
498
529
|
*
|
|
499
|
-
*
|
|
530
|
+
* A token may be a plain string, a registry `Symbol`, an `SfToken`, or an
|
|
531
|
+
* `SfItem` that wraps such a value.
|
|
532
|
+
*
|
|
533
|
+
* @param value - The value to resolve.
|
|
500
534
|
*
|
|
501
|
-
* @returns `
|
|
535
|
+
* @returns The token text, or `undefined` when the value is not a token.
|
|
502
536
|
*
|
|
503
537
|
* @internal
|
|
504
538
|
*/
|
|
505
|
-
function
|
|
506
|
-
return
|
|
539
|
+
function toTokenString(value) {
|
|
540
|
+
if (value instanceof SfItem) return toTokenString(value.value);
|
|
541
|
+
if (typeof value === "string") return value;
|
|
542
|
+
if (typeof value === "symbol" || value instanceof SfToken) return symbolToStr(value);
|
|
507
543
|
}
|
|
508
544
|
|
|
509
545
|
//#endregion
|
|
510
|
-
//#region src/
|
|
546
|
+
//#region src/normalizeValue.ts
|
|
547
|
+
function toText(value, spec, version) {
|
|
548
|
+
const max = (version === 1 ? spec.v1Max : void 0) ?? spec.max ?? Infinity;
|
|
549
|
+
return typeof value === "string" && value !== "" && value.length <= max ? value : void 0;
|
|
550
|
+
}
|
|
551
|
+
function normalizeBare(value, spec, version) {
|
|
552
|
+
switch (spec.type) {
|
|
553
|
+
case "boolean": return typeof value === "boolean" ? value : void 0;
|
|
554
|
+
case "integer":
|
|
555
|
+
case "ot-list": return typeof value === "number" && Number.isFinite(value) ? Math.round(value / spec.round) * spec.round : void 0;
|
|
556
|
+
case "decimal": return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
557
|
+
case "string": return toText(value, spec, version);
|
|
558
|
+
case "string-list": return typeof value === "string" && value !== "" ? value : void 0;
|
|
559
|
+
case "token": {
|
|
560
|
+
const text = toTokenString(value);
|
|
561
|
+
const v1Map = version === 1 ? spec.v1Map : void 0;
|
|
562
|
+
const token = text !== void 0 && v1Map !== void 0 && Object.prototype.hasOwnProperty.call(v1Map, text) ? v1Map[text] : text;
|
|
563
|
+
return token ? new SfToken(token) : void 0;
|
|
564
|
+
}
|
|
565
|
+
case "custom":
|
|
566
|
+
if (typeof value === "string") return toText(value, spec, version);
|
|
567
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
568
|
+
if (Array.isArray(value)) return value.length > 0 ? value : void 0;
|
|
569
|
+
return typeof value === "boolean" || typeof value === "symbol" || value instanceof SfToken ? value : void 0;
|
|
570
|
+
default: return;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function normalizeItem(value, spec, version) {
|
|
574
|
+
if (value instanceof SfItem) {
|
|
575
|
+
const bare = normalizeBare(value.value, spec, version);
|
|
576
|
+
return bare === void 0 ? void 0 : new SfItem(bare, value.params);
|
|
577
|
+
}
|
|
578
|
+
return normalizeBare(value, spec, version);
|
|
579
|
+
}
|
|
580
|
+
function normalizeList(value, spec, version) {
|
|
581
|
+
const wrapper = value instanceof SfItem && Array.isArray(value.value) ? value : void 0;
|
|
582
|
+
const items = wrapper ? wrapper.value : Array.isArray(value) ? value : [value];
|
|
583
|
+
const list = [];
|
|
584
|
+
for (const item of items) {
|
|
585
|
+
const normalized = normalizeItem(item, spec, version);
|
|
586
|
+
if (normalized !== void 0) list.push(normalized);
|
|
587
|
+
}
|
|
588
|
+
if (list.length === 0) return;
|
|
589
|
+
return wrapper ? new SfItem(list, wrapper.params) : list;
|
|
590
|
+
}
|
|
511
591
|
/**
|
|
512
|
-
*
|
|
592
|
+
* Applies the value rule of a key type from the key table.
|
|
513
593
|
*
|
|
514
|
-
*
|
|
594
|
+
* An `SfItem` keeps its parameters, and the rule applies to the value inside it.
|
|
595
|
+
* In version 2, a list key wraps one value in a list.
|
|
596
|
+
* The rules do not check the limits of RFC 8941, such as the integer range.
|
|
597
|
+
*
|
|
598
|
+
* @param value - The value to prepare.
|
|
599
|
+
* @param spec - The row of the key in the key table.
|
|
600
|
+
* @param options - The version and `baseUrl` of the report.
|
|
515
601
|
*
|
|
516
|
-
* @returns `
|
|
602
|
+
* @returns The value to send, or `undefined` when the value fails the rule.
|
|
517
603
|
*
|
|
518
604
|
* @internal
|
|
519
605
|
*/
|
|
520
|
-
function
|
|
521
|
-
if (
|
|
522
|
-
|
|
606
|
+
function normalizeValue(value, spec, options) {
|
|
607
|
+
if (spec.type === "nor") return formatNor(value, options);
|
|
608
|
+
if (spec.type === "string-list" || spec.type === "ot-list" && options.version !== 1) return normalizeList(value, spec, options.version);
|
|
609
|
+
return normalizeItem(value, spec, options.version);
|
|
523
610
|
}
|
|
524
611
|
|
|
525
612
|
//#endregion
|
|
526
|
-
//#region src/
|
|
613
|
+
//#region src/toBareValue.ts
|
|
527
614
|
/**
|
|
528
|
-
*
|
|
615
|
+
* Returns the value inside an `SfItem`, or the value itself.
|
|
529
616
|
*
|
|
530
|
-
*
|
|
531
|
-
* `SfItem`
|
|
617
|
+
* `decodeCmcd` returns a member with parameters as an `SfItem`. For an
|
|
618
|
+
* inner list with parameters, the value of the `SfItem` is the list.
|
|
532
619
|
*
|
|
533
|
-
* @param value - The value to
|
|
534
|
-
*
|
|
535
|
-
* @returns The token text, or `undefined` when the value is not a token.
|
|
620
|
+
* @param value - The value to read.
|
|
621
|
+
* @returns The value without its parameters.
|
|
536
622
|
*
|
|
537
623
|
* @internal
|
|
538
624
|
*/
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
if (typeof value === "string") return value;
|
|
542
|
-
if (typeof value === "symbol" || value instanceof SfToken) return symbolToStr(value);
|
|
625
|
+
function toBareValue(value) {
|
|
626
|
+
return value instanceof SfItem ? value.value : value;
|
|
543
627
|
}
|
|
544
628
|
|
|
545
629
|
//#endregion
|
|
546
630
|
//#region src/prepareCmcdData.ts
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
[CMCD_REQUEST_MODE]: isCmcdRequestKey
|
|
550
|
-
};
|
|
551
|
-
/**
|
|
552
|
-
* Unwrap an inner list or SfItem value to a plain scalar.
|
|
553
|
-
*/
|
|
554
|
-
function unwrapValue(value, ot) {
|
|
555
|
-
if (Array.isArray(value)) {
|
|
556
|
-
let item;
|
|
557
|
-
const otText = toTokenString(ot);
|
|
558
|
-
if (otText) item = value.find((item$1) => toTokenString(item$1.params?.ot) === otText);
|
|
559
|
-
if (!item) item = value[0];
|
|
560
|
-
return unwrapValue(item);
|
|
561
|
-
}
|
|
562
|
-
if (value instanceof SfItem) return value.value;
|
|
563
|
-
return value;
|
|
631
|
+
function hasKey(spec, reportingMode, isV1) {
|
|
632
|
+
return (spec.mode === void 0 || spec.mode === reportingMode) && (spec.version === void 0 || spec.version === 1 === isV1);
|
|
564
633
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
function
|
|
572
|
-
|
|
573
|
-
for (const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
634
|
+
function isRequired(spec, event) {
|
|
635
|
+
return spec.requiredOn === "always" || event !== void 0 && spec.requiredOn === event;
|
|
636
|
+
}
|
|
637
|
+
function hasObjectTypes(spec) {
|
|
638
|
+
return (spec.type === "integer" || spec.type === "ot-list") && spec.ot !== void 0;
|
|
639
|
+
}
|
|
640
|
+
function needsV1ObjectType(obj, filter) {
|
|
641
|
+
if (typeof filter !== "function" || filter("ot")) return true;
|
|
642
|
+
for (const key of CMCD_INNER_LIST_KEYS) if (Array.isArray(toBareValue(obj[key])) && filter(key)) return true;
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
function allowsObjectType(spec, objectType) {
|
|
646
|
+
if (objectType === void 0 || spec.type !== "integer" && spec.type !== "ot-list" || spec.ot === void 0) return true;
|
|
647
|
+
return spec.ot.includes(objectType);
|
|
648
|
+
}
|
|
649
|
+
function isDefaultValue(spec, value) {
|
|
650
|
+
return "omitDefault" in spec && toBareValue(value) === spec.omitDefault;
|
|
651
|
+
}
|
|
652
|
+
function insertKey(keys, specs, key) {
|
|
653
|
+
let index = 0;
|
|
654
|
+
while (index < keys.length && keys[index] < key) index++;
|
|
655
|
+
keys.splice(index, 0, key);
|
|
656
|
+
specs.splice(index, 0, CMCD_KEY_SPECS[key]);
|
|
657
|
+
}
|
|
658
|
+
function prepareValue(key, value, spec, options, formatterOptions) {
|
|
659
|
+
const formatter = options.formatters?.[key];
|
|
660
|
+
if (typeof formatter !== "function") return value == null ? void 0 : normalizeValue(value, spec, formatterOptions);
|
|
661
|
+
const formatted = isValid(value) ? formatter(value, formatterOptions) : value;
|
|
662
|
+
if (!isValid(formatted) && !(formatted === false && spec.type === "boolean")) return;
|
|
663
|
+
if (spec.type === "token") {
|
|
664
|
+
if (typeof formatted === "string") return new SfToken(formatted);
|
|
665
|
+
if (formatted instanceof SfItem && typeof formatted.value === "string") return new SfItem(new SfToken(formatted.value), formatted.params);
|
|
586
666
|
}
|
|
587
|
-
return
|
|
667
|
+
return formatted;
|
|
588
668
|
}
|
|
589
669
|
/**
|
|
590
670
|
* Convert a generic object to CMCD data.
|
|
@@ -600,48 +680,66 @@ function downConvertToV1(obj) {
|
|
|
600
680
|
function prepareCmcdData(obj, options = {}) {
|
|
601
681
|
const results = {};
|
|
602
682
|
if (obj == null || typeof obj !== "object") return results;
|
|
603
|
-
const version = options.version || obj["v"] || CMCD_V2;
|
|
683
|
+
const version = options.version || toBareValue(obj["v"]) || CMCD_V2;
|
|
604
684
|
const reportingMode = options.reportingMode || CMCD_REQUEST_MODE;
|
|
605
|
-
const
|
|
606
|
-
const
|
|
607
|
-
const keyFilter = version === 1 ? isCmcdV1Key : filterMap[reportingMode];
|
|
608
|
-
let keys = Object.keys(data).filter(keyFilter);
|
|
609
|
-
if (data["e"] && eventType !== CMCD_EVENT_RESPONSE_RECEIVED) keys = keys.filter((key) => !isCmcdResponseReceivedKey(key));
|
|
610
|
-
const filter = options.filter;
|
|
611
|
-
if (typeof filter === "function") keys = keys.filter(filter);
|
|
612
|
-
const isEventMode = reportingMode === CMCD_EVENT_MODE;
|
|
613
|
-
if (isEventMode) {
|
|
614
|
-
if (!keys.includes("e") && data["e"] != null) keys.push("e");
|
|
615
|
-
if (!keys.includes("ts")) keys.push("ts");
|
|
616
|
-
if (!keys.includes("cen") && data["cen"] != null && eventType === CMCD_EVENT_CUSTOM_EVENT) keys.push("cen");
|
|
617
|
-
if (!keys.includes("ec") && data["ec"] != null && eventType === CMCD_EVENT_ERROR) keys.push("ec");
|
|
618
|
-
if (!keys.includes("url") && data["url"] != null && eventType === CMCD_EVENT_RESPONSE_RECEIVED) keys.push("url");
|
|
619
|
-
const requiredField = eventType ? CMCD_STATE_EVENT_FIELDS.get(eventType) : void 0;
|
|
620
|
-
if (requiredField && data[requiredField] != null && !keys.includes(requiredField)) keys.push(requiredField);
|
|
621
|
-
}
|
|
622
|
-
if (keys.length === 0) return results;
|
|
623
|
-
if (version > 1 && !keys.includes("v")) keys.push("v");
|
|
685
|
+
const isV1 = version === 1;
|
|
686
|
+
const isEventReport = !isV1 && reportingMode === CMCD_EVENT_MODE;
|
|
624
687
|
const formatterOptions = {
|
|
625
688
|
version,
|
|
626
689
|
reportingMode,
|
|
627
690
|
baseUrl: options.baseUrl
|
|
628
691
|
};
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
692
|
+
const filter = options.filter;
|
|
693
|
+
let objectTypeValue;
|
|
694
|
+
let objectType;
|
|
695
|
+
if (isV1 && needsV1ObjectType(obj, filter)) {
|
|
696
|
+
objectTypeValue = prepareValue("ot", obj["ot"], CMCD_KEY_SPECS["ot"], options, formatterOptions);
|
|
697
|
+
objectType = toTokenString(objectTypeValue);
|
|
698
|
+
}
|
|
699
|
+
const data = isV1 ? downConvertToV1(obj, objectType) : obj;
|
|
700
|
+
const eventValue = isEventReport ? prepareValue("e", data["e"], CMCD_KEY_SPECS["e"], options, formatterOptions) : void 0;
|
|
701
|
+
const event = toTokenString(eventValue);
|
|
702
|
+
const keys = [];
|
|
703
|
+
const specs = [];
|
|
704
|
+
let passed = false;
|
|
705
|
+
let needsObjectType = false;
|
|
706
|
+
for (const key of Object.keys(data).sort()) {
|
|
707
|
+
const spec = getKeySpec(key);
|
|
708
|
+
if (spec === void 0 || !hasKey(spec, reportingMode, isV1)) continue;
|
|
709
|
+
if (typeof filter !== "function" || filter(key)) passed = true;
|
|
710
|
+
else if (!isRequired(spec, event)) continue;
|
|
711
|
+
if (spec.onlyOn !== void 0 && spec.onlyOn !== event) continue;
|
|
712
|
+
if (key === "ot" || hasObjectTypes(spec)) needsObjectType = true;
|
|
713
|
+
keys.push(key);
|
|
714
|
+
specs.push(spec);
|
|
715
|
+
}
|
|
716
|
+
if (!passed && !isEventReport) return results;
|
|
717
|
+
if (needsObjectType && !isV1) {
|
|
718
|
+
objectTypeValue = prepareValue("ot", data["ot"], CMCD_KEY_SPECS["ot"], options, formatterOptions);
|
|
719
|
+
objectType = toTokenString(objectTypeValue);
|
|
720
|
+
}
|
|
721
|
+
if (isEventReport && !keys.includes("ts")) insertKey(keys, specs, "ts");
|
|
722
|
+
if (!isV1 && !keys.includes("v")) insertKey(keys, specs, "v");
|
|
723
|
+
const values = new Array(keys.length);
|
|
724
|
+
for (let i = keys.length - 1; i >= 0; i--) {
|
|
725
|
+
const key = keys[i];
|
|
726
|
+
const spec = specs[i];
|
|
727
|
+
if (spec.type === "ot-list" && spec.supersededBy !== void 0) {
|
|
728
|
+
const exact = keys.indexOf(spec.supersededBy, i + 1);
|
|
729
|
+
if (exact !== -1 && values[exact] !== void 0) continue;
|
|
730
|
+
}
|
|
731
|
+
if (key === "v") values[i] = version;
|
|
732
|
+
else if (key === "ot") values[i] = objectTypeValue;
|
|
733
|
+
else if (key === "e") values[i] = eventValue;
|
|
734
|
+
else if (isV1 || allowsObjectType(spec, objectType)) values[i] = prepareValue(key, data[key], spec, options, formatterOptions);
|
|
735
|
+
if (key === "ts") {
|
|
736
|
+
const timestamp = toBareValue(values[i]);
|
|
737
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) values[i] = Date.now();
|
|
643
738
|
}
|
|
644
|
-
|
|
739
|
+
}
|
|
740
|
+
for (let i = 0; i < keys.length; i++) {
|
|
741
|
+
const value = values[i];
|
|
742
|
+
if (value !== void 0 && (!isDefaultValue(specs[i], value) || isRequired(specs[i], event))) results[keys[i]] = value;
|
|
645
743
|
}
|
|
646
744
|
return results;
|
|
647
745
|
}
|
|
@@ -703,9 +801,9 @@ const CMCD_HEADER_FIELDS = [
|
|
|
703
801
|
/**
|
|
704
802
|
* The map of CMCD keys to their appropriate header shard.
|
|
705
803
|
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
804
|
+
* Event-only keys (e, ts, cen, h) and response-received keys (rc, ttfb,
|
|
805
|
+
* ttlb, url, and others) are intentionally absent. They are transmitted
|
|
806
|
+
* in the event-mode POST body, not HTTP headers.
|
|
709
807
|
*
|
|
710
808
|
* @public
|
|
711
809
|
*/
|
|
@@ -760,9 +858,9 @@ function createHeaderMap(headerMap) {
|
|
|
760
858
|
}, {});
|
|
761
859
|
}
|
|
762
860
|
/**
|
|
763
|
-
* Group a CMCD
|
|
861
|
+
* Group a CMCD object into header shards
|
|
764
862
|
*
|
|
765
|
-
* @param cmcd - The CMCD
|
|
863
|
+
* @param cmcd - The CMCD object to convert.
|
|
766
864
|
* @param customHeaderMap - A map of CMCD header fields to custom CMCD keys.
|
|
767
865
|
*
|
|
768
866
|
* @returns The CMCD header shards.
|
|
@@ -808,10 +906,10 @@ function toPreparedCmcdHeaders(data, customHeaderMap) {
|
|
|
808
906
|
//#endregion
|
|
809
907
|
//#region src/toCmcdHeaders.ts
|
|
810
908
|
/**
|
|
811
|
-
* Convert a CMCD
|
|
909
|
+
* Convert a CMCD object to request headers
|
|
812
910
|
*
|
|
813
|
-
* @param cmcd - The CMCD
|
|
814
|
-
* @param options - Options for encoding
|
|
911
|
+
* @param cmcd - The CMCD object to convert.
|
|
912
|
+
* @param options - Options for encoding.
|
|
815
913
|
*
|
|
816
914
|
* @returns The CMCD header shards.
|
|
817
915
|
*
|
|
@@ -830,13 +928,13 @@ function toCmcdHeaders(cmcd, options = {}) {
|
|
|
830
928
|
//#endregion
|
|
831
929
|
//#region src/appendCmcdHeaders.ts
|
|
832
930
|
/**
|
|
833
|
-
* Append CMCD
|
|
931
|
+
* Append CMCD header shards to a header object.
|
|
834
932
|
*
|
|
835
933
|
* @param headers - The headers to append to.
|
|
836
934
|
* @param cmcd - The CMCD object to append.
|
|
837
|
-
* @param options -
|
|
935
|
+
* @param options - Options for encoding.
|
|
838
936
|
*
|
|
839
|
-
* @returns The headers with
|
|
937
|
+
* @returns The headers with CMCD header shards appended.
|
|
840
938
|
*
|
|
841
939
|
* @public
|
|
842
940
|
*
|
|
@@ -897,10 +995,10 @@ function encodeCmcd(cmcd, options = {}) {
|
|
|
897
995
|
//#endregion
|
|
898
996
|
//#region src/toCmcdUrl.ts
|
|
899
997
|
/**
|
|
900
|
-
* Convert a CMCD
|
|
998
|
+
* Convert a CMCD object to a URL encoded string.
|
|
901
999
|
*
|
|
902
1000
|
* @param cmcd - The CMCD object to convert.
|
|
903
|
-
* @param options - Options for encoding
|
|
1001
|
+
* @param options - Options for encoding.
|
|
904
1002
|
*
|
|
905
1003
|
* @returns The URL encoded CMCD data.
|
|
906
1004
|
*
|
|
@@ -917,10 +1015,10 @@ function toCmcdUrl(cmcd, options = {}) {
|
|
|
917
1015
|
//#endregion
|
|
918
1016
|
//#region src/toCmcdQuery.ts
|
|
919
1017
|
/**
|
|
920
|
-
* Convert a CMCD
|
|
1018
|
+
* Convert a CMCD object to a query arg.
|
|
921
1019
|
*
|
|
922
1020
|
* @param cmcd - The CMCD object to convert.
|
|
923
|
-
* @param options - Options for encoding
|
|
1021
|
+
* @param options - Options for encoding.
|
|
924
1022
|
*
|
|
925
1023
|
* @returns The CMCD query arg.
|
|
926
1024
|
*
|
|
@@ -944,91 +1042,402 @@ const REGEX = /CMCD=[^&#]+/;
|
|
|
944
1042
|
*
|
|
945
1043
|
* @param url - The URL to append to.
|
|
946
1044
|
* @param cmcd - The CMCD object to append.
|
|
947
|
-
* @param options - Options for encoding
|
|
1045
|
+
* @param options - Options for encoding.
|
|
948
1046
|
*
|
|
949
1047
|
* @returns The URL with the CMCD query args appended.
|
|
950
1048
|
*
|
|
951
1049
|
* @public
|
|
952
|
-
*
|
|
953
|
-
* @example
|
|
954
|
-
* {@includeCode ../test/appendCmcdQuery.test.ts#example}
|
|
955
|
-
*
|
|
956
|
-
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#query-argument-definition | CTA-5004-B Query Argument Definition}
|
|
1050
|
+
*
|
|
1051
|
+
* @example
|
|
1052
|
+
* {@includeCode ../test/appendCmcdQuery.test.ts#example}
|
|
1053
|
+
*
|
|
1054
|
+
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#query-argument-definition | CTA-5004-B Query Argument Definition}
|
|
1055
|
+
*/
|
|
1056
|
+
function appendCmcdQuery(url, cmcd, options) {
|
|
1057
|
+
const query = toCmcdQuery(cmcd, options);
|
|
1058
|
+
if (!query) return url;
|
|
1059
|
+
if (REGEX.test(url)) return url.replace(REGEX, query);
|
|
1060
|
+
return `${url}${url.includes("?") ? "&" : "?"}${query}`;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
//#endregion
|
|
1064
|
+
//#region src/CMCD_DEFAULT_TIME_INTERVAL.ts
|
|
1065
|
+
/**
|
|
1066
|
+
* The default time interval in seconds when using event mode
|
|
1067
|
+
*
|
|
1068
|
+
* @public
|
|
1069
|
+
*/
|
|
1070
|
+
const CMCD_DEFAULT_TIME_INTERVAL = 30;
|
|
1071
|
+
|
|
1072
|
+
//#endregion
|
|
1073
|
+
//#region src/CMCD_EVENT_KEYS.ts
|
|
1074
|
+
/**
|
|
1075
|
+
* Defines the event-specific keys for CMCD (Common Media Client Data) version 2.
|
|
1076
|
+
*
|
|
1077
|
+
* @public
|
|
1078
|
+
*/
|
|
1079
|
+
const CMCD_EVENT_KEYS = [
|
|
1080
|
+
"cen",
|
|
1081
|
+
"e",
|
|
1082
|
+
"h",
|
|
1083
|
+
"ts"
|
|
1084
|
+
];
|
|
1085
|
+
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region src/CMCD_FORMATTER_MAP.ts
|
|
1088
|
+
const formatNumbers = (value, format) => {
|
|
1089
|
+
if (Array.isArray(value)) {
|
|
1090
|
+
const list = [];
|
|
1091
|
+
for (const item of value) {
|
|
1092
|
+
const formatted = formatNumbers(item, format);
|
|
1093
|
+
if (Number.isFinite(formatted instanceof SfItem ? formatted.value : formatted)) list.push(formatted);
|
|
1094
|
+
}
|
|
1095
|
+
return list;
|
|
1096
|
+
}
|
|
1097
|
+
if (value instanceof SfItem) {
|
|
1098
|
+
const formatted = formatNumbers(value.value, format);
|
|
1099
|
+
return isValid(formatted) ? new SfItem(formatted, value.params) : NaN;
|
|
1100
|
+
}
|
|
1101
|
+
return typeof value === "number" ? format(value) : NaN;
|
|
1102
|
+
};
|
|
1103
|
+
const toRounded = (value) => formatNumbers(value, Math.round);
|
|
1104
|
+
const toNumber = (value) => formatNumbers(value, Number);
|
|
1105
|
+
const roundToHundred = (value) => Math.round(value / 100) * 100;
|
|
1106
|
+
const toHundred = (value) => formatNumbers(value, roundToHundred);
|
|
1107
|
+
/**
|
|
1108
|
+
* Formatters for CMCD values.
|
|
1109
|
+
*
|
|
1110
|
+
* The encoder applies the same rounding through its key table, and `nor` is the `nor` rule of the encoder.
|
|
1111
|
+
* Use an entry to build a custom formatter for `CmcdEncodeOptions.formatters`.
|
|
1112
|
+
* The formatter of a numeric key drops a value or a list element that is not a finite number.
|
|
1113
|
+
*
|
|
1114
|
+
* @public
|
|
1115
|
+
*/
|
|
1116
|
+
const CMCD_FORMATTER_MAP = {
|
|
1117
|
+
br: toRounded,
|
|
1118
|
+
d: toRounded,
|
|
1119
|
+
bl: toHundred,
|
|
1120
|
+
dl: toHundred,
|
|
1121
|
+
mtp: toHundred,
|
|
1122
|
+
nor: (value, options) => formatNor(value, options) ?? "",
|
|
1123
|
+
rtp: toHundred,
|
|
1124
|
+
tb: toRounded,
|
|
1125
|
+
tbl: toHundred,
|
|
1126
|
+
ab: toRounded,
|
|
1127
|
+
bsa: toRounded,
|
|
1128
|
+
bsd: toRounded,
|
|
1129
|
+
bsda: toRounded,
|
|
1130
|
+
dfa: toRounded,
|
|
1131
|
+
lab: toRounded,
|
|
1132
|
+
lb: toRounded,
|
|
1133
|
+
ltc: toRounded,
|
|
1134
|
+
msd: toRounded,
|
|
1135
|
+
pb: toRounded,
|
|
1136
|
+
pr: toNumber,
|
|
1137
|
+
pt: toRounded,
|
|
1138
|
+
rc: toRounded,
|
|
1139
|
+
sn: toRounded,
|
|
1140
|
+
tab: toRounded,
|
|
1141
|
+
tpb: toRounded,
|
|
1142
|
+
ts: toRounded,
|
|
1143
|
+
ttfb: toRounded,
|
|
1144
|
+
ttfbb: toRounded,
|
|
1145
|
+
ttlb: toRounded
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
//#endregion
|
|
1149
|
+
//#region src/CMCD_REQUEST_KEYS.ts
|
|
1150
|
+
/**
|
|
1151
|
+
* Defines the request-specific keys for CMCD (Common Media Client Data) version 2.
|
|
1152
|
+
*
|
|
1153
|
+
* @public
|
|
1154
|
+
*/
|
|
1155
|
+
const CMCD_REQUEST_KEYS = [
|
|
1156
|
+
"ab",
|
|
1157
|
+
"bg",
|
|
1158
|
+
"bl",
|
|
1159
|
+
"br",
|
|
1160
|
+
"bs",
|
|
1161
|
+
"bsa",
|
|
1162
|
+
"bsd",
|
|
1163
|
+
"bsda",
|
|
1164
|
+
"cdn",
|
|
1165
|
+
"cid",
|
|
1166
|
+
"cs",
|
|
1167
|
+
"d",
|
|
1168
|
+
"dfa",
|
|
1169
|
+
"dl",
|
|
1170
|
+
"ec",
|
|
1171
|
+
"lab",
|
|
1172
|
+
"lb",
|
|
1173
|
+
"ltc",
|
|
1174
|
+
"msd",
|
|
1175
|
+
"mtp",
|
|
1176
|
+
"nor",
|
|
1177
|
+
"nr",
|
|
1178
|
+
"ot",
|
|
1179
|
+
"pb",
|
|
1180
|
+
"pr",
|
|
1181
|
+
"pt",
|
|
1182
|
+
"rtp",
|
|
1183
|
+
"sf",
|
|
1184
|
+
"sid",
|
|
1185
|
+
"sn",
|
|
1186
|
+
"st",
|
|
1187
|
+
"sta",
|
|
1188
|
+
"su",
|
|
1189
|
+
"tab",
|
|
1190
|
+
"tb",
|
|
1191
|
+
"tbl",
|
|
1192
|
+
"tpb",
|
|
1193
|
+
"v"
|
|
1194
|
+
];
|
|
1195
|
+
|
|
1196
|
+
//#endregion
|
|
1197
|
+
//#region src/CMCD_RESPONSE_KEYS.ts
|
|
1198
|
+
/**
|
|
1199
|
+
* Response-only and timing keys for version 2.
|
|
1200
|
+
*
|
|
1201
|
+
* @public
|
|
1202
|
+
*/
|
|
1203
|
+
const CMCD_RESPONSE_KEYS = [
|
|
1204
|
+
"cmsdd",
|
|
1205
|
+
"cmsds",
|
|
1206
|
+
"rc",
|
|
1207
|
+
"smrt",
|
|
1208
|
+
"ttfb",
|
|
1209
|
+
"ttfbb",
|
|
1210
|
+
"ttlb",
|
|
1211
|
+
"url"
|
|
1212
|
+
];
|
|
1213
|
+
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region src/CMCD_V1_KEYS.ts
|
|
1216
|
+
/**
|
|
1217
|
+
* Defines the keys for CMCD (Common Media Client Data) version 1.
|
|
1218
|
+
*
|
|
1219
|
+
* @public
|
|
1220
|
+
*/
|
|
1221
|
+
const CMCD_V1_KEYS = [
|
|
1222
|
+
"bl",
|
|
1223
|
+
"br",
|
|
1224
|
+
"bs",
|
|
1225
|
+
"cid",
|
|
1226
|
+
"d",
|
|
1227
|
+
"dl",
|
|
1228
|
+
"mtp",
|
|
1229
|
+
"nor",
|
|
1230
|
+
"nrr",
|
|
1231
|
+
"ot",
|
|
1232
|
+
"pr",
|
|
1233
|
+
"rtp",
|
|
1234
|
+
"sf",
|
|
1235
|
+
"sid",
|
|
1236
|
+
"st",
|
|
1237
|
+
"su",
|
|
1238
|
+
"tb",
|
|
1239
|
+
"v"
|
|
1240
|
+
];
|
|
1241
|
+
|
|
1242
|
+
//#endregion
|
|
1243
|
+
//#region src/CMCD_KEYS.ts
|
|
1244
|
+
/**
|
|
1245
|
+
* A list of all CMCD keys.
|
|
1246
|
+
*
|
|
1247
|
+
* @public
|
|
1248
|
+
*/
|
|
1249
|
+
const CMCD_KEYS = /* @__PURE__ */ [
|
|
1250
|
+
...CMCD_V1_KEYS,
|
|
1251
|
+
...CMCD_REQUEST_KEYS,
|
|
1252
|
+
...CMCD_RESPONSE_KEYS,
|
|
1253
|
+
...CMCD_EVENT_KEYS
|
|
1254
|
+
].filter((key, index, arr) => arr.indexOf(key) === index);
|
|
1255
|
+
|
|
1256
|
+
//#endregion
|
|
1257
|
+
//#region src/CMCD_MIME_TYPE.ts
|
|
1258
|
+
/**
|
|
1259
|
+
* CMCD MIME type for event report payloads.
|
|
1260
|
+
*
|
|
1261
|
+
* @public
|
|
1262
|
+
*/
|
|
1263
|
+
const CMCD_MIME_TYPE = "application/cmcd";
|
|
1264
|
+
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region src/CMCD_REQUEST_PROVENANCE.ts
|
|
1267
|
+
/**
|
|
1268
|
+
* The `customData` property where `CmcdReporter.createRequestReport()`
|
|
1269
|
+
* writes session provenance on every request it returns.
|
|
1270
|
+
*
|
|
1271
|
+
* The value is a frozen `CmcdRequestProvenance` record. `sid` names the
|
|
1272
|
+
* session that issued the request. `cid` is the content id at the time the
|
|
1273
|
+
* request was issued. `data` is the request's per-call data as an encoded
|
|
1274
|
+
* CMCD string. `CmcdReporter.recordResponseReceived()` reads the record to
|
|
1275
|
+
* attribute a late response to its session and to rebuild the request-time
|
|
1276
|
+
* report keys.
|
|
1277
|
+
* Spread and `Object.assign` copy the record into ordinary request clones.
|
|
1278
|
+
* `JSON.stringify` and structured clone drop symbol-keyed properties. Before
|
|
1279
|
+
* either operation, read the value and restore it afterward. JSON preserves
|
|
1280
|
+
* the record's fields. Attribution uses the `sid` value rather than object
|
|
1281
|
+
* identity, so a parsed plain-object copy attributes correctly. For a record
|
|
1282
|
+
* naming a `sid` the reporter does not retain, the reporter drops the
|
|
1283
|
+
* response rather than reattributing it.
|
|
1284
|
+
*
|
|
1285
|
+
* The symbol registry provides the symbol, so duplicated copies of this
|
|
1286
|
+
* library in one bundle interoperate. The registry key is stable across
|
|
1287
|
+
* versions.
|
|
1288
|
+
*
|
|
1289
|
+
* @public
|
|
1290
|
+
*/
|
|
1291
|
+
const CMCD_REQUEST_PROVENANCE = /* @__PURE__ */ Symbol.for("@svta/cml-cmcd/request-provenance");
|
|
1292
|
+
|
|
1293
|
+
//#endregion
|
|
1294
|
+
//#region src/CMCD_V1.ts
|
|
1295
|
+
/**
|
|
1296
|
+
* CMCD Version 1
|
|
1297
|
+
*
|
|
1298
|
+
* @public
|
|
1299
|
+
*/
|
|
1300
|
+
const CMCD_V1 = 1;
|
|
1301
|
+
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region src/CmcdEventType.ts
|
|
1304
|
+
/**
|
|
1305
|
+
* CMCD event type 'bc' (bitrate change).
|
|
1306
|
+
*
|
|
1307
|
+
* @public
|
|
1308
|
+
*/
|
|
1309
|
+
const CMCD_EVENT_BITRATE_CHANGE = "bc";
|
|
1310
|
+
/**
|
|
1311
|
+
* CMCD event type 'ps' (play state change).
|
|
1312
|
+
*
|
|
1313
|
+
* @public
|
|
1314
|
+
*/
|
|
1315
|
+
const CMCD_EVENT_PLAY_STATE = "ps";
|
|
1316
|
+
/**
|
|
1317
|
+
* CMCD event type 'pr' (playback rate change).
|
|
1318
|
+
*
|
|
1319
|
+
* @public
|
|
1320
|
+
*/
|
|
1321
|
+
const CMCD_EVENT_PLAYBACK_RATE = "pr";
|
|
1322
|
+
/**
|
|
1323
|
+
* CMCD event type 'e' (error).
|
|
1324
|
+
*
|
|
1325
|
+
* @public
|
|
1326
|
+
*/
|
|
1327
|
+
const CMCD_EVENT_ERROR = "e";
|
|
1328
|
+
/**
|
|
1329
|
+
* CMCD event type 't' (time interval).
|
|
1330
|
+
*
|
|
1331
|
+
* @public
|
|
1332
|
+
*/
|
|
1333
|
+
const CMCD_EVENT_TIME_INTERVAL = "t";
|
|
1334
|
+
/**
|
|
1335
|
+
* CMCD event type 'c' (content ID).
|
|
1336
|
+
*
|
|
1337
|
+
* @public
|
|
1338
|
+
*/
|
|
1339
|
+
const CMCD_EVENT_CONTENT_ID = "c";
|
|
1340
|
+
/**
|
|
1341
|
+
* CMCD event type 'b' (backgrounded mode).
|
|
1342
|
+
*
|
|
1343
|
+
* @public
|
|
1344
|
+
*/
|
|
1345
|
+
const CMCD_EVENT_BACKGROUNDED_MODE = "b";
|
|
1346
|
+
/**
|
|
1347
|
+
* CMCD event type 'm' (mute).
|
|
1348
|
+
*
|
|
1349
|
+
* @public
|
|
1350
|
+
*/
|
|
1351
|
+
const CMCD_EVENT_MUTE = "m";
|
|
1352
|
+
/**
|
|
1353
|
+
* CMCD event type 'um' (unmute).
|
|
1354
|
+
*
|
|
1355
|
+
* @public
|
|
1356
|
+
*/
|
|
1357
|
+
const CMCD_EVENT_UNMUTE = "um";
|
|
1358
|
+
/**
|
|
1359
|
+
* CMCD event type 'pe' (player expand).
|
|
1360
|
+
*
|
|
1361
|
+
* @public
|
|
1362
|
+
*/
|
|
1363
|
+
const CMCD_EVENT_PLAYER_EXPAND = "pe";
|
|
1364
|
+
/**
|
|
1365
|
+
* CMCD event type 'pc' (player collapse).
|
|
1366
|
+
*
|
|
1367
|
+
* @public
|
|
1368
|
+
*/
|
|
1369
|
+
const CMCD_EVENT_PLAYER_COLLAPSE = "pc";
|
|
1370
|
+
/**
|
|
1371
|
+
* CMCD event type 'rr' (response received).
|
|
1372
|
+
*
|
|
1373
|
+
* @public
|
|
957
1374
|
*/
|
|
958
|
-
|
|
959
|
-
const query = toCmcdQuery(cmcd, options);
|
|
960
|
-
if (!query) return url;
|
|
961
|
-
if (REGEX.test(url)) return url.replace(REGEX, query);
|
|
962
|
-
return `${url}${url.includes("?") ? "&" : "?"}${query}`;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
//#endregion
|
|
966
|
-
//#region src/CMCD_DEFAULT_TIME_INTERVAL.ts
|
|
1375
|
+
const CMCD_EVENT_RESPONSE_RECEIVED = "rr";
|
|
967
1376
|
/**
|
|
968
|
-
*
|
|
1377
|
+
* CMCD event type 'as' (ad start).
|
|
969
1378
|
*
|
|
970
1379
|
* @public
|
|
971
1380
|
*/
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
//#endregion
|
|
975
|
-
//#region src/CMCD_KEYS.ts
|
|
1381
|
+
const CMCD_EVENT_AD_START = "as";
|
|
976
1382
|
/**
|
|
977
|
-
*
|
|
1383
|
+
* CMCD event type 'ae' (ad end).
|
|
978
1384
|
*
|
|
979
1385
|
* @public
|
|
980
1386
|
*/
|
|
981
|
-
const
|
|
982
|
-
...CMCD_V1_KEYS,
|
|
983
|
-
...CMCD_REQUEST_KEYS,
|
|
984
|
-
...CMCD_RESPONSE_KEYS,
|
|
985
|
-
...CMCD_EVENT_KEYS
|
|
986
|
-
].filter((key, index, arr) => arr.indexOf(key) === index);
|
|
987
|
-
|
|
988
|
-
//#endregion
|
|
989
|
-
//#region src/CMCD_MIME_TYPE.ts
|
|
1387
|
+
const CMCD_EVENT_AD_END = "ae";
|
|
990
1388
|
/**
|
|
991
|
-
* CMCD
|
|
1389
|
+
* CMCD event type 'abs' (ad break start).
|
|
992
1390
|
*
|
|
993
1391
|
* @public
|
|
994
1392
|
*/
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
//#endregion
|
|
998
|
-
//#region src/CMCD_REQUEST_PROVENANCE.ts
|
|
1393
|
+
const CMCD_EVENT_AD_BREAK_START = "abs";
|
|
999
1394
|
/**
|
|
1000
|
-
*
|
|
1001
|
-
* stamps session provenance on every request it returns.
|
|
1395
|
+
* CMCD event type 'abe' (ad break end).
|
|
1002
1396
|
*
|
|
1003
|
-
*
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
*
|
|
1008
|
-
* the request-time report keys.
|
|
1009
|
-
* Spread and `Object.assign` carry the record through ordinary request
|
|
1010
|
-
* clones, but `JSON.stringify` and structured clone drop symbol-keyed
|
|
1011
|
-
* properties: read the value before such a boundary and restore it
|
|
1012
|
-
* afterward. The record itself survives JSON, and attribution is by the
|
|
1013
|
-
* `sid` value rather than object identity, so a revived plain-object copy
|
|
1014
|
-
* attributes exactly. A record naming a `sid` the reporter does not retain
|
|
1015
|
-
* drops the response rather than relabeling it.
|
|
1397
|
+
* @public
|
|
1398
|
+
*/
|
|
1399
|
+
const CMCD_EVENT_AD_BREAK_END = "abe";
|
|
1400
|
+
/**
|
|
1401
|
+
* CMCD event type 'sk' (skip).
|
|
1016
1402
|
*
|
|
1017
|
-
*
|
|
1018
|
-
|
|
1403
|
+
* @public
|
|
1404
|
+
*/
|
|
1405
|
+
const CMCD_EVENT_SKIP = "sk";
|
|
1406
|
+
/**
|
|
1407
|
+
* CMCD event type 'ce' (custom event).
|
|
1019
1408
|
*
|
|
1020
1409
|
* @public
|
|
1021
1410
|
*/
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
//#endregion
|
|
1025
|
-
//#region src/CMCD_V1.ts
|
|
1411
|
+
const CMCD_EVENT_CUSTOM_EVENT = "ce";
|
|
1026
1412
|
/**
|
|
1027
|
-
* CMCD
|
|
1413
|
+
* CMCD event types for the 'e' key (event mode).
|
|
1414
|
+
*
|
|
1415
|
+
* @enum
|
|
1416
|
+
*
|
|
1417
|
+
* @see {@link https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html#event | CTA-5004-B Event}
|
|
1028
1418
|
*
|
|
1029
1419
|
* @public
|
|
1030
1420
|
*/
|
|
1031
|
-
const
|
|
1421
|
+
const CmcdEventType = {
|
|
1422
|
+
BITRATE_CHANGE: CMCD_EVENT_BITRATE_CHANGE,
|
|
1423
|
+
PLAY_STATE: CMCD_EVENT_PLAY_STATE,
|
|
1424
|
+
PLAYBACK_RATE: CMCD_EVENT_PLAYBACK_RATE,
|
|
1425
|
+
ERROR: CMCD_EVENT_ERROR,
|
|
1426
|
+
TIME_INTERVAL: CMCD_EVENT_TIME_INTERVAL,
|
|
1427
|
+
CONTENT_ID: CMCD_EVENT_CONTENT_ID,
|
|
1428
|
+
BACKGROUNDED_MODE: CMCD_EVENT_BACKGROUNDED_MODE,
|
|
1429
|
+
MUTE: CMCD_EVENT_MUTE,
|
|
1430
|
+
UNMUTE: CMCD_EVENT_UNMUTE,
|
|
1431
|
+
PLAYER_EXPAND: CMCD_EVENT_PLAYER_EXPAND,
|
|
1432
|
+
PLAYER_COLLAPSE: CMCD_EVENT_PLAYER_COLLAPSE,
|
|
1433
|
+
RESPONSE_RECEIVED: CMCD_EVENT_RESPONSE_RECEIVED,
|
|
1434
|
+
AD_START: CMCD_EVENT_AD_START,
|
|
1435
|
+
AD_END: CMCD_EVENT_AD_END,
|
|
1436
|
+
AD_BREAK_START: CMCD_EVENT_AD_BREAK_START,
|
|
1437
|
+
AD_BREAK_END: CMCD_EVENT_AD_BREAK_END,
|
|
1438
|
+
SKIP: CMCD_EVENT_SKIP,
|
|
1439
|
+
CUSTOM_EVENT: CMCD_EVENT_CUSTOM_EVENT
|
|
1440
|
+
};
|
|
1032
1441
|
|
|
1033
1442
|
//#endregion
|
|
1034
1443
|
//#region src/CmcdObjectType.ts
|
|
@@ -1056,7 +1465,7 @@ const CmcdObjectType = {
|
|
|
1056
1465
|
//#endregion
|
|
1057
1466
|
//#region src/CmcdPlayerState.ts
|
|
1058
1467
|
/**
|
|
1059
|
-
* CMCD
|
|
1468
|
+
* CMCD version 2 player states for the 'sta' key.
|
|
1060
1469
|
*
|
|
1061
1470
|
*
|
|
1062
1471
|
* @enum
|
|
@@ -1081,29 +1490,29 @@ const CmcdPlayerState = {
|
|
|
1081
1490
|
//#endregion
|
|
1082
1491
|
//#region src/CmcdRecordedReportMode.ts
|
|
1083
1492
|
/**
|
|
1084
|
-
* CMCD query reporting mode
|
|
1493
|
+
* CMCD query reporting mode: CMCD data transmitted as a `CMCD=` URL
|
|
1085
1494
|
* query parameter.
|
|
1086
1495
|
*
|
|
1087
1496
|
* @public
|
|
1088
1497
|
*/
|
|
1089
1498
|
const CMCD_RECORDED_REPORT_MODE_QUERY = "query";
|
|
1090
1499
|
/**
|
|
1091
|
-
* CMCD header reporting mode
|
|
1500
|
+
* CMCD header reporting mode: CMCD data transmitted as `CMCD-*` HTTP
|
|
1092
1501
|
* headers.
|
|
1093
1502
|
*
|
|
1094
1503
|
* @public
|
|
1095
1504
|
*/
|
|
1096
1505
|
const CMCD_RECORDED_REPORT_MODE_HEADER = "header";
|
|
1097
1506
|
/**
|
|
1098
|
-
* CMCD event reporting mode
|
|
1507
|
+
* CMCD event reporting mode: CMCD data transmitted as a POST body to an
|
|
1099
1508
|
* event-target URL.
|
|
1100
1509
|
*
|
|
1101
1510
|
* @public
|
|
1102
1511
|
*/
|
|
1103
1512
|
const CMCD_RECORDED_REPORT_MODE_EVENT = "event";
|
|
1104
1513
|
/**
|
|
1105
|
-
* Reporting mode
|
|
1106
|
-
*
|
|
1514
|
+
* Reporting mode in which `CmcdReportRecorder` observed a captured
|
|
1515
|
+
* report.
|
|
1107
1516
|
*
|
|
1108
1517
|
* @enum
|
|
1109
1518
|
*
|
|
@@ -1137,7 +1546,7 @@ const CMCD_RECORDED_REQUEST_TYPE_SEGMENT = "segment";
|
|
|
1137
1546
|
*/
|
|
1138
1547
|
const CMCD_RECORDED_REQUEST_TYPE_EVENT = "event";
|
|
1139
1548
|
/**
|
|
1140
|
-
*
|
|
1549
|
+
* Default classification for requests the recorder did not recognize
|
|
1141
1550
|
* as manifest, segment, or event.
|
|
1142
1551
|
*
|
|
1143
1552
|
* @public
|
|
@@ -1179,17 +1588,17 @@ async function toHttpRequest(request) {
|
|
|
1179
1588
|
};
|
|
1180
1589
|
}
|
|
1181
1590
|
/**
|
|
1182
|
-
* Create a transport adapter
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
* safe for the body types CMCD reports use
|
|
1189
|
-
* `Blob`, `ArrayBuffer`, `FormData`,
|
|
1190
|
-
*
|
|
1191
|
-
* a `ReadableStream` as `init.body
|
|
1192
|
-
*
|
|
1591
|
+
* Create a transport adapter for `CmcdReportRecorder` that patches
|
|
1592
|
+
* `globalThis.fetch` to capture requests with CMCD data. The patched
|
|
1593
|
+
* `fetch` normalizes each request to `HttpRequest` (lowercase headers,
|
|
1594
|
+
* body read as a UTF-8 string).
|
|
1595
|
+
*
|
|
1596
|
+
* The patched `fetch` reads each body once with `Request.text()` for
|
|
1597
|
+
* inspection. This read is safe for the body types that CMCD reports use
|
|
1598
|
+
* in practice: `string`, `Blob`, `ArrayBuffer`, `FormData`, and
|
|
1599
|
+
* `URLSearchParams`. The underlying `fetch` can read those bodies again.
|
|
1600
|
+
* Do not pass a `ReadableStream` as `init.body`. The patched `fetch`
|
|
1601
|
+
* consumes the stream, and the underlying `fetch` receives an
|
|
1193
1602
|
* already-disturbed stream.
|
|
1194
1603
|
*
|
|
1195
1604
|
* @public
|
|
@@ -1255,9 +1664,8 @@ async function completeXhrWith(xhr, response) {
|
|
|
1255
1664
|
});
|
|
1256
1665
|
}
|
|
1257
1666
|
/**
|
|
1258
|
-
* Create a transport adapter
|
|
1259
|
-
*
|
|
1260
|
-
* `CmcdReportRecorder`.
|
|
1667
|
+
* Create a transport adapter for `CmcdReportRecorder` that patches
|
|
1668
|
+
* `XMLHttpRequest.prototype` to capture requests with CMCD data.
|
|
1261
1669
|
*
|
|
1262
1670
|
* @public
|
|
1263
1671
|
*/
|
|
@@ -1320,10 +1728,10 @@ function detectReportingMode(url, headers, isEventTarget) {
|
|
|
1320
1728
|
return url.includes(`${CMCD_PARAM}=`) ? "query" : null;
|
|
1321
1729
|
}
|
|
1322
1730
|
/**
|
|
1323
|
-
* Test helper that records CMCD
|
|
1324
|
-
* transports for
|
|
1325
|
-
*
|
|
1326
|
-
* tests are identical regardless of which transport the player uses.
|
|
1731
|
+
* Test helper that records CMCD reports from the XHR and fetch
|
|
1732
|
+
* transports for assertions in e2e tests. The recorder normalizes each
|
|
1733
|
+
* captured request to {@link @svta/cml-utils!HttpRequest | HttpRequest},
|
|
1734
|
+
* so tests are identical regardless of which transport the player uses.
|
|
1327
1735
|
*
|
|
1328
1736
|
* @example
|
|
1329
1737
|
* {@includeCode ../test/CmcdReportRecorder.test.ts#example}
|
|
@@ -1391,11 +1799,11 @@ var CmcdReportRecorder = class {
|
|
|
1391
1799
|
});
|
|
1392
1800
|
}
|
|
1393
1801
|
/**
|
|
1394
|
-
* Install transport patches and
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1802
|
+
* Install transport patches and start recording CMCD reports.
|
|
1803
|
+
* If the recorder is already attached, this call does nothing. The
|
|
1804
|
+
* recorder keeps the previous options and silently ignores the new
|
|
1805
|
+
* `options` argument. To re-attach with different options, call
|
|
1806
|
+
* `detach()` first.
|
|
1399
1807
|
*
|
|
1400
1808
|
* @public
|
|
1401
1809
|
*/
|
|
@@ -1436,7 +1844,7 @@ var CmcdReportRecorder = class {
|
|
|
1436
1844
|
this.#reports = [];
|
|
1437
1845
|
}
|
|
1438
1846
|
/**
|
|
1439
|
-
* Return a
|
|
1847
|
+
* Return a copy of all recorded reports.
|
|
1440
1848
|
*
|
|
1441
1849
|
* @public
|
|
1442
1850
|
*/
|
|
@@ -1445,8 +1853,8 @@ var CmcdReportRecorder = class {
|
|
|
1445
1853
|
}
|
|
1446
1854
|
/**
|
|
1447
1855
|
* Wait until at least `count` reports of any type are recorded.
|
|
1448
|
-
* Resolves with all matching reports
|
|
1449
|
-
* on timeout. `count` defaults to 1
|
|
1856
|
+
* Resolves with all matching reports. Rejects with a diagnostic error
|
|
1857
|
+
* on timeout. `count` defaults to 1. `timeout` defaults to the
|
|
1450
1858
|
* recorder's `waitTimeout` option (default 15000 ms).
|
|
1451
1859
|
*
|
|
1452
1860
|
* @public
|
|
@@ -1456,8 +1864,8 @@ var CmcdReportRecorder = class {
|
|
|
1456
1864
|
}
|
|
1457
1865
|
/**
|
|
1458
1866
|
* Wait until at least `count` manifest reports are recorded.
|
|
1459
|
-
* Resolves with all matching reports
|
|
1460
|
-
* on timeout. `count` defaults to 1
|
|
1867
|
+
* Resolves with all matching reports. Rejects with a diagnostic error
|
|
1868
|
+
* on timeout. `count` defaults to 1. `timeout` defaults to the
|
|
1461
1869
|
* recorder's `waitTimeout` option (default 15000 ms).
|
|
1462
1870
|
*
|
|
1463
1871
|
* @public
|
|
@@ -1467,8 +1875,8 @@ var CmcdReportRecorder = class {
|
|
|
1467
1875
|
}
|
|
1468
1876
|
/**
|
|
1469
1877
|
* Wait until at least `count` segment reports are recorded.
|
|
1470
|
-
* Resolves with all matching reports
|
|
1471
|
-
* on timeout. `count` defaults to 1
|
|
1878
|
+
* Resolves with all matching reports. Rejects with a diagnostic error
|
|
1879
|
+
* on timeout. `count` defaults to 1. `timeout` defaults to the
|
|
1472
1880
|
* recorder's `waitTimeout` option (default 15000 ms).
|
|
1473
1881
|
*
|
|
1474
1882
|
* @public
|
|
@@ -1478,8 +1886,8 @@ var CmcdReportRecorder = class {
|
|
|
1478
1886
|
}
|
|
1479
1887
|
/**
|
|
1480
1888
|
* Wait until at least `count` event-mode reports are recorded.
|
|
1481
|
-
* Resolves with all matching reports
|
|
1482
|
-
* on timeout. `count` defaults to 1
|
|
1889
|
+
* Resolves with all matching reports. Rejects with a diagnostic error
|
|
1890
|
+
* on timeout. `count` defaults to 1. `timeout` defaults to the
|
|
1483
1891
|
* recorder's `waitTimeout` option (default 15000 ms).
|
|
1484
1892
|
*
|
|
1485
1893
|
* @public
|
|
@@ -1489,6 +1897,39 @@ var CmcdReportRecorder = class {
|
|
|
1489
1897
|
}
|
|
1490
1898
|
};
|
|
1491
1899
|
|
|
1900
|
+
//#endregion
|
|
1901
|
+
//#region src/CMCD_STATE_EVENT_FIELDS.ts
|
|
1902
|
+
/**
|
|
1903
|
+
* Maps each state-change event type to the persistent field whose value
|
|
1904
|
+
* the event signals.
|
|
1905
|
+
*
|
|
1906
|
+
* The key table (`CMCD_KEY_SPECS`) marks the field as always sent on its
|
|
1907
|
+
* event, so the key filter of `prepareCmcdData` does not remove it.
|
|
1908
|
+
* `CmcdReporter` deduplicates events against the field value. It drops an
|
|
1909
|
+
* event whose field has no value and restores the field if a transform
|
|
1910
|
+
* removes it.
|
|
1911
|
+
*
|
|
1912
|
+
* Sending the field with each state-change event is a library choice.
|
|
1913
|
+
* The exception is the exit from backgrounded mode, which CTA-5004-B
|
|
1914
|
+
* defines as a `b` event without `bg`. CTA-5004-B requires the field only
|
|
1915
|
+
* on the `ps` event, which MUST carry `sta`. CTA-5004-B states no such rule
|
|
1916
|
+
* for `pr`, `c`, or `bc`. `validateCmcdStructure` follows CTA-5004-B and
|
|
1917
|
+
* does not read this map.
|
|
1918
|
+
*
|
|
1919
|
+
* Iteration order matters: `CmcdReporter.update()` records state-change
|
|
1920
|
+
* events in map order when several tracked fields change in one call.
|
|
1921
|
+
* Do not reorder entries without auditing reporter behavior.
|
|
1922
|
+
*
|
|
1923
|
+
* @internal
|
|
1924
|
+
*/
|
|
1925
|
+
const CMCD_STATE_EVENT_FIELDS = /* @__PURE__ */ new Map([
|
|
1926
|
+
[CMCD_EVENT_PLAY_STATE, "sta"],
|
|
1927
|
+
[CMCD_EVENT_PLAYBACK_RATE, "pr"],
|
|
1928
|
+
[CMCD_EVENT_CONTENT_ID, "cid"],
|
|
1929
|
+
[CMCD_EVENT_BACKGROUNDED_MODE, "bg"],
|
|
1930
|
+
[CMCD_EVENT_BITRATE_CHANGE, "br"]
|
|
1931
|
+
]);
|
|
1932
|
+
|
|
1492
1933
|
//#endregion
|
|
1493
1934
|
//#region src/CmcdTransmissionMode.ts
|
|
1494
1935
|
/**
|
|
@@ -1527,24 +1968,26 @@ const CmcdTransmissionMode = {
|
|
|
1527
1968
|
//#endregion
|
|
1528
1969
|
//#region src/upConvertToV2.ts
|
|
1529
1970
|
/**
|
|
1530
|
-
* Up-convert
|
|
1971
|
+
* Up-convert version 1 CMCD data to version 2.
|
|
1531
1972
|
*
|
|
1532
|
-
* - Wraps
|
|
1973
|
+
* - Wraps scalar values in arrays for inner-list keys.
|
|
1533
1974
|
* - Wraps `nor` string in an array.
|
|
1534
1975
|
*
|
|
1535
|
-
*
|
|
1976
|
+
* The function returns version 2 data (has `v: 2`) unchanged. An inner list
|
|
1977
|
+
* with parameters, which `decodeCmcd` returns as an `SfItem` that wraps the
|
|
1978
|
+
* list, also stays unchanged.
|
|
1536
1979
|
*
|
|
1537
1980
|
* @internal
|
|
1538
1981
|
*/
|
|
1539
1982
|
function upConvertToV2(obj) {
|
|
1540
|
-
if (obj["v"] === CMCD_V2) return obj;
|
|
1983
|
+
if (toBareValue(obj["v"]) === CMCD_V2) return obj;
|
|
1541
1984
|
const result = {};
|
|
1542
1985
|
for (const [key, value] of Object.entries(obj)) {
|
|
1543
1986
|
if (value == null) {
|
|
1544
1987
|
result[key] = value;
|
|
1545
1988
|
continue;
|
|
1546
1989
|
}
|
|
1547
|
-
if (CMCD_INNER_LIST_KEYS.has(key) && !Array.isArray(value)) result[key] = [value];
|
|
1990
|
+
if (CMCD_INNER_LIST_KEYS.has(key) && !Array.isArray(toBareValue(value))) result[key] = [value];
|
|
1548
1991
|
else if (key === "nor" && typeof value === "string") result[key] = [value];
|
|
1549
1992
|
else result[key] = value;
|
|
1550
1993
|
}
|
|
@@ -1581,12 +2024,11 @@ function createEncodingOptions(reportingMode, config, baseUrl) {
|
|
|
1581
2024
|
};
|
|
1582
2025
|
}
|
|
1583
2026
|
/**
|
|
1584
|
-
* Deep equality for CmcdObjectTypeList (used for `br`
|
|
2027
|
+
* Deep equality for CmcdObjectTypeList (used for `br` deduplication).
|
|
1585
2028
|
*
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1589
|
-
* safer failure mode.
|
|
2029
|
+
* Arrays with the same elements in different positions count as different.
|
|
2030
|
+
* Players that construct `br` consistently get correct deduplication.
|
|
2031
|
+
* Reordered arrays produce extra events, which is the safer failure.
|
|
1590
2032
|
*/
|
|
1591
2033
|
function cmcdObjectTypeListEqual(a, b) {
|
|
1592
2034
|
if (a === b) return true;
|
|
@@ -1595,10 +2037,9 @@ function cmcdObjectTypeListEqual(a, b) {
|
|
|
1595
2037
|
const ai = a[i];
|
|
1596
2038
|
const bi = b[i];
|
|
1597
2039
|
if (ai === bi) continue;
|
|
1598
|
-
if (typeof ai === "number"
|
|
1599
|
-
|
|
1600
|
-
const
|
|
1601
|
-
const bp = bi.params;
|
|
2040
|
+
if ((typeof ai === "number" ? ai : ai.value) !== (typeof bi === "number" ? bi : bi.value)) return false;
|
|
2041
|
+
const ap = typeof ai === "number" ? void 0 : ai.params;
|
|
2042
|
+
const bp = typeof bi === "number" ? void 0 : bi.params;
|
|
1602
2043
|
const ak = ap && Object.keys(ap)[0];
|
|
1603
2044
|
const bk = bp && Object.keys(bp)[0];
|
|
1604
2045
|
if (ak !== bk) return false;
|
|
@@ -1606,6 +2047,18 @@ function cmcdObjectTypeListEqual(a, b) {
|
|
|
1606
2047
|
}
|
|
1607
2048
|
return true;
|
|
1608
2049
|
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Equality for `br` deduplication. A `br` value can be an `SfItem` that
|
|
2052
|
+
* wraps the list, as `decodeCmcd` returns for an inner list with
|
|
2053
|
+
* parameters. Two values are equal when their lists are equal. The
|
|
2054
|
+
* parameters of the list are not compared.
|
|
2055
|
+
*/
|
|
2056
|
+
function brEqual(a, b) {
|
|
2057
|
+
const listA = toBareValue(a);
|
|
2058
|
+
const listB = toBareValue(b);
|
|
2059
|
+
if (!Array.isArray(listA) || !Array.isArray(listB)) return Object.is(a, b);
|
|
2060
|
+
return cmcdObjectTypeListEqual(listA, listB);
|
|
2061
|
+
}
|
|
1609
2062
|
const equal = Object.is;
|
|
1610
2063
|
const identity = (v) => v;
|
|
1611
2064
|
/**
|
|
@@ -1616,8 +2069,8 @@ const STATE_FIELDS = /* @__PURE__ */ Array.from(CMCD_STATE_EVENT_FIELDS, ([event
|
|
|
1616
2069
|
if (field === "br") return {
|
|
1617
2070
|
event,
|
|
1618
2071
|
field,
|
|
1619
|
-
equal:
|
|
1620
|
-
snapshot: (v) => v.slice()
|
|
2072
|
+
equal: brEqual,
|
|
2073
|
+
snapshot: (v) => Array.isArray(v) ? v.slice() : copyItemValue(v)
|
|
1621
2074
|
};
|
|
1622
2075
|
return {
|
|
1623
2076
|
event,
|
|
@@ -1627,57 +2080,48 @@ const STATE_FIELDS = /* @__PURE__ */ Array.from(CMCD_STATE_EVENT_FIELDS, ([event
|
|
|
1627
2080
|
};
|
|
1628
2081
|
});
|
|
1629
2082
|
const STATE_FIELDS_BY_EVENT = /* @__PURE__ */ new Map(/* @__PURE__ */ STATE_FIELDS.map((e) => [e.event, e]));
|
|
2083
|
+
function buildRequiredEventKeys() {
|
|
2084
|
+
return new Map([
|
|
2085
|
+
...CMCD_STATE_EVENT_FIELDS,
|
|
2086
|
+
[CMCD_EVENT_CUSTOM_EVENT, "cen"],
|
|
2087
|
+
[CMCD_EVENT_ERROR, "ec"],
|
|
2088
|
+
[CMCD_EVENT_RESPONSE_RECEIVED, "url"]
|
|
2089
|
+
]);
|
|
2090
|
+
}
|
|
1630
2091
|
/**
|
|
1631
|
-
* Maps each event type to the key
|
|
1632
|
-
* Built from the state-change table plus the three
|
|
1633
|
-
* required key
|
|
1634
|
-
*/
|
|
1635
|
-
const CMCD_REQUIRED_EVENT_KEYS = /* @__PURE__ */ new Map([
|
|
1636
|
-
...CMCD_STATE_EVENT_FIELDS,
|
|
1637
|
-
[CMCD_EVENT_CUSTOM_EVENT, "cen"],
|
|
1638
|
-
[CMCD_EVENT_ERROR, "ec"],
|
|
1639
|
-
[CMCD_EVENT_RESPONSE_RECEIVED, "url"]
|
|
1640
|
-
]);
|
|
1641
|
-
/**
|
|
1642
|
-
* Whether a required key's value will survive report preparation.
|
|
1643
|
-
*
|
|
1644
|
-
* This is `isValid` minus its `false` exclusion, plus an empty-array check.
|
|
1645
|
-
* `false` must count as usable because `bg: false` is a legitimate value on a
|
|
1646
|
-
* backgrounded-mode event, which the encoder emits as `?0`; treating it as
|
|
1647
|
-
* unusable would let restoration silently revert a transform that cleared it.
|
|
1648
|
-
* Empty strings, empty lists and non-finite numbers are dropped downstream, so
|
|
1649
|
-
* a transform substituting one leaves the report short a required key.
|
|
2092
|
+
* Maps each event type to the key that a report of that type must keep,
|
|
2093
|
+
* beyond `e` and `ts`. Built from the state-change table plus the three
|
|
2094
|
+
* event types whose required key is event data, not player state.
|
|
1650
2095
|
*/
|
|
1651
|
-
|
|
1652
|
-
if (value == null || value === "") return false;
|
|
1653
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
1654
|
-
return !Array.isArray(value) || value.length > 0;
|
|
1655
|
-
}
|
|
2096
|
+
const CMCD_REQUIRED_EVENT_KEYS = /* @__PURE__ */ buildRequiredEventKeys();
|
|
1656
2097
|
/**
|
|
1657
|
-
* Copies
|
|
2098
|
+
* Copies a value with the `SfItem` structure, including its `params` record
|
|
2099
|
+
* and the list of an `SfItem` that wraps a list.
|
|
1658
2100
|
*
|
|
1659
|
-
* The
|
|
2101
|
+
* The copy keeps the prototype because `prepareCmcdData`, the formatter map,
|
|
1660
2102
|
* validation, and the structured-field encoder all branch on
|
|
1661
2103
|
* `instanceof SfItem`. A plain spread (and `structuredClone`) would return a
|
|
1662
|
-
* prototype-less object and silently change
|
|
2104
|
+
* prototype-less object and silently change the encoded output.
|
|
1663
2105
|
*/
|
|
1664
2106
|
function copyItemValue(value) {
|
|
1665
2107
|
if (value === null || typeof value !== "object") return value;
|
|
1666
2108
|
const copy = Object.assign(Object.create(Object.getPrototypeOf(value)), value);
|
|
1667
2109
|
if (copy.params !== null && typeof copy.params === "object") copy.params = { ...copy.params };
|
|
2110
|
+
if (Array.isArray(copy.value)) copy.value = copy.value.map(copyItemValue);
|
|
1668
2111
|
return copy;
|
|
1669
2112
|
}
|
|
1670
2113
|
/**
|
|
1671
|
-
* Copies the nested values of a report in place
|
|
1672
|
-
* the reporter's
|
|
1673
|
-
*
|
|
2114
|
+
* Copies the nested values of a report in place. A transform cannot mutate
|
|
2115
|
+
* the reporter's data store, or another target's report for the same event,
|
|
2116
|
+
* through an array or an `SfItem` it received.
|
|
1674
2117
|
*
|
|
1675
|
-
*
|
|
2118
|
+
* The copy is complete for the CMCD value space. `CmcdValue` and
|
|
1676
2119
|
* `CmcdCustomValue` admit only primitives, `SfItem<primitive>`, and arrays of
|
|
1677
|
-
* those
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1680
|
-
* player-facing view.
|
|
2120
|
+
* those. `decodeCmcd` also returns an `SfItem` that wraps a list, and the
|
|
2121
|
+
* copy includes that list. `SfItem.params` is a flat record. The reporter calls this function
|
|
2122
|
+
* where a transform is configured, at session end on the ended session's
|
|
2123
|
+
* store, and on the request's stored player-facing view. The session-end
|
|
2124
|
+
* copy detaches the frozen snapshot from caller-held references.
|
|
1681
2125
|
*/
|
|
1682
2126
|
function copyReportValues(data) {
|
|
1683
2127
|
const record = data;
|
|
@@ -1692,14 +2136,13 @@ function copyReportValues(data) {
|
|
|
1692
2136
|
return data;
|
|
1693
2137
|
}
|
|
1694
2138
|
/**
|
|
1695
|
-
* Decodes the per-call data snapshot a provenance record
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1699
|
-
*
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
1702
|
-
* over the session's data alone.
|
|
2139
|
+
* Decodes the per-call data snapshot of a provenance record into new,
|
|
2140
|
+
* encodable report data. Tokens decode as `SfToken` and items with params
|
|
2141
|
+
* as `SfItem` (`useSymbol: false`), so every value re-encodes with its
|
|
2142
|
+
* original type. Only `recordResponseReceived()` calls this function.
|
|
2143
|
+
* Returns an empty object when the record is absent, has no snapshot, or
|
|
2144
|
+
* the snapshot does not parse (a value this reporter did not write). The
|
|
2145
|
+
* response then reports its derived keys over the session's data alone.
|
|
1703
2146
|
*/
|
|
1704
2147
|
function decodeSnapshot(provenance) {
|
|
1705
2148
|
const encoded = provenance !== null && typeof provenance === "object" ? provenance.data : void 0;
|
|
@@ -1711,10 +2154,10 @@ function decodeSnapshot(provenance) {
|
|
|
1711
2154
|
}
|
|
1712
2155
|
}
|
|
1713
2156
|
/**
|
|
1714
|
-
*
|
|
1715
|
-
* the `cid`
|
|
1716
|
-
* change
|
|
1717
|
-
*
|
|
2157
|
+
* Creates a session's frozen base provenance record: the issuing `sid`, and
|
|
2158
|
+
* the `cid` that is current at creation time. `update()` creates a new record
|
|
2159
|
+
* on every `cid` change. Requests issued before a mid-session content change
|
|
2160
|
+
* keep their original `cid`, while later requests get the new one.
|
|
1718
2161
|
*/
|
|
1719
2162
|
function mintProvenance(sid, cid) {
|
|
1720
2163
|
return Object.freeze(typeof cid === "string" && cid ? {
|
|
@@ -1753,12 +2196,12 @@ function createCmcdReporterConfig(config) {
|
|
|
1753
2196
|
/**
|
|
1754
2197
|
* The CMCD reporter.
|
|
1755
2198
|
*
|
|
1756
|
-
* `C` describes the player's own `customData`, which the reporter passes
|
|
1757
|
-
*
|
|
1758
|
-
*
|
|
1759
|
-
*
|
|
2199
|
+
* `C` describes the player's own `customData`, which the reporter passes to
|
|
2200
|
+
* each configured `transform`. TypeScript infers `C` from the configuration,
|
|
2201
|
+
* so an annotation on one `transform` types the request in every other
|
|
2202
|
+
* `transform`. The default leaves `customData` values `unknown`.
|
|
1760
2203
|
*
|
|
1761
|
-
* @typeParam C - The
|
|
2204
|
+
* @typeParam C - The type of the player's `customData`. Defaults to
|
|
1762
2205
|
* `Record<string, unknown>`.
|
|
1763
2206
|
*
|
|
1764
2207
|
* @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}
|
|
@@ -1770,7 +2213,7 @@ var CmcdReporter = class {
|
|
|
1770
2213
|
* Creates a new CMCD reporter.
|
|
1771
2214
|
*
|
|
1772
2215
|
* @param config - The configuration for the CMCD reporter.
|
|
1773
|
-
* @param requester - The function
|
|
2216
|
+
* @param requester - The function that sends the request.
|
|
1774
2217
|
* The default is a simple wrapper around the
|
|
1775
2218
|
* native `fetch` API.
|
|
1776
2219
|
*/
|
|
@@ -1788,8 +2231,8 @@ var CmcdReporter = class {
|
|
|
1788
2231
|
this.requester = requester;
|
|
1789
2232
|
}
|
|
1790
2233
|
/**
|
|
1791
|
-
* Creates the state for a new session:
|
|
1792
|
-
*
|
|
2234
|
+
* Creates the state for a new session: initial counters, flags, queues, and
|
|
2235
|
+
* deduplication baseline for every configured target.
|
|
1793
2236
|
*/
|
|
1794
2237
|
createSession(sid, data) {
|
|
1795
2238
|
const eventTargets = /* @__PURE__ */ new Map();
|
|
@@ -1804,7 +2247,7 @@ var CmcdReporter = class {
|
|
|
1804
2247
|
provenance: mintProvenance(sid, data.cid),
|
|
1805
2248
|
data,
|
|
1806
2249
|
msd: NaN,
|
|
1807
|
-
lastEmitted: {},
|
|
2250
|
+
lastEmitted: toBareValue(data.bg) === true ? {} : { bg: false },
|
|
1808
2251
|
eventTargets,
|
|
1809
2252
|
requestTarget: {
|
|
1810
2253
|
sn: 0,
|
|
@@ -1813,11 +2256,12 @@ var CmcdReporter = class {
|
|
|
1813
2256
|
};
|
|
1814
2257
|
}
|
|
1815
2258
|
/**
|
|
1816
|
-
* Starts the CMCD reporter.
|
|
2259
|
+
* Starts the CMCD reporter. The player calls this method when it enables the
|
|
2260
|
+
* reporter.
|
|
1817
2261
|
*
|
|
1818
|
-
*
|
|
1819
|
-
* before the first interval elapses.
|
|
1820
|
-
*
|
|
2262
|
+
* This method fires an initial time-interval event immediately (synchronously),
|
|
2263
|
+
* before the first interval elapses. Populate the CMCD data (sid, cid, and
|
|
2264
|
+
* others) before calling start().
|
|
1821
2265
|
*/
|
|
1822
2266
|
start() {
|
|
1823
2267
|
this.started = true;
|
|
@@ -1836,9 +2280,9 @@ var CmcdReporter = class {
|
|
|
1836
2280
|
if (failure) throw failure.error;
|
|
1837
2281
|
}
|
|
1838
2282
|
/**
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1841
|
-
* whichever session is current when
|
|
2283
|
+
* Starts the time-interval timer for an event target when its config
|
|
2284
|
+
* requires one. Returns whether a timer started. The callback reports into
|
|
2285
|
+
* whichever session is current when the timer fires.
|
|
1842
2286
|
*/
|
|
1843
2287
|
armInterval(config) {
|
|
1844
2288
|
if (config.interval === 0 || !config.events.includes(CMCD_EVENT_TIME_INTERVAL)) return false;
|
|
@@ -1853,7 +2297,8 @@ var CmcdReporter = class {
|
|
|
1853
2297
|
return true;
|
|
1854
2298
|
}
|
|
1855
2299
|
/**
|
|
1856
|
-
* Stops the CMCD reporter.
|
|
2300
|
+
* Stops the CMCD reporter. The player calls this method when it disables the
|
|
2301
|
+
* reporter.
|
|
1857
2302
|
*
|
|
1858
2303
|
* @param flush - Whether to flush the event targets.
|
|
1859
2304
|
*/
|
|
@@ -1863,8 +2308,8 @@ var CmcdReporter = class {
|
|
|
1863
2308
|
for (const config of this.config.eventTargets) this.disarmInterval(config);
|
|
1864
2309
|
}
|
|
1865
2310
|
/**
|
|
1866
|
-
*
|
|
1867
|
-
*
|
|
2311
|
+
* Sends all outstanding event reports, regardless of the batch size or
|
|
2312
|
+
* interval. Use this method when the player is destroyed or a playback
|
|
1868
2313
|
* session ends.
|
|
1869
2314
|
*/
|
|
1870
2315
|
flush() {
|
|
@@ -1873,40 +2318,45 @@ var CmcdReporter = class {
|
|
|
1873
2318
|
/**
|
|
1874
2319
|
* Updates the CMCD data.
|
|
1875
2320
|
*
|
|
1876
|
-
*
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
1879
|
-
*
|
|
1880
|
-
*
|
|
1881
|
-
*
|
|
1882
|
-
*
|
|
1883
|
-
* across the `sid` boundary.
|
|
2321
|
+
* The player calls this method when data changes. The tracked state fields
|
|
2322
|
+
* are `sta`, `pr`, `cid`, `bg`, and `br`. If a new value for one of them
|
|
2323
|
+
* differs from the last reported value for that field, the reporter fires
|
|
2324
|
+
* the matching state-change event. The comparison uses the last reported
|
|
2325
|
+
* value, not the previous persisted value. The first state-change event in
|
|
2326
|
+
* a new session therefore always fires, even when the persisted value did
|
|
2327
|
+
* not change across the `sid` boundary.
|
|
1884
2328
|
*
|
|
1885
|
-
*
|
|
1886
|
-
* `
|
|
1887
|
-
*
|
|
2329
|
+
* The `bg` field is an exception. A session that does not start with
|
|
2330
|
+
* `bg: true` begins with `false` as its last reported `bg` value.
|
|
2331
|
+
* `bg: false` therefore fires `BACKGROUNDED_MODE` only if the session
|
|
2332
|
+
* started with `bg: true` or reported `bg: true`. CTA-5004-B defines a
|
|
2333
|
+
* `b` event without `bg` as the exit from backgrounded mode, so the exit
|
|
2334
|
+
* report does not carry `bg`. A target transform can add `bg: false` to
|
|
2335
|
+
* the exit report. The encoder writes that value as `?0`.
|
|
1888
2336
|
*
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
* state field in the same `update()` call, or persist them via earlier
|
|
1893
|
-
* `update()` calls — auto-fired events emit whatever is currently in
|
|
1894
|
-
* the persistent data store. This is also how to keep `TIME_INTERVAL`
|
|
1895
|
-
* reports useful — those events draw from the persistent data store
|
|
1896
|
-
* with no caller hook for per-event data, so fields the player wants
|
|
1897
|
-
* in periodic reports must be kept fresh here.
|
|
2337
|
+
* Multi-field updates fire the events in this order: `sta`, `pr`, `cid`,
|
|
2338
|
+
* `bg`, `br`. The order of keys in the input object does not affect the
|
|
2339
|
+
* firing order.
|
|
1898
2340
|
*
|
|
1899
|
-
* A
|
|
2341
|
+
* A state-change event can include context fields, for example `bl`, `mtp`,
|
|
2342
|
+
* `pt`, `ltc`. Those fields must be in the data store before the state field
|
|
2343
|
+
* changes. Either pass them with the state field in the same `update()`
|
|
2344
|
+
* call, or persist them with earlier `update()` calls. An auto-fired event
|
|
2345
|
+
* reports the current content of the data store. `TIME_INTERVAL` events also
|
|
2346
|
+
* read the data store, and the caller cannot add per-call data to them. The
|
|
2347
|
+
* player must therefore update the fields it wants in periodic reports with
|
|
2348
|
+
* this method.
|
|
1900
2349
|
*
|
|
1901
|
-
* `sid`
|
|
1902
|
-
*
|
|
2350
|
+
* A `sid` change resets the deduplication baseline.
|
|
2351
|
+
*
|
|
2352
|
+
* `sid` and `msd` are session-owned. The reporter tracks them itself and
|
|
2353
|
+
* writes them into every outgoing report, so this method is the only way
|
|
1903
2354
|
* to change them. A per-call value on {@link CmcdReporter.recordEvent},
|
|
1904
2355
|
* {@link CmcdReporter.createRequestReport}, or
|
|
1905
|
-
* {@link CmcdReporter.recordResponseReceived} has no effect
|
|
1906
|
-
* attribution is by the provenance record alone.
|
|
2356
|
+
* {@link CmcdReporter.recordResponseReceived} has no effect.
|
|
1907
2357
|
* `msd` must be a finite number of milliseconds between `0` and
|
|
1908
|
-
* `999_999_999_999_999` (the RFC 8941 integer maximum)
|
|
1909
|
-
* the nearest integer
|
|
2358
|
+
* `999_999_999_999_999` (the RFC 8941 integer maximum). The reporter
|
|
2359
|
+
* rounds `msd` to the nearest integer and ignores invalid values.
|
|
1910
2360
|
*
|
|
1911
2361
|
* @param data - The data to update.
|
|
1912
2362
|
*/
|
|
@@ -1928,19 +2378,20 @@ var CmcdReporter = class {
|
|
|
1928
2378
|
for (const entry of STATE_FIELDS) if (entry.field in data && !entry.equal(session.data[entry.field], session.lastEmitted[entry.field])) this.recordEvent(entry.event);
|
|
1929
2379
|
}
|
|
1930
2380
|
/**
|
|
1931
|
-
* Starts a new session
|
|
1932
|
-
*
|
|
1933
|
-
* the oldest
|
|
2381
|
+
* Starts a new session. The reporter retains the current session for late
|
|
2382
|
+
* responses and creates a new current session. It then sends the ended
|
|
2383
|
+
* sessions' queued reports and removes the oldest sessions beyond the
|
|
2384
|
+
* configured retention.
|
|
1934
2385
|
*
|
|
1935
|
-
* The
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
*
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
2386
|
+
* The ended session's snapshot is frozen: the reporter detaches its value
|
|
2387
|
+
* graph, so nested values from earlier `update()` calls cannot mutate the
|
|
2388
|
+
* snapshot. The new session starts from a shallow copy of that graph, which
|
|
2389
|
+
* preserves the data store. All fields, including `cid`, `br`, and custom
|
|
2390
|
+
* keys, survive a `sid` change as before. CTA-5004-B expects a `sid` to be
|
|
2391
|
+
* unique per playback session, so a reused `sid` replaces the retained
|
|
2392
|
+
* session with that `sid`. The replaced session's remaining state is lost,
|
|
2393
|
+
* as if removed, and the reporter attributes its late responses to the
|
|
2394
|
+
* replacement.
|
|
1944
2395
|
*/
|
|
1945
2396
|
startSession(sid) {
|
|
1946
2397
|
this.session.data = copyReportValues({ ...this.session.data });
|
|
@@ -1957,59 +2408,57 @@ var CmcdReporter = class {
|
|
|
1957
2408
|
}
|
|
1958
2409
|
}
|
|
1959
2410
|
/**
|
|
1960
|
-
* Records an event.
|
|
2411
|
+
* Records an event. The player calls this method when an event occurs.
|
|
1961
2412
|
*
|
|
1962
2413
|
* For state-change events (`PLAY_STATE`, `PLAYBACK_RATE`, `CONTENT_ID`,
|
|
1963
2414
|
* `BACKGROUNDED_MODE`, `BITRATE_CHANGE`), this method:
|
|
1964
|
-
* 1.
|
|
1965
|
-
*
|
|
1966
|
-
* 2.
|
|
1967
|
-
*
|
|
1968
|
-
*
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
2415
|
+
* 1. Writes the state field from `data` (if present) into the reporter's
|
|
2416
|
+
* data store, as `update()` would.
|
|
2417
|
+
* 2. Discards the event if the state field has no value after that write
|
|
2418
|
+
* (never set, or cleared with `update({ field: undefined })`).
|
|
2419
|
+
* 3. Suppresses the event if the field's current value equals the
|
|
2420
|
+
* last reported value (no state transition). {@link CmcdReporter.update}
|
|
2421
|
+
* describes the `bg` value that a new session starts with.
|
|
1971
2422
|
*
|
|
1972
|
-
*
|
|
2423
|
+
* The reporter always records all other event types.
|
|
1973
2424
|
*
|
|
1974
|
-
* For state-change events, prefer {@link CmcdReporter.update}
|
|
1975
|
-
*
|
|
1976
|
-
* `update({ sta: 'p', bl: [3000], mtp: [8500] })`.
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
1979
|
-
*
|
|
2425
|
+
* For state-change events, prefer {@link CmcdReporter.update}, also for
|
|
2426
|
+
* context fields in a combined call like
|
|
2427
|
+
* `update({ sta: 'p', bl: [3000], mtp: [8500] })`. After `update()`
|
|
2428
|
+
* auto-fires a state-change event, deduplication suppresses a
|
|
2429
|
+
* `recordEvent()` call for the same event, and that call's `data` is
|
|
2430
|
+
* silently lost.
|
|
1980
2431
|
*
|
|
1981
|
-
*
|
|
1982
|
-
*
|
|
1983
|
-
*
|
|
1984
|
-
* `
|
|
1985
|
-
*
|
|
1986
|
-
* per-response fields automatically.
|
|
2432
|
+
* Call `recordEvent()` directly for events whose data comes with the event
|
|
2433
|
+
* call. Examples: `CUSTOM_EVENT` with `cen`, `ERROR` with `ec`, ad lifecycle
|
|
2434
|
+
* events, `MUTE`/`UNMUTE`, `PLAYER_EXPAND`/`PLAYER_COLLAPSE`, `SKIP`. For
|
|
2435
|
+
* `RESPONSE_RECEIVED`, prefer {@link CmcdReporter.recordResponseReceived},
|
|
2436
|
+
* which derives the per-response fields automatically.
|
|
1987
2437
|
*
|
|
1988
2438
|
* @param type - The type of event to record.
|
|
1989
|
-
* @param data - Additional data to record with the event. This data
|
|
1990
|
-
* only
|
|
1991
|
-
*
|
|
1992
|
-
*
|
|
1993
|
-
* (`sid`, `msd`) supplied here
|
|
1994
|
-
* stamps its own values.
|
|
2439
|
+
* @param data - Additional data to record with the event. This data applies
|
|
2440
|
+
* only to this event report. The exception is the state field
|
|
2441
|
+
* of a state-change event, which the reporter also writes into
|
|
2442
|
+
* its data store. The reporter ignores session-owned keys
|
|
2443
|
+
* (`sid`, `msd`) supplied here and writes its own values.
|
|
1995
2444
|
*/
|
|
1996
2445
|
recordEvent(type, data = {}) {
|
|
1997
2446
|
this.emitEvent(this.session, type, data);
|
|
1998
2447
|
}
|
|
1999
2448
|
/**
|
|
2000
|
-
* Records an event
|
|
2001
|
-
*
|
|
2449
|
+
* Records an event for every configured target of a session. State-change
|
|
2450
|
+
* deduplication runs once, before the per-target loop.
|
|
2002
2451
|
*
|
|
2003
2452
|
* @param session - The session the event belongs to. Everything the event
|
|
2004
|
-
* touches (data store,
|
|
2005
|
-
* is that session's
|
|
2006
|
-
* passes an
|
|
2453
|
+
* touches (data store, deduplication baseline, counters,
|
|
2454
|
+
* queues) is that session's. Only `recordResponseReceived()`
|
|
2455
|
+
* ever passes an ended session.
|
|
2007
2456
|
* @param type - The type of event to record.
|
|
2008
2457
|
* @param data - Additional data to record with the event.
|
|
2009
2458
|
* @param request - The media request that triggered the event, when
|
|
2010
|
-
* one exists. Only `recordResponseReceived()`
|
|
2011
|
-
*
|
|
2012
|
-
*
|
|
2459
|
+
* one exists. Only `recordResponseReceived()` sets this
|
|
2460
|
+
* parameter, which the reporter passes to each target's
|
|
2461
|
+
* `transform`.
|
|
2013
2462
|
*/
|
|
2014
2463
|
emitEvent(session, type, data, request) {
|
|
2015
2464
|
const entry = STATE_FIELDS_BY_EVENT.get(type);
|
|
@@ -2034,16 +2483,17 @@ var CmcdReporter = class {
|
|
|
2034
2483
|
if (failure) throw failure.error;
|
|
2035
2484
|
}
|
|
2036
2485
|
/**
|
|
2037
|
-
* Records an event for a target.
|
|
2486
|
+
* Records an event for a target. The reporter calls this method when an
|
|
2487
|
+
* event occurs.
|
|
2038
2488
|
*
|
|
2039
2489
|
* @param target - The target to record the event for.
|
|
2040
2490
|
* @param config - The configuration for the target.
|
|
2041
2491
|
* @param type - The type of event to record.
|
|
2042
2492
|
* @param data - Additional data to record with the event. This data
|
|
2043
|
-
* only applies to this event report.
|
|
2044
|
-
*
|
|
2493
|
+
* only applies to this event report. Use `update()` for
|
|
2494
|
+
* persistent data.
|
|
2045
2495
|
* @param request - The media request that triggered the event, when
|
|
2046
|
-
* one exists.
|
|
2496
|
+
* one exists. The target's `transform` receives it.
|
|
2047
2497
|
*/
|
|
2048
2498
|
recordTargetEvent(session, target, config, type, data = {}, request) {
|
|
2049
2499
|
if (target.disposed || !config.events.includes(type)) return;
|
|
@@ -2054,6 +2504,7 @@ var CmcdReporter = class {
|
|
|
2054
2504
|
e: type,
|
|
2055
2505
|
ts: data.ts ?? Date.now()
|
|
2056
2506
|
};
|
|
2507
|
+
if (type === CMCD_EVENT_BACKGROUNDED_MODE && toBareValue(item.bg) === false) delete item.bg;
|
|
2057
2508
|
const { transform } = config;
|
|
2058
2509
|
if (!transform) {
|
|
2059
2510
|
this.queueTargetEvent(session, target, config, item, type);
|
|
@@ -2065,20 +2516,19 @@ var CmcdReporter = class {
|
|
|
2065
2516
|
const ts = item.ts;
|
|
2066
2517
|
const report = transform(item, request);
|
|
2067
2518
|
if (report == null) return;
|
|
2068
|
-
if (!
|
|
2069
|
-
if (requiredKey &&
|
|
2519
|
+
if (!isValid(report.ts)) report.ts = ts;
|
|
2520
|
+
if (requiredKey && isValid(requiredValue) && !isValid(report[requiredKey])) Object.assign(report, { [requiredKey]: requiredValue });
|
|
2070
2521
|
this.queueTargetEvent(session, target, config, report, type);
|
|
2071
2522
|
}
|
|
2072
2523
|
/**
|
|
2073
|
-
*
|
|
2074
|
-
* and pushes the
|
|
2524
|
+
* Writes the reporter-owned fields into a finished event report, encodes
|
|
2525
|
+
* the report, and pushes the encoded line to the target's queue.
|
|
2075
2526
|
*
|
|
2076
|
-
*
|
|
2077
|
-
* target's `events` filter
|
|
2078
|
-
* the session identity
|
|
2079
|
-
*
|
|
2080
|
-
*
|
|
2081
|
-
* forever.
|
|
2527
|
+
* This method runs after any transform. A transform cannot bypass the
|
|
2528
|
+
* target's `events` filter through `e`, break `sn` continuity, or replace
|
|
2529
|
+
* the session identity in `sid` and `msd`. Encoding here makes a report
|
|
2530
|
+
* that cannot serialize throw inside the recording call, instead of failing
|
|
2531
|
+
* the batch send and re-queueing forever.
|
|
2082
2532
|
*
|
|
2083
2533
|
* @param target - The target to queue the report for.
|
|
2084
2534
|
* @param config - The configuration for the target.
|
|
@@ -2096,10 +2546,10 @@ var CmcdReporter = class {
|
|
|
2096
2546
|
target.queue.push(encodeCmcd(report, createEncodingOptions(CMCD_EVENT_MODE, config)));
|
|
2097
2547
|
}
|
|
2098
2548
|
/**
|
|
2099
|
-
* Records a response-received event.
|
|
2100
|
-
* request response has
|
|
2549
|
+
* Records a response-received event. The player calls this method when a
|
|
2550
|
+
* media request response has fully arrived.
|
|
2101
2551
|
*
|
|
2102
|
-
* This method
|
|
2552
|
+
* This method derives these `rr` event keys automatically:
|
|
2103
2553
|
*
|
|
2104
2554
|
* - `url` - the original requested URL (before any redirects)
|
|
2105
2555
|
* - `rc` - the HTTP response status code
|
|
@@ -2107,60 +2557,57 @@ var CmcdReporter = class {
|
|
|
2107
2557
|
* - `ttfb` - time to first byte (from `resourceTiming.responseStart`)
|
|
2108
2558
|
* - `ttlb` - time to last byte (from `resourceTiming.duration`)
|
|
2109
2559
|
*
|
|
2110
|
-
*
|
|
2111
|
-
*
|
|
2560
|
+
* If the player has additional keys like `ttfbb`, `cmsdd`, `cmsds`, and
|
|
2561
|
+
* `smrt`, pass them in the `data` parameter.
|
|
2112
2562
|
*
|
|
2113
|
-
* The request's `customData` is generic, so a player can pass a request
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2116
|
-
* {@link CmcdReporter.createRequestReport} writes there
|
|
2117
|
-
*
|
|
2118
|
-
*
|
|
2563
|
+
* The request's `customData` is generic, so a player can pass a request with
|
|
2564
|
+
* its own keys (for example `{ requestType: 'segment' }`) without a cast. The
|
|
2565
|
+
* reporter reads only the provenance record that
|
|
2566
|
+
* {@link CmcdReporter.createRequestReport} writes there. Every other key
|
|
2567
|
+
* stays unchanged and visible to an event target's `transform` through its
|
|
2568
|
+
* `request` argument.
|
|
2119
2569
|
*
|
|
2120
|
-
* A reporter
|
|
2121
|
-
*
|
|
2122
|
-
*
|
|
2570
|
+
* A reporter with a concrete `C` requires the request to satisfy that type.
|
|
2571
|
+
* TypeScript therefore rejects a call whose request the configured
|
|
2572
|
+
* transforms could not read. See {@link CmcdReporterCustomData}.
|
|
2123
2573
|
*
|
|
2124
|
-
* The
|
|
2125
|
-
*
|
|
2126
|
-
* {@link
|
|
2127
|
-
*
|
|
2128
|
-
*
|
|
2129
|
-
*
|
|
2130
|
-
*
|
|
2131
|
-
*
|
|
2132
|
-
*
|
|
2133
|
-
*
|
|
2134
|
-
* `sid`
|
|
2135
|
-
*
|
|
2136
|
-
* A response that completes after a `sid` change reports under its own
|
|
2137
|
-
* retained session, with that session's data snapshot and sequence
|
|
2138
|
-
* numbers.
|
|
2574
|
+
* The provenance record that {@link CmcdReporter.createRequestReport}
|
|
2575
|
+
* stored on the request's `customData` (under
|
|
2576
|
+
* {@link CMCD_REQUEST_PROVENANCE}) selects the session by `sid`. A record
|
|
2577
|
+
* that names no retained session drops the response (see
|
|
2578
|
+
* `CmcdReporterConfig.sessionRetention`). A request without a record reports
|
|
2579
|
+
* under the current session, as in version 2.4.0. The request-time data then
|
|
2580
|
+
* comes from `customData.cmcd`. A per-call `data.sid` cannot substitute. The
|
|
2581
|
+
* reporter accepts any record whose `sid` names a retained session. The
|
|
2582
|
+
* record may come from another reporter with the same session attributes,
|
|
2583
|
+
* or be hand-built. If the request has a record, a response that completes
|
|
2584
|
+
* after a `sid` change reports under its own retained session, with that
|
|
2585
|
+
* session's data snapshot and sequence numbers.
|
|
2139
2586
|
*
|
|
2140
|
-
*
|
|
2141
|
-
* `data` snapshot,
|
|
2142
|
-
*
|
|
2143
|
-
* completes after a mid-session content change
|
|
2144
|
-
*
|
|
2587
|
+
* With a record, request-time report keys come from the record's encoded
|
|
2588
|
+
* per-call `data` snapshot, which the reporter decodes for each response.
|
|
2589
|
+
* The record's `cid` replaces the session's current `cid` in the report. A
|
|
2590
|
+
* response that completes after a mid-session content change therefore
|
|
2591
|
+
* reports its request-time values.
|
|
2145
2592
|
*
|
|
2146
|
-
* @typeParam RD - The `customData` this request
|
|
2593
|
+
* @typeParam RD - The `customData` of this request. Defaults to the
|
|
2147
2594
|
* reporter's own `C`.
|
|
2148
2595
|
*
|
|
2149
2596
|
* @param response - The HTTP response received.
|
|
2150
2597
|
* @param data - Additional CMCD data to include with the event.
|
|
2151
|
-
* Values provided here override
|
|
2152
|
-
*
|
|
2153
|
-
*
|
|
2598
|
+
* Values provided here override the derived values.
|
|
2599
|
+
* The reporter ignores session-owned keys (`sid`, `msd`)
|
|
2600
|
+
* supplied here.
|
|
2154
2601
|
*/
|
|
2155
2602
|
recordResponseReceived(response, data = {}) {
|
|
2156
2603
|
const { request } = response;
|
|
2157
2604
|
const url = data.url ?? request?.url;
|
|
2158
2605
|
if (!url) return;
|
|
2159
2606
|
const provenance = request.customData?.[CMCD_REQUEST_PROVENANCE];
|
|
2160
|
-
const session = this.resolveSession(provenance);
|
|
2607
|
+
const session = provenance === void 0 ? this.session : this.resolveSession(provenance);
|
|
2161
2608
|
if (!session) return;
|
|
2162
|
-
const cmcd = decodeSnapshot(provenance);
|
|
2163
|
-
const { cid } = provenance;
|
|
2609
|
+
const cmcd = provenance === void 0 ? { ...request.customData?.cmcd } : decodeSnapshot(provenance);
|
|
2610
|
+
const { cid } = provenance ?? {};
|
|
2164
2611
|
const urlObj = new URL(url);
|
|
2165
2612
|
urlObj.searchParams.delete(CMCD_PARAM);
|
|
2166
2613
|
const derived = {
|
|
@@ -2183,13 +2630,13 @@ var CmcdReporter = class {
|
|
|
2183
2630
|
}, request);
|
|
2184
2631
|
}
|
|
2185
2632
|
/**
|
|
2186
|
-
* Resolves the session a
|
|
2187
|
-
*
|
|
2188
|
-
*
|
|
2189
|
-
*
|
|
2190
|
-
*
|
|
2191
|
-
*
|
|
2192
|
-
*
|
|
2633
|
+
* Resolves the session that a provenance record names. The record's `sid`
|
|
2634
|
+
* must name one of this reporter's retained sessions, or the reporter
|
|
2635
|
+
* discards the response. A record that names a removed or unknown `sid`
|
|
2636
|
+
* resolves nothing. Any other attribution would be wrong. The reporter reads
|
|
2637
|
+
* the `sid` as a plain property. A record
|
|
2638
|
+
* copied through JSON therefore resolves the same session, and a hand-built
|
|
2639
|
+
* record that names a retained session is accepted.
|
|
2193
2640
|
*/
|
|
2194
2641
|
resolveSession(provenance) {
|
|
2195
2642
|
if (provenance === null || typeof provenance !== "object") return;
|
|
@@ -2197,8 +2644,8 @@ var CmcdReporter = class {
|
|
|
2197
2644
|
return typeof sid === "string" ? this.sessions.get(sid) : void 0;
|
|
2198
2645
|
}
|
|
2199
2646
|
/**
|
|
2200
|
-
* Applies the CMCD request report data to the request.
|
|
2201
|
-
* before sending the request.
|
|
2647
|
+
* Applies the CMCD request report data to the request. The player calls this
|
|
2648
|
+
* method before sending the request.
|
|
2202
2649
|
*
|
|
2203
2650
|
* @param req - The request to apply the CMCD request report to.
|
|
2204
2651
|
* @returns The request with the CMCD request report applied.
|
|
@@ -2209,23 +2656,22 @@ var CmcdReporter = class {
|
|
|
2209
2656
|
return this.createRequestReport(req) ?? req;
|
|
2210
2657
|
}
|
|
2211
2658
|
/**
|
|
2212
|
-
* Checks
|
|
2659
|
+
* Checks whether request reporting is enabled.
|
|
2213
2660
|
*
|
|
2214
|
-
* @returns `true` if
|
|
2661
|
+
* @returns `true` if request reporting is enabled, `false` otherwise.
|
|
2215
2662
|
*/
|
|
2216
2663
|
isRequestReportingEnabled() {
|
|
2217
2664
|
return !!this.config.enabledKeys?.length;
|
|
2218
2665
|
}
|
|
2219
2666
|
/**
|
|
2220
|
-
* The provenance record to
|
|
2221
|
-
* frozen base record, extended with the caller's per-call data encoded
|
|
2222
|
-
*
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
*
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
* side.
|
|
2667
|
+
* The provenance record to write on a returned request. It is the session's
|
|
2668
|
+
* frozen base record, extended with the caller's per-call data encoded as a
|
|
2669
|
+
* CMCD string when there is any. Requests without per-call data share the
|
|
2670
|
+
* base record, so the reporter writes only one property. The snapshot never
|
|
2671
|
+
* contains reporter-written fields. A value that cannot encode contributes
|
|
2672
|
+
* no snapshot and does not fail the request. The response still attributes
|
|
2673
|
+
* and reports derived keys over session data, matching the tolerance of
|
|
2674
|
+
* `decodeSnapshot()` when reading.
|
|
2229
2675
|
*/
|
|
2230
2676
|
createRequestProvenance(data, baseUrl) {
|
|
2231
2677
|
const base = this.session.provenance;
|
|
@@ -2245,21 +2691,21 @@ var CmcdReporter = class {
|
|
|
2245
2691
|
}
|
|
2246
2692
|
}
|
|
2247
2693
|
/**
|
|
2248
|
-
* Creates a new request with the CMCD request report data applied.
|
|
2249
|
-
* before sending the request.
|
|
2694
|
+
* Creates a new request with the CMCD request report data applied. The
|
|
2695
|
+
* player calls this method before sending the request.
|
|
2250
2696
|
*
|
|
2251
|
-
* A reporter
|
|
2252
|
-
* satisfy
|
|
2253
|
-
*
|
|
2697
|
+
* A reporter with a concrete `C` requires the request's `customData` to
|
|
2698
|
+
* satisfy that type. TypeScript therefore rejects a call whose request the
|
|
2699
|
+
* configured transform could not read. See
|
|
2254
2700
|
* {@link CmcdReporterCustomData}.
|
|
2255
2701
|
*
|
|
2256
|
-
* @typeParam R - The
|
|
2702
|
+
* @typeParam R - The type of the request. Its `customData` must satisfy
|
|
2257
2703
|
* the reporter's `C`.
|
|
2258
2704
|
*
|
|
2259
2705
|
* @param request - The request to apply the CMCD request report to.
|
|
2260
2706
|
* @param data - The data to apply to the request. This data only
|
|
2261
|
-
* applies to this request report.
|
|
2262
|
-
*
|
|
2707
|
+
* applies to this request report. Use `update()` for
|
|
2708
|
+
* persistent data.
|
|
2263
2709
|
* @returns The request with the CMCD request report applied.
|
|
2264
2710
|
*/
|
|
2265
2711
|
createRequestReport(request, data) {
|
|
@@ -2311,7 +2757,8 @@ var CmcdReporter = class {
|
|
|
2311
2757
|
return report;
|
|
2312
2758
|
}
|
|
2313
2759
|
/**
|
|
2314
|
-
* Processes the event targets.
|
|
2760
|
+
* Processes the event targets. The reporter calls this method when an event
|
|
2761
|
+
* occurs.
|
|
2315
2762
|
*
|
|
2316
2763
|
* @param flush - Whether to flush the event targets.
|
|
2317
2764
|
*/
|
|
@@ -2334,7 +2781,8 @@ var CmcdReporter = class {
|
|
|
2334
2781
|
if (reprocess) this.processEventTargets();
|
|
2335
2782
|
}
|
|
2336
2783
|
/**
|
|
2337
|
-
* Sends an event report.
|
|
2784
|
+
* Sends an event report. The reporter calls this method when a batch is
|
|
2785
|
+
* ready.
|
|
2338
2786
|
*
|
|
2339
2787
|
* @param config - The target config to send the event report to.
|
|
2340
2788
|
* @param data - The encoded report lines to send in the event report.
|
|
@@ -2344,27 +2792,27 @@ var CmcdReporter = class {
|
|
|
2344
2792
|
url: config.url,
|
|
2345
2793
|
method: "POST",
|
|
2346
2794
|
headers: { "Content-Type": CMCD_MIME_TYPE },
|
|
2347
|
-
body: data.join("\n")
|
|
2795
|
+
body: data.join("\n")
|
|
2348
2796
|
});
|
|
2349
2797
|
if (status === 410) this.disposeEventTarget(session, config);
|
|
2350
2798
|
else if (status === 429 || status > 499 && status < 600) throw new Error(`Event report failed with status ${status}`);
|
|
2351
2799
|
}
|
|
2352
2800
|
/**
|
|
2353
2801
|
* Cancels the time-interval timer for an event target config and clears the
|
|
2354
|
-
* stored id. Safe to call when no timer is
|
|
2355
|
-
*
|
|
2802
|
+
* stored id. Safe to call when no timer is active (clearInterval(undefined)
|
|
2803
|
+
* does nothing).
|
|
2356
2804
|
*/
|
|
2357
2805
|
disarmInterval(config) {
|
|
2358
2806
|
clearInterval(this.intervals.get(config));
|
|
2359
2807
|
this.intervals.delete(config);
|
|
2360
2808
|
}
|
|
2361
2809
|
/**
|
|
2362
|
-
* Silences an event target URL for the remainder of its session
|
|
2363
|
-
*
|
|
2364
|
-
*
|
|
2365
|
-
*
|
|
2366
|
-
*
|
|
2367
|
-
* the disposal
|
|
2810
|
+
* Silences an event target URL for the remainder of its session after the
|
|
2811
|
+
* collector signals that the target is gone (HTTP 410). The method clears
|
|
2812
|
+
* the queues and blocks further enqueues for every config in the session
|
|
2813
|
+
* that reports to the URL (the CTA-5004-B suppression scope). For the
|
|
2814
|
+
* current session, the method also cancels those configs' timers. A session
|
|
2815
|
+
* started after the disposal gets new target state and is unaffected.
|
|
2368
2816
|
*/
|
|
2369
2817
|
disposeEventTarget(session, config) {
|
|
2370
2818
|
session.eventTargets.forEach((target, sibling) => {
|
|
@@ -2475,6 +2923,63 @@ function fromCmcdUrl(url, options) {
|
|
|
2475
2923
|
return decodeCmcd(decodeURIComponent(url.replace(/^CMCD=/, "")), options);
|
|
2476
2924
|
}
|
|
2477
2925
|
|
|
2926
|
+
//#endregion
|
|
2927
|
+
//#region src/isCmcdRequestKey.ts
|
|
2928
|
+
const CMCD_REQUEST_KEY_SET = /* @__PURE__ */ new Set(CMCD_REQUEST_KEYS);
|
|
2929
|
+
/**
|
|
2930
|
+
* Check if a key is a valid CMCD request key.
|
|
2931
|
+
*
|
|
2932
|
+
* @param key - The key to check.
|
|
2933
|
+
*
|
|
2934
|
+
* @returns `true` if the key is a valid CMCD request key, `false` otherwise.
|
|
2935
|
+
*
|
|
2936
|
+
* @public
|
|
2937
|
+
*
|
|
2938
|
+
* @example
|
|
2939
|
+
* {@includeCode ../test/isCmcdRequestKey.test.ts#example}
|
|
2940
|
+
*/
|
|
2941
|
+
function isCmcdRequestKey(key) {
|
|
2942
|
+
return CMCD_REQUEST_KEY_SET.has(key) || isCmcdCustomKey(key);
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
//#endregion
|
|
2946
|
+
//#region src/isCmcdResponseReceivedKey.ts
|
|
2947
|
+
const CMCD_RESPONSE_KEY_SET = /* @__PURE__ */ new Set(CMCD_RESPONSE_KEYS);
|
|
2948
|
+
/**
|
|
2949
|
+
* Check if a key is a valid CMCD response key.
|
|
2950
|
+
*
|
|
2951
|
+
* @param key - The key to check.
|
|
2952
|
+
*
|
|
2953
|
+
* @returns `true` if the key is a valid CMCD response key, `false` otherwise.
|
|
2954
|
+
*
|
|
2955
|
+
* @public
|
|
2956
|
+
*
|
|
2957
|
+
* @example
|
|
2958
|
+
* {@includeCode ../test/isCmcdResponseReceivedKey.test.ts#example}
|
|
2959
|
+
*/
|
|
2960
|
+
function isCmcdResponseReceivedKey(key) {
|
|
2961
|
+
return CMCD_RESPONSE_KEY_SET.has(key);
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
//#endregion
|
|
2965
|
+
//#region src/isCmcdEventKey.ts
|
|
2966
|
+
const CMCD_EVENT_KEY_SET = /* @__PURE__ */ new Set(CMCD_EVENT_KEYS);
|
|
2967
|
+
/**
|
|
2968
|
+
* Check if a key is a valid CMCD event key.
|
|
2969
|
+
*
|
|
2970
|
+
* @param key - The key to check.
|
|
2971
|
+
*
|
|
2972
|
+
* @returns `true` if the key is a valid CMCD event key, `false` otherwise.
|
|
2973
|
+
*
|
|
2974
|
+
* @public
|
|
2975
|
+
*
|
|
2976
|
+
* @example
|
|
2977
|
+
* {@includeCode ../test/isCmcdEventKey.test.ts#example}
|
|
2978
|
+
*/
|
|
2979
|
+
function isCmcdEventKey(key) {
|
|
2980
|
+
return isCmcdRequestKey(key) || isCmcdResponseReceivedKey(key) || CMCD_EVENT_KEY_SET.has(key);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2478
2983
|
//#endregion
|
|
2479
2984
|
//#region src/isCmcdV1Data.ts
|
|
2480
2985
|
/**
|
|
@@ -2493,6 +2998,25 @@ function isCmcdV1Data(data) {
|
|
|
2493
2998
|
return data.v !== CMCD_V2;
|
|
2494
2999
|
}
|
|
2495
3000
|
|
|
3001
|
+
//#endregion
|
|
3002
|
+
//#region src/isCmcdV1Key.ts
|
|
3003
|
+
const CMCD_V1_KEY_SET$1 = /* @__PURE__ */ new Set(CMCD_V1_KEYS);
|
|
3004
|
+
/**
|
|
3005
|
+
* Filter for CMCD version 1 keys.
|
|
3006
|
+
*
|
|
3007
|
+
* @param key - The CMCD key to filter.
|
|
3008
|
+
*
|
|
3009
|
+
* @returns `true` if the key should be included, `false` otherwise.
|
|
3010
|
+
*
|
|
3011
|
+
* @public
|
|
3012
|
+
*
|
|
3013
|
+
* @example
|
|
3014
|
+
* {@includeCode ../test/isCmcdV1Key.test.ts#example}
|
|
3015
|
+
*/
|
|
3016
|
+
function isCmcdV1Key(key) {
|
|
3017
|
+
return CMCD_V1_KEY_SET$1.has(key) || isCmcdCustomKey(key);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
2496
3020
|
//#endregion
|
|
2497
3021
|
//#region src/isCmcdV2Data.ts
|
|
2498
3022
|
/**
|
|
@@ -2517,7 +3041,7 @@ function isCmcdV2Data(data) {
|
|
|
2517
3041
|
* Convert a value to a CMCD value.
|
|
2518
3042
|
*
|
|
2519
3043
|
* @param value - The value to convert to a CMCD value.
|
|
2520
|
-
* @param params - The parameters
|
|
3044
|
+
* @param params - The parameters of the CMCD value.
|
|
2521
3045
|
* @returns The CMCD value.
|
|
2522
3046
|
*
|
|
2523
3047
|
* @public
|
|
@@ -2544,13 +3068,13 @@ function mergeValidationResults(...results) {
|
|
|
2544
3068
|
//#endregion
|
|
2545
3069
|
//#region src/resolveVersion.ts
|
|
2546
3070
|
/**
|
|
2547
|
-
* Resolves the CMCD version from
|
|
3071
|
+
* Resolves the CMCD version from options, the payload's `v` key, or the default (version 1).
|
|
2548
3072
|
*
|
|
2549
3073
|
* @internal
|
|
2550
3074
|
*/
|
|
2551
3075
|
function resolveVersion(data, options) {
|
|
2552
3076
|
if (options?.version === 1 || options?.version === 2) return options.version;
|
|
2553
|
-
const payloadVersion = data["v"];
|
|
3077
|
+
const payloadVersion = toBareValue(data["v"]);
|
|
2554
3078
|
if (payloadVersion === 1 || payloadVersion === 2) return payloadVersion;
|
|
2555
3079
|
return CMCD_V1;
|
|
2556
3080
|
}
|
|
@@ -2591,6 +3115,51 @@ function validateCmcdKeys(data, options) {
|
|
|
2591
3115
|
};
|
|
2592
3116
|
}
|
|
2593
3117
|
|
|
3118
|
+
//#endregion
|
|
3119
|
+
//#region src/CMCD_AGGREGATE_BITRATE_KEYS.ts
|
|
3120
|
+
/**
|
|
3121
|
+
* Maps each aggregate bitrate key to the exact bitrate key that supersedes it.
|
|
3122
|
+
*
|
|
3123
|
+
* Per CTA-5004-B, `ab` MUST NOT be sent when the encoded bitrate `br` is
|
|
3124
|
+
* known. `lab` and `tab` follow the same rule for the lowest bitrate `lb`
|
|
3125
|
+
* and the top bitrate `tb`.
|
|
3126
|
+
*
|
|
3127
|
+
* @internal
|
|
3128
|
+
*/
|
|
3129
|
+
const CMCD_AGGREGATE_BITRATE_KEYS = {
|
|
3130
|
+
ab: "br",
|
|
3131
|
+
lab: "lb",
|
|
3132
|
+
tab: "tb"
|
|
3133
|
+
};
|
|
3134
|
+
|
|
3135
|
+
//#endregion
|
|
3136
|
+
//#region src/CMCD_KEY_OBJECT_TYPES.ts
|
|
3137
|
+
/**
|
|
3138
|
+
* Maps a key to the object types (`ot`) for which CTA-5004-B allows the key.
|
|
3139
|
+
*
|
|
3140
|
+
* A key that is absent from this map is allowed for every object type.
|
|
3141
|
+
* `d` (Object duration) and `tpb` (Top playable bitrate) MUST NOT be sent for
|
|
3142
|
+
* other object types.
|
|
3143
|
+
*
|
|
3144
|
+
* @internal
|
|
3145
|
+
*/
|
|
3146
|
+
const CMCD_KEY_OBJECT_TYPES = {
|
|
3147
|
+
d: [
|
|
3148
|
+
"a",
|
|
3149
|
+
"v",
|
|
3150
|
+
"av",
|
|
3151
|
+
"tt",
|
|
3152
|
+
"c",
|
|
3153
|
+
"o"
|
|
3154
|
+
],
|
|
3155
|
+
tpb: [
|
|
3156
|
+
"a",
|
|
3157
|
+
"v",
|
|
3158
|
+
"av",
|
|
3159
|
+
"c"
|
|
3160
|
+
]
|
|
3161
|
+
};
|
|
3162
|
+
|
|
2594
3163
|
//#endregion
|
|
2595
3164
|
//#region src/validateCmcdStructure.ts
|
|
2596
3165
|
/**
|
|
@@ -2658,9 +3227,9 @@ function validateCmcdStructure(data, options) {
|
|
|
2658
3227
|
message: `Response key "${key}" must only be present when e="rr".`,
|
|
2659
3228
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2660
3229
|
});
|
|
2661
|
-
|
|
2662
|
-
key:
|
|
2663
|
-
message:
|
|
3230
|
+
if (eventType === CMCD_EVENT_PLAY_STATE && !("sta" in data)) issues.push({
|
|
3231
|
+
key: "sta",
|
|
3232
|
+
message: "Play state change event (e=\"ps\") requires the \"sta\" key to be present.",
|
|
2664
3233
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2665
3234
|
});
|
|
2666
3235
|
if (eventType === CMCD_EVENT_ERROR && !("ec" in data)) issues.push({
|
|
@@ -2669,9 +3238,27 @@ function validateCmcdStructure(data, options) {
|
|
|
2669
3238
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2670
3239
|
});
|
|
2671
3240
|
}
|
|
2672
|
-
|
|
3241
|
+
const ot = version > 1 ? toTokenString(data["ot"]) : void 0;
|
|
3242
|
+
if (ot !== void 0) for (const key in CMCD_KEY_OBJECT_TYPES) {
|
|
3243
|
+
const objectTypes = CMCD_KEY_OBJECT_TYPES[key];
|
|
3244
|
+
if (key in data && !objectTypes.includes(ot)) issues.push({
|
|
3245
|
+
key,
|
|
3246
|
+
message: `Key "${key}" must not be present when "ot" is "${ot}". Expected "ot" to be one of: ${objectTypes.join(", ")}.`,
|
|
3247
|
+
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
if (version > 1) for (const key in CMCD_AGGREGATE_BITRATE_KEYS) {
|
|
3251
|
+
const exactKey = CMCD_AGGREGATE_BITRATE_KEYS[key];
|
|
3252
|
+
if (key in data && exactKey in data) issues.push({
|
|
3253
|
+
key,
|
|
3254
|
+
message: `Key "${key}" must not be present when "${exactKey}" is present.`,
|
|
3255
|
+
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
const v = toBareValue(data["v"]);
|
|
3259
|
+
if ("v" in data && v !== 1 && v !== 2) issues.push({
|
|
2673
3260
|
key: "v",
|
|
2674
|
-
message: `Unsupported CMCD version "${String(
|
|
3261
|
+
message: `Unsupported CMCD version "${String(v)}". Expected 1 or 2.`,
|
|
2675
3262
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2676
3263
|
});
|
|
2677
3264
|
else if (version > 1 && !("v" in data)) issues.push({
|
|
@@ -2693,7 +3280,7 @@ function validateCmcdStructure(data, options) {
|
|
|
2693
3280
|
//#endregion
|
|
2694
3281
|
//#region src/CMCD_KEY_TYPES.ts
|
|
2695
3282
|
/**
|
|
2696
|
-
* CMCD key value type: inner list of
|
|
3283
|
+
* CMCD key value type: inner list of integers with token identifiers.
|
|
2697
3284
|
*
|
|
2698
3285
|
* @internal
|
|
2699
3286
|
*/
|
|
@@ -2735,8 +3322,8 @@ const CMCD_KEY_TYPE_STRING = "string";
|
|
|
2735
3322
|
*/
|
|
2736
3323
|
const CMCD_KEY_TYPE_TOKEN = "token";
|
|
2737
3324
|
/**
|
|
2738
|
-
* Maps each CMCD
|
|
2739
|
-
*
|
|
3325
|
+
* Maps each CMCD key to its value type for version 2.
|
|
3326
|
+
* CMCD_V1_KEY_TYPE_OVERRIDES handles the keys that differ between version 1 and version 2.
|
|
2740
3327
|
*
|
|
2741
3328
|
* @internal
|
|
2742
3329
|
*/
|
|
@@ -2794,15 +3381,15 @@ const CMCD_KEY_TYPES = {
|
|
|
2794
3381
|
sta: CMCD_KEY_TYPE_TOKEN
|
|
2795
3382
|
};
|
|
2796
3383
|
/**
|
|
2797
|
-
*
|
|
3384
|
+
* Version 1 types of the keys that differ from version 2.
|
|
2798
3385
|
*
|
|
2799
3386
|
* @internal
|
|
2800
3387
|
*/
|
|
2801
3388
|
const CMCD_V1_KEY_TYPE_OVERRIDES = {
|
|
2802
3389
|
bl: CMCD_KEY_TYPE_INTEGER,
|
|
2803
|
-
br:
|
|
3390
|
+
br: CMCD_KEY_TYPE_INTEGER,
|
|
2804
3391
|
mtp: CMCD_KEY_TYPE_INTEGER,
|
|
2805
|
-
tb:
|
|
3392
|
+
tb: CMCD_KEY_TYPE_INTEGER,
|
|
2806
3393
|
nor: CMCD_KEY_TYPE_STRING
|
|
2807
3394
|
};
|
|
2808
3395
|
|
|
@@ -2893,22 +3480,31 @@ const CMCD_TOKEN_VALUES = {
|
|
|
2893
3480
|
|
|
2894
3481
|
//#endregion
|
|
2895
3482
|
//#region src/validateCmcdValues.ts
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
const INTEGER_ROUNDING_KEYS = /* @__PURE__ */ new Set([
|
|
2904
|
-
"br",
|
|
2905
|
-
"d",
|
|
2906
|
-
"tb"
|
|
2907
|
-
]);
|
|
3483
|
+
const HUNDRED_ROUNDING_UNITS = {
|
|
3484
|
+
bl: "ms",
|
|
3485
|
+
dl: "ms",
|
|
3486
|
+
mtp: "kbps",
|
|
3487
|
+
rtp: "kbps",
|
|
3488
|
+
tbl: "ms"
|
|
3489
|
+
};
|
|
2908
3490
|
function isFiniteNumber(value) {
|
|
2909
3491
|
return typeof value === "number" && Number.isFinite(value);
|
|
2910
3492
|
}
|
|
2911
|
-
function
|
|
3493
|
+
function isInteger(value) {
|
|
3494
|
+
return Number.isInteger(value);
|
|
3495
|
+
}
|
|
3496
|
+
function validateRounding(key, value, version, issues, index) {
|
|
3497
|
+
const unit = HUNDRED_ROUNDING_UNITS[key];
|
|
3498
|
+
if (!unit || value % 100 === 0) return;
|
|
3499
|
+
const should = key === "tbl" || key === "bl" && version !== CMCD_V1;
|
|
3500
|
+
const target = index === void 0 ? `Key "${key}"` : `Key "${key}" array element [${index}]`;
|
|
3501
|
+
issues.push({
|
|
3502
|
+
key,
|
|
3503
|
+
message: `${target} ${should ? "should" : "must"} be rounded to the nearest 100 ${unit}. Received ${value}.`,
|
|
3504
|
+
severity: should ? CMCD_VALIDATION_SEVERITY_WARNING : CMCD_VALIDATION_SEVERITY_ERROR
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
function validateListValue(key, value, version, issues) {
|
|
2912
3508
|
if (!Array.isArray(value)) {
|
|
2913
3509
|
issues.push({
|
|
2914
3510
|
key,
|
|
@@ -2918,18 +3514,13 @@ function validateListValue(key, value, issues) {
|
|
|
2918
3514
|
return;
|
|
2919
3515
|
}
|
|
2920
3516
|
for (let i = 0; i < value.length; i++) {
|
|
2921
|
-
const element = value[i];
|
|
2922
|
-
if (element
|
|
2923
|
-
if (!isFiniteNumber(element.value)) issues.push({
|
|
2924
|
-
key,
|
|
2925
|
-
message: `Key "${key}" array element [${i}] must be a finite number.`,
|
|
2926
|
-
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2927
|
-
});
|
|
2928
|
-
} else if (!isFiniteNumber(element)) issues.push({
|
|
3517
|
+
const element = toBareValue(value[i]);
|
|
3518
|
+
if (!isInteger(element)) issues.push({
|
|
2929
3519
|
key,
|
|
2930
|
-
message: `Key "${key}" array element [${i}] must be a finite
|
|
3520
|
+
message: `Key "${key}" array element [${i}] must be a finite integer.`,
|
|
2931
3521
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2932
3522
|
});
|
|
3523
|
+
else validateRounding(key, element, version, issues, i);
|
|
2933
3524
|
}
|
|
2934
3525
|
}
|
|
2935
3526
|
function validateStringArrayValue(key, value, issues) {
|
|
@@ -2941,20 +3532,11 @@ function validateStringArrayValue(key, value, issues) {
|
|
|
2941
3532
|
});
|
|
2942
3533
|
return;
|
|
2943
3534
|
}
|
|
2944
|
-
for (let i = 0; i < value.length; i++) {
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
message: `Key "${key}" array element [${i}] must be a string.`,
|
|
2950
|
-
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2951
|
-
});
|
|
2952
|
-
} else if (typeof element !== "string") issues.push({
|
|
2953
|
-
key,
|
|
2954
|
-
message: `Key "${key}" array element [${i}] must be a string.`,
|
|
2955
|
-
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
2956
|
-
});
|
|
2957
|
-
}
|
|
3535
|
+
for (let i = 0; i < value.length; i++) if (typeof toBareValue(value[i]) !== "string") issues.push({
|
|
3536
|
+
key,
|
|
3537
|
+
message: `Key "${key}" array element [${i}] must be a string.`,
|
|
3538
|
+
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3539
|
+
});
|
|
2958
3540
|
}
|
|
2959
3541
|
/**
|
|
2960
3542
|
* Validates that all values in a CMCD payload conform to the expected types and constraints.
|
|
@@ -2973,7 +3555,8 @@ function validateStringArrayValue(key, value, issues) {
|
|
|
2973
3555
|
function validateCmcdValues(data, options) {
|
|
2974
3556
|
const version = resolveVersion(data, options);
|
|
2975
3557
|
const issues = [];
|
|
2976
|
-
for (const [key,
|
|
3558
|
+
for (const [key, entry] of Object.entries(data)) {
|
|
3559
|
+
const value = toBareValue(entry);
|
|
2977
3560
|
if (isCmcdCustomKey(key)) {
|
|
2978
3561
|
if (typeof value !== "string") issues.push({
|
|
2979
3562
|
key,
|
|
@@ -3000,16 +3583,12 @@ function validateCmcdValues(data, options) {
|
|
|
3000
3583
|
if (version === CMCD_V1 && key in CMCD_V1_KEY_TYPE_OVERRIDES) expectedType = CMCD_V1_KEY_TYPE_OVERRIDES[key];
|
|
3001
3584
|
switch (expectedType) {
|
|
3002
3585
|
case CMCD_KEY_TYPE_INTEGER:
|
|
3003
|
-
if (!
|
|
3586
|
+
if (!isInteger(value)) issues.push({
|
|
3004
3587
|
key,
|
|
3005
3588
|
message: `Key "${key}" must be a finite integer.`,
|
|
3006
3589
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3007
3590
|
});
|
|
3008
|
-
else
|
|
3009
|
-
key,
|
|
3010
|
-
message: `Key "${key}" should be rounded to the nearest 100.`,
|
|
3011
|
-
severity: CMCD_VALIDATION_SEVERITY_WARNING
|
|
3012
|
-
});
|
|
3591
|
+
else validateRounding(key, value, version, issues);
|
|
3013
3592
|
break;
|
|
3014
3593
|
case CMCD_KEY_TYPE_NUMBER:
|
|
3015
3594
|
if (!isFiniteNumber(value)) issues.push({
|
|
@@ -3017,16 +3596,6 @@ function validateCmcdValues(data, options) {
|
|
|
3017
3596
|
message: `Key "${key}" must be a finite number.`,
|
|
3018
3597
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3019
3598
|
});
|
|
3020
|
-
else if (HUNDRED_ROUNDING_KEYS.has(key) && value % 100 !== 0) issues.push({
|
|
3021
|
-
key,
|
|
3022
|
-
message: `Key "${key}" should be rounded to the nearest 100.`,
|
|
3023
|
-
severity: CMCD_VALIDATION_SEVERITY_WARNING
|
|
3024
|
-
});
|
|
3025
|
-
else if (INTEGER_ROUNDING_KEYS.has(key) && !Number.isInteger(value)) issues.push({
|
|
3026
|
-
key,
|
|
3027
|
-
message: `Key "${key}" should be rounded to an integer.`,
|
|
3028
|
-
severity: CMCD_VALIDATION_SEVERITY_WARNING
|
|
3029
|
-
});
|
|
3030
3599
|
break;
|
|
3031
3600
|
case CMCD_KEY_TYPE_BOOLEAN:
|
|
3032
3601
|
if (typeof value !== "boolean") issues.push({
|
|
@@ -3058,7 +3627,7 @@ function validateCmcdValues(data, options) {
|
|
|
3058
3627
|
break;
|
|
3059
3628
|
}
|
|
3060
3629
|
case CMCD_KEY_TYPE_NUMBER_LIST:
|
|
3061
|
-
validateListValue(key, value, issues);
|
|
3630
|
+
validateListValue(key, value, version, issues);
|
|
3062
3631
|
break;
|
|
3063
3632
|
case CMCD_KEY_TYPE_STRING_LIST:
|
|
3064
3633
|
validateStringArrayValue(key, value, issues);
|
|
@@ -3098,8 +3667,8 @@ function validateCmcd(data, options) {
|
|
|
3098
3667
|
*
|
|
3099
3668
|
* This function decodes the string internally and validates it with
|
|
3100
3669
|
* `reportingMode` set to `'event'`. The input may contain multiple
|
|
3101
|
-
* newline-separated events
|
|
3102
|
-
*
|
|
3670
|
+
* newline-separated events, for example an `application/cmcd` POST body.
|
|
3671
|
+
* The function validates each line independently and merges the results.
|
|
3103
3672
|
*
|
|
3104
3673
|
* @param cmcd - The raw CMCD-encoded string to validate. May contain
|
|
3105
3674
|
* multiple newline-separated event lines.
|
|
@@ -3127,6 +3696,11 @@ function validateCmcdEvents(cmcd, options) {
|
|
|
3127
3696
|
}],
|
|
3128
3697
|
data: []
|
|
3129
3698
|
};
|
|
3699
|
+
const bodyIssues = [];
|
|
3700
|
+
if (cmcd.endsWith("\n")) bodyIssues.push({
|
|
3701
|
+
message: "Event report body must not end with a line feed.",
|
|
3702
|
+
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3703
|
+
});
|
|
3130
3704
|
const decodedLines = [];
|
|
3131
3705
|
const lineResults = [];
|
|
3132
3706
|
for (let i = 0; i < lines.length; i++) try {
|
|
@@ -3144,7 +3718,10 @@ function validateCmcdEvents(cmcd, options) {
|
|
|
3144
3718
|
});
|
|
3145
3719
|
}
|
|
3146
3720
|
return {
|
|
3147
|
-
...mergeValidationResults(
|
|
3721
|
+
...mergeValidationResults({
|
|
3722
|
+
valid: true,
|
|
3723
|
+
issues: bodyIssues
|
|
3724
|
+
}, ...lineResults),
|
|
3148
3725
|
data: decodedLines
|
|
3149
3726
|
};
|
|
3150
3727
|
}
|
|
@@ -3157,8 +3734,8 @@ function validateCmcdEvents(cmcd, options) {
|
|
|
3157
3734
|
* Accepts an {@link @svta/cml-utils!HttpRequest | HttpRequest} object.
|
|
3158
3735
|
*
|
|
3159
3736
|
* This function validates that the request uses the POST method and has
|
|
3160
|
-
* the correct `Content-Type` header (`application/cmcd`)
|
|
3161
|
-
*
|
|
3737
|
+
* the correct `Content-Type` header (`application/cmcd`). It also
|
|
3738
|
+
* validates the body content with {@link validateCmcdEvents}.
|
|
3162
3739
|
*
|
|
3163
3740
|
* @param request - An `HttpRequest` to validate.
|
|
3164
3741
|
* @param options - Validation options (excluding `reportingMode`).
|
|
@@ -3224,15 +3801,15 @@ function validateCmcdEventReport(request, options) {
|
|
|
3224
3801
|
/**
|
|
3225
3802
|
* Validates CMCD HTTP headers by checking shard placement and payload validity.
|
|
3226
3803
|
*
|
|
3227
|
-
* This function accepts raw CMCD header strings
|
|
3228
|
-
* internally
|
|
3229
|
-
* then merges all shards and runs
|
|
3230
|
-
* structure) on the merged data.
|
|
3804
|
+
* This function accepts raw CMCD header strings and decodes each shard
|
|
3805
|
+
* internally. It verifies that each key is in its correct header shard.
|
|
3806
|
+
* The function then merges all shards and runs payload validation (keys,
|
|
3807
|
+
* values, and structure) on the merged data.
|
|
3231
3808
|
*
|
|
3232
3809
|
* @example
|
|
3233
3810
|
* {@includeCode ../test/validateCmcdHeaders.test.ts#example}
|
|
3234
3811
|
*
|
|
3235
|
-
* @param headers - A `Headers` instance or a record of CMCD header fields to
|
|
3812
|
+
* @param headers - A `Headers` instance or a record of CMCD header fields to raw encoded strings.
|
|
3236
3813
|
* @param options - Validation options (excluding `reportingMode`).
|
|
3237
3814
|
* @returns The validation result including decoded data.
|
|
3238
3815
|
*
|
|
@@ -3292,10 +3869,12 @@ function validateCmcdHeaders(headers, options) {
|
|
|
3292
3869
|
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Request | Request}
|
|
3293
3870
|
* object or an {@link @svta/cml-utils!HttpRequest | HttpRequest} object.
|
|
3294
3871
|
*
|
|
3295
|
-
* The function checks for CMCD data
|
|
3296
|
-
*
|
|
3297
|
-
*
|
|
3298
|
-
*
|
|
3872
|
+
* The function checks the HTTP headers for CMCD data first. If the
|
|
3873
|
+
* request has CMCD headers, {@link validateCmcdHeaders} validates them,
|
|
3874
|
+
* including shard placement. Otherwise, the function validates the CMCD
|
|
3875
|
+
* query parameter of the URL. A request with CMCD data in both the
|
|
3876
|
+
* headers and the `CMCD` query parameter is an error. The function still
|
|
3877
|
+
* validates the headers and returns their data.
|
|
3299
3878
|
*
|
|
3300
3879
|
* @param request - A `Request` or `HttpRequest` to validate.
|
|
3301
3880
|
* @param options - Validation options (excluding `reportingMode`).
|
|
@@ -3310,12 +3889,25 @@ function validateCmcdHeaders(headers, options) {
|
|
|
3310
3889
|
*/
|
|
3311
3890
|
function validateCmcdRequest(request, options) {
|
|
3312
3891
|
const headers = extractHeaderRecord(request.headers);
|
|
3313
|
-
|
|
3314
|
-
|
|
3892
|
+
const param = getCmcdQueryParam(request.url);
|
|
3893
|
+
if (headers) {
|
|
3894
|
+
const result = validateCmcdHeaders(headers, options);
|
|
3895
|
+
if (!param) return result;
|
|
3896
|
+
return {
|
|
3897
|
+
...mergeValidationResults({
|
|
3898
|
+
valid: false,
|
|
3899
|
+
issues: [{
|
|
3900
|
+
message: "CMCD data found in both request headers and the \"CMCD\" query parameter. A request must use only one transmission mode.",
|
|
3901
|
+
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3902
|
+
}]
|
|
3903
|
+
}, result),
|
|
3904
|
+
data: result.data
|
|
3905
|
+
};
|
|
3906
|
+
}
|
|
3315
3907
|
if (!param) return {
|
|
3316
3908
|
valid: false,
|
|
3317
3909
|
issues: [{
|
|
3318
|
-
message: "No CMCD data found in request headers or query parameters.",
|
|
3910
|
+
message: hasCmcdInFragment(request.url) ? "No CMCD data found in request headers or query parameters. The URL fragment contains a \"CMCD\" parameter. A server never receives the fragment." : "No CMCD data found in request headers or query parameters.",
|
|
3319
3911
|
severity: CMCD_VALIDATION_SEVERITY_ERROR
|
|
3320
3912
|
}],
|
|
3321
3913
|
data: {}
|
|
@@ -3341,6 +3933,18 @@ function validateCmcdRequest(request, options) {
|
|
|
3341
3933
|
data
|
|
3342
3934
|
};
|
|
3343
3935
|
}
|
|
3936
|
+
const CMCD_IN_FRAGMENT = /(?:^|[?&])CMCD=/;
|
|
3937
|
+
function getCmcdQueryParam(url) {
|
|
3938
|
+
const hash = url.indexOf("#");
|
|
3939
|
+
const path = hash < 0 ? url : url.slice(0, hash);
|
|
3940
|
+
const start = path.indexOf("?");
|
|
3941
|
+
if (start < 0) return null;
|
|
3942
|
+
return new URLSearchParams(path.slice(start + 1)).get(CMCD_PARAM);
|
|
3943
|
+
}
|
|
3944
|
+
function hasCmcdInFragment(url) {
|
|
3945
|
+
const hash = url.indexOf("#");
|
|
3946
|
+
return hash >= 0 && CMCD_IN_FRAGMENT.test(url.slice(hash + 1));
|
|
3947
|
+
}
|
|
3344
3948
|
function extractHeaderRecord(headers) {
|
|
3345
3949
|
if (!headers) return;
|
|
3346
3950
|
const h = ensureHeaders(headers);
|