1/** 2 * DDO Utilities 3 * 4 * Two utilities for the DDO product assessment flow: 5 * 6 * 1. DDO Filter API util â calls the DDO Product Filter API and caches the result 7 * in module-level memory for the lifetime of the page. 8 * 9 * 2. DDO Product Assessment API util â reads the cached filter result, assembles 10 * the product assessment params, persists to sessionStorage, then calls the 11 * MEDB flow (currently a console.log placeholder). 12 * 13 * Usage: 14 * - Call initDdoFilterApi() once on page load (scripts.js loadLazy) 15 * - Call callDdoProductAssessmentApi(surveyAnswers) either: 16 * a) on page load when consent is NOT mandatory, or 17 * b) from the confirmation block CTA click when consent IS mandatory 18 */ 19 20import { getMetadata } from '../aem.js'; 21// eslint-disable-next-line import/no-unresolved 22import { transformMedbLinks } from '../medb-link-transformer.js'; 23// eslint-disable-next-line import/no-cycle 24import handleDdoError from './ddo-error-utils.js'; 25import { getPlaceholderValue } from './placeholder-cache.js'; 26import validateSelectionConfigurationDropdowns from './selection-configuration-validation.js'; 27 28// ============================================================================= 29// Constants 30// ============================================================================= 31 32const PRODUCT_ASSESSMENTS_SESSION_KEY = 'productAssessmentsParams'; 33const FILTER_API_ORIGINATING_SYSTEM_ID = 'A019BF'; 34const FILTER_API_CONSUMER_TYPE = 'Public'; 35const PRODUCT_ASSESSMENT_ORIGINATING_SYSTEM_ID = 'A019BF'; 36const DATA_LAYER_PAGE_NAME = 'rs2 submitted'; 37const DATA_LAYER_SITE_NAME = 'ddo-product-selector'; 38 39// Placeholder assessmentId â will be replaced with the real value from the 40// Product Assessment API POST response once that integration is complete. 41const ASSESSMENT_ID_PLACEHOLDER = ''; 42 43// ============================================================================= 44// Module-level cache â shared across all importers on the same page 45// ============================================================================= 46 47/** @type {Object|null} Cached Filter API response, set by initDdoFilterApi() */ 48let filterApiResult = null; 49 50// ============================================================================= 51// Private helpers 52// ============================================================================= 53 54/** 55 * Returns true when the current page is served from a Westpac production domain 56 * (*.westpac.com.au). handleDdoError() is only called on production â not on 57 * localhost, AEM preview (.aem.page), or Universal Editor authoring. 58 * 59 * @returns {boolean} 60 */ 61function isWestpacDomain() { 62 const { hostname } = window.location; 63 return hostname === 'www.westpac.com.au' || hostname.endsWith('.westpac.com.au'); 64} 65 66/** 67 * Persist product assessment params to sessionStorage. 68 * 69 * @param {Object} params 70 */ 71function writeProductAssessmentParams(params) { 72 sessionStorage.setItem(PRODUCT_ASSESSMENTS_SESSION_KEY, JSON.stringify(params)); 73} 74 75/** 76 * Remove persist:root from sessionStorage if it exists. 77 * Called before triggering MEDB link transformation. 78 */ 79function clearPersistRoot() { 80 if (sessionStorage.getItem('persist:root') !== null) { 81 sessionStorage.removeItem('persist:root'); 82 } 83} 84 85/** 86 * Build the productID array for DDO pages from the cached Filter API result. 87 * 88 * @returns {Array<Object>} 89 */ 90function buildDdoProductId() { 91 const filterProducts = filterApiResult?.data?.products || []; 92 const contentId = filterApiResult?.data?.contentId || ''; 93 94 return filterProducts.map((product) => { 95 const enterpriseProductId = String(product?.enterpriseProductId || '').toLowerCase(); 96 const productName = String(product?.productName || '').toLowerCase(); 97 98 return { 99 Product: enterpriseProductId, 100 ProductName: productName, 101 ProductCategory: contentId, 102 ProductFamily: contentId, 103 ProductSubCategory: productName, 104 }; 105 }); 106} 107 108/** 109 * Build toolOutcome for DDO pages from cached Filter API product names. 110 * 111 * @returns {string} 112 */ 113function buildDdoToolOutcome() { 114 const filterProducts = filterApiResult?.data?.products || []; 115 116 return filterProducts 117 .map((product) => String(product?.productName || '').replaceAll(':', '').toLowerCase().trim()) 118 .filter(Boolean) 119 .join('|'); 120} 121 122/** 123 * Add the DDO event key once using the analytics data-layer format. 124 * 125 * @param {Object} pageDetails 126 * @param {number} timestamp 127 */ 128export function addDdoEventKey(pageDetails, timestamp = Date.now()) { 129 const { 130 eventKey, 131 pageName = '', 132 pageType = '', 133 siteBrand = '', 134 siteExperience = '', 135 }
135 = pageDetails; 136 const eventTimestamp = Math.floor(timestamp / 1000); 137 const nextEventKey = `${siteBrand}:${siteExperience}_${pageType}_${pageName}_${eventTimestamp}`; 138 139 Object.assign(pageDetails, { 140 eventKey: eventKey ?? nextEventKey, 141 }); 142} 143 144/** 145 * Build the x-wmi header value from window.pageDetails. 146 * Encodes: appName, appPath (URL path only), brand, pageName (last ':' segment). 147 * 148 * @param {Object} pageDetails - window.pageDetails set by analytics.js 149 * @returns {string} Base64-encoded wmi string 150 */ 151function buildWmiHeader(pageDetails) { 152 const appName = pageDetails?.journeyType === 'pub' ? 'web' : pageDetails?.journeyType || 'web'; 153 const appPath = pageDetails?.pageURL ? new URL(pageDetails.pageURL).pathname : '/'; 154 const brand = pageDetails?.siteBrand || 'wbc'; 155 const rawPageName = pageDetails?.pageName || ''; 156 const pageName = rawPageName.includes(':') 157 ? rawPageName.split(':').pop() 158 : rawPageName || 'home'; 159 const wmiString = `appName:${appName};appPath:${appPath};brand:${brand};pageName:${pageName}`; 160 return btoa(wmiString); 161} 162 163/** 164 * Execute the DDO Product Filter API GET request. 165 * Reads contentIdForProductSchema and productVariantCode from page metadata. 166 * 167 * @returns {Promise<Object>} Parsed Filter API response 168 */ 169async function fetchFilterApi() { 170 const pageDetails = window.pageDetails || {}; 171 const contentId = getMetadata('contentidforproductschema') || ''; 172 173 // Read all product variant code fields from metadata and combine 174 const productVariantCode = getMetadata('productvariantcode') || ''; 175 const bundleProductVariantCode = getMetadata('bundleproductvariantcode') || ''; 176 const crossSellProductVariantCode = getMetadata('crosssellproductvariantcode') || ''; 177 178 // Combine and deduplicate variant codes 179 const variantCodes = new Set(); 180 if (productVariantCode) { 181 productVariantCode.split(',').forEach((code) => variantCodes.add(code.trim())); 182 } 183 if (bundleProductVariantCode) { 184 bundleProductVariantCode.split(',').forEach((code) => variantCodes.add(code.trim())); 185 } 186 if (crossSellProductVariantCode) { 187 crossSellProductVariantCode.split(',').forEach((code) => variantCodes.add(code.trim())); 188 } 189 190 const queryParts = []; 191 if (contentId) queryParts.push(`id=${encodeURIComponent(contentId)}`); 192 if (variantCodes.size > 0) { 193 // Encode each code individually and join with literal commas (not %2C) 194 // e.g. productVariantCode=in:LifeJoint,ChoiceJoint,eSaverJoint 195 const variantCodeString = Array.from(variantCodes).map((code) => encodeURIComponent(code)).join(','); 196 queryParts.push(`productVariantCode=in:${variantCodeString}`); 197 } 198 199 const filterApiUrl = await loadPlaceholder('Environment Variables', 'ddo-filter-api'); // eslint-disable-line no-use-before-define 200 const url = queryParts.length ? `${filterApiUrl}?${queryParts.join('&')}` : filterApiUrl; 201 const wmi = buildWmiHeader(pageDetails); 202 const messageId = crypto.randomUUID(); 203 204 const response = await fetch(url, { 205 method: 'GET', 206 headers: { 207 'x-messageid': messageId, 208 'x-originatingsystemid': FILTER_API_ORIGINATING_SYSTEM_ID, 209 'x-consumertype': FILTER_API_CONSUMER_TYPE, 210 'x-wmi': wmi, 211 }, 212 }); 213 214 if (!response.ok) { 215 throw new Error(`DDO Filter API responded with ${response.status} ${response.statusText}`); 216 } 217 218 const data = await response.json(); 219 return data; 220} 221 222// ============================================================================= 223// Public exports 224// ============================================================================= 225 226/** 227 * Load a single placeholder value from /site-configuration/placeholders.json. 228 * Legacy URL-encoded sheet names are normalized before using the shared placeholder cache. 229 * 230 * Supported sheets and their column shapes: 231 * - 'Labels' : { Name, Value } 232 * - 'Environment+Variables' : { Variable Name, Domain } 233 * 234 * @param {string} sheet - Sheet query param (e.g. 'Labels', 'Environment+Variables') 235 * @param {string} key - Key to look up in the first column 236 * @returns {Promise<string>} Matched value, or empty string if not found 237 */ 238export async function loadPlaceholder(sheet, key) { 239 const sheetName = sheet.replaceAll('+', ' '); 240 return getPlaceholderValue(sheetName, key); 241} 242 243/** 244 * Utility 1 â DDO Filter API 245 * 246 * Calls the DDO Product Filter API and caches the result in module memory. 247 * Call once on page load for DDO template pages (scripts.js loadLazy). 248 * Safe to call multiple times â subsequent calls are no-ops if already cached. 249 * 250 * @returns {Promise<void>} 251 */ 252export async function initDdoFilterApi() { 253 if (filterApiResult) { 254 return true; 255 } 256 try { 257 filterApiResult = await fetchFilterApi(); 258 return true; 259 } catch (err) { 260 filterApiResult = null; 261 if (isWestpacDomain()) await handleDdoError(); 262 return false;
263 } 264} 265 266/** 267 * Returns the cached DDO Filter API result. 268 * Returns null if initDdoFilterApi() has not been called or failed. 269 * 270 * @returns {Object|null} 271 */ 272export function getDdoFilterResult() { 273 return filterApiResult; 274} 275 276/** 277 * Triggers MEDB link transformation on the CTA button using the persisted 278 * product assessment params already written to sessionStorage. 279 * transformMedbLinks() reads sessionStorage internally, so params is used 280 * only for logging. 281 * 282 * @param {Object} params - Assembled product assessment params (for logging) 283 * @param {HTMLElement|null} ctaEl - The CTA button element to transform 284 */ 285export function callMedbFlow(params, ctaEl) { 286 if (!ctaEl) { 287 return; 288 } 289 transformMedbLinks(ctaEl, params); 290} 291 292// ============================================================================= 293// Private helpers â dynamic payload building 294// ============================================================================= 295 296/** 297 * Determine the optedInType for a product by checking which metadata variant 298 * code list its productVariantCode belongs to. 299 * 300 * @param {string} productVariantCode - from Filter API response 301 * @param {Set<string>} bundleCodes - from bundleproductvariantcode metadata 302 * @param {Set<string>} crossSellCodes - from crosssellproductvariantcode metadata
303 * @returns {'Bundled'|'CrossSell'|'Primary'} 304 */ 305function determineOptedInType(productVariantCode, bundleCodes, crossSellCodes) { 306 if (bundleCodes.has(productVariantCode)) return 'Bundled'; 307 if (crossSellCodes.has(productVariantCode)) return 'CrossSell'; 308 return 'Primary'; 309} 310 311/** 312 * Map confirmation block survey answers to the API survey payload format. 313 * Labels are base64-encoded for API transmission. 314 * 315 * @param {Array<{id: string, label: string, isSelected: boolean}>} surveyAnswers 316 * @returns {Array|undefined} 317 */ 318function buildSurveyPayload(surveyAnswers) { 319 if (!surveyAnswers || surveyAnswers.length === 0) return undefined; 320 return surveyAnswers.map((answer) => ({ 321 question: { id: answer.id, label: btoa(answer.label) }, 322 answers: [{ id: answer.id, label: btoa(answer.label), isSelected: answer.isSelected }], 323 })); 324} 325 326/** 327 * Build the optedInProducts array by mapping Filter API products to the 328 * Product Assessment API payload format. 329 * 330 * @param {Object} filterData - filterApiResult.data 331 * @param {Array<{id: string, label: string, isSelected: boolean}>} surveyAnswers 332 * @param {string} consentType - 'pageload' | 'linkclick' 333 * @param {Set<string>} bundleCodes 334 * @param {Set<string>} crossSellCodes 335 * @returns {Array} 336 */ 337function buildOptedInProducts(filterData, surveyAnswers, consentType, bundleCodes, crossSellCodes) { 338 const { 339 contentId, 340 moduleId, 341 versionId, 342 products = [], 343 } = filterData; 344 const survey = buildSurveyPayload(surveyAnswers); 345 346 return products.map((product) => { 347 const optedInType = determineOptedInType( 348 product.productVariantCode, 349 bundleCodes, 350 crossSellCodes, 351 ); 352 const isProductWithinTMD = !!product.tmdId; 353 354 const optedInProduct = { 355 productId: product.productId, 356 productName: btoa(product.productName || ''), 357 productVariantCode: product.productVariantCode, 358 enterpriseProductId: product.enterpriseProductId, 359 optedInType, 360 isEligible: true, 361 hasConsentAccepted: true, 362 consentType, 363 isProductWithinTMD, 364 productContentReference: { 365 contentId, 366 moduleId, 367 versionId, 368 }, 369 originalProductId: product.productId, 370 }; 371 372 if (isProductWithinTMD) { 373 optedInProduct.targetMarketDetermination = { 374 TMDNumber: product.tmdId, 375 TMDVersion: product.tmdVersion, 376 TMDEffectiveDate: product.tmdDate, 377 }; 378 } 379 380 if (survey) { 381 optedInProduct.survey = survey; 382 } 383 384 return optedInProduct; 385 }); 386} 387 388/** 389 * Build the productAssessmentsParams object from the cached Filter API result 390 * and page metadata. All fields are derived dynamically except assessmentId, 391 * which is kept as a constant placeholder until the Product Assessment POST 392 * response integration is complete. 393 * 394 * Primary product = the product in filterApiResult whose productVariantCode 395 * is NOT in the bundle or cross-sell metadata lists. 396 * Bundle product = the product whose productVariantCode is in bundleproductvariantcode. 397 * Cross-sell product = the product whose productVariantCode is in crosssellproductvariantcode. 398 * 399 * @param {string} assessmentId - constant placeholder (real value from POST response) 400 * @returns {Object} productAssessmentsParams 401 */ 402function buildProductAssessmentParams(assessmentId) { 403 const productVariantCode = getMetadata('productvariantcode') || ''; 404 const bundleProductVariantCode = getMetadata('bundleproductvariantcode') || ''; 405 const crossSellProductVariantCode = getMetadata('crosssellproductvariantcode') || ''; 406 407 const bundleCodes = new Set( 408 bundleProductVariantCode.split(',').map((c) => c.trim()).filter(Boolean), 409 ); 410 const crossSellCodes = new Set( 411 crossSellProductVariantCode.split(',').map((c) => c.trim()).filter(Boolean), 412 ); 413 414 const products = filterApiResult?.data?.products || []; 415 416 // Find the primary product: variant code is not in bundle or cross-sell sets 417 const primaryProduct = products.find( 418 (p) => determineOptedInType(p.productVariantCode, bundleCodes, crossSellCodes) === 'Primary', 419 ) || {}; 420 421 // Find the bundle product: variant code is in bundleCodes 422 const bundleProduct = products.find( 423 (p) => determineOptedInType(p.productVariantCode, bundleCodes, crossSellCodes) === 'Bundled', 424 ) || {}; 425 426 // Find the cross-sell product: variant code is in crossSellCodes 427 const crossSellProduct = products.find( 428 (p) => determineOptedInType(p.productVariantCode, bundleCodes, crossSellCodes) === 'CrossSell', 429 ) || {}; 430 431 const params = { 432 enterpriseProductId: primaryProduct.enterpriseProductId || '', 433 productId: primaryProduct.productId || '', 434 productVariantCode, 435 bundleProductId: bundleProduct.productId || '', 436 bundleProductVariantCode, 437 crossSellProductId: crossSellProduct.productId || '', 438 crossSellProductVariantCode, 439 assessmentId, 440 }; 441 442 return params; 443} 444 445/**
446 * Build the eligibleProducts array from page metadata variant codes. 447 * 448 * @param {string} primaryProductVariantCode 449 * @param {string} bundleProductVariantCode 450 * @param {string} crossSellProductVariantCode 451 * @returns {Array} 452 */ 453function buildEligibleProducts( 454 primaryProductVariantCode, 455 bundleProductVariantCode, 456 crossSellProductVariantCode, 457) { 458 const eligibleProduct = { isReferToBranch: false }; 459 if (primaryProductVariantCode) eligibleProduct.primaryProductId = primaryProductVariantCode; 460 if (bundleProductVariantCode) eligibleProduct.bundledProductId = bundleProductVariantCode; 461 if (crossSellProductVariantCode) eligibleProduct.crossSellProductId = crossSellProductVariantCode; 462 return [eligibleProduct]; 463} 464 465/** 466 * Fire the DDO Product Assessment API POST with a dynamic payload built from 467 * the cached Filter API result and page metadata. 468 * Response is logged only â sessionStorage is still written from mock. 469 * 470 * @param {Array<{id: string, label: string, isSelected: boolean}>} surveyAnswers 471 * @param {string} consentType - 'pageload' | 'linkclick' 472 * @returns {Promise<void>} 473 */ 474async function postProductAssessmentApi(surveyAnswers, consentType) { 475 const productGroup = getMetadata('productgroup') || ''; 476 const productVariantCode = getMetadata('productvariantcode') || ''; 477 const bundleProductVariantCode = getMetadata('bundleproductvariantcode') || ''; 478 const crossSellProductVariantCode = getMetadata('crosssellproductvariantcode') || ''; 479 480 const bundleCodes = new Set( 481 bundleProductVariantCode 482 .split(',') 483 .map((c) => c.trim()) 484 .filter(Boolean), 485 ); 486 const crossSellCodes = new Set( 487 crossSellProductVariantCode 488 .split(',') 489 .map((c) => c.trim()) 490 .filter(Boolean), 491 ); 492 493 const filterData = filterApiResult?.data || {}; 494 const optedInProducts = buildOptedInProducts( 495 filterData, 496 surveyAnswers, 497 consentType, 498 bundleCodes, 499 crossSellCodes, 500 ); 501 const eligibleProducts = buildEligibleProducts( 502 productVariantCode, 503 bundleProductVariantCode, 504 crossSellProductVariantCode, 505 ); 506 507 const body = { productGroup, eligibleProducts, optedInProducts }; 508 const messageId = crypto.randomUUID(); 509 const correlationId = crypto.randomUUID(); 510 const assessmentApiUrl = await loadPlaceholder('Environment+Variables', 'ddo-product-assesment-api'); 511 512 const response = await fetch(assessmentApiUrl, { 513 method: 'POST', 514 headers: { 515 'Content-Type': 'application/json', 516 'x-appcorrelationid': correlationId, 517 'x-brandid': 'WBC', 518 'x-channeltype': 'Internal', 519 'x-messageid': messageId, 520 'x-originatingsystemid': PRODUCT_ASSESSMENT_ORIGINATING_SYSTEM_ID, 521 }, 522 body: JSON.stringify(body), 523 }); 524 525 if (!response.ok) { 526 const errorText = await response.text(); 527 // eslint-disable-next-line no-console 528 console.error('ddo-utils.js:postProductAssessmentApi error', response.status, errorText); 529 throw new Error(`DDO Product Assessment API responded with ${response.status} ${response.statusText}`); 530 } 531 const responseText = await response.text(); 532 533 try { 534 const responseJson = JSON.parse(responseText); 535 const assessmentId = responseJson?.entities?.productAssessments?.[0]?.id || ''; 536 return assessmentId; 537 } catch (err) { 538 // eslint-disable-next-line no-console 539 console.error('ddo-utils.js:postProductAssessmentApi could not parse response JSON', err); 540 return ''; 541 } 542} 543 544/** 545 * Utility 2 â DDO Product Assessment API 546 * 547 * Builds a dynamic payload from the cached Filter API result and page metadata, 548 * fires the Product Assessment API POST, persists mock params to sessionStorage, 549 * then optionally calls callMedbFlow(). 550 * 551 * Call either: 552 * a) on page load when consent is NOT mandatory (no .confirmation__consent in DOM), or 553 * b) from the confirmation CTA click handler when consent IS mandatory. 554 * 555 * @param {Array<{id: string, label: string, isSelected: boolean}>} surveyAnswers 556 * @param {string} [consentType='pageload'] - 'pageload' | 'linkclick' 557 * @param {boolean} [skipMedb=false] - When true, skips callMedbFlow (deferred to CTA click) 558 * @param {HTMLElement|null} [ctaEl=null] - The CTA button element (from the click event context) 559 * @returns {Promise<boolean>} true on success, false on failure 560 */ 561export async function callDdoProductAssessmentApi( 562 surveyAnswers, 563 consentType = 'pageload', 564 skipMedb = false, 565 ctaEl = null, 566) { 567 try { 568 // Fire POST with dynamic payload built from filterApiResult â response returns asse
568ssmentId. 569 const assessmentId = await postProductAssessmentApi(surveyAnswers, consentType); 570 571 // Build params dynamically from Filter API result + page metadata. 572 const params = buildProductAssessmentParams(assessmentId || ASSESSMENT_ID_PLACEHOLDER); 573 writeProductAssessmentParams(params); 574 // Only call MEDB immediately when not deferred (linkclick path always calls it) 575 if (!skipMedb) { 576 clearPersistRoot(); 577 callMedbFlow(params, ctaEl); 578 } 579 return true; 580 } catch (err) { 581 // eslint-disable-next-line no-console 582 console.error('ddo-utils.js:callDdoProductAssessmentApi error', err); 583 if (isWestpacDomain()) await handleDdoError(); 584 return false; 585 } 586} 587 588/** 589 * Read the persisted product assessment params from sessionStorage and call 590 * callMedbFlow with them. Used for the no-consent page-load path where 591 * callMedbFlow is deferred until the CTA is clicked. 592 * 593 * @param {HTMLElement|null} ctaEl - The CTA button element (from the click event context) 594 */ 595export function callMedbFlowFromSession(ctaEl) { 596 try { 597 const raw = sessionStorage.getItem('productAssessmentsParams'); 598 const params = raw ? JSON.parse(raw) : null; 599 if (params) { 600 clearPersistRoot(); 601 callMedbFlow(params, ctaEl); 602 } 603 } catch (err) { 604 // eslint-disable-next-line no-console 605 console.error('ddo-utils.js:callMedbFlowFromSession error', err); 606 } 607} 608 609/** 610 * Update window.pageDetails. 611 */ 612function updatePageDetailsForDdoPages() { 613 const productGroup = getMetadata('productgroup') || ''; 614 if (!productGroup) return; 615 616 if (!window.pageDetails) { 617 window.pageDetails = {}; 618 } 619 620 window.pageDetails.pageName = DATA_LAYER_PAGE_NAME; 621 window.pageDetails.siteName = DATA_LAYER_SITE_NAME; 622 window.pageDetails.pageProperty = productGroup; 623 const productID = buildDdoProductId(); 624 const toolOutcome = buildDdoToolOutcome(); 625 if (productID.length > 0) { 626 window.pageDetails.productID = productID; 627 } 628 if (toolOutcome) { 629 window.pageDetails.toolOutcome = toolOutcome; 630 } 631 addDdoEventKey(window.pageDetails); 632} 633 634async function continueDdoProductAssessmentFlow(doc) { 635 const assessmentSuccess = await callDdoProductAssessmentApi([], 'pageload', true); 636 if (!assessmentSuccess) return; 637 638 const ctaElements = doc.querySelectorAll('.confirmation__cta'); 639 ctaElements.forEach((ctaElement) => { 640 const handleCtaClick = (event) => { 641 if (!validateSelectionConfigurationDropdowns(doc)) { 642 event.preventDefault(); 643 return; 644 } 645 646 ctaElement.removeEventListener('click', handleCtaClick); 647 callMedbFlowFromSession(ctaElement); 648 }; 649 650 ctaElement.addEventListener('click', handleCtaClick); 651 }); 652} 653 654/** 655 * Initialises DDO page details from the filter API. The product assessment API 656 * continues independently because it is not required by page-view analytics. 657 * 658 * @param {Document} doc - Document context 659 */ 660export async function initDdoPageFlow(doc) { 661 const templateType = getMetadata('templatetype'); 662 if (templateType !== 'ddo') return; 663 const filterApiSuccess = await initDdoFilterApi(); 664 if (!filterApiSuccess) return; 665 updatePageDetailsForDdoPages(); 666 const isConsentMandatory = !!doc.querySelector('.confirmation__consent'); 667 if (!isConsentMandatory) { 668 continueDdoProductAssessmentFlow(doc).catch((error) => { 669 // eslint-disable-next-line no-console 670 console.error('ddo-utils.js:continueDdoProductAssessmentFlow error', error); 671 }); 672 } 673}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.