Skip to content
Closed
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
152523b
docs(MWPW-191059): add Freyja preview migration spec and implementati…
Axelcureno Apr 1, 2026
a7dd497
feat(MWPW-191059): add Freyja and Odin preview URL constants to paths.js
Axelcureno Apr 1, 2026
41ba251
feat(MWPW-191059): add auth header injection and Freyja-first fallbac…
Axelcureno Apr 1, 2026
ba6ff8f
test(MWPW-191059): update pipeline and replace test mocks for Freyja …
Axelcureno Apr 1, 2026
6d05b53
test(MWPW-191059): update Nala variation spec for Freyja preview URLs
Axelcureno Apr 1, 2026
e0a787d
feat(MWPW-191059): switch fragment-client preview to Freyja v2 with O…
Axelcureno Apr 1, 2026
fecc895
feat(MWPW-191059): switch mas-repository preview URLs to Freyja v2
Axelcureno Apr 1, 2026
4a8b27b
feat(MWPW-191059): add backend toggle, manual token injection, and be…
Axelcureno Apr 1, 2026
67bd494
feat(MWPW-191059): add standalone Freyja benchmark page and configura…
Axelcureno Apr 2, 2026
60957df
Cache Freyja token to sessionStorage after IMS initialization
Axelcureno Apr 3, 2026
89b0a66
Improve Freyja token caching to only cache after user sign-in
Axelcureno Apr 3, 2026
d2976dd
Use Freyja endpoint in fragment-client when token is available
Axelcureno Apr 3, 2026
2196ec6
Fix Freyja Authorization header not being sent in fragment-client.js
Axelcureno Apr 3, 2026
44cdd31
Use production Freyja endpoint instead of localhost
Axelcureno Apr 3, 2026
03ae91f
Merge branch 'main' into MWPW-191059
npeltier Apr 7, 2026
c72cddf
Merge branch 'main' into MWPW-191059
Axelcureno Apr 9, 2026
e060cb1
MWPW-191059: address PR review feedback — remove Odin fallback, conso…
Axelcureno Apr 9, 2026
8eb6848
Merge branch 'main' into MWPW-191059
honstar Apr 10, 2026
68aee50
Merge branch 'main' into MWPW-191059
Axelcureno Apr 13, 2026
1c0bd44
remove docs/superpowers from tracking, add to .gitignore
Axelcureno Apr 13, 2026
cae09d2
MWPW-191059 - use env-aware Freyja URL when aem.env param is set
Axelcureno Apr 15, 2026
d2d3fe5
MWPW-191059 - also use env-aware Freyja URL when freyjaToken is expli…
Axelcureno Apr 15, 2026
a6fab73
MWPW-191059 - point Studio preview at MAS gateway, drop Odin/Freyja-d…
Axelcureno Apr 21, 2026
385bcea
MWPW-191059 - migrate editor translated-locale discovery to gateway p…
Axelcureno Apr 21, 2026
d05668e
MWPW-191059 - remove masFreyjaToken session-storage plumbing
Axelcureno Apr 21, 2026
3b7b1ee
MWPW-191059 - swap ODIN_PREVIEW_URL/FREYJA_PREVIEW_URL for GATEWAY_PR…
Axelcureno Apr 21, 2026
d713277
MWPW-191059 - update doc comment and drop obsolete Freyja-vs-Odin ben…
Axelcureno Apr 21, 2026
7d7206d
MWPW-191059 - rebuild web-components dist after removing masFreyjaToken
Axelcureno Apr 21, 2026
5364aac
Merge remote-tracking branch 'origin/main' into MWPW-191059
Apr 21, 2026
2513393
MWPW-191059 - remove Authorization header injection from internalFetch
Apr 21, 2026
812b539
MWPW-191059 - revert unjustified refactoring of internalFetch
Apr 21, 2026
96fa975
MWPW-191059 - delete io/www/test/fragment/common.test.js
Apr 21, 2026
00307ff
MWPW-191059 - keep dictionary fetchTimeout at 10s
Apr 21, 2026
cb35e47
MWPW-191059 - fix prettier formatting on merged test files
Apr 21, 2026
efd76ee
MWPW-191059 - point variations self-ref test at gateway, not Freyja d…
Apr 21, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ nala/.auth/
/agents/
reports/
/.superpowers
docs/superpowers/
.logs

