From 8682b408282f226175ce6ca246f22441d417168a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Fran=C3=A7on?= Date: Wed, 19 Nov 2025 14:45:00 +0100 Subject: [PATCH 1/2] [parser] Add option to keep failing sequence in ad pod --- docs/api/class-reference.md | 3 +- spec/vast_parser.spec.js | 179 ++++++++++++++++++++++++++++++++++++ src/parser/vast_parser.js | 13 ++- 3 files changed, 193 insertions(+), 2 deletions(-) diff --git a/docs/api/class-reference.md b/docs/api/class-reference.md index 58bbd8ce..7c2df2cc 100644 --- a/docs/api/class-reference.md +++ b/docs/api/class-reference.md @@ -30,6 +30,7 @@ This object represents a single parsed Ad - `creatives: Array` [go to object](#creative) - `extensions: Array` [go to object](#extension) - `adVerifications: Array` [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 @@ -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` \ No newline at end of file +- `height: String|null` diff --git a/spec/vast_parser.spec.js b/spec/vast_parser.spec.js index 25ebccd4..6d58e868 100644 --- a/spec/vast_parser.spec.js +++ b/spec/vast_parser.spec.js @@ -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( + ` + + + Test + Failed Ad + + + + + `, + '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( + ` + + + Test + Failed Ad in Pod + + + + + `, + '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( + ` + + + Test + Failed Standalone Ad + + + + + `, + '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( + ` + + + Test + Ad 1 + + + + 00:00:15 + + + + + + + + + + + + + Test + Ad 2 - Failed + + + + + + + Test + Ad 3 + + + + 00:00:15 + + + + + + + + + + + `, + '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); + }); + }); + }); }); diff --git a/src/parser/vast_parser.js b/src/parser/vast_parser.js index a4bd4cae..724ebe68 100644 --- a/src/parser/vast_parser.js +++ b/src/parser/vast_parser.js @@ -31,6 +31,7 @@ export class VASTParser extends EventEmitter { this.remainingAds = []; this.parsingOptions = {}; this.fetcher = fetcher || null; + this.keepFailedAdPod = false; } /** @@ -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); @@ -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); + } } } } From 60f4951c75a11568d8821269f0f4e2abb1d490b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7on=20Cl=C3=A9ment?= Date: Wed, 19 Nov 2025 16:37:21 +0100 Subject: [PATCH 2/2] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- spec/vast_parser.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/vast_parser.spec.js b/spec/vast_parser.spec.js index 6d58e868..9fc02714 100644 --- a/spec/vast_parser.spec.js +++ b/spec/vast_parser.spec.js @@ -781,7 +781,7 @@ describe('VASTParser', () => { const response = await VastParser.parseVAST(adWithSequence, { keepFailedAdPod: false - }) + }); expect(response.ads.length).toBe(0); });