Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/api/class-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This object represents a single parsed Ad
- `creatives: Array<Object>` [go to object](#creative)
- `extensions: Array<Object>` [go to object](#extension)
- `adVerifications: Array<Object>` [go to object](#ad-verification)
- `hasFailed: Boolean` Indicates that the parsed node has failed to be unwrapped or parsed. Only available with the option keepFailedAdPod on ads with a sequence.

## Creative<a name="creative"></a>

Expand Down Expand Up @@ -229,4 +230,4 @@ This object represents a generic Creative. It's used as a parent object for more

- `url: String|null`,
- `width: String|null`,
- `height: String|null`
- `height: String|null`
179 changes: 179 additions & 0 deletions spec/vast_parser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -757,4 +757,183 @@ describe('VASTParser', () => {
expect(VastParser.getEstimatedBitrate()).toEqual(42);
});
});

describe('keepFailedAdPod option', () => {
describe('when keepFailedAdPod is false (default)', () => {
it('should remove ads with errors from the response', async () => {
fetcher.setOptions({ urlHandler: nodeUrlHandler });
VastParser = new VASTParser({ fetcher });

const parser = new DOMParser();
const adWithSequence = parser.parseFromString(
`<VAST version="4.3">
<Ad sequence="1">
<InLine>
<AdSystem>Test</AdSystem>
<AdTitle>Failed Ad</AdTitle>
<Creatives>
</Creatives>
</InLine>
</Ad>
</VAST>`,
'text/xml'
);

const response = await VastParser.parseVAST(adWithSequence, {
keepFailedAdPod: false
});

expect(response.ads.length).toBe(0);
});

it('should remove ads that failed to unwrap', async () => {
fetcher.setOptions({ urlHandler: nodeUrlHandler });
VastParser = new VASTParser({ fetcher });

const wrapperFailXml = await nodeUrlHandler.get(
'./spec/samples/wrapper-empty-no-creative.xml'
);

const response = await VastParser.parseVAST(wrapperFailXml.xml);

expect(response.ads.length).toBe(0);
});
});

describe('when keepFailedAdPod is true', () => {
it('should keep ads with errors that have a sequence (ad pod)', async () => {
fetcher.setOptions({ urlHandler: nodeUrlHandler });
VastParser = new VASTParser({ fetcher });

const parser = new DOMParser();
const adPodWithFailure = parser.parseFromString(
`<VAST version="4.3">
<Ad sequence="1">
<InLine>
<AdSystem>Test</AdSystem>
<AdTitle>Failed Ad in Pod</AdTitle>
<Creatives>
</Creatives>
</InLine>
</Ad>
</VAST>`,
'text/xml'
);

const response = await VastParser.parseVAST(adPodWithFailure, {
keepFailedAdPod: true
});

expect(response.ads.length).toBe(1);
const ad = response.ads[0];
expect(ad.hasFailed).toBe(true);
expect(ad.sequence).toBe('1');
const hasValidCreatives = ad.creatives.some(
(creative) =>
creative.mediaFiles?.length > 0 || creative.variations?.length > 0
);
expect(hasValidCreatives).toBe(false);
});

it('should remove standalone ads without sequence even when keepFailedAdPod is true', async () => {
fetcher.setOptions({ urlHandler: nodeUrlHandler });
VastParser = new VASTParser({ fetcher });

const parser = new DOMParser();
const standaloneFailedAd = parser.parseFromString(
`<VAST version="4.3">
<Ad>
<InLine>
<AdSystem>Test</AdSystem>
<AdTitle>Failed Standalone Ad</AdTitle>
<Creatives>
</Creatives>
</InLine>
</Ad>
</VAST>`,
'text/xml'
);

const response = await VastParser.parseVAST(standaloneFailedAd, {
keepFailedAdPod: true
});

expect(response.ads.length).toBe(0);
});

it('should maintain ad pod sequence with failed ads', async () => {
const parser = new DOMParser();
const adPodWithFailureXml = parser.parseFromString(
`<VAST version="4.3">
<Ad sequence="1">
<InLine>
<AdSystem>Test</AdSystem>
<AdTitle>Ad 1</AdTitle>
<Creatives>
<Creative>
<Linear>
<Duration>00:00:15</Duration>
<MediaFiles>
<MediaFile delivery="progressive" type="video/mp4" width="1280" height="720">
<![CDATA[http://example.com/video.mp4]]>
</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine>
</Ad>
<Ad sequence="2">
<InLine>
<AdSystem>Test</AdSystem>
<AdTitle>Ad 2 - Failed</AdTitle>
<Creatives>
</Creatives>
</InLine>
</Ad>
<Ad sequence="3">
<InLine>
<AdSystem>Test</AdSystem>
<AdTitle>Ad 3</AdTitle>
<Creatives>
<Creative>
<Linear>
<Duration>00:00:15</Duration>
<MediaFiles>
<MediaFile delivery="progressive" type="video/mp4" width="1280" height="720">
<![CDATA[http://example.com/video2.mp4]]>
</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine>
</Ad>
</VAST>`,
'text/xml'
);

fetcher.setOptions({ urlHandler: nodeUrlHandler });
VastParser = new VASTParser({ fetcher });

const responseWithKeepFailed = await VastParser.parseVAST(
adPodWithFailureXml,
{ keepFailedAdPod: true }
);

expect(responseWithKeepFailed.ads.length).toBe(3);
expect(responseWithKeepFailed.ads[0].sequence).toBe('1');
expect(responseWithKeepFailed.ads[1].sequence).toBe('2');
expect(responseWithKeepFailed.ads[1].hasFailed).toBe(true);
expect(responseWithKeepFailed.ads[2].sequence).toBe('3');

const responseWithoutKeepFailed = await VastParser.parseVAST(
adPodWithFailureXml,
{ keepFailedAdPod: false }
);

expect(responseWithoutKeepFailed.ads.length).toBe(2);
});
});
});
});
13 changes: 12 additions & 1 deletion src/parser/vast_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class VASTParser extends EventEmitter {
this.remainingAds = [];
this.parsingOptions = {};
this.fetcher = fetcher || null;
this.keepFailedAdPod = false;
}

/**
Expand Down Expand Up @@ -72,6 +73,7 @@ export class VASTParser extends EventEmitter {
initParsingStatus(options = {}) {
this.maxWrapperDepth = options.wrapperLimit || DEFAULT_MAX_WRAPPER_DEPTH;
this.parsingOptions = { allowMultipleAds: options.allowMultipleAds };
this.keepFailedAdPod = options.keepFailedAdPod || false;
this.rootURL = '';
this.resetParsingStatus();
updateEstimatedBitrate(options.byteLength, options.requestDuration);
Expand Down Expand Up @@ -465,7 +467,16 @@ export class VASTParser extends EventEmitter {
{ extensions: ad.extensions },
{ system: ad.system }
);
vastResponse.ads.splice(index, 1);

// Only remove failed ads if keepFailedAdPod is not enabled
// This is useful for ad pods where failed ads should remain in the response
// to maintain sequence structure and enable fallback mechanisms
if (this.keepFailedAdPod && ad.sequence) {
ad.hasFailed = true;
}
else {
vastResponse.ads.splice(index, 1);
}
}
}
}
Expand Down