1/*! 2 * Svea A/B client split â deterministic per-visitor arm assignment, in the browser. 3 * 4 * The server-side A/B mode swaps CONTENT, which needs a singular main query and 5 * cannot survive full-page caching. A `client_split` test varies nothing the server 6 * rendered: every visitor gets byte-identical HTML, this script is on the page for 7 * all of them, and the arms only diverge at the moment somebody clicks the control 8 * being tested. That is what makes it cache-safe. 9 * 10 * The bucket function is PHP's, character for character: 11 * 12 * abs(crc32(uuid . ':' . test_id)) % 100 < traffic_percent -> 'variant' 13 * 14 * CRC-32/ISO-HDLC (reflected polynomial 0xEDB88320, init and final xor 15 * 0xFFFFFFFF) over the UTF-8 BYTES of the string, which is what PHP's crc32() 16 * hashes. `>>> 0` is the JS half of PHP's abs(): PHP returns the value unsigned on 17 * 64-bit, JS's bitwise ops return it signed, and a signed remainder would put a 18 * chunk of visitors in the wrong arm rather than merely disagreeing about a few. 19 * 20 * Parity is not assumed - tools/fixtures/ab-crc32-vectors.json holds PHP-generated 21 * vectors across the bucket boundaries and tools/ab-rig.mjs replays every one of 22 * them through THIS file. A crc32 implementation with one wrong table entry still 23 * agrees with PHP on most inputs, so "it looked right on a few uuids" proves 24 * nothing. 25 * 26 * It writes nothing, touches no DOM and reads no state it did not find already 27 * present. A visitor with no `svea_visitor` value gets null - the control arm and 28 * no measurement - which is the same posture the server-side system takes, and the 29 * only honest one: an unidentified visitor cannot be counted in a denominator. 30 */ 31(function () { 32 if (typeof window === 'undefined' || typeof document === 'undefined') return; 33 34 var COOKIE_NAME = 'svea_visitor'; 35 var LS_KEY = 'svea_visitor'; 36 37 var CRC_TABLE = null; 38 39 function crcTable() { 40 if (CRC_TABLE) return CRC_TABLE; 41 var table = new Int32Array(256); 42 for (var n = 0; n < 256; n++) { 43 var c = n; 44 for (var k = 0; k < 8; k++) { 45 c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; 46 } 47 table[n] = c; 48 } 49 CRC_TABLE = table; 50 return CRC_TABLE; 51 } 52 53 /** 54 * UTF-8 bytes of a string. PHP hashes bytes, JS strings are UTF-16 code units, 55 * and the difference is invisible for the ASCII a uuid is made of - which is 56 * exactly why it has to be done properly here rather than discovered later by 57 * the one test id or reason string that is not ASCII. 58 */ 59 function utf8Bytes(str) { 60 if (typeof TextEncoder === 'function') { 61 return new TextEncoder().encode(str); 62 } 63 var utf8 = unescape(encodeURIComponent(str)); 64 var bytes = new Uint8Array(utf8.length); 65 for (var i = 0; i < utf8.length; i++) bytes[i] = utf8.charCodeAt(i) & 0xff; 66 return bytes; 67 } 68 69 function crc32(str) { 70 var table = crcTable(); 71 var bytes = utf8Bytes(str); 72 var crc = -1; 73 for (var i = 0; i < bytes.length; i++) { 74 crc = (crc >>> 8) ^ table[(crc ^ bytes[i]) & 0xff]; 75 } 76 return (crc ^ -1) >>> 0; 77 } 78 79 function readCookie(name) { 80 var re = new RegExp('(?:^|; )' + name.replace(/([.*+?^$|(){}[\]\\])/g, '\\$1') + '=([^;]*)'); 81 var match = document.cookie.match(re); 82 return match ? decodeURIComponent(match[1]) : null; 83 } 84 85 /** The visitor id, read exactly the way tracker.js reads it: cookie, then localStorage. */ 86 function visitorUuid() { 87 var fromCookie = readCookie(COOKIE_NAME); 88 if (fromCookie) return fromCookie; 89 try { 90 return (window.localStorage && localStorage.getItem(LS_KEY)) || null; 91 } catch (e) { 92 return null; 93 } 94 } 95 96 function findTest(testId) { 97 var config = window.sveaAbSplit; 98 var tests = config && Object.prototype.toString.call(config.tests) === '[object Array]' ? config.tests : null; 99 if (!tests) return null; 100 for (var i = 0; i < tests.length; i++) { 101 if (tests[i] && Number(tests[i].id) === Number(testId)) return tests[i]; 102 } 103 return null; 104 } 105 106 /** 107 * @returns {'original'|'variant'|null} null = no config for this test, or no 108 * visitor id to hash. Both mean "behave exactly as if no test existed". 109 */ 110 function assign(testId) { 111 var test = findTest(testId); 112 if (!test) return null; 113 114 var percent = Number(test.traffic_percent); 115 if (!isFinite(percent)) return null; 116 117 var uuid = visitorUuid(); 118 if (!uuid) return null; 119 120 var bucket = crc32(uuid + ':' + Number(testId)) % 100; 121 122 return bucket < percent ? 'variant' : 'original'; 123 } 124 125 window.SveaAB = { 126 assign: assign, 127 uuid: visitorUuid, 128 // Exposed so the parity rig can hash the PHP-generated vectors through the 129 // real implementation instead of a copy of it. 130 _crc32: crc32, 131 }; 132})();
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.