PageSourceSearch

https://www.subway.com/scripts/delayed.js

js subway.com collected 2026-09-24 08:36:59 UTC 14,653 bytes, 359 lines download raw bytes

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.