diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index 05d984dd1..f43514658 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -41,7 +41,10 @@ import { parseLegacyCookies } from './cookie-migration'; import { DEFAULT_IDENTITY_STORAGE, DEFAULT_SERVER_ZONE } from './constants'; import { AmplitudeBrowser } from './browser-client'; import { VERSION } from './version'; -import { getDomain } from './attribution/helpers'; +import { getDomain, isSubdomainOf } from './attribution/helpers'; + +const TLD_CACHE_KEY = 'AMP_TLD'; +const tldCache = new SessionStorage(); // Exported for testing purposes only. Do not expose to public interface. export class BrowserConfig extends Config implements IBrowserConfig { @@ -486,6 +489,14 @@ export const createTransport = (transport?: TransportTypeOrOptions) => { }; export const getTopLevelDomain = async (url?: string, diagnosticsClient?: IDiagnosticsClient) => { + // see if the TLD is cached returun that and skip the cookie check + if (!url && typeof location !== 'undefined') { + const cachedTld = await tldCache.get(TLD_CACHE_KEY); + // also do a sanity check that the TLD is a subdomain of the hostname + if (cachedTld && isSubdomainOf(location.hostname, cachedTld)) { + return cachedTld; + } + } if ( !(await new CookieStorage(undefined, { diagnosticsClient }).isEnabled()) || (!url && (typeof location === 'undefined' || !location.hostname)) @@ -506,6 +517,7 @@ export const getTopLevelDomain = async (url?: string, diagnosticsClient?: IDiagn // if the transaction succeeded, the domain is valid if (result) { + !url && (await tldCache.set(TLD_CACHE_KEY, '.' + domain)); return '.' + domain; } } catch (e) { diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index a0449d183..d021095ec 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -534,6 +534,50 @@ describe('config', () => { BrowserUtils.CookieStorage.isDomainWritable = isDomainWritableBefore; } }); + + describe('tldCache', () => { + /* eslint-disable-next-line @typescript-eslint/unbound-method */ + const isDomainWritableBefore = BrowserUtils.CookieStorage.isDomainWritable; + const originalLocation = window.location; + beforeAll(() => { + // mock isDomainWritable to return true + BrowserUtils.CookieStorage.isDomainWritable = jest.fn().mockImplementation((domain: string) => { + if (domain === 'co.uk') return Promise.resolve(false); + if (domain === 'example.co.uk') return Promise.resolve(true); + return Promise.resolve(false); + }); + Object.defineProperty(window, 'location', { + value: { + hostname: 'example.co.uk', + }, + configurable: true, + }); + }); + + afterAll(() => { + BrowserUtils.CookieStorage.isDomainWritable = isDomainWritableBefore; + Object.defineProperty(window, 'location', { + value: originalLocation, + configurable: true, + }); + }); + + test('should return cached TLD when available', async () => { + const tld1 = await Config.getTopLevelDomain(); + expect(sessionStorage.getItem('AMP_TLD')).toBe('".example.co.uk"'); + const tld2 = await Config.getTopLevelDomain(); + expect(tld1).toBe('.example.co.uk'); + expect(tld2).toBe('.example.co.uk'); + }); + + test('should not return cached TLD if it is not a subdomain of the hostname', async () => { + const tld1 = await Config.getTopLevelDomain(); + sessionStorage.setItem('AMP_TLD', 'CORRUPTED_VALUE'); + const tld2 = await Config.getTopLevelDomain(); + expect(tld1).toBe('.example.co.uk'); + expect(tld2).toBe('.example.co.uk'); + }); + }); }); describe('fetchRemoteConfig', () => {