CML
    Preparing search index...

    CMCD User Guide

    The CmcdReporter class provides a centralized way to manage CMCD (Common Media Client Data) reporting in a video player. It handles both request-mode reporting (adding CMCD data to segment requests) and event-mode reporting (sending periodic reports to analytics endpoints).

    npm install @svta/cml-cmcd
    

    Create a new CmcdReporter instance with configuration options:

    import {
    CmcdReporter,
    CmcdEventType,
    CMCD_QUERY,
    CMCD_V2,
    } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    // Optional: Provide a content ID for the current asset
    cid: "content-12345",

    // Optional: Provide a session ID (auto-generated if not provided)
    sid: "session-abc-123",

    // Optional: CMCD version (defaults to CMCD_V2)
    version: CMCD_V2,

    // Optional: Transmission mode for request reports (defaults to 'query')
    transmissionMode: CMCD_QUERY,

    // Optional: Keys to include in request reports (if not provided, request reporting is disabled)
    enabledKeys: ["br", "bl", "d", "ot", "sid", "cid", "mtp", "sf", "st"],

    // Event reporting targets
    eventTargets: [
    {
    url: "https://analytics.example.com/cmcd",
    // The events to report. If not provided, the event target is disabled.
    events: [
    CmcdEventType.PLAY_STATE,
    CmcdEventType.ERROR,
    CmcdEventType.TIME_INTERVAL,
    ],
    interval: 30, // seconds between TIME_INTERVAL reports
    batchSize: 5, // number of events to batch before sending
    enabledKeys: ["br", "bl", "sid", "cid"],
    },
    ],
    });

    For basic request-mode reporting without event reporting:

    import { CmcdReporter } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    cid: "my-video-id",
    });
    Note

    The library defaults to the latest CMCD version (CMCD_V2). When upgrading the library, if you need to maintain compatibility with an older CMCD version, explicitly set the version option (e.g., version: CMCD_V1).

    Use the update() method to set or update CMCD data. The reporter maintains an internal state that is applied to all subsequent requests and events.

    // Update multiple fields at once
    reporter.update({
    br: [5000], // Encoded bitrate (kbps)
    d: 4000, // Segment duration (ms)
    ot: "v", // Object type: video
    bl: [25000], // Buffer length (ms)
    mtp: [12000], // Measured throughput (kbps)
    sf: "d", // Streaming format: DASH
    st: "v", // Stream type: VOD
    });

    // Update individual fields as playback state changes
    reporter.update({ bl: [30000] }); // Buffer increased
    reporter.update({ pr: 1.5 }); // Playback rate changed
    reporter.update({ bs: true }); // Buffer starvation occurred
    Note

    In CMCD v2, several keys that were previously plain numbers are now typed as CmcdObjectTypeList — an inner list that supports per-object-type annotations. Keys like br, bl, mtp, tb, lb, ab, tab, lab, tbl, pb, bsa, bsd, bsda, and tpb must be passed as arrays (e.g., br: [5000]).

    The CMCD specification requires certain keys to be formatted before transmission. The CmcdReporter handles this automatically, so always pass raw, unformatted values in their base units. Do not round or truncate values yourself. For example, pass the exact buffer length in milliseconds:

    // Correct: pass the raw value, the reporter rounds to nearest 100
    reporter.update({ bl: [25432] }); // encoded as bl=(25400)

    // Incorrect: do not pre-round the value
    reporter.update({ bl: [25400] });

    Some CMCD keys like nor (next object request) and br (encoded bitrate) support parameterized values in CMCD v2. Use the toCmcdValue helper function to attach parameters to values:

    import { CmcdReporter, toCmcdValue } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    cid: "video-123",
    });

    // Using toCmcdValue to add byte-range parameters to next object requests
    reporter.update({
    nor: [
    toCmcdValue("segment_002.m4s", { r: "0-50000" }),
    toCmcdValue("segment_003.m4s", { r: "0-50000" }),
    ],
    });

    // This encodes to: nor=("segment_002.m4s";r="0-50000" "segment_003.m4s";r="0-50000")

    The toCmcdValue utility accepts two arguments:

    1. value - The bare item value (string, number, boolean, etc.)
    2. params - An object containing key-value pairs for parameters
    reporter.update({
    nor: [
    toCmcdValue("seg1.m4s", { r: "0-5000" }),
    toCmcdValue("seg1.m4s", { r: "5000-10000" }),
    ],
    });

    // This encodes to: nor=("seg1.m4s";r="0-5000" "seg1.m4s";r="5000-10000")

    Many keys allow for a list of numeric values with an associated object type, represented by a boolean flag.

    reporter.update({
    br: [toCmcdValue(5000, { v: true }), toCmcdValue(3000, { a: true })],
    });

    // This encodes to: br=(5000;v 3000;a)

    The CMCD specification defines nor (next object request) as a path relative to the current request URL. For convenience, CmcdReporter also accepts absolute URLs and converts them automatically — same-origin URLs are emitted as relative paths, and cross-origin or already-relative values are passed through unchanged.

    // When the current request URL is:
    // https://cdn.example.com/streams/video/segment_001.m4s
    // either form below produces the same encoded output.

    // Already relative (per spec):
    reporter.update({
    nor: ["segment_002.m4s"],
    });

    // Absolute URL — converted automatically:
    reporter.update({
    nor: ["https://cdn.example.com/streams/video/segment_002.m4s"],
    });

    // Both encode to: nor=("segment_002.m4s")

    CMCD allows player-defined key/value pairs alongside the standard keys. Per CTA-5004-B, custom key names MUST carry a hyphenated prefix to ensure there is no namespace collision with future revisions of the specification, and clients SHOULD use reverse-DNS syntax when defining their own prefix (e.g., com.example-mykey).

    The CmcdCustomKey type requires a lowercase hyphenated string at compile time. At runtime, isCmcdCustomKey enforces the full rule, and keys that fail it are silently dropped: a lowercase first letter, then characters from a-z 0-9 . -, with a hyphen that is neither the first nor the last character. These are the CTA-5004-B custom-key rules restricted to names that survive RFC 8941 key serialization, so a key that passes the check is guaranteed to reach the wire.

    In practice: use lowercase reverse-DNS names like com.example-mykey.

    Custom keys pass through the same enabledKeys filter as standard keys — the top-level enabledKeys for request reports, and each target's enabledKeys for event reports. There is no wildcard: a custom key that is not listed is silently dropped.

    import { CmcdReporter, CmcdEventType } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    cid: "video-123",
    // Request mode: the custom key must be listed here
    enabledKeys: ["br", "bl", "sid", "cid", "com.example-experiment"],
    eventTargets: [
    {
    url: "https://analytics.example.com/cmcd",
    events: [CmcdEventType.TIME_INTERVAL, CmcdEventType.ERROR],
    // Event mode: and here, for each target that should receive it
    enabledKeys: ["br", "bl", "sid", "cid", "com.example-experiment"],
    },
    ],
    });

    // Persist the value; it rides all subsequent request and event reports
    reporter.update({ "com.example-experiment": "variant-b" });

    // Or attach it to a single event report only
    reporter.recordEvent(CmcdEventType.ERROR, {
    ec: ["FATAL"],
    "com.example-experiment": "variant-b",
    });

    Custom values may be strings, numbers, booleans, or tokens, optionally wrapped with toCmcdValue() to attach structured-field parameters (see the CmcdCustomValue type). Notes on the wire format:

    • true is encoded as a bare valueless key (com.example-flag), per the RFC 8941 boolean convention; false is treated like other empty values and dropped entirely.
    • Values that cannot be serialized as RFC 8941 structured fields — for example strings containing control characters, or integers outside ±999,999,999,999,999 — currently cause encoding to throw. Validate custom values before handing them to the reporter. Graceful handling of unserializable values is tracked in #327.
    • The package's receiving-side validators expect custom values to be strings of at most 64 characters — prefer short string values for maximum interoperability. See the Validation Guide.

    In headers transmission mode, custom keys are emitted in the CMCD-Request header by default. Use the customHeaderMap option to route custom keys into other CMCD header shards — for example, a session-scoped custom key belongs in CMCD-Session so intermediaries can treat it as invariant for the session:

    import { CmcdReporter, CMCD_HEADERS } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    cid: "video-123",
    transmissionMode: CMCD_HEADERS,
    enabledKeys: ["sid", "cid", "com.example-experiment"],
    customHeaderMap: {
    "CMCD-Session": ["com.example-experiment"],
    },
    });

    reporter.update({ "com.example-experiment": "variant-b" });

    const request = reporter.createRequestReport({
    url: "https://cdn.example.com/video/segment_001.m4s",
    });

    // request.headers will contain:
    // {
    // 'CMCD-Session': 'cid="video-123",com.example-experiment="variant-b",sid="…",v=2',
    // }

    Custom keys not listed in any shard still default to CMCD-Request. Standard keys have fixed shards per the CMCD specification and cannot be re-routed. The option has no effect in query transmission mode or on event reports (which POST a body and have no header shards).

    State-change events (PLAY_STATE, PLAYBACK_RATE, CONTENT_ID, BACKGROUNDED_MODE, BITRATE_CHANGE) are fired automatically by update() whenever a tracked field's value differs from the last reported value. Use update() as the canonical entry point.

    import { CmcdEventType } from "@svta/cml-cmcd";

    reporter.update({ sta: "p" }); // → fires PLAY_STATE
    reporter.update({ pr: 1.5 }); // → fires PLAYBACK_RATE
    reporter.update({ cid: "movie-42" }); // → fires CONTENT_ID
    reporter.update({ bg: true }); // → fires BACKGROUNDED_MODE
    reporter.update({ br: [5000] }); // → fires BITRATE_CHANGE

    // Consecutive updates with the same value are deduplicated.
    reporter.update({ sta: "p" }); // dropped (unchanged)

    Auto-fired events emit whatever is currently in the reporter's persistent data store. Snapshot context can be attached either by combining the continuous metrics with the state field in a single update() call:

    reporter.update({ sta: "p", bl: [3000], mtp: [8500], pt: 12500 });
    // → fires PLAY_STATE with sta="p", bl=[3000], mtp=[8500], pt=12500

    …or by persisting them via earlier update() calls so they're already in the data store when the state field changes:

    // Keep metrics fresh as the player computes them
    reporter.update({ bl: [3000], mtp: [8500], pt: 12500 });

    // Later, when state changes
    reporter.update({ sta: "p" });
    // → fires PLAY_STATE with sta="p", bl=[3000], mtp=[8500], pt=12500

    The same pattern keeps TIME_INTERVAL reports useful: those events are emitted by the reporter on a timer and carry whatever is in the reporter's data store at that moment, with no caller hook for per-event data. Keep continuous metrics fresh via update() as they change in the player.

    The second call's payload is dropped by dedup:

    reporter.update({ sta: "p" });                              // auto-fires PLAY_STATE
    reporter.recordEvent(CmcdEventType.PLAY_STATE, { // suppressed:
    sta: "p", // sta unchanged
    bl: [3000], // bl never emitted
    });

    Use the single-call form shown above instead.

    recordEvent() is for events whose payload is intrinsic to the event call — they don't represent a persisted state transition:

    // Custom event: the name and any payload only make sense for this call.
    // See "Custom Events" below for the required target configuration.
    reporter.recordEvent(CmcdEventType.CUSTOM_EVENT, { cen: "ad-quartile" });

    // Error: ec carries the player error code(s). Per CTA-5004-B the list
    // notation is required even for a single code.
    reporter.recordEvent(CmcdEventType.ERROR, { ec: ["FATAL"] });

    // Ad lifecycle, mute/unmute, player expand/collapse, skip.
    reporter.recordEvent(CmcdEventType.AD_START, { /* ad metadata */ });
    reporter.recordEvent(CmcdEventType.MUTE);

    For RESPONSE_RECEIVED, prefer recordResponseReceived() (see below) — it derives the per-response fields for you.

    Note

    The response-received keys (url, rc, ttfb, ttlb, ttfbb, cmsdd, cmsds, smrt) are only valid on RESPONSE_RECEIVED reports. If passed in the data of any other event, they are stripped even when listed in enabledKeys. Custom hyphenated keys are not affected.

    Custom events (e=ce) report player-defined events by name via the cen key (a string of at most 64 characters). Two configuration rules apply:

    • An event target only receives custom events if CmcdEventType.CUSTOM_EVENT is listed in its events array — otherwise the event is silently dropped for that target.
    • cen is always included on custom events and does not need to be in enabledKeys, but any additional payload — including custom keys — is still subject to the target's enabledKeys.
    import { CmcdReporter, CmcdEventType } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    cid: "video-123",
    eventTargets: [
    {
    url: "https://analytics.example.com/cmcd",
    // CUSTOM_EVENT must be listed for the target to receive it
    events: [CmcdEventType.CUSTOM_EVENT],
    // cen is force-included; the custom payload key must be enabled
    enabledKeys: ["sid", "cid", "com.example-quartile"],
    },
    ],
    });

    reporter.recordEvent(CmcdEventType.CUSTOM_EVENT, {
    cen: "ad-quartile",
    "com.example-quartile": "q3",
    });

    // The event report contains:
    // cen="ad-quartile",cid="video-123",com.example-quartile="q3",e=ce,sid=…,ts=…,v=2

    Per CTA-5004-B, when transferring a value with a custom event, the chosen names SHOULD associate the custom key name with the custom event name (as com.example-quartile does with ad-quartile above). See Custom Keys for the naming rules.

    The recordResponseReceived() method provides a convenient way to record RESPONSE_RECEIVED events with automatic derivation of timing metrics from the HTTP response. This method is typically called after a segment request completes.

    import { CmcdReporter, CmcdEventType } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    cid: "video-123",
    enabledKeys: ["br", "d", "ot", "url", "rc", "ttfb", "ttlb"],
    eventTargets: [
    {
    url: "https://analytics.example.com/cmcd",
    events: [CmcdEventType.RESPONSE_RECEIVED],
    enabledKeys: ["url", "rc", "ttfb", "ttlb", "br", "d", "ot"],
    },
    ],
    });

    The method accepts a CommonMediaResponse object and automatically derives the following keys:

    Key Description Source
    url The requested URL (without CMCD) response.request.url (CMCD query param stripped)
    rc HTTP response status code response.status
    ts Request initiation timestamp response.resourceTiming.startTime
    ttfb Time to first byte (ms) responseStart - startTime
    ttlb Time to last byte (ms) response.resourceTiming.duration
    // After receiving a response
    const response = {
    status: 200,
    request: decoratedRequest,
    resourceTiming: {
    startTime: performance.now(),
    responseStart: performance.now() + 50,
    duration: 150,
    },
    };

    reporter.recordResponseReceived(response);

    For full request/response tracking, use createRequestReport() before the request and recordResponseReceived() after the response. The CMCD data from the original request is automatically included in the response event:

    async function fetchSegment(
    url: string,
    segmentInfo: SegmentInfo,
    ): Promise<ArrayBuffer> {
    // Update player state
    reporter.update({
    bl: [this.getBufferLength()],
    mtp: [this.getMeasuredThroughput()],
    });

    // Decorate the request with CMCD data
    const request = reporter.createRequestReport(
    { url, method: "GET" },
    {
    br: [segmentInfo.bitrate],
    d: segmentInfo.duration,
    ot: segmentInfo.type,
    },
    );

    // Fetch the segment and capture timing
    const startTime = performance.now();
    const response = await fetch(request.url, request);
    const responseStart = performance.now();
    const buffer = await response.arrayBuffer();
    const duration = performance.now() - startTime;

    // Record the response received event
    reporter.recordResponseReceived({
    status: response.status,
    request,
    resourceTiming: {
    startTime,
    responseStart,
    duration,
    },
    });

    return buffer;
    }

    You can supply additional CMCD keys that cannot be auto-derived, such as server-provided metrics:

    // Include server-reported metrics from response headers
    const serverDeliveryDuration = parseFloat(
    fetchResponse.headers.get("X-Server-Duration") || "0",
    );

    reporter.recordResponseReceived(response, {
    ttfbb: 25, // Time to first body byte (player-measured)
    cmsdd: serverDeliveryDuration, // CMS delivery duration (from server)
    cmsds: 1500, // CMS delivery speed (from server)
    smrt: 2000, // Server measured round-trip time (from server)
    });

    Values provided in the data parameter override any auto-derived values.

    Use the createRequestReport() method to add CMCD data to segment requests. This method returns a new request object with CMCD data added via query parameters or headers (depending on configuration).

    import { CmcdReporter, CMCD_QUERY } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    transmissionMode: CMCD_QUERY,
    });

    // Update CMCD data before the request
    reporter.update({
    br: [5000],
    bl: [25000],
    d: 4000,
    ot: "v",
    });

    // Create the original request
    const request = {
    url: "https://cdn.example.com/video/segment_001.m4s",
    method: "GET",
    headers: {},
    };

    // Create the decorated request
    const decoratedRequest = reporter.createRequestReport(request);

    // Use the decorated request with your HTTP client
    fetch(decoratedRequest.url, decoratedRequest);
    import { CmcdReporter, CMCD_HEADERS } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    transmissionMode: CMCD_HEADERS,
    });

    reporter.update({
    br: [5000],
    bl: [25000],
    d: 4000,
    });

    const request = {
    url: "https://cdn.example.com/video/segment_001.m4s",
    method: "GET",
    headers: {},
    };

    const decoratedRequest = reporter.createRequestReport(request, { ot: "v" });

    // decoratedRequest.headers will contain:
    // {
    // 'CMCD-Object': 'br=(5000),d=4000,ot=v',
    // 'CMCD-Request': 'bl=(25000)',
    // }

    Call start() to begin automatic TIME_INTERVAL event reporting:

    reporter.start();
    

    This starts an interval timer that automatically records TIME_INTERVAL events based on the configured interval.

    Call stop() to stop automatic reporting:

    reporter.stop();
    

    Call flush() to immediately send all queued events, regardless of batch size:

    // Send all pending events (useful when playback ends)
    reporter.flush();
    const reporter = new CmcdReporter({
    cid: "video-123",
    eventTargets: [
    {
    url: "https://analytics.example.com/cmcd",
    events: [CmcdEventType.TIME_INTERVAL, CmcdEventType.PLAY_STATE],
    interval: 30,
    batchSize: 3,
    },
    ],
    });

    // Start reporting when playback begins
    function onPlaybackStart() {
    reporter.start();
    }

    // Stop reporting and flush when playback ends
    function onPlaybackEnd() {
    reporter.stop();
    reporter.flush();
    }

    // Clean up when the player is destroyed
    function onPlayerDestroy() {
    reporter.stop();
    reporter.flush();
    }

    By default, CmcdReporter uses the native fetch API to send event reports. You can provide a custom requester function:

    import { CmcdReporter } from "@svta/cml-cmcd";

    const customRequester = async (request) => {
    // Use your preferred HTTP client
    const response = await axios({
    url: request.url,
    method: request.method,
    headers: request.headers,
    data: request.body,
    });
    return { status: response.status };
    };

    const reporter = new CmcdReporter(
    {
    cid: "video-123",
    eventTargets: [
    {
    url: "https://analytics.example.com/cmcd",
    events: [CmcdEventType.TIME_INTERVAL],
    },
    ],
    },
    customRequester,
    );

    You can limit which CMCD keys are included in reports using the enabledKeys option:

    import { CmcdReporter, CMCD_KEYS } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    cid: "video-123",
    // Only include these keys in request reports
    enabledKeys: ["br", "bl", "d", "ot", "sid", "cid"],
    eventTargets: [
    {
    url: "https://analytics.example.com/cmcd",
    events: [CmcdEventType.TIME_INTERVAL],
    // Override enabled keys for this specific target
    enabledKeys: ["br", "bl", "sid", "cid"],
    },
    ],
    });
    Note

    Certain keys, such as v (version), are required by the CMCD specification and will always be included in reports regardless of the enabledKeys setting. The enabledKeys property cannot be used to disable these keys.

    enabledKeys decides which keys a destination may ever receive. When a decision depends on the individual report — this request, this response, this collector — use a transform instead. A transform is a synchronous function that receives the assembled CMCD data plus the associated media request, and returns the data to send it or null to cancel the report:

    import { CmcdEventType, CmcdReporter } from "@svta/cml-cmcd";

    const reporter = new CmcdReporter({
    cid: "video-123",
    enabledKeys: ["br", "bl", "sid", "cid", "v", "sn"],
    // Request mode: never decorate license requests with CMCD
    transform: (data, request) =>
    request.url.includes("/license") ? null : data,
    eventTargets: [
    {
    url: "https://analytics.example.com/cmcd",
    events: [CmcdEventType.RESPONSE_RECEIVED],
    enabledKeys: ["url", "rc", "sid", "cid", "v", "e", "ts", "sn"],
    // Event mode, this target only: report segment responses only
    transform: (data, request) =>
    request?.customData?.["requestType"] === "segment" ? data : null,
    },
    ],
    });

    The bracket access on the last line is the default, because the library cannot know the player's shape. Typing customData removes it.

    Placement determines scope, the same way it does for enabledKeys:

    Placement Applies to
    transform at the top level Request reports from createRequestReport()
    transform on an event target Event reports bound for that target

    There is deliberately no single hook spanning both paths. An event report carries its own type in data.e, the target's configuration is in scope where you write its transform, and anything else the transform needs can be closed over. Policy that applies in more than one place is a shared function you reference from each placement:

    import type { Cmcd } from "@svta/cml-cmcd";
    import { CmcdEventType } from "@svta/cml-cmcd";

    const sampled = Math.random() < 0.1;

    const sampleIntervals = (data: Cmcd): Cmcd | null =>
    data.e === CmcdEventType.TIME_INTERVAL && !sampled ? null : data;

    Composing several concerns at one placement is ordinary function composition, which you own:

    import type { Cmcd } from "@svta/cml-cmcd";
    import type { HttpRequest } from "@svta/cml-utils";

    const scrubPii = (data: Cmcd, request: HttpRequest | undefined): Cmcd | null =>
    request?.url.includes("/private/") ? null : { ...data, cid: undefined };

    const transform = (data: Cmcd, request: HttpRequest | undefined): Cmcd | null => {
    const scrubbed = scrubPii(data, request);
    return scrubbed === null ? null : sampleIntervals(scrubbed);
    };

    The second argument is the media request the report belongs to. In request mode it is always present, and it is the object you passed to createRequestReport() — mutating it does not change the report that comes back. In event mode it is present for events recorded through recordResponseReceived() and undefined for everything else, including state-change events fired by update() and periodic TIME_INTERVAL reports.

    This is where player-specific taxonomy belongs. CMCD has no concept of a "segment request" or an "init request", so a player that wants to filter on one puts it on request.customData and reads it back in the transform. For the narrower question of manifest versus media, the ot key already answers it in pure CMCD terms when the player populates it: data.ot === "m" is a manifest.

    The request is a read-only view (CmcdTransformRequest). It is context for the decision, not something to change: every member is readonly. Two caveats the types cannot cover. A mutable body such as FormData or URLSearchParams has mutating methods of its own, and JavaScript callers get no compile-time enforcement. Mutating the request either way is unsupported, and the outgoing report may reflect it, so treat the request as immutable regardless of what the compiler can prove.

    By default customData values are unknown, which is what forces the bracket access above. Describe the player's shape once and those reads become ordinary dot access, checked at compile time. Annotating a single transform is enough: the rest of the configuration infers the same type, including the top-level transform.

    import { CmcdEventType, CmcdReporter } from "@svta/cml-cmcd";
    import type { CmcdEventReportTransform } from "@svta/cml-cmcd";

    // The player's own request taxonomy, carried on customData.
    type PlayerData = { requestType: "segment" | "manifest" | "license" };

    // The only annotation in the configuration.
    const segmentsOnly: CmcdEventReportTransform<PlayerData> = (data, request) =>
    request?.customData?.requestType === "segment" ? data : null;

    const reporter = new CmcdReporter({
    cid: "video-123",
    enabledKeys: ["br", "bl", "sid", "cid", "v", "sn"],
    // Not annotated, but `customData` is typed here too, so a misspelt
    // "licence" is a compile error rather than a filter that never matches.
    transform: (data, request) =>
    request.customData?.requestType === "license" ? null : data,
    eventTargets: [
    {
    url: "https://analytics.example.com/cmcd",
    events: [CmcdEventType.RESPONSE_RECEIVED],
    enabledKeys: ["url", "rc", "sid", "cid", "v", "e", "ts", "sn"],
    transform: segmentsOnly,
    },
    ],
    });

    new CmcdReporter<PlayerData>({ ... }) states the same thing explicitly, which is the clearer option when no transform is annotated. Either way the cost is one annotation per reporter, not one per transform. Omit it entirely and nothing changes from the previous section: values stay unknown and bracket access still works.

    Once a reporter has a type, createRequestReport() and recordResponseReceived() require the requests you pass to satisfy it. A typo or a request from a different code path is a compile error at the call site, rather than a transform that reads undefined and silently cancels the report:

    // Error: 'requestTypo' does not exist in type 'PlayerData'
    reporter.createRequestReport({
    url: "https://cdn.example.com/segment.mp4",
    customData: { requestTypo: "segment" },
    });

    A reporter left on the default requires nothing and accepts any customData, exactly as before.

    customData is readonly at every depth, not just at the top level, so describing a nested shape does not cost you the read-only guarantee:

    type PlayerData = { timing: { start: number } };

    const transform: CmcdEventReportTransform<PlayerData> = (data, request) => {
    const customData = request?.customData;

    // Error: cannot assign to 'start' because it is a read-only property
    if (customData) customData.timing.start = 0;

    return data;
    };

    The first argument is a copy made for this one report, so you can mutate it in place without affecting the reporter's persistent data or the reports going to other targets. That holds for nested values too: array keys such as br and ec are copied, so data.ec.push("E100") is safe, and so is changing the params of an SfItem inside one.

    The reporter re-stamps e and assigns sn and msd after your transform returns, so a transform cannot change a report's event type to slip past a target's events filter, cannot create gaps in sequence numbering, and cannot replay the media-start-delay marker. Cancelling a report consumes neither a sequence number nor msd: wire sn values stay contiguous per destination, and msd rides the next report that is actually sent.

    A transform also cannot remove a key the event requires. Every event needs e and ts; state-change events need the field they signal (sta, pr, cid, bg, br), custom events need cen, error events need ec, and response-received events need url. If your transform drops one of these, the reporter puts back the value it had beforehand. It does not invent values: a required key that was already missing before your transform ran stays missing, since that is a bug at the call site rather than something the transform did.

    Required keys cannot be configured away either. They are force-included after the enabledKeys filter, so omitting one from enabledKeys does not suppress it, it just leaves the payload valid. If a destination must not receive the field an event carries, leave that event out of the target's events rather than trying to strip the key.

    enabledKeys is still the wire allowlist and still runs after the transform. A key your transform adds must also be enabled at the same placement to reach the collector, and a key it removes stays removed unless the event requires it.

    Cancelling a state-change report does not roll back dedup. The transition still happened; the transform only suppressed its transmission, so the next update() with the same value is still deduplicated.

    Transforms shape CMCD data only. They cannot modify the outgoing HTTP request.

    Important

    Transforms must not throw. Exceptions propagate to whatever called into the reporter, which for TIME_INTERVAL events is the interval timer and surfaces as an unhandled error. The library does not swallow them: failing open would leak exactly the data a redaction transform exists to remove, and failing closed would make data loss undebuggable. Wrap risky logic in try/catch and choose explicitly, returning the data to fail open or null to fail closed.

    A throw is isolated to the target whose transform threw. The remaining targets still receive the report, queued batches are still sent, and the error reaches your code once the reporter has finished with the event. Only the throwing target loses its report, and it consumes no sequence number doing so. Without that isolation a single throwing transform would starve every target configured after it, because the state-change dedup baseline commits before the reporter fans out to targets and is never rolled back.

    Property Type Default Description
    sid string Auto-generated UUID Session ID
    cid string undefined Content ID
    version CmcdVersion CMCD_V2 CMCD protocol version
    transmissionMode CmcdTransmissionMode 'query' How to transmit CMCD data in request mode
    customHeaderMap Partial<CmcdHeaderMap> undefined Routes custom keys into specific CMCD header shards in headers mode.
    enabledKeys CmcdKey[] undefined Keys to include in request reports. If not provided, no keys will be reported. Custom keys must be listed explicitly.
    eventTargets CmcdEventReportConfig[] [] Event reporting targets
    transform CmcdRequestReportTransform undefined Transforms or cancels each request report. See Transforming and Cancelling Reports and Typing customData.
    Property Type Default Description
    url string Required Analytics endpoint URL
    events CmcdEventType[] undefined Events to report. If no events are provided, the target is disabled.
    interval number 30 Seconds between TIME_INTERVAL reports
    batchSize number 1 Events to batch before sending
    version CmcdVersion CMCD_V2 CMCD version for this target (must be v2 or higher)
    enabledKeys CmcdKey[] undefined Keys to include for this target. If not provided, no keys will be reported. Custom keys must be listed explicitly.
    transform CmcdEventReportTransform undefined Transforms or cancels each of this target's event reports. See Typing customData.