A tracking request containing [PAGEURL] tells you that a substitution did not happen before that request left the system. It does not tell you which component should have performed it.
That ownership question becomes important when a VAST chain crosses a player, an ad server and a server-side insertion service. A template may legitimately pass through several systems before the component making the final HTTP request has the right context to expand it.
VAST assigns macro replacement to the party performing the request. A useful debugging record therefore names the request owner and preserves both the template and its expanded form.
Follow the component that sends the request
For each URI in a test run, record the source document, event context, component making the request and substitutions expected at that point. A player-generated request and a server-generated request can have different available context.
Avoid a request-wide macro dictionary that assumes every component knows every value. If the server does not know the page URL or the player has not observed the relevant event yet, that absence needs deliberate handling under the macro's definition and your integration contract.
Also distinguish a template from a network observation. Brackets in an unexecuted template are not proof of a production defect. A literal supported token in the final request needs investigation against the expectations for that request.
Encode the value at the URI boundary
Nested URLs make a useful regression case because they contain characters with meaning in the outer URL. An unencoded ampersand can turn part of the nested page URL into a separate tracker parameter.
This complete Node.js program handles one scalar macro. It performs no network requests:
import assert from 'node:assert/strict';
function expandPageUrl(template, pageUrl) {
if (typeof pageUrl !== 'string' || pageUrl.length === 0) {
throw new TypeError('page URL required for this test');
}
return template.replaceAll('[PAGEURL]', encodeURIComponent(pageUrl));
}
const page = 'https://publisher.example/watch?id=7&mode=full#chapter-2';
const template = 'https://tracker.example/view?page=[PAGEURL]&source=video';
const expanded = expandPageUrl(template, page);
const parsed = new URL(expanded);
assert.equal(parsed.searchParams.get('page'), page);
assert.equal(parsed.searchParams.get('source'), 'video');
assert.equal(parsed.searchParams.has('mode'), false);
assert.equal(parsed.hash, '');
assert.equal(expanded.includes('[PAGEURL]'), false);
const unencoded = new URL(template.replace('[PAGEURL]', page));
assert.notEqual(unencoded.searchParams.get('page'), page);
const doubleEncoded = new URL(expandPageUrl(template, encodeURIComponent(page)));
assert.notEqual(doubleEncoded.searchParams.get('page'), page);
assert.throws(() => expandPageUrl(template, ''));
The outer parser should recover the original nested URL as one parameter value. The tests also ensure that mode and the fragment remain inside that value rather than becoming outer URL structure.
The double-encoding case is useful when two components both believe they own substitution. The final URL can look safely escaped while the receiver obtains an encoded string instead of the original value.
Keep XML escaping separate from URI encoding
A URI embedded in XML has another representation layer. XML escaping allows the document to contain reserved markup characters. URI encoding protects a value inside the URI. Applying one does not automatically satisfy the other.
Inspect the value after XML parsing when diagnosing URI behavior. If an ampersand appears as & in the source document, determine whether the parser decoded it before the request builder ran. Expanding or comparing raw XML text can mistake the document representation for the request value.
For a regression fixture, retain the XML source, parsed URI template and final expanded request separately. That gives each transformation an input and expected output instead of one opaque end-to-end string comparison.
Use the macro definition for its type and context
The sample handles a scalar string. It is not a generic expansion engine. Array-valued macros have their own representation rules, and a blanket encodeURIComponent around an entire list can encode separators that the definition expects to preserve.
Likewise, a macro meaningful in an error callback may not belong in an impression callback. Keep context validation in the expansion plan rather than substituting whatever value happens to exist in a shared object.
Build a supported-macro registry from the contracts you implement. For each entry, record value type, permitted context, source of the value and encoding behavior. Pin the reference revision used by the test so a catalog update does not silently change a historical expectation.
Unsupported and unavailable values also need explicit behavior. Follow the applicable definition and partner contract. Replacing every unknown token with an empty string can conceal a missing implementation and produce misleading tracking requests.
Add a request-owner trace to the failing fixture
Suppose a wrapper introduces a tracking URI, a server copies it into a stitched manifest and a player later fires it. Record where ownership changes and which context survives each handoff.
If the player is the final requester, an earlier server-side trace should show a template awaiting player-owned substitution. If the server fires the request, it needs to supply the values it owns at that moment.
Include repeated macro occurrences in the fixture. A single-call replace can expand the first token and leave the second untouched. Also test a value containing a percent sign, spaces and a nested query so the test covers actual parsing behavior.
Keep these tests local. Debugging expansion does not require firing impression, error or verification tracking URLs. A captured URL and a parsed assertion provide useful evidence without creating artificial delivery signals.
I maintain VASTlint. The macro reference helps review value shapes and contexts. Pair it with the request-owner trace so the resulting ticket identifies the component that has the information and responsibility to fix the expansion.
Sources
Technical sources checked October 25, 2026. Drafted with AI; examples are synthetic unless explicitly stated otherwise.
Top comments (0)