fix(browser-plugin-media-tracking): omit media_element entity when no source is attached - #1503
Merged
Matus Tomlein (matus-tomlein) merged 4 commits intoSep 9, 2026
Conversation
… source is attached The org.whatwg/media_element schema requires currentSrc and constrains it to `format: uri`, which an empty string does not satisfy. Both currentSrc and src return empty while no source is attached (networkState NETWORK_EMPTY) — most notably between element creation and MediaSource attachment in MSE players such as hls.js and dash.js, where tracking commonly starts. Ping events fire on a wall-clock timer from that point regardless of readiness, so the invalid entity recurs for the whole pre-attachment window and the events are dropped as schema violations. Omit the entity while there is no source to describe rather than emitting one the pipeline rejects. The entity is resolved per event, so it reappears as soon as a source attaches and is omitted again if the element returns to having no source. Dynamic context callbacks already permit null and resolveDynamicContext filters them out, so the media event and all other entities are unaffected. Also replace the 'DATA_URL' placeholder with 'data:'. That sentinel was a second, independent violation of the same `format: uri` constraint: a bare string with no scheme fails validation for the same reason an empty string does. Note that blob: URLs were never affected — they are valid absolute URIs and pass validation today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot started reviewing on behalf of
Matus Tomlein (matus-tomlein)
September 9, 2026 09:47
View session
The change is a user-facing behaviour fix in a published package, matching the 'patch' type used for comparable plugin fixes, rather than 'none'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
dataUrlHandler can incorrectly treat non-data URLs containing data: as data URIs, which can mutate valid URLs unexpectedly.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes a schema-validation failure in @snowplow/browser-plugin-media-tracking by omitting the org.whatwg/media_element entity when an HTMLMediaElement has no source attached (i.e., currentSrc/src are empty), preventing events from being dropped as bad rows.
Changes:
- Make
buildHTMLMediaElementEntityreturnnull(and therefore omit the entity) while no media source is attached, and ensurecurrentSrc/srcdon’t serialize as''. - Replace the data-URI placeholder from
'DATA_URL'to a URI-valid placeholder ('data:') and update tests accordingly. - Widen the player HTML context callback typing to allow nullable dynamic contexts.
File summaries
| File | Description |
|---|---|
| plugins/browser-plugin-media-tracking/tests/media.test.ts | Adds/updates unit tests for omitted entity behavior and the new data-URI placeholder. |
| plugins/browser-plugin-media-tracking/src/player.ts | Allows nullable dynamic context callbacks so the media_element entity can be omitted when needed. |
| plugins/browser-plugin-media-tracking/src/helperFunctions.ts | Introduces a URI-valid data-URI placeholder constant and uses it in dataUrlHandler. |
| plugins/browser-plugin-media-tracking/src/entities.ts | Omits media_element entity when there is no source, and avoids empty-string URI fields. |
| common/changes/@snowplow/browser-plugin-media-tracking/media-element-empty-src_2026-09-09-10-30.json | Adds changelog entry for the behavioral fix. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dataUrlHandler used a substring check, which was wrong in both directions: it replaced valid URLs that merely contain 'data:' somewhere (a path segment such as 'metadata:9' matches) and it missed an uppercase 'DATA:' scheme, shipping the large base64 payload the function exists to avoid. URI schemes are case-insensitive per RFC 3986. Match /^data:/i instead, and cover both directions with tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matus Tomlein (matus-tomlein)
changed the base branch from
master
to
release/4.10.2
September 9, 2026 11:32
Matus Tomlein (matus-tomlein)
deleted the
claude/empty-strings-blob-prefixes-1d2612
branch
September 9, 2026 11:38
Merged
Matus Tomlein (matus-tomlein)
added a commit
that referenced
this pull request
Sep 9, 2026
… source is attached (#1503) ## Problem The `org.whatwg/media_element` schema requires `currentSrc` and constrains it to `format: uri`, which an **empty string does not satisfy**. Both `currentSrc` and `src` return empty while no source is attached (`networkState` = `NETWORK_EMPTY`), so the entity emitted in that window is rejected as a schema violation and the whole event is dropped as a bad row. The window matters more than it first appears: 1. **Media Source Extensions (MSE) players never set a `src` attribute.** [MSE](https://www.w3.org/TR/media-source-2/) is the browser API behind all adaptive streaming (HLS/DASH): JavaScript fetches the media itself and feeds bytes to the element, rather than the browser loading a file from a URL. The player creates a `MediaSource`, assigns it via `URL.createObjectURL()` — which yields a `blob:` URL — and attaches it. Between element creation and that attachment, both `currentSrc` and `src` are `''`. This affects hls.js, dash.js, Shaka Player and Video.js; a plain `<video src="movie.mp4">` is never affected. 2. **Tracking commonly starts inside that window** — `startMediaTracking` is called synchronously in `setUpListeners`. 3. **Ping events fire on a wall-clock timer** from that point regardless of `readyState`, so the invalid entity recurs for the entire pre-attachment window rather than once. Reported by a customer running js-4.6.6 with ~17.5k `schema_violations` bad rows over 7 days, during a limited test-phase rollout. ### `blob:` URLs were never affected The original report also claimed `blob:` URLs fail validation. They don't, and no change is needed for them. Verified two ways: - Enrich validates via `com.networknt:json-schema-validator` (pinned 1.5.8 through iglu-scala-client 4.2.1). Its `format: uri` is `UriFormat` → `new java.net.URI(value)` → `uri.isAbsolute()`. `blob:https://…` parses as scheme `blob` with an opaque scheme-specific part, so `isAbsolute()` is `true` → **passes**. An empty string parses but has no scheme → **fails**. - The customer's own successfully-tracked events contain `blob:https://…` values. Worth noting for future schema work: `format` is a **hard assertion** here, not an annotation — Iglu leaves `formatAssertionsEnabled` unset and the library defaults it to `true` for dialects below draft 2019-09. ## Fix Omit the entity while there is no source to describe, rather than emitting one the pipeline rejects. - **`entities.ts`** — `buildHTMLMediaElementEntity` now returns `SelfDescribingJson | null`, returning `null` when neither `currentSrc` nor `src` has a value. `currentSrc`, `src`, and `fileExtension` all derive from two locals so no field can serialize as `''`. - **`helperFunctions.ts`** — the data-URI placeholder is now `'data:'` instead of `'DATA_URL'`. That sentinel was a **second, independent** violation of the same `format: uri` constraint: a bare string with no scheme fails for the same reason an empty string does. It has no timing component, so it would have kept producing bad rows even after the empty-string fix. - **`player.ts`** — `htmlContext` widened to the nullable callback type. This needs no changes to the context plumbing: `DynamicContext` callbacks are already typed to return `SelfDescribingJson | null`, and `resolveDynamicContext` ends with `.filter(Boolean)`. Returning `null` drops only that entity — the media event and every other entity are unaffected. Because the context is resolved **per event**, this also handles the mid-session cases a caller-side timing workaround cannot: a source swap, `load()`, or playlist advance that returns the element to `NETWORK_EMPTY` re-omits the entity, and it reappears automatically once a source reattaches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
org.whatwg/media_elementschema requirescurrentSrcand constrains it toformat: uri, which an empty string does not satisfy. BothcurrentSrcandsrcreturn empty while no source is attached (networkState=NETWORK_EMPTY), so the entity emitted in that window is rejected as a schema violation and the whole event is dropped as a bad row.The window matters more than it first appears:
srcattribute. MSE is the browser API behind all adaptive streaming (HLS/DASH): JavaScript fetches the media itself and feeds bytes to the element, rather than the browser loading a file from a URL. The player creates aMediaSource, assigns it viaURL.createObjectURL()— which yields ablob:URL — and attaches it. Between element creation and that attachment, bothcurrentSrcandsrcare''. This affects hls.js, dash.js, Shaka Player and Video.js; a plain<video src="movie.mp4">is never affected.startMediaTrackingis called synchronously insetUpListeners.readyState, so the invalid entity recurs for the entire pre-attachment window rather than once.Reported by a customer running js-4.6.6 with ~17.5k
schema_violationsbad rows over 7 days, during a limited test-phase rollout.blob:URLs were never affectedThe original report also claimed
blob:URLs fail validation. They don't, and no change is needed for them. Verified two ways:com.networknt:json-schema-validator(pinned 1.5.8 through iglu-scala-client 4.2.1). Itsformat: uriisUriFormat→new java.net.URI(value)→uri.isAbsolute().blob:https://…parses as schemeblobwith an opaque scheme-specific part, soisAbsolute()istrue→ passes. An empty string parses but has no scheme → fails.blob:https://…values.Worth noting for future schema work:
formatis a hard assertion here, not an annotation — Iglu leavesformatAssertionsEnabledunset and the library defaults it totruefor dialects below draft 2019-09.Fix
Omit the entity while there is no source to describe, rather than emitting one the pipeline rejects.
entities.ts—buildHTMLMediaElementEntitynow returnsSelfDescribingJson | null, returningnullwhen neithercurrentSrcnorsrchas a value.currentSrc,src, andfileExtensionall derive from two locals so no field can serialize as''.helperFunctions.ts— the data-URI placeholder is now'data:'instead of'DATA_URL'. That sentinel was a second, independent violation of the sameformat: uriconstraint: a bare string with no scheme fails for the same reason an empty string does. It has no timing component, so it would have kept producing bad rows even after the empty-string fix.player.ts—htmlContextwidened to the nullable callback type.This needs no changes to the context plumbing:
DynamicContextcallbacks are already typed to returnSelfDescribingJson | null, andresolveDynamicContextends with.filter(Boolean). Returningnulldrops only that entity — the media event and every other entity are unaffected.Because the context is resolved per event, this also handles the mid-session cases a caller-side timing workaround cannot: a source swap,
load(), or playlist advance that returns the element toNETWORK_EMPTYre-omits the entity, and it reappears automatically once a source reattaches.Why not relax the schema instead
currentSrcmeans "the absolute URL of the media resource";''means there is no media resource. Encoding "absent" as an empty string that satisfies a URI constraint is the underlying modelling error, and relaxing a shared Iglu Central schema would bless it permanently for every tracker and every downstream consumer. Fixing the emit site keeps the contract intact. A companion dbt change that assumed the schema would be relaxed has been closed as unnecessary.Testing
rollup --failAfterWarningsbuild clean;rush buildsucceeds through the dependency chain.buildHTMLMediaElementEntityunit tests cover: entity omitted with no source, built once a source attaches,blob:sources,currentSrc→srcfallback, andsrcnever serializing as''.if (false),omits the entity while no source is attachedfails; restored, it passes. It detects the regression rather than passing vacuously.DATA_URLtest now asserts the valid-URI placeholder.buildHTMLMediaElementEntityanddataUrlHandlerare internal to this plugin — no other consumers in the monorepo.javascript-trackere2e media test is unaffected: it uses real video files with actualsrcattributes, so the guard never fires. Not run locally (needs the browser grid) — worth confirming in CI.Notes for reviewers
'data:'placeholder is a judgment call. It's valid per RFC 3986 and passes the validator, but it's an odd value to land in the warehouse. Alternatives:data:,, or omitting the optionalsrcfield entirely. Happy to change it — I picked the minimal edit that restores validity while preserving the "this was a data URI" signal.buffered[].startis deliberately out of scope. The original report mentions aminimumviolation there and calls it secondary. The likely mechanism isstart || 0intimeRangesToObjectArray, which normalizesNaNbut passes negatives through againstminimum: 0. Needs a real bad row to confirm before fixing.<video src="…">, where this bug cannot occur. That gap is why this surfaced as a support ticket.Closes AISP-1701
🤖 Generated with Claude Code