1// add delayed functionality here 2import { 3 derivePageContext, 4 resolveEdsPageName, 5 resolveEdsSiteSection, 6 resolveRenderedScreen, 7 xdmAnalytics, 8} from '../../scripts/shared-utils/xdm-analytics/index.js'; 9import { parseJwtPayload, parseJwtExpSeconds } from '../../scripts/shared-utils/jwt/index.js'; 10import { 11 readAnalyticsUserCache, 12 clearAnalyticsUserCache, 13 warmAnalyticsUserCache, 14 readCachedEmailId, 15 writeCachedEmailId, 16 sha256Hex, 17} from '../../scripts/shared-utils/analytics-user/index.js'; 18import { 19 readMcmidFromCookie, 20 waitForMcmid, 21} from '../../scripts/shared-utils/adobe-mcmid/index.js'; 22import { decorateActionIdsForDocument } from './action-id-utils.js'; 23import { CLICK_DELEGATE_META, initClickActionDelegateIfEnabled } from './click-action.js'; 24import { getMetadata, loadScript } from './aem.js'; 25import { createBackToTopButton } from './back-to-top.js'; 26import { isPerformanceCookiesAllowed } from './onetrust.js'; 27import { createSkipToMainButton } from './skip-to-main.js'; 28import { getCookieValue } from './utils.js'; 29 30const NA = 'n/a'; 31const ID_TOKEN_COOKIE = 'cogx_id_token'; 32const ACCESS_TOKEN_COOKIE = 'cogx_access_token'; 33/** Time budget for resolving the Adobe ECID after Launch loads (AB#1813993). */ 34const MCID_RESOLVE_BUDGET_MS = 10000; 35/** Max the first pageView waits for the in-flight profile warm (AB#1813993). */ 36const PROFILE_WARM_BUDGET_MS = 1500; 37 38// Inject the synchronous ECID reader into the shared XDM library so every event 39// it builds carries mcid as soon as the cookie exists (mirrors the Next app). 40// Consent-gated like the Next bridge â the ECID is identity. AB#1813993. 41xdmAnalytics.setMcidResolver(() => (isPerformanceCookiesAllowed() ? readMcmidFromCookie() : null)); 42 43const ADOBE_LAUNCH_URLS = { 44 dev: 'https://assets.adobedtm.com/041a2403188d/9d63e17f4f6a/launch-40f5fbc453a7-development.min.js', 45 stage: 'https://assets.adobedtm.com/041a2403188d/9d63e17f4f6a/launch-7887dc8dd196-staging.min.js', 46 prod: 'https://assets.adobedtm.com/041a2403188d/9d63e17f4f6a/launch-3dd26578a99a.min.js', 47}; 48 49/** 50 * Returns the Adobe Launch script URL, using the AEM metadata-driven 51 * approach as the primary mechanism with hostname-based fallback. 52 * 53 * Resolution order: 54 * 1. AEM metadata tag `adobe-launch-env` (set per environment in the 55 * metadata sheet â acts as the EDS equivalent of an environment variable). 56 * Valid values: "dev", "stage", "prod", or "none" to disable analytics. 57 * 2. Hostname-based fallback (for environments where the metadata tag 58 * has not been configured yet): 59 * *.hlx.page / *.aem.page â undefined (author preview; no analytics) 60 * *.test.subway.com â dev 61 * localhost / develop-- / developing-- / features-- â dev 62 * stage-- / staging-- / qe-- â stage 63 * everything else â prod 64 * 65 * @param {string} hostname 66 * @returns {string|undefined} 67 */ 68export default function getAdobeLaunchUrl(hostname) { 69 // 1. Prefer explicit environment from AEM metadata (set in metadata sheet). 70 const launchEnv = getMetadata('adobe-launch-env'); 71 if (launchEnv) { 72 if (launchEnv === 'none') return undefined; 73 return ADOBE_LAUNCH_URLS[launchEnv]; 74 } 75 76 // 2. Hostname-based fallback â used when metadata is not yet configured. 77 78 // HLX/AEM preview pages must never load analytics. 79 if (hostname.endsWith('.page')) { 80 return undefined; 81 } 82 83 // All *.test.subway.com hosts are non-production environments. 84 if (hostname.endsWith('.test.subway.com')) { 85 return ADOBE_LAUNCH_URLS.dev; 86 } 87 88 // AEM/Franklin branch-naming conventions (Phoenix targets: DEV, QE, STG, Prod). 89 if ( 90 hostname === 'localhost' 91 || hostname.startsWith('develop--') 92 || hostname.startsWith('developing--') 93 || hostname.startsWith('features--') 94 ) { 95 return ADOBE_LAUNCH_URLS.dev; 96 } 97 if ( 98 hostname.startsWith('stage--') 99 || hostname.startsWith('staging--') 100 || hostname.startsWith('qe--') 101 ) { 102 return ADOBE_LAUNCH_URLS.stage; 103 } 104 105 return ADOBE_LAUNCH_URLS.prod; 106} 107 108/** 109 * First path segment is treated as locale only when it matches `aa-bb` (same rule as ordering links in `utils.js`). 110 * @returns {{ localeKey: string, localeId: string }} `localeKey` for `derivePageContext` path stripping; `localeId` for XDM (e.g. en-US). 111 */ 112function resolveEdsLocaleForXdm() {
113 const first = window.location.pathname.split('/').filter(Boolean)[0]?.toLowerCase() || ''; 114 const localeKey = /^[a-z]{2}-[a-z]{2}$/.test(first) 115 ? first 116 : (document.documentElement.lang || 'en-us').replace('_', '-').toLowerCase(); 117 118 const m = /^([a-z]{2})-([a-z]{2})$/.exec(localeKey); 119 const localeId = m ? `${m[1]}-${m[2].toUpperCase()}` : localeKey; 120 121 return { localeKey, localeId }; 122} 123 124/** 125 * Guest / signed-out / expired-token defaults â every `_subway.user.*` identity 126 * and profile field is `n/a` (loginStatus `logged-out`, guestID empty), per the 127 * AB#1813993 PBI. Mirrors the sign-out branch of the Next.js 128 * `useAnalyticsUserBridge` (same `'logged-in'` / `'logged-out'` values). 129 */ 130function applyEdsGuestUser() { 131 xdmAnalytics.setLoginStatus('logged-out'); 132 xdmAnalytics.setGuestID(''); 133 xdmAnalytics.setUserEmailID(NA); 134 xdmAnalytics.setUserCountry(NA); 135 xdmAnalytics.setUserEmailOptIn(NA); 136 xdmAnalytics.setLoyaltyFlag(NA); 137 xdmAnalytics.setUserPhoneNumber(NA); 138 xdmAnalytics.setUserTierLevel(NA); 139} 140 141/** Apply the profile-derived (cached) fields to window.xdm. */ 142function applyCachedProfileFields(cached) { 143 // guestID = Subway Loyalty Guest ID from the Profile API (AB#1824122), never 144 // the JWT sub; blank when absent, matching the guest contract. 145 xdmAnalytics.setGuestID(cached.guestId && cached.guestId !== NA ? cached.guestId : ''); 146 xdmAnalytics.setUserCountry(cached.country); 147 xdmAnalytics.setUserEmailOptIn(cached.emailOptIn); 148 xdmAnalytics.setLoyaltyFlag(cached.loyaltyFlag); 149 xdmAnalytics.setUserPhoneNumber(cached.phoneNumber); 150 xdmAnalytics.setUserTierLevel(cached.tierLevel); 151} 152 153/** Current `cogx_id_token` sub, or null â drops stale async writes after a sign-out / user switch. */ 154function currentIdTokenSub() { 155 const token = getCookieValue(ID_TOKEN_COOKIE); 156 const claims = token ? parseJwtPayload(token) : null; 157 return claims?.sub ? String(claims.sub) : null; 158} 159 160/** 161 * Populate `_subway.user.*` on EDS pages, mirroring the Next.js bridge 162 * (AB#1813993): identity (emailID, loginStatus) from the `cogx_id_token` JWT; 163 * profile fields from the sessionStorage cache warmed by prefetchEdsProfile. 164 * Synchronous â never delays the pageView. emailID/phoneNumber are SHA-256 hashed. 165 */ 166function applyEdsUserContext() { 167 const idToken = getCookieValue(ID_TOKEN_COOKIE); 168 const claims = idToken ? parseJwtPayload(idToken) : null; 169 const expSeconds = idToken ? parseJwtExpSeconds(idToken) : null; 170 const isExpired = typeof expSeconds === 'number' && expSeconds * 1000 <= Date.now(); 171 172 // Guest / signed-out / expired: reset to n/a and drop any stale cached profile. 173 if (!claims?.sub || isExpired) { 174 clearAnalyticsUserCache(); 175 applyEdsGuestUser(); 176 return; 177 } 178 179 const sub = String(claims.sub); 180 xdmAnalytics.setLoginStatus('logged-in'); 181 182 // emailID: warm cache is sync; else hash locally (fast microtask, no network). 183 const cachedEmail = readCachedEmailId(sub); 184 if (cachedEmail) { 185 xdmAnalytics.setUserEmailID(cachedEmail); 186 } else if (claims.email) { 187 xdmAnalytics.setUserEmailID(NA); 188 sha256Hex(String(claims.email).trim().toLowerCase()) 189 .then((hash) => { 190 if (currentIdTokenSub() !== sub) return; 191 xdmAnalytics.setUserEmailID(hash); 192 writeCachedEmailId(sub, hash); 193 }) 194 .catch(() => { 195 if (currentIdTokenSub() === sub) xdmAnalytics.setUserEmailID(NA); 196 }); 197 } else { 198 xdmAnalytics.setUserEmailID(NA); 199 } 200 201 // Profile fields from the shared cache; n/a until prefetchEdsProfile resolves. 202 applyCachedProfileFields( 203 readAnalyticsUserCache(sub) 204 || { guestId: NA, country: NA, emailOptIn: NA, loyaltyFlag: NA, phoneNumber: NA, tierLevel: NA }, 205 ); 206} 207 208/** 209 * Warm the shared profile cache (parallel with the Launch download) so the first 210 * pageView can read it. Returns the in-flight promise so the caller can await it 211 * up to a budget; resolves immediately on the early-exit paths. AB#1813993. 212 */ 213function prefetchEdsProfile(localeId) { 214 const idToken = getCookieValue(ID_TOKEN_COOKIE); 215 const claims = idToken ? parseJwtPayload(idToken) : null; 216 const expSeconds = idToken ? parseJwtExpSeconds(idToken) : null; 217 const isExpired = typeof expSeconds === 'number' && expSeconds * 1000 <= Date.now(); 218 // Signed-out / expired: don't fetch (applyEdsUserContext clears the cache). 219 if (!claims?.sub || isExpired) return Promise.resolve(); 220 const sub = String(claims.sub); 221 if (readAnalyticsUserCache(sub)) return Promise.resolve(); 222 223 const accessToken = getCookieValue(ACCESS_TOKEN_COOKIE); 224 if (!accessToken || !isPerformanceCookiesAllowed()) return Promise.resolve(); 225 226 return warmAnalyticsUserCache({ 227 sub, 228 fetchProfile: () => 229 fetch('/api/dashboard/profile', { 230 headers: { Authorization: `Bearer ${accessToken}`, 'x-locale': localeId.toLowerCase() }, 231 }).then((res) => (res.ok ? res.json() : null)), 232 isCurrent: () => currentIdTokenSub() === sub, 233 }) 234 .then((user) => { 235 if (user) applyCachedProfileFields(user); 236 }) 237 .catch(() => {}); 238} 239 240/** 241 * Classify the viewport into `_subway.device.renderedScreen` (mobile / tablet / 242 * desktop), sharing the Next.js breakpoints via `resolveRenderedScreen`. 243 * 244 * EDS never measured the viewport at all, so `buildDefaultXdm`'s `'web'` 245 * placeholder rode every event on every EDS page. 246 */ 247function syncEdsRenderedScreen() { 248 xdmAnalytics.setRenderedScreen(resolveRenderedScreen(window.innerWidth)); 249} 250 251/** 252 * Page-level XDM for every EDS view: locale, screen name, site section, device 253 * class, then `pageView` to Adobe Launch. 254 * 255 * The site section is derived from the pathname (`/en-us` â `home`, 256 * `/en-us/menunutrition/menu` â `menu`); authors can pin one per page with an 257 * `xdm-site-section` metadata tag. It was previously hard-coded to `'ordering'` 258 * for every EDS page, which also leaked into `_subway.page.componentName` 259 * (`buildGenericPageViewXdm` derives that from the section). 260 */ 261function applyEdsXdmPageContext() { 262 const { localeKey, localeId } = resolveEdsLocaleForXdm(); 263 const derived = derivePageContext(window.location.pathname, localeKey); 264 // `document.title` is customer-facing SEO copy ("Rewards"), which CJA does not want as the 265 // page identity ("sub club"). An authored `xdm-page-name` overrides it; with no tag the 266 // behaviour is byte-identical to before. AB#1871592. 267 const pageCtx = { 268 localeID: localeId, 269 name: resolveEdsPageName(getMetadata('xdm-page-name'), document.title) || derived.name, 270 sitesection: resolveEdsSiteSection( 271 getMetadata('xdm-site-section'), 272 window.location.pathname, 273 localeKey, 274 ), 275 }; 276 277 xdmAnalytics.setWindowXdm(xdmAnalytics.buildDefaultXdm()); 278 // Measure AFTER setWindowXdm: buildDefaultXdm resets `device.renderedScreen` 279 // to the 'web' placeholder on a cold load, so an earlier read is clobbered. 280 // Before trackGenericPageView below, so the first hit carries the real class. 281 syncEdsRenderedScreen(); 282 window.addEventListener('resize', syncEdsRenderedScreen); 283 xdmAnalytics.setLocaleID(pageCtx.localeID); 284 xdmAnalytics.setPageName(pageCtx.name); 285 xdmAnalytics.setSiteSection(pageCtx.sitesection); 286 // Populate `_subway.user.*` before the pageView (AB#1813993). Profile fields 287 // come from the cache prefetchEdsProfile warmed â no fetch or delay here. 288 applyEdsUserContext(); 289 290 // AB#1813993 â Adobe ECID. Sync cookie read first so a returning user's mcid 291 // rides the pageView below (and persists on every later event via 292 // buildDefaultXdm's carry-forward); if the cookie isn't written yet (first 293 // visit, before Alloy's handshake), poll for it afterwards. `_subway.user.mcid` 294 // is set BEFORE the pageView so the call carries it; the standard 295 // `identityMap.ECID` is set just AFTER (buildDefaultXdm doesn't carry it, so a 296 // pre-pageView set would be discarded by setWindowXdm). Mirrors the Next.js 297 // AdobeLaunchScript so ECID stitching works on EDS too. 298 const mcidNow = readMcmidFromCookie(); 299 if (mcidNow) xdmAnalytics.setMcid(mcidNow); 300 301 xdmAnalytics.trackGenericPageView(pageCtx); 302 303 if (mcidNow) { 304 // setIdentityMapECID after the pageView's setWindowXdm so it isn't 305 // overwritten (mcid already rode the pageView and carries forward). 306 xdmAnalytics.setIdentityMapECID(mcidNow); 307 } else { 308 waitForMcmid(MCID_RESOLVE_BUDGET_MS) 309 .then((ecid) => { 310 if (!ecid) return; 311 xdmAnalytics.setMcid(ecid); 312 xdmAnalytics.setIdentityMapECID(ecid); 313 }) 314 .catch(() => { 315 // Best-effort â mcid stays n/a if the ECID never resolves. 316 }); 317 } 318} 319
320// initOneTrustModalLinks() runs in scripts.js loadEager before this module loads; do not call again here. 321 322/** Resolve when `promise` settles or after `ms`, clearing the timer either way. */ 323function raceWithBudget(promise, ms) { 324 let timer; 325 const budget = new Promise((resolve) => { 326 timer = setTimeout(resolve, ms); 327 }); 328 return Promise.race([promise, budget]).finally(() => clearTimeout(timer)); 329} 330 331let adobeLaunchInjected = false; 332 333function injectAdobeLaunchIfAllowed() { 334 const adobeLaunchUrl = getAdobeLaunchUrl(window.location.hostname); 335 if (!adobeLaunchUrl || adobeLaunchInjected || !isPerformanceCookiesAllowed()) { 336 return; 337 } 338 adobeLaunchInjected = true; 339 // Warm the profile in parallel with the Launch download, then wait for it up to 340 // a budget before the first pageView so it isn't n/a. AB#1813993. 341 const warmingProfile = prefetchEdsProfile(resolveEdsLocaleForXdm().localeId); 342 loadScript(adobeLaunchUrl) 343 .then(() => raceWithBudget(warmingProfile, PROFILE_WARM_BUDGET_MS)) 344 .then(() => applyEdsXdmPageContext()) 345 .catch((err) => { 346 // eslint-disable-next-line no-console 347 console.error('[delayed] Adobe Launch script failed to load:', err); 348 }); 349} 350 351injectAdobeLaunchIfAllowed(); 352window.addEventListener('consent.onetrust', () => injectAdobeLaunchIfAllowed()); 353 354createBackToTopButton(); 355createSkipToMainButton(); 356decorateActionIdsForDocument(document.body); 357// Opt-in per page (ADR-0017): not site-wide, because `cards.js` binds its own per-element 358// listeners; not per-block, because `links-column` renders 4x in the site-wide footer. 359initClickActionDelegateIfEnabled(getMetadata(CLICK_DELEGATE_META));
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.