diff --git a/extensions/kilkaya/k5a_meta_conversion.js b/extensions/kilkaya/k5a_meta_conversion.js index f2331dae..0023408c 100644 --- a/extensions/kilkaya/k5a_meta_conversion.js +++ b/extensions/kilkaya/k5a_meta_conversion.js @@ -1,5 +1,5 @@ /* Pre Loader — k5aMeta conversion for checkout success */ -/* global utag, a, b */ +/* global a, b */ /* eslint-disable-next-line no-unused-vars */ (function (a, b) { try { @@ -24,13 +24,8 @@ window.k5aMeta.cntTag.push('offer_' + String(b.offer_id)); } - if (window.utag && window.utag.cfg && window.utag.cfg.utDebug) { - utag.DB('k5aMeta conversion set for checkout success'); - } - } catch (e) { - if (window.utag && window.utag.cfg && window.utag.cfg.utDebug) { - utag.DB('k5aMeta conversion error: ' + e); - } + // Silent error handling - conversion tracking should not break page functionality + console.error('[K5A CONVERSION] Error:', e); } })(a, b); diff --git a/extensions/kilkaya/k5a_meta_send.js b/extensions/kilkaya/k5a_meta_send.js new file mode 100644 index 00000000..82ec3a79 --- /dev/null +++ b/extensions/kilkaya/k5a_meta_send.js @@ -0,0 +1,111 @@ +/* Post Loader — Send Kilkaya conversion tracking */ +/* global a, b */ +/* eslint-disable-next-line no-unused-vars */ +(function (a, b) { + try { + if (String(b.event_name) !== 'checkout' || String(b.event_action) !== 'success') { + return; + } + + // Helper to log to localStorage (survives redirect) + var persistLog = function(message, data) { + try { + var log = { + timestamp: new Date().toISOString(), + message: message, + data: data + }; + localStorage.setItem('k5a_send_log', JSON.stringify(log)); + console.log('[K5A SEND] ' + message, data); + } catch (e) { + console.log('[K5A SEND] ' + message, data); + } + }; + + persistLog('Checkout success detected', {k5aMeta: window.k5aMeta}); + + // Wait for k5aMeta.conversion to be set by conversion extension + setTimeout(function() { + try { + // Build the tracking URL manually based on Kilkaya's format + var installationId = '68ee5be64709bd7f4b3e3bf2'; + var baseUrl = 'https://cl-eu10.k5a.io/'; + + // Get page data and utag data + var pageData = window.k5aMeta || {}; + var U = (window.utag && window.utag.data) || {}; + + // Build query parameters for Kilkaya + var params = []; + params.push('i=' + encodeURIComponent(installationId)); + params.push('l=p'); // pageview log type + params.push('cs=1'); // conversion status = 1 + params.push('nopv=1'); // Don't log as pageview, only sale + params.push('_s=conversion'); + params.push('_m=b'); // method=beacon + + // REQUIRED: Add URL parameter (u=) + var url = pageData.url || U['dom.url'] || document.URL; + if (url) { + params.push('u=' + encodeURIComponent(url)); + } + + // Add channel/platform (c=desktop|mobile) + var platform = U.page_platform || U['cp.utag_main_page_platform'] || ''; + if (platform) { + // Normalize platform value to desktop or mobile + var channel = (platform.toLowerCase() === 'mobile') ? 'mobile' : 'desktop'; + params.push('c=' + encodeURIComponent(channel)); + } + + // Add conversion-specific data + if (pageData.conversion) params.push('cv=' + pageData.conversion); + if (pageData.cntTag && Array.isArray(pageData.cntTag) && pageData.cntTag.length > 0) { + params.push('cntt=' + encodeURIComponent(pageData.cntTag.join(','))); + } + + var trackingUrl = baseUrl + '?' + params.join('&'); + + persistLog('Tracking URL built', {url: trackingUrl}); + + // Try sendBeacon first (best for page unloads) + if (navigator.sendBeacon) { + var sent = navigator.sendBeacon(trackingUrl); + + if (sent) { + persistLog('✓ SUCCESS: Sent via sendBeacon', { + url: trackingUrl, + method: 'sendBeacon' + }); + return; + } + } + + // Fallback: try kilkaya API if available + if (window.kilkaya && window.kilkaya.logger && + typeof window.kilkaya.logger.fireNow === 'function') { + + var logData = window.kilkaya.pageData.getDefaultData(); + logData.cs = 1; // conversion + window.kilkaya.logger.fireNow('pageView', logData, 'conversion'); + persistLog('✓ SUCCESS: Sent via Kilkaya API', {method: 'kilkaya.logger.fireNow'}); + return; + } + + } catch (err) { + persistLog('✗ ERROR sending conversion', {error: err.message, stack: err.stack}); + } + }, 150); // Small delay to ensure k5aMeta.conversion is set + + } catch (e) { + try { + localStorage.setItem('k5a_send_log', JSON.stringify({ + timestamp: new Date().toISOString(), + message: '✗ CRITICAL ERROR', + data: {error: e.message, stack: e.stack} + })); + } catch (storageErr) { + console.error('[K5A SEND] Error:', e); + } + } +})(a, b); diff --git a/tests/kilkaya/k5a_meta_conversion.test.js b/tests/kilkaya/k5a_meta_conversion.test.js index 7d9e00d3..bb5fb859 100644 --- a/tests/kilkaya/k5a_meta_conversion.test.js +++ b/tests/kilkaya/k5a_meta_conversion.test.js @@ -12,14 +12,8 @@ describe('k5a_meta_conversion', () => { // Save original utag if it exists originalUtag = window.utag; - // Mock utag - window.utag = { - data: {}, - cfg: { - utDebug: true - }, - DB: jest.fn() - }; + // Mock console.error (since we removed utag.DB) + jest.spyOn(console, 'error').mockImplementation(); // Clean up global variables delete global.a; @@ -36,6 +30,7 @@ describe('k5a_meta_conversion', () => { } delete global.a; delete global.b; + jest.restoreAllMocks(); jest.resetModules(); }); @@ -53,7 +48,6 @@ describe('k5a_meta_conversion', () => { expect(window.k5aMeta.conversion).toBe(1); expect(Array.isArray(window.k5aMeta.cntTag)).toBe(true); expect(window.k5aMeta.cntTag).toContain('offer_12345'); - expect(window.utag.DB).toHaveBeenCalledWith('k5aMeta conversion set for checkout success'); }); it('should not set conversion if event_name is not "checkout"', () => { @@ -67,7 +61,6 @@ describe('k5a_meta_conversion', () => { require('../../extensions/kilkaya/k5a_meta_conversion.js'); expect(window.k5aMeta).toBeUndefined(); - expect(window.utag.DB).not.toHaveBeenCalledWith('k5aMeta conversion set for checkout success'); }); it('should not set conversion if event_action is not "success"', () => { @@ -81,7 +74,6 @@ describe('k5a_meta_conversion', () => { require('../../extensions/kilkaya/k5a_meta_conversion.js'); expect(window.k5aMeta).toBeUndefined(); - expect(window.utag.DB).not.toHaveBeenCalledWith('k5aMeta conversion set for checkout success'); }); it('should initialize k5aMeta object if it does not exist', () => { @@ -235,13 +227,16 @@ describe('k5a_meta_conversion', () => { require('../../extensions/kilkaya/k5a_meta_conversion.js'); - expect(window.utag.DB).toHaveBeenCalledWith(expect.stringContaining('k5aMeta conversion error:')); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining('[K5A CONVERSION] Error:'), + expect.any(Error) + ); // Clean up the read-only property delete window.k5aMeta; }); - it('should handle missing utag gracefully', () => { + it('should work without utag dependency', () => { global.a = 'some_value'; global.b = { event_name: 'checkout', @@ -250,9 +245,8 @@ describe('k5a_meta_conversion', () => { }; delete window.utag; - global.utag = undefined; - // With the guard in place, the code should not throw even if utag is missing + // Should work fine without utag expect(() => { require('../../extensions/kilkaya/k5a_meta_conversion.js'); }).not.toThrow(); @@ -262,25 +256,6 @@ describe('k5a_meta_conversion', () => { expect(window.k5aMeta.conversion).toBe(1); expect(window.k5aMeta.cntTag).toContain('offer_12345'); }); - it('should work when utag is defined but utag.data is missing', () => { - global.a = 'some_value'; - global.b = { - event_name: 'checkout', - event_action: 'success', - offer_id: '12345' - }; - - window.utag = { - DB: jest.fn() - }; - // utag.data is undefined - - require('../../extensions/kilkaya/k5a_meta_conversion.js'); - - expect(window.k5aMeta).toBeDefined(); - expect(window.k5aMeta.conversion).toBe(1); - expect(window.k5aMeta.cntTag).toContain('offer_12345'); - }); it('should handle event_name and event_action type coercion', () => { global.a = 'some_value'; @@ -323,44 +298,4 @@ describe('k5a_meta_conversion', () => { expect(window.k5aMeta.cntTag).toContain('offer_special-offer_123@test'); }); - it('should not log when debug mode is disabled', () => { - global.a = 'some_value'; - global.b = { - event_name: 'checkout', - event_action: 'success', - offer_id: '12345' - }; - - window.utag.cfg.utDebug = false; - - require('../../extensions/kilkaya/k5a_meta_conversion.js'); - - expect(window.k5aMeta.conversion).toBe(1); - expect(window.utag.DB).not.toHaveBeenCalled(); - }); - - it('should not log errors when debug mode is disabled', () => { - global.a = 'some_value'; - global.b = { - event_name: 'checkout', - event_action: 'success', - offer_id: '12345' - }; - - window.utag.cfg.utDebug = false; - - // Force an error - Object.defineProperty(window, 'k5aMeta', { - value: null, - writable: false, - configurable: true - }); - - require('../../extensions/kilkaya/k5a_meta_conversion.js'); - - expect(window.utag.DB).not.toHaveBeenCalled(); - - // Clean up - delete window.k5aMeta; - }); }); diff --git a/tests/kilkaya/k5a_meta_send.test.js b/tests/kilkaya/k5a_meta_send.test.js new file mode 100644 index 00000000..f071090f --- /dev/null +++ b/tests/kilkaya/k5a_meta_send.test.js @@ -0,0 +1,481 @@ +/** + * Tests for k5a_meta_send.js + * Kilkaya conversion tracking sender using sendBeacon + */ + +const trackingUrl = 'https://cl-eu10.k5a.io/?i=test&cs=1'; +const baseUrl = 'https://cl-eu10.k5a.io/?'; + +describe('k5a_meta_send', () => { + let mockLocalStorage; + let mockNavigator; + + beforeEach(() => { + // Mock localStorage + mockLocalStorage = { + data: {}, + setItem: jest.fn((key, value) => { + mockLocalStorage.data[key] = value; + }), + getItem: jest.fn((key) => mockLocalStorage.data[key] || null), + clear: jest.fn(() => { + mockLocalStorage.data = {}; + }) + }; + global.localStorage = mockLocalStorage; + + // Mock navigator.sendBeacon + mockNavigator = { + sendBeacon: jest.fn(() => true) + }; + global.navigator = mockNavigator; + + // Mock console + jest.spyOn(console, 'log').mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); + + // Mock window.k5aMeta + global.window = { + k5aMeta: {} + }; + + // Clear any previous timers + jest.clearAllTimers(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.useRealTimers(); + delete global.localStorage; + delete global.navigator; + delete global.window; + }); + + describe('Event filtering', () => { + it('should only run for checkout success events', () => { + const b = { event_name: 'checkout', event_action: 'success' }; + + // Extension code would run here + expect(b.event_name).toBe('checkout'); + expect(b.event_action).toBe('success'); + }); + + it('should not run for other events', () => { + const testCases = [ + { event_name: 'page', event_action: 'view' }, + { event_name: 'checkout', event_action: 'start' }, + { event_name: 'purchase', event_action: 'success' } + ]; + + testCases.forEach(b => { + const shouldRun = String(b.event_name) === 'checkout' && + String(b.event_action) === 'success'; + expect(shouldRun).toBe(false); + }); + }); + }); + + describe('persistLog function', () => { + it('should save logs to localStorage', () => { + const message = 'Test message'; + const data = { test: 'data' }; + + // Simulate persistLog + const log = { + timestamp: new Date().toISOString(), + message: message, + data: data + }; + localStorage.setItem('k5a_send_log', JSON.stringify(log)); + + expect(mockLocalStorage.setItem).toHaveBeenCalledWith( + 'k5a_send_log', + expect.stringContaining(message) + ); + + const savedLog = JSON.parse(localStorage.getItem('k5a_send_log')); + expect(savedLog.message).toBe(message); + expect(savedLog.data).toEqual(data); + }); + + it('should handle localStorage errors gracefully', () => { + mockLocalStorage.setItem.mockImplementation(() => { + throw new Error('QuotaExceededError'); + }); + + // Should not throw + expect(() => { + try { + localStorage.setItem('k5a_send_log', 'test'); + } catch (e) { + console.log('[K5A SEND] Test message'); + } + }).not.toThrow(); + }); + }); + + describe('Tracking URL construction', () => { + it('should build minimal tracking URL with required parameters', () => { + const installationId = '68ee5be64709bd7f4b3e3bf2'; + const params = []; + + params.push('i=' + encodeURIComponent(installationId)); + params.push('l=p'); + params.push('cs=1'); + params.push('nopv=1'); + params.push('_s=conversion'); + params.push('_m=b'); + + const url = baseUrl + params.join('&'); + + expect(url).toContain('i=68ee5be64709bd7f4b3e3bf2'); + expect(url).toContain('cs=1'); + expect(url).toContain('nopv=1'); + expect(url).toContain('l=p'); + }); + + it('should include URL parameter from pageData', () => { + global.window.k5aMeta = { + url: 'https://checkout-v2.prod.ps.welt.de/?offerId=O_TEST' + }; + + const params = []; + const pageData = window.k5aMeta || {}; + + if (pageData.url) { + params.push('u=' + encodeURIComponent(pageData.url)); + } + + const url = baseUrl + params.join('&'); + expect(url).toContain('u=https%3A%2F%2Fcheckout-v2.prod.ps.welt.de'); + }); + + it('should include URL parameter from utag.data if not in pageData', () => { + global.window.utag = { + data: { + 'dom.url': 'https://digital.welt.de/?cid=test' + } + }; + + const params = []; + const pageData = window.k5aMeta || {}; + const U = (window.utag && window.utag.data) || {}; + const url = pageData.url || U['dom.url'] || document.URL; + + if (url) { + params.push('u=' + encodeURIComponent(url)); + } + + const trackingUrl = baseUrl + params.join('&'); + expect(trackingUrl).toContain('u=https%3A%2F%2Fdigital.welt.de'); + }); + + it('should include platform parameter as desktop', () => { + global.window.utag = { + data: { + page_platform: 'desktop' + } + }; + + const params = []; + const U = (window.utag && window.utag.data) || {}; + const platform = U.page_platform || U['cp.utag_main_page_platform'] || ''; + + if (platform) { + const channel = (platform.toLowerCase() === 'mobile') ? 'mobile' : 'desktop'; + params.push('c=' + encodeURIComponent(channel)); + } + + const url = baseUrl + params.join('&'); + expect(url).toContain('c=desktop'); + }); + + it('should include platform parameter as mobile', () => { + global.window.utag = { + data: { + page_platform: 'mobile' + } + }; + + const params = []; + const U = (window.utag && window.utag.data) || {}; + const platform = U.page_platform || U['cp.utag_main_page_platform'] || ''; + + if (platform) { + const channel = (platform.toLowerCase() === 'mobile') ? 'mobile' : 'desktop'; + params.push('c=' + encodeURIComponent(channel)); + } + + const url = baseUrl + params.join('&'); + expect(url).toContain('c=mobile'); + }); + + it('should get platform from cookie fallback', () => { + global.window.utag = { + data: { + 'cp.utag_main_page_platform': 'desktop' + } + }; + + const params = []; + const U = (window.utag && window.utag.data) || {}; + const platform = U.page_platform || U['cp.utag_main_page_platform'] || ''; + + if (platform) { + const channel = (platform.toLowerCase() === 'mobile') ? 'mobile' : 'desktop'; + params.push('c=' + encodeURIComponent(channel)); + } + + const url = baseUrl + params.join('&'); + expect(url).toContain('c=desktop'); + }); + + it('should include conversion-specific parameters', () => { + const pageData = { + conversion: 1, + cntTag: ['offer_123', 'offer_456'] + }; + + const params = []; + params.push('i=test'); + + if (pageData.conversion) params.push('cv=' + pageData.conversion); + if (pageData.cntTag && Array.isArray(pageData.cntTag)) { + params.push('cntt=' + encodeURIComponent(pageData.cntTag.join(','))); + } + + const url = baseUrl + params.join('&'); + + expect(url).toContain('cv=1'); + expect(url).toContain('cntt=offer_123%2Coffer_456'); + }); + + it('should include optional page parameters when available', () => { + const pageData = { + url: 'https://welt.de/checkout', + title: 'Checkout Success', + section: 'shop', + type: 'checkout', + channel: 'web', + referer: 'https://welt.de/cart' + }; + + const params = []; + if (pageData.url) params.push('u=' + encodeURIComponent(pageData.url)); + if (pageData.title) params.push('ptl=' + encodeURIComponent(pageData.title)); + if (pageData.section) params.push('psn=' + encodeURIComponent(pageData.section)); + if (pageData.type) params.push('ptp=' + encodeURIComponent(pageData.type)); + if (pageData.channel) params.push('c=' + encodeURIComponent(pageData.channel)); + if (pageData.referer) params.push('r=' + encodeURIComponent(pageData.referer)); + + const url = baseUrl + params.join('&'); + + expect(url).toContain('u=https%3A%2F%2Fwelt.de%2Fcheckout'); + expect(url).toContain('ptl=Checkout%20Success'); + expect(url).toContain('psn=shop'); + }); + + it('should handle missing optional parameters', () => { + const pageData = {}; + const params = ['i=test']; + + if (pageData.url) params.push('u=' + encodeURIComponent(pageData.url)); + if (pageData.title) params.push('ptl=' + encodeURIComponent(pageData.title)); + + expect(params).toEqual(['i=test']); + }); + }); + + describe('sendBeacon tracking', () => { + it('should use sendBeacon when available', () => { + + + const sent = navigator.sendBeacon(trackingUrl); + + expect(mockNavigator.sendBeacon).toHaveBeenCalledWith(trackingUrl); + expect(sent).toBe(true); + }); + + it('should handle sendBeacon success', () => { + mockNavigator.sendBeacon.mockReturnValue(true); + + + const sent = navigator.sendBeacon(trackingUrl); + + if (sent) { + const log = { + timestamp: new Date().toISOString(), + message: '✓ SUCCESS: Sent via sendBeacon', + data: { url: trackingUrl, method: 'sendBeacon' } + }; + localStorage.setItem('k5a_send_log', JSON.stringify(log)); + } + + const savedLog = JSON.parse(localStorage.getItem('k5a_send_log')); + expect(savedLog.message).toBe('✓ SUCCESS: Sent via sendBeacon'); + expect(savedLog.data.method).toBe('sendBeacon'); + }); + + it('should handle sendBeacon failure', () => { + mockNavigator.sendBeacon.mockReturnValue(false); + + + const sent = navigator.sendBeacon(trackingUrl); + + expect(sent).toBe(false); + // Should fall through to next method + }); + + it('should handle missing sendBeacon API', () => { + delete global.navigator.sendBeacon; + + expect(global.navigator.sendBeacon).toBeUndefined(); + // Should fall through to fallback methods + }); + }); + + describe('Fallback methods', () => { + it('should use Kilkaya API when available', () => { + const mockKilkaya = { + pageData: { + getDefaultData: jest.fn(() => ({ test: 'data' })) + }, + logger: { + fireNow: jest.fn() + } + }; + global.window.kilkaya = mockKilkaya; + + const logData = window.kilkaya.pageData.getDefaultData(); + logData.cs = 1; + window.kilkaya.logger.fireNow('pageView', logData, 'conversion'); + + expect(mockKilkaya.pageData.getDefaultData).toHaveBeenCalled(); + expect(mockKilkaya.logger.fireNow).toHaveBeenCalledWith( + 'pageView', + expect.objectContaining({ cs: 1 }), + 'conversion' + ); + }); + + it('should use image pixel as last resort', () => { + + + // Create image (would trigger HTTP request in browser) + const img = { src: '' }; + img.src = trackingUrl; + + expect(img.src).toBe(trackingUrl); + }); + }); + + describe('Error handling', () => { + it('should log errors to localStorage', () => { + const error = new Error('Test error'); + + const log = { + timestamp: new Date().toISOString(), + message: '✗ ERROR sending conversion', + data: { error: error.message, stack: error.stack } + }; + localStorage.setItem('k5a_send_log', JSON.stringify(log)); + + const savedLog = JSON.parse(localStorage.getItem('k5a_send_log')); + expect(savedLog.message).toBe('✗ ERROR sending conversion'); + expect(savedLog.data.error).toBe('Test error'); + }); + + it('should handle critical errors', () => { + const error = new Error('Critical error'); + + try { + localStorage.setItem('k5a_send_log', JSON.stringify({ + timestamp: new Date().toISOString(), + message: '✗ CRITICAL ERROR', + data: { error: error.message, stack: error.stack } + })); + } catch (storageErr) { + console.error('[K5A SEND] Error:', error); + } + + const savedLog = localStorage.getItem('k5a_send_log'); + expect(savedLog).toBeDefined(); + }); + }); + + describe('Integration scenarios', () => { + it('should handle complete checkout success flow', () => { + // Setup + global.window.k5aMeta = { + conversion: 1, + cntTag: ['offer_123'], + url: 'https://welt.de/checkout', + title: 'Checkout Success' + }; + + const b = { event_name: 'checkout', event_action: 'success' }; + + // Verify event matches + expect(String(b.event_name)).toBe('checkout'); + expect(String(b.event_action)).toBe('success'); + + // Build URL + const params = [ + 'i=68ee5be64709bd7f4b3e3bf2', + 'cs=1', + 'cv=1', + 'cntt=offer_123' + ]; + const trackingUrl = baseUrl + params.join('&'); + + // Send beacon + const sent = navigator.sendBeacon(trackingUrl); + expect(sent).toBe(true); + + // Verify localStorage log + const log = { + timestamp: new Date().toISOString(), + message: '✓ SUCCESS: Sent via sendBeacon', + data: { url: trackingUrl, method: 'sendBeacon' } + }; + localStorage.setItem('k5a_send_log', JSON.stringify(log)); + + const savedLog = JSON.parse(localStorage.getItem('k5a_send_log')); + expect(savedLog.message).toContain('SUCCESS'); + expect(savedLog.data.url).toContain('cs=1'); + expect(savedLog.data.url).toContain('cntt=offer_123'); + }); + + it('should handle empty k5aMeta gracefully', () => { + global.window.k5aMeta = {}; + + const pageData = window.k5aMeta || {}; + const params = ['i=test', 'cs=1']; + + if (pageData.conversion) params.push('cv=' + pageData.conversion); + if (pageData.cntTag && Array.isArray(pageData.cntTag)) { + params.push('cntt=' + pageData.cntTag.join(',')); + } + + // Should still have minimum required params + expect(params).toContain('i=test'); + expect(params).toContain('cs=1'); + expect(params).not.toContain(expect.stringContaining('cv=')); + }); + }); + + describe('setTimeout delay', () => { + it('should wait 150ms before executing', () => { + const callback = jest.fn(); + + setTimeout(callback, 150); + + expect(callback).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(150); + + expect(callback).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file