Applying Variants
Episode 7: a variant is a list of modifications applied to elements the SDK must first find, and every step has a named failure
Copy this guide and your current settings as a prompt for your AI agent.
Preview agent promptSee the guide, code and settings included when you copy.
Every episode so far ended at the same sentence: and then the variant is applied. This one opens that. A variant is not a page. It is a list of modifications, each pointed at an element the SDK has to find first, in a document that may have changed since the selector was recorded. Finding is where it gets interesting.
On this page
01What a variant is
A variant carries modifications keyed by element id. The frame carries
elements, the selectors for those ids. The loop (applyVariantChanges in
sdk-js/src/dom.ts) runs over the modifications, not the elements. That is
deliberate: a modification pointing at an element id the frame no longer
describes still gets caught and logged instead of silently doing nothing. An
empty modification list is the original variant: it signals rendered and
returns.
02Finding the element
Each element id carries a list of selectors, tried in order; first hit wins.
A selector is stricter than you might expect (getElementUniversal in
shared/universalSelector.ts): an XPath that matches zero nodes fails, and an
XPath that matches more than one also fails. Exactly one, or it is not a
match. Selectors can nest with and and or: an or returns the first that
matches, an and requires all of them and returns the last.
03When it is not found
When no selector matches, the SDK logs element_not_found with the element
id, every selector it tried, and the variant and experiment ids, then
continues to the next modification. One missing element does not abandon the
rest of the variant. This is the single most common defect in the field, and
it is usually the page that changed, not the selector that was wrong.
04Update
Three modification types; update is the common one, and its rules are
specific (applyUpdateModification in shared/dom.ts):
innerHTMLwins overtextContentif both are present.textContenton an input button setsvalue, not text.srcis set for images.- Styles are converted, then applied with
!important, but only where the value actually differs from what is already there. - Anything else in the attribute bag is logged as
unknown_attributes, not silently dropped.
05Delete and execute
delete removes the node from its parent. execute_js is the escape hatch:
arbitrary JavaScript for anything the other two cannot express. One subtlety:
update and delete both signal that rendering is done. execute_js does
not. It hands that responsibility to the variant’s own script, because only
that script knows when it is finished.
06How the JavaScript actually runs
Not with eval. The SDK builds a <script> element, sets a nonce if your
policy needs one, appends it to <head>, and immediately removes it
(executeJs in shared/dom.ts). Two details earn their place: the variant’s
code is wrapped with a leading newline, so a payload ending in a line comment
cannot swallow the closing brackets; and a marker is set at the very end. If
that marker is missing the script never parsed, and the SDK raises a
SyntaxError instead of reporting success.
07Stamping and revealing
Every edited element gets stamped coframe-exp-id, set to the variant id.
That stamp is what episode 5’s view
tracking looks for, and what you look for in a live page to see whether a
variant actually landed. Every path through this code calls
variantRendered, which is the command the reveal in
episode 3 is waiting on.
08The failure map
| Name | Cause |
|---|---|
element_not_found |
The selector matched nothing |
element_not_edited |
Found, but the edit did not take |
no_modifications_for_element |
An element id with nothing to do |
unknown_modification_type |
A payload this bundle does not understand |
unknown_attributes |
An update carrying keys the applier ignores |
Five names, five distinct causes, all in the monitoring stream, all searchable.
09In short
Find the element, exactly one or nothing. Apply one of three modification types. Stamp it. Signal rendered. When any of that fails, fail by name and keep going.
10Where in the code
| File | What to look for |
|---|---|
sdk/sdk-js/src/dom.ts |
applyVariantChanges, editElement, querySelector nesting |
sdk/shared/dom.ts |
applyUpdateModification, executeJs |
sdk/shared/universalSelector.ts |
getElementUniversal |
# Applying Variants Episode 7: a variant is a list of modifications applied to elements the SDK must first find, and every step has a named failure Every episode so far ended at the same sentence: and then the variant is applied. This one opens that. A variant is not a page. It is a list of modifications, each pointed at an element the SDK has to find first, in a document that may have changed since the selector was recorded. Finding is where it gets interesting. [Video: Video: Runtime Walkthrough, episode 7: what a variant is, finding the element, when it is not found, update, delete and execute, how the JS runs, stamping and revealing, and the failure map (3m 51s, narrated)](assets/videos/sdk-internals-07-applying-variants.mp4) Watch: 3m 51s — Runtime Walkthrough, episode 7: what a variant is, finding the element, when it is not found, update, delete and execute, how the JS runs, stamping and revealing, and the failure map ## What a variant is A variant carries modifications keyed by element id. The frame carries elements, the selectors for those ids. The loop (`applyVariantChanges` in `sdk-js/src/dom.ts`) runs over the modifications, not the elements. That is deliberate: a modification pointing at an element id the frame no longer describes still gets caught and logged instead of silently doing nothing. An empty modification list is the original variant: it signals rendered and returns. ## Finding the element Each element id carries a list of selectors, tried in order; first hit wins. A selector is stricter than you might expect (`getElementUniversal` in `shared/universalSelector.ts`): an XPath that matches zero nodes fails, and an XPath that matches more than one also fails. Exactly one, or it is not a match. Selectors can nest with `and` and `or`: an `or` returns the first that matches, an `and` requires all of them and returns the last. ## When it is not found When no selector matches, the SDK logs `element_not_found` with the element id, every selector it tried, and the variant and experiment ids, then continues to the next modification. One missing element does not abandon the rest of the variant. This is the single most common defect in the field, and it is usually the page that changed, not the selector that was wrong. ## Update Three modification types; `update` is the common one, and its rules are specific (`applyUpdateModification` in `shared/dom.ts`): - `innerHTML` wins over `textContent` if both are present. - `textContent` on an input button sets `value`, not text. - `src` is set for images. - Styles are converted, then applied with `!important`, but only where the value actually differs from what is already there. - Anything else in the attribute bag is logged as `unknown_attributes`, not silently dropped. ## Delete and execute `delete` removes the node from its parent. `execute_js` is the escape hatch: arbitrary JavaScript for anything the other two cannot express. One subtlety: `update` and `delete` both signal that rendering is done. `execute_js` does not. It hands that responsibility to the variant’s own script, because only that script knows when it is finished. ## How the JavaScript actually runs Not with `eval`. The SDK builds a `<script>` element, sets a nonce if your policy needs one, appends it to `<head>`, and immediately removes it (`executeJs` in `shared/dom.ts`). Two details earn their place: the variant’s code is wrapped with a leading newline, so a payload ending in a line comment cannot swallow the closing brackets; and a marker is set at the very end. If that marker is missing the script never parsed, and the SDK raises a `SyntaxError` instead of reporting success. ## Stamping and revealing Every edited element gets stamped `coframe-exp-id`, set to the variant id. That stamp is what [episode 5](../docs/sessions-and-delivery.html)’s view tracking looks for, and what you look for in a live page to see whether a variant actually landed. Every path through this code calls `variantRendered`, which is the command the reveal in [episode 3](../docs/boot-and-config.html) is waiting on. ## The failure map Name | Cause | `element_not_found` | The selector matched nothing | `element_not_edited` | Found, but the edit did not take | `no_modifications_for_element` | An element id with nothing to do | `unknown_modification_type` | A payload this bundle does not understand | `unknown_attributes` | An update carrying keys the applier ignores | Five names, five distinct causes, all in the monitoring stream, all searchable. ## In short Find the element, exactly one or nothing. Apply one of three modification types. Stamp it. Signal rendered. When any of that fails, fail by name and keep going. ## Where in the code File | What to look for | `sdk/sdk-js/src/dom.ts` | `applyVariantChanges`, `editElement`, `querySelector` nesting | `sdk/shared/dom.ts` | `applyUpdateModification`, `executeJs` | `sdk/shared/universalSelector.ts` | `getElementUniversal` |