# IO Runtime Config
Expand Down
20 changes: 14 additions & 6 deletions io/www/src/fragment/utils/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ function measureTiming(context, label, startLabel = label) {
async function fetchAttempt(path, context, timeout, marker) {
try {
mark(context, marker);
const responsePromise = fetch(path, {
headers: context.DEFAULT_HEADERS,
});
const headers = { ...context.DEFAULT_HEADERS };
if (context.preview?.authToken) {
headers['Authorization'] = `Bearer ${context.preview.authToken}`;
}
const responsePromise = fetch(path, { headers });

// Race the fetch promise with a timeout
const response = await Promise.race([responsePromise, createTimeoutPromise(timeout)]);
Expand Down Expand Up @@ -104,23 +106,29 @@ async function fetchAttempt(path, context, timeout, marker) {
async function internalFetch(path, context, marker) {
mark(context, `${marker}`);
const { retries = 3, fetchTimeout = 2000, retryDelay = 100 } = context.networkConfig || {};

const response = await fetchWithRetries(path, context, fetchTimeout, retries, retryDelay, marker);

measureTiming(context, `main-fetch-${marker}`, marker);
return response;
}

async function fetchWithRetries(path, context, fetchTimeout, retries, retryDelay, marker) {
let delay = retryDelay;
let response;
for (let attempt = 0; attempt < retries; attempt++) {
// Race the fetch promise with a timeout
response = await fetchAttempt(path, context, fetchTimeout, `fetch-${marker}-${attempt}`);
if ([503, 504].includes(response.status)) {
log(
`fetch ${path} (attempt #${attempt}) failed with status ${response.status}, retrying in ${delay}ms...`,
context,
);
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
delay *= 2;
} else {
break;
}
}
measureTiming(context, `main-fetch-${marker}`, marker);
return response;
}

Expand Down
27 changes: 26 additions & 1 deletion io/www/src/fragment/utils/paths.js
Comment thread
honstar marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ const MAS_ROOT = '/content/dam/mas';

const FRAGMENT_URL_PREFIX = 'https://odin.adobe.com/adobe/sites/fragments';

const FREYJA_ENV_IDS = {
qa: 'e155390',
stage: 'e59471',
prod: 'e59433',
};

function freyjaUrl(env) {
const id = FREYJA_ENV_IDS[env] ?? FREYJA_ENV_IDS.prod;
return `https://preview-p22655-${id}.adobeaemcloud.com/adobe/contentFragments`;
}

const FREYJA_PREVIEW_URL = freyjaUrl('prod');

const ODIN_PREVIEW_URL = 'https://odinpreview.corp.adobe.com/adobe/sites/cf/fragments';

const PATH_TOKENS = /\/content\/dam\/mas\/(?<surface>[\w-_]+)\/(?<parsedLocale>[\w-_]+)\/(?<fragmentPath>.+)/;

function rootURL(preview) {
Expand Down Expand Up @@ -43,4 +58,14 @@ function odinUrl(surface, { locale, fragmentPath, preview }) {
return `${rootURL(preview)}?path=${MAS_ROOT}/${surface}/${locale}/${fragmentPath}`;
}

export { PATH_TOKENS, FRAGMENT_URL_PREFIX, MAS_ROOT, odinUrl, odinId, odinReferences };
export {
PATH_TOKENS,
FRAGMENT_URL_PREFIX,
FREYJA_PREVIEW_URL,
ODIN_PREVIEW_URL,
MAS_ROOT,
odinUrl,
odinId,
odinReferences,
freyjaUrl,
};
7 changes: 6 additions & 1 deletion io/www/test/client/fragment-client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const localStorageStub = {
let objectKeysStub;

describe('FragmentClient', () => {
const baseUrl = 'https://odinpreview.corp.adobe.com/adobe/sites/cf/fragments';
const baseUrl = 'https://preview-p22655-e59433.adobeaemcloud.com/adobe/contentFragments';
let fetchStub;

before(() => {
Expand All @@ -42,6 +42,7 @@ describe('FragmentClient', () => {
// Stub window.localStorage
globalThis.window = globalThis.window || { localStorage: {} };
sinon.stub(globalThis.window, 'localStorage').value(localStorageStub);
globalThis.window.adobeIMS = { getAccessToken: () => ({ token: 'test-ims-token' }) };
globalThis.localStorage = localStorageStub;
objectKeysStub = sinon.stub(Object, 'keys').callThrough();
objectKeysStub.withArgs(localStorageStub).callsFake(() => Object.keys(storage));
Expand Down Expand Up @@ -144,10 +145,14 @@ describe('FragmentClient', () => {

it('maps non-200 preview pipeline to body.message, logs, and preserves status in fullContext', async () => {
const fragmentId = 'non-existent';
const odinBaseUrl = 'https://odinpreview.corp.adobe.com/adobe/sites/cf/fragments';

fetchStub
.withArgs(`${baseUrl}/${fragmentId}?references=all-hydrated`)
.returns(createResponse(404, { detail: 'Not Found' }, 'Not Found'));
fetchStub
.withArgs(`${odinBaseUrl}/${fragmentId}?references=all-hydrated`)
.returns(createResponse(404, { detail: 'Not Found' }, 'Not Found'));

const consoleErrorSpy = sinon.spy(console, 'error');
try {
Expand Down
92 changes: 92 additions & 0 deletions io/www/test/fragment/common.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { fetch as internalFetch } from '../../src/fragment/utils/common.js';

describe('internalFetch', () => {
let fetchStub;

beforeEach(() => {
fetchStub = sinon.stub(globalThis, 'fetch');
});

afterEach(() => {
fetchStub.restore();
});

describe('auth header injection', () => {
it('should include Authorization header when preview.authToken is set', async () => {
fetchStub.resolves(new Response(JSON.stringify({ id: '123' }), { status: 200 }));

const context = {
preview: { url: 'https://example.com', authToken: 'test-ims-token' },
DEFAULT_HEADERS: { Accept: 'application/json' },
networkConfig: { retries: 1, fetchTimeout: 5000 },
};

await internalFetch('https://example.com/fragment/123', context, 'test');

const callArgs = fetchStub.firstCall.args;
expect(callArgs[1].headers).to.have.property('Authorization', 'Bearer test-ims-token');
expect(callArgs[1].headers).to.have.property('Accept', 'application/json');
});

it('should not include Authorization header when preview.authToken is absent', async () => {
fetchStub.resolves(new Response(JSON.stringify({ id: '123' }), { status: 200 }));

const context = {
preview: { url: 'https://example.com' },
DEFAULT_HEADERS: { Accept: 'application/json' },
networkConfig: { retries: 1, fetchTimeout: 5000 },
};

await internalFetch('https://example.com/fragment/123', context, 'test');

const callArgs = fetchStub.firstCall.args;
expect(callArgs[1].headers).to.not.have.property('Authorization');
});

it('should not include Authorization header when preview is absent', async () => {
fetchStub.resolves(new Response(JSON.stringify({ id: '123' }), { status: 200 }));

const context = {
DEFAULT_HEADERS: { Accept: 'application/json' },
networkConfig: { retries: 1, fetchTimeout: 5000 },
};

await internalFetch('https://example.com/fragment/123', context, 'test');

const callArgs = fetchStub.firstCall.args;
expect(callArgs[1].headers).to.not.have.property('Authorization');
});
});

describe('retry logic', () => {
it('should retry on 503/504 errors', async () => {
fetchStub
.onFirstCall()
.resolves(new Response('Error', { status: 503 }))
.onSecondCall()
.resolves(new Response(JSON.stringify({ id: '123' }), { status: 200 }));

const context = {
networkConfig: { retries: 3, fetchTimeout: 5000, retryDelay: 1 },
};

const result = await internalFetch('https://example.com/fragment/123', context, 'test');
expect(result.status).to.equal(200);
expect(fetchStub.callCount).to.equal(2);
});

it('should not retry on non-retryable errors', async () => {
fetchStub.resolves(new Response('Not Found', { status: 404 }));

const context = {
networkConfig: { retries: 3, fetchTimeout: 5000 },
};

const result = await internalFetch('https://example.com/fragment/123', context, 'test');
expect(result.status).to.equal(404);
expect(fetchStub.callCount).to.equal(1);
});
});
});
36 changes: 35 additions & 1 deletion io/www/test/fragment/paths.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { expect } from 'chai';
import { PATH_TOKENS, odinReferences, FRAGMENT_URL_PREFIX } from '../../src/fragment/utils/paths.js';
import {
PATH_TOKENS,
odinReferences,
FRAGMENT_URL_PREFIX,
FREYJA_PREVIEW_URL,
ODIN_PREVIEW_URL,
freyjaUrl,
} from '../../src/fragment/utils/paths.js';

describe('PATH_TOKENS', () => {
it('should work with adobe-home surface', async () => {
Expand Down Expand Up @@ -27,3 +34,30 @@ describe('odinReferences', () => {
expect(result).to.equal(`${FRAGMENT_URL_PREFIX}/test-id`);
});
});

describe('FREYJA_PREVIEW_URL', () => {
it('should point to Freyja v2 contentFragments endpoint', () => {
expect(FREYJA_PREVIEW_URL).to.equal('https://preview-p22655-e59433.adobeaemcloud.com/adobe/contentFragments');
});
});

describe('ODIN_PREVIEW_URL', () => {
it('should point to Odin preview fragments endpoint', () => {
expect(ODIN_PREVIEW_URL).to.equal('https://odinpreview.corp.adobe.com/adobe/sites/cf/fragments');
});
});

describe('freyjaUrl', () => {
it('defaults to prod when no env given', () => {
expect(freyjaUrl()).to.include('e59433');
});
it('returns stage URL for stage env', () => {
expect(freyjaUrl('stage')).to.include('e59471');
});
it('returns qa URL for qa env', () => {
expect(freyjaUrl('qa')).to.include('e155390');
});
it('falls back to prod for unknown env', () => {
expect(freyjaUrl('dev')).to.include('e59433');
});
});
12 changes: 7 additions & 5 deletions io/www/test/fragment/pipeline.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ const EXPECTED_HEADERS = {

const SETTINGS_INDEX_URL_SANDBOX = 'https://odin.adobe.com/adobe/sites/fragments?path=/content/dam/mas/sandbox/settings/index';
const SETTINGS_INDEX_URL_PREVIEW =
'https://odinpreview.corp.adobe.com/adobe/sites/cf/fragments?path=/content/dam/mas/sandbox/settings/index';
'https://preview-p22655-e59433.adobeaemcloud.com/adobe/contentFragments?path=/content/dam/mas/sandbox/settings/index';
const SETTINGS_CONTENT_URL = (settingsId) =>
`https://odin.adobe.com/adobe/sites/fragments/${settingsId}?references=all-hydrated`;
const SETTINGS_CONTENT_URL_PREVIEW = (settingsId) =>
`https://odinpreview.corp.adobe.com/adobe/sites/cf/fragments/${settingsId}?references=all-hydrated`;
`https://preview-p22655-e59433.adobeaemcloud.com/adobe/contentFragments/${settingsId}?references=all-hydrated`;

function mockSettings(fetchStub, preview = false, settingsId = 'settings-id') {
const indexUrl = preview ? SETTINGS_INDEX_URL_PREVIEW : SETTINGS_INDEX_URL_SANDBOX;
Expand All @@ -60,8 +60,10 @@ function setupFragmentMocks(fetchStub, { id, path, fields = {} }, preview = fals
// setup settings mocks so pipeline context gets settings
mockSettings(fetchStub, preview);

const odinDomain = `https://${preview ? 'odinpreview.corp' : 'odin'}.adobe.com`;
const odinUriRoot = preview ? '/adobe/sites/cf/fragments' : '/adobe/sites/fragments';
const odinDomain = preview
? 'https://preview-p22655-e59433.adobeaemcloud.com'
: 'https://odin.adobe.com';
const odinUriRoot = preview ? '/adobe/contentFragments' : '/adobe/sites/fragments';
// english fragment by id
fetchStub
.withArgs(`${odinDomain}${odinUriRoot}/some-en-us-fragment?references=all-hydrated`)
Expand Down Expand Up @@ -176,7 +178,7 @@ describe('pipeline full use case', () => {
const result = await getFragment({
id: 'some-en-us-fragment',
preview: {
url: 'https://odinpreview.corp.adobe.com/adobe/sites/cf/fragments',
url: 'https://preview-p22655-e59433.adobeaemcloud.com/adobe/contentFragments',
},
state: state,
locale: 'fr_FR',
Expand Down
6 changes: 4 additions & 2 deletions io/www/test/fragment/replace.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ const mockDictionaryBySurfaceLocale = (
dictionaryResponse = DICTIONARY_RESPONSE,
stub = fetchStub,
) => {
const odinDomain = `https://${preview ? 'odinpreview.corp' : 'odin'}.adobe.com`;
const odinUriRoot = preview ? '/adobe/sites/cf/fragments' : '/adobe/sites/fragments';
const odinDomain = preview
? 'https://preview-p22655-e59433.adobeaemcloud.com'
: 'https://odin.adobe.com';
const odinUriRoot = preview ? '/adobe/contentFragments' : '/adobe/sites/fragments';
const dictionaryId = dictionaryIdFor(surface, locale);

stub.withArgs(`${odinDomain}${odinUriRoot}?path=/content/dam/mas/${surface}/${locale}/dictionary/index`).returns(
Expand Down
2 changes: 1 addition & 1 deletion nala/studio/regional-variations/specs/variations.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export default {
locale: 'en_CA',
localeName: 'Canada',
price: PRICE_PATTERN.CA.mo,
referencesBaseUrl: 'https://odinpreview.corp.adobe.com/adobe/sites/cf/fragments',
referencesBaseUrl: 'https://preview-p22655-e59433.adobeaemcloud.com/adobe/contentFragments',
editedTitle: '[selfref] Edited title',
},
tags: '@mas-studio @regional-variations',
Expand Down
Loading