feat(cmcd): custom events, custom keys and ec#7887
Conversation
robwalch
left a comment
There was a problem hiding this comment.
Exposing the CMCD controller and allowing any event or arbitrary data to be recorded or updated seems extreme and prone to tampering. I like that in the example in the description, the event and keys are included, but there is no similar example in the docs, nor any enforcement in the implementation.
If you really want unrestricted access, you might as well expose the reporter. Otherwise, if we want only responsible access to set custom fields and record custom events on top of an existing session, then I think a more careful plan or interface is needed. One way that similar customizations work in hls.js is by passing in callbacks to the config.
Thanks for the review! I looked into the config callback approach (similar to xhrSetup/fetchSetup) and here is what I found: It's a great pattern for the update() use case: I could add something like cmcd.cmcdSetup(data, { requestType }) that gets called automatically on every request, so the consumer never needs a reference to the controller. The problem is that recordEvent() is caller-triggered, the consumer decides when to fire it based on external events. hls.js has no internal trigger point for that, so I think a config callback doesn't fit exactly. If there's no natural callback entry point for that in the config, we'd still need to expose something on the Hls instance for it. Given that, I am thinking about a narrow interface instead, which covers both methods without exposing controller internals: hls.cmcdController is now typed as CmcdControllerAPI rather than the concrete CMCDController class. Let me know what you think |
That only narrows the interface to what I already expressed concern for. Exposing the controller in this way would encourage its use not only externally but internally, and we want to keep the cmcd implementation encapsulated in the controller and it's configuration. I agree that update is easy to solve. You could even have something as simple as a dictionary object with custom key value pairs - no callback needed to spy on requests. If you want to record custom events, and the like, I guess you do want access to the reporter. Yes, it gives you full access, but at least it's not access to the controller from top level HLS.js. A config option could be set to expose it for custom reporting. This is also how you might provide access with a callback. The callback would be invoked during |
|
Hi @robwalch ! Thanks for your suggestion, here is a Summary of what I implemented:
Let me know what you think, happy to keep improving the solution |
robwalch
left a comment
There was a problem hiding this comment.
I think the customKeys options should be removed completely in favor of developers using the reporter (.update) themselves. This implementation only updates the reporter with those options on start, but then requires them to update themselves anyway. I don't see the value in that.
| ], | ||
| eventTargets: (cmcd.eventTargets ?? []).map( | ||
| ({ includeKeys, ...rest }) => ({ | ||
| ({ includeKeys, customKeys: targetCustomKeys, ...rest }) => ({ |
There was a problem hiding this comment.
Is this not being used anymore?
| // Seed persistent state for event-target custom keys. Each target's enabledKeys | ||
| // controls which of these actually reach that target; they are excluded from | ||
| // request reports because they are not in the top-level enabledKeys. | ||
| const eventTargetCustomKeys: Cmcd = {}; | ||
| (cmcd.eventTargets ?? []).forEach((t) => { | ||
| if (t.customKeys) Object.assign(eventTargetCustomKeys, t.customKeys); | ||
| }); | ||
| if (Object.keys(eventTargetCustomKeys).length > 0) { | ||
| this.reporter.update(this.sanitizeCmcdData(eventTargetCustomKeys)); | ||
| } |
There was a problem hiding this comment.
What if you want to have the same custom key with different values across targets? I guess there's only one state dictionary per reporter?
This block seems like a good candidate for optimization. It does a few things that it probably should not when there are no custom keys or event targets (call forEach, and sanitize and update).
Ultimately, after reviewing and understanding the implementation, I think both customKeys options should be removed in favor of developers using the reporter directly.
| const eventTargetCustomKeys: Cmcd = {}; | ||
| (cmcd.eventTargets ?? []).forEach((t) => { |
There was a problem hiding this comment.
Would prefer an if (CMCD.eventTargets) { block to skip all this rather than the eager evaluation with ?? [].
| const invalidKeys = new Set(issues.map(({ key }) => key)); | ||
| issues.forEach(({ message }) => this.hls.logger.warn(message)); | ||
| const filtered: Record<string, unknown> = {}; | ||
| Object.entries(data).forEach(([key, value]) => { | ||
| if (!invalidKeys.has(key)) filtered[key] = value; | ||
| }); | ||
| return filtered as Cmcd; |
There was a problem hiding this comment.
Do we really need the warning here (and one per invalid key/message)? This could get very chatty on each event update.
There was a problem hiding this comment.
Ultimately I'm questioning the need for customKeys and validation/filtering when these are only set before start. If developers need to use the reporter to update customKeys maybe they should do that from the start and handle validation themselves. validateCmcdKeys adds to the hls.js payload from something that should only be done in debugging. I don't like the idea of that being a responsibility of hls.js.
There was a problem hiding this comment.
Removed sanitizeCmcdData and added Validating custom keys section to API docs 1d60d9e
| customKeys?: Partial<Cmcd>; | ||
| })[]; | ||
| loader?: (request: { | ||
| url: string; | ||
| method?: string; | ||
| headers?: Record<string, string>; | ||
| body?: BodyInit; | ||
| }) => Promise<{ status: number }>; | ||
| customKeys?: Partial<Cmcd>; |
There was a problem hiding this comment.
Is Partial<Cmcd> too broad of a type definition for custom keys?
(Partial < type Cmcd = CmcdRequest & CmcdResponse & CmcdEvent >)
Are any parts allowed? Are standard keys even allowed to be used as custom ones?
It seems the part you want is:
[index: CmcdCustomKey]: CmcdCustomValue | undefined;
| data.su = this.buffering; | ||
| } | ||
|
|
||
| const { customKeys } = this.config.cmcd ?? {}; |
There was a problem hiding this comment.
Please use the nullish coalescing operator (??) sparingly.
| const { customKeys } = this.config.cmcd ?? {}; | |
| const customKeys = this.config.cmcd?.customKeys; |
|
|
||
| const { customKeys } = this.config.cmcd ?? {}; | ||
| if (customKeys) { | ||
| Object.assign(data, this.sanitizeCmcdData(customKeys as Cmcd)); |
There was a problem hiding this comment.
Type coercion should not be needed here. Non-nullishcustomKeys should satisfy the input for sanitizeCmcdData
…github.com:qualabs/hls.js into issue/7886-cmcdv2-custom-events-custom-keys-and-ec
|
Hi @robwalch , all the comments have been addressed |
robwalch
left a comment
There was a problem hiding this comment.
Looks good to me. Holding on merging. I'd like to get feedback from @littlespex.
Here are some topics for discussion before taking the change:
- Could there be issues with the api not being available until after
report.start()(like custom fields not included in start event)? - Does the reporter have methods we would not want to expose? Should the exposed reporter just be an object that wraps
updateandrecordEvent(as in the suggestedinterface CmcdControllerAPI)? - Is it spec compliant for
ecto be an hls.jsErrorDetailsvalue (ex: "manifestLoadError"), or should these be mapped to SVTA Standardized Player Error Codes?
|
I'm still reviewing the latest round of changes, but here are some quick answers to the open questions:
It would only effect the first set of time-interval events, but yes, if you wanted custom fields in those events the callback would need to be run before
Yes. For example, all of the time interval events, start/stop/flush, are a part of the reporter's public interface and could cause inconsistencies in reporting if an end user were to call them directly.
Ideally this would be a standard error code, but the spec doesn't explicitly enforce a format:
That said, this does bring up the question of how an end user would be able to provide their own error code value. I'm still thinking this through. |
robwalch
left a comment
There was a problem hiding this comment.
@cotid-qualabs some suggestions to consider based on @littlespex's feedback. Thanks for all the work and responsiveness to feedback!
Co-authored-by: Rob Walch <robwalch@users.noreply.github.com>
…github.com:qualabs/hls.js into issue/7886-cmcdv2-custom-events-custom-keys-and-ec
|
Thanks @robwalch and @littlespex ! Changes have been implemented 😄 |
| - `loader`: An optional async function `(request) => Promise<{ status }>` used to deliver CMCD v2 event reports. When omitted, event reports are delivered via `fetch` (honoring the Hls `xhrSetup`/`fetchSetup` hooks). Only used when `eventTargets` is configured. | ||
| - `reporterCallback`: An optional `(reporter: CmcdCustomControllerAPI) => void` callback. Called once per `MANIFEST_LOADING`, before the reporter starts. Use it to seed custom CMCD keys or store the reference for firing custom events at runtime. Always use the most recently received reference, since a new source load yields a new instance. | ||
|
|
||
| The `CmcdCustomControllerAPI` facade exposes two methods: |
There was a problem hiding this comment.
| The `CmcdCustomControllerAPI` facade exposes two methods: | |
| The `CmcdCustomReporter` exposes two methods: |
I think this should be called CmcdCustomReporter. API is redundant and it isn't really a Controller. No one reading the docs needs to know it's a facade either.
robwalch
left a comment
There was a problem hiding this comment.
Would be nice to improve the name of CmcdCustomControllerAPI. CmcdCustomReporter?
Summary
This PR adds
cmcd.reporterCallbacktoCMCDControllerConfig, enabling runtime custom key injection and programmatic custom event reporting for CMCD v2.What changed
cmcd.reporterCallback— an optional(reporter: CmcdCustomControllerAPI) => voidcallback invoked once perMANIFEST_LOADING, before the reporter starts. This ordering ensures any custom data seeded in the callback is included in the first set of time-interval events. A new source load produces a new reporter instance, so the callback fires again on each load.The callback receives a
CmcdCustomControllerAPIfacade (not the rawCmcdReporter) that exposes two methods:updateCustomData(data)— sets persistent custom key/value pairs included in every subsequent report. Keys that don't follow the CMCD custom key convention (<reverse-dns>-<label>) are silently dropped viaisCmcdCustomKey.recordCustomEvent(eventName, data?)— fires a one-off CMCD custom event (ce) with the given name and optional custom data. Requirescmcd.eventTargetsto be configured with a target whoseeventsarray includes'ce'.Restricting the exposed interface to these two methods prevents consumers from calling internal reporter lifecycle methods (
start,stop,flush) that could cause inconsistencies in reporting.Fatal error events now include
{ ec: [data.details] }in the event payload, so the hls.js error detail is captured in the CMCD error report.Key design decisions
CMCDControllerorHls. The facade is surfaced exclusively viareporterCallback, keeping CMCD optional and off the top-level player API surface.includeKeysarray (top-level for request reports, per-target for event reports) so they pass the reporter's key filter.isCmcdCustomKeyfrom@svta/cml-cmcd— no additional bundle weight from the heaviervalidateCmcdKeyshelper. Advanced users can import it directly for more rigorous validation (see API.md).com.example-myKey). See the SVTA custom keys registry.Usage