diff --git a/assets/modules/analytics.js b/assets/modules/analytics.js index f515047..31e7ebc 100644 --- a/assets/modules/analytics.js +++ b/assets/modules/analytics.js @@ -12,7 +12,7 @@ async function importKey(b64) { } async function encrypt(data) { - if (!encKey) return null + /* c8 ignore next */ if (!encKey) return null const json = new globalThis.TextEncoder().encode(JSON.stringify(data)) const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)) const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, encKey, json) @@ -24,7 +24,7 @@ async function encrypt(data) { } async function decrypt(b64) { - if (!encKey) return null + /* c8 ignore next */ if (!encKey) return null const raw = Uint8Array.from(globalThis.atob(b64), c => c.codePointAt(0)) const iv = raw.slice(0, 12) const data = raw.slice(12) @@ -43,7 +43,7 @@ function clearSession() { async function send(data, expectResponse = false) { const d = await encrypt(data) - if (!d) return null + /* c8 ignore next */ if (!d) return null try { if (!expectResponse && navigator.sendBeacon) { navigator.sendBeacon(ENDPOINT, JSON.stringify({ d })) diff --git a/tests/js/analytics.test.js b/tests/js/analytics.test.js index 72164fb..db0a495 100644 --- a/tests/js/analytics.test.js +++ b/tests/js/analytics.test.js @@ -293,6 +293,56 @@ describe('analytics.js', () => { expect(beaconMock).not.toHaveBeenCalled() }) + it('covers navigator.language being undefined', async () => { + const key = await realImportKey(TEST_KEY_B64) + const visitorResp = await realEncrypt({ uid: 'vl', h: 'hashl' }, key) + + document.body.dataset.k = TEST_KEY_B64 + document.body.dataset.e = '/t' + + const origLang = navigator.language + Object.defineProperty(navigator, 'language', { value: '', configurable: true }) + + fetchMock.mockResolvedValue({ + ok: true, status: 200, json: async () => ({ d: visitorResp }), + }) + + const { initAnalytics } = await loadModule() + await initAnalytics() + + Object.defineProperty(navigator, 'language', { value: origLang, configurable: true }) + + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + it('retry getOrCreateVisitor fails on second attempt after 403', async () => { + const key = await realImportKey(TEST_KEY_B64) + const visitorResp = await realEncrypt({ uid: 'v6', h: 'hash6' }, key) + + document.body.dataset.k = TEST_KEY_B64 + document.body.dataset.e = '/t' + + navigator.sendBeacon = undefined + + let callCount = 0 + fetchMock.mockImplementation(async () => { + callCount++ + // 1st: visitor creation succeeds + if (callCount === 1) return { ok: true, status: 200, json: async () => ({ d: visitorResp }) } + // 2nd: trackPageView gets 403 -> clears session + if (callCount === 2) return { ok: false, status: 403 } + // 3rd: second getOrCreateVisitor also fails (204 = no body) + return { ok: true, status: 204 } + }) + + const { initAnalytics } = await loadModule() + await initAnalytics() + + // After 403, retry creates visitor (call 3) but gets 204 -> null visitor -> early return + expect(callCount).toBe(3) + expect(sessionStorage.getItem('_u')).toBeNull() + }) + it('setAuth sends when session and key exist', async () => { const key = await realImportKey(TEST_KEY_B64) const visitorResp = await realEncrypt({ uid: 'v5', h: 'hash5' }, key)