Skip to content
Chrome 151 → 152

Chrome 152: what changed for fingerprinting and automation

Chrome 152 changed what a page can observe in 43 places, and it is the first version in this series whose TLS handshake changes: a new trust_anchors extension moves the JA4 fingerprint, so a server can tell 151 from 152 before any page code runs. The CPU Performance API, <code>OpaqueRange</code> and a configurable <code>window.chrome</code> arrive, and the Web Audio fingerprint shifts in its last digits.

Pim
Pim · Clearcote Research
Stable release
August 25, 2026
Measured on (Linux x64)
151.0.7922.173 → 152.0.7977.82
Source read at
151.0.7922.176 → 152.0.7977.85
V8
15.1.206.23 → 15.2.124.21

Key takeaways

  • The TLS ClientHello gains the trust_anchors extension (0xCA34) and a GREASE signature algorithm, so JA4 moves from t13d1516h2_…_806a8c22fdea to t13d1517h2_…_cb7bf5808d99: the value Chrome 153 and 154 keep.
  • New in one line: 'cpuPerformance' in navigator, typeof OpaqueRange, FeaturePolicy.name (now "PermissionsPolicy") and DeviceOrientationEvent.requestPermission.
  • window.chrome becomes configurable and deletable, and moves after WebAssembly in the global property order.
  • Web Audio's FFT moved to a Rust library, so the classic oscillator-and-compressor audio fingerprint changes in its last digits: 124.04347527516074 on 151, 124.04347776696522 from 152.
  • The new GREASE brand is Not?A_Brand version 24, with Chromium listed first. Nothing Chrome 152 changed was reverted in 153.

Every check, both versions, and your browser

70 one-line checks across 43 changes. The strip shows where they are; the table shows what each returns on Chrome 151 and Chrome 152. Run them in this browser to see which version its engine matches.

Chrome 151 → Chrome 152 · 70 checks

Run the checks to see which Chrome version this browser's engine matches.

Where the changes are. Select a surface to filter the rows.

CheckChrome 151Chrome 152This browser
TLS handshake
TLS ClientHello gains the trust_anchors extension and a GREASE signature algorithmJSON.parse(document.querySelector('pre').textContent).tls.ja4"t13d1516h2_8daaf6152771_806a8c22fdea""t13d1517h2_8daaf6152771_cb7bf5808d99"n/a
TLS ClientHello gains the trust_anchors extension and a GREASE signature algorithmJSON.parse(document.querySelector('pre').textContent).tls.extensions.some((e) => e.name === 'Unknown extension 51764')falsetruen/a
Header order
Connection-Allowlist response header is enforced for every sitefetch('https://example.com/', { mode: 'no-cors' }).then(() => 'sent', (e) => e.name)"sent""TypeError"n/a
Topics and Attribution Reporting request options and headers removed(() => { let read = false; new Request('/', { get attributionReporting() { read = true; } }); return read; })()truefalse–
Topics and Attribution Reporting request options and headers removed(() => { let read = false; new Request('/', { get browsingTopics() { read = true; } }); return read; })()truefalse–
Client Hints
New GREASE brand and brand order in User-Agent Client Hintsnavigator.userAgentData.brands.map((b) => `${b.brand};v=${b.version}`).join(', ')"Not=A?Brand;v=99, Google Chrome;v=151, Chromium;v=151""Chromium;v=152, Not?A_Brand;v=24, Google Chrome;v=152"–
JavaScript and DOM
navigator.cpuPerformance (CPU Performance API)'cpuPerformance' in navigatorfalsetrue–
navigator.cpuPerformance (CPU Performance API)typeof navigator.cpuPerformance"undefined""number"–
OpaqueRange, NodeRange and createValueRange(): the Range prototype chain changestypeof OpaqueRange"undefined""function"–
OpaqueRange, NodeRange and createValueRange(): the Range prototype chain changesObject.getPrototypeOf(Range.prototype).constructor.name"AbstractRange""NodeRange"–
OpaqueRange, NodeRange and createValueRange(): the Range prototype chain changes'startContainer' in AbstractRange.prototypetruefalse–
OpaqueRange, NodeRange and createValueRange(): the Range prototype chain changestypeof HTMLInputElement.prototype.createValueRange"undefined""function"–
FeaturePolicy is renamed PermissionsPolicytypeof PermissionsPolicy"undefined""function"–
FeaturePolicy is renamed PermissionsPolicyFeaturePolicy.name"FeaturePolicy""PermissionsPolicy"–
FeaturePolicy is renamed PermissionsPolicyObject.prototype.toString.call(document.featurePolicy)"[object FeaturePolicy]""[object PermissionsPolicy]"–
window.chrome is now a configurable property, created laterObject.getOwnPropertyDescriptor(window, 'chrome').configurablefalsetrue–
window.chrome is now a configurable property, created laterObject.getOwnPropertyNames(window).indexOf('chrome') < Object.getOwnPropertyNames(window).indexOf('WebAssembly')truefalse–
Protected Audience, Topics and Shared Storage are stubbed out: still exposed, no longer working(() => { try { return navigator.adAuctionComponents(1).length; } catch (e) { return e.name; } })()"InvalidStateError"0–
Protected Audience, Topics and Shared Storage are stubbed out: still exposed, no longer workingdocument.browsingTopics().then(() => 'resolved', (e) => e.name)"resolved""NotSupportedError"–
Protected Audience, Topics and Shared Storage are stubbed out: still exposed, no longer workingnavigator.protectedAudience.queryFeatureSupport('sellerNonce')truefalse–
Protected Audience, Topics and Shared Storage are stubbed out: still exposed, no longer workingnavigator.canLoadAdAuctionFencedFrame()truefalsen/a
DeviceOrientationEvent.requestPermission() and DeviceMotionEvent.requestPermission()typeof DeviceOrientationEvent.requestPermission"undefined""function"–
DeviceOrientationEvent.requestPermission() and DeviceMotionEvent.requestPermission()typeof DeviceMotionEvent.requestPermission"undefined""function"–
console.context() objects get dirxml instead of dirXmltypeof console.context().dirxml"undefined""function"–
console.context() objects get dirxml instead of dirXmltypeof console.context().dirXml"function""undefined"–
V8's internal built-in closures are strict: .caller throws, f.caller is null(() => { try { return typeof Proxy.revocable({}, {}).revoke.caller; } catch (e) { return e.name; } })()"object""TypeError"–
V8's internal built-in closures are strict: .caller throws, f.caller is nullnew Promise((r) => Promise.resolve().finally(function f() { r(typeof f.caller); }))"function""object"n/a
Property order moved on window, Document, PointerEvent and the Performance interfacesObject.getOwnPropertyNames(window).indexOf('onpagereveal') < Object.getOwnPropertyNames(window).indexOf('onpageswap')falsetrue–
Property order moved on window, Document, PointerEvent and the Performance interfacesObject.getOwnPropertyNames(PointerEvent.prototype).indexOf('persistentDeviceId') < Object.getOwnPropertyNames(PointerEvent.prototype).indexOf('getPredictedEvents')falsetrue–
Property order moved on window, Document, PointerEvent and the Performance interfacesObject.getOwnPropertyNames(PerformanceNavigationTiming.prototype).indexOf('confidence') < Object.getOwnPropertyNames(PerformanceNavigationTiming.prototype).indexOf('toJSON')falsetrue–
Property order moved on window, Document, PointerEvent and the Performance interfacesObject.getOwnPropertyNames(PerformancePaintTiming.prototype)[0]"paintTime""toJSON"–
Property order moved on window, Document, PointerEvent and the Performance interfacesObject.getOwnPropertyNames(window).indexOf('PerformanceLongAnimationFrameTiming') < Object.getOwnPropertyNames(window).indexOf('Performance')truefalse–
The attribution-reporting permissions-policy feature is removeddocument.featurePolicy.features().includes('attribution-reporting')truefalse–
Shadow DOM reference target: ShadowRoot.referenceTarget'referenceTarget' in ShadowRoot.prototypefalsetrue–
Shadow DOM reference target: ShadowRoot.referenceTarget'shadowRootReferenceTarget' in HTMLTemplateElement.prototypefalsetrue–
Declarative shadow DOM: shadowrootslotassignment'shadowRootSlotAssignment' in HTMLTemplateElement.prototypefalsetrue–
Sanitizer API: processing instructions in the configuration, streaming sanitizer on'processingInstructions' in new Sanitizer().get()falsetrue–
Sanitizer API: processing instructions in the configuration, streaming sanitizer onDocument.parseHTML('a<!--x-->b').body.childNodes.length21–
RegExp split with the v flag steps by code point on the spec path(() => { const v = []; class R extends RegExp { exec() { v.push(this.lastIndex); return null; } } ('a' + String.fromCodePoint(128512) + 'b').split(new R('x', 'v')); return v.join(); })()"0,1,2,3""0,1,3"–
WebAssembly compile errors name --wasm-* flags instead of --experimental-wasm-*(() => { try { new WebAssembly.Module(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 10, 6, 1, 4, 0, 224, 0, 11])); return 'compiled'; } catch (e) { return e.message.slice(e.message.indexOf('(enable')); } })()"(enable with --experimental-wasm-wasmfx) @+23""(enable with --wasm-wasmfx) @+23"–
Error stacks include WebAssembly frames across JSPI suspensions(async () => { const b = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 2, 7, 1, 1, 109, 1, 102, 0, 0, 3, 2, 1, 0, 7, 7, 1, 3, 114, 117, 110, 0, 1, 10, 6, 1, 4, 0, 16, 0, 11]); const { instance: i } = await WebAssembly.instantiate(b, { m: { f: new WebAssembly.Suspending(async () => { await 0; throw Error(); }) } }); try { await WebAssembly.promising(i.exports.run)(); } catch (e) { return e.stack.includes('wasm-function'); } })()falsetrue–
Parsing and rendering
HTML parser: a truncated <! markup declaration becomes a comment instead of eating the restnew DOMParser().parseFromString('a<!d>b', 'text/html').body.innerHTML"a""a<!--d-->b"–
Processing instructions keep their casedocument.createRange().createContextualFragment('<?Foo bar?>').firstChild.target"foo""Foo"–
Processing instructions keep their casedocument.createProcessingInstruction('x', 'A="1"').getAttribute('A')null"1"–
URL parser keeps newlines in DATA: URLs regardless of casenew URL('DATA:,a' + String.fromCharCode(10) + 'b').href"data:,ab""data:,a%0Ab"–
requestAnimationFrame IDs are no longer consumed by internal callbacks[requestAnimationFrame(() => {})].map((a) => { const m = document.createElement('marquee'); m.start(); m.stop(); return requestAnimationFrame(() => {}) - a; })[0]21–
createImageBitmap resolves one microtask later, and a zero resize has its own error(async () => { const o = []; createImageBitmap(new ImageData(1, 1)).then(() => o.push(1)); Promise.resolve().then(() => o.push(2)); await new Promise((r) => setTimeout(r, 50)); return o.join(); })()"1,2""2,1"–
createImageBitmap resolves one microtask later, and a zero resize has its own errorcreateImageBitmap(new ImageData(1, 1), { resizeWidth: 0 }).then(() => 'ok', (e) => e.message.includes('resizeWidth'))falsetrue–
Disabling a focused form control no longer blurs it synchronously((i) => { document.body.append(i); i.focus(); i.disabled = true; const r = document.activeElement === i; i.remove(); return r; })(document.createElement('input'))falsetrue–
CSS
New CSS property: window-dragCSS.supports('window-drag', 'move')falsetrue–
New CSS property: window-drag'windowDrag' in document.body.stylefalsetrue–
corner-shape computed values always serialise as superellipse()getComputedStyle(document.documentElement).cornerShape"round""superellipse(1)"–
CSS relative alpha colours: alpha()CSS.supports('color', 'alpha(from red / 50%)')falsetrue–
CSS serialisation: background-size keeps its implicit auto, calc() division reads a / bObject.assign(document.createElement('div').style, { backgroundSize: '10px' }).backgroundSize"10px""10px auto"–
CSS serialisation: background-size keeps its implicit auto, calc() division reads a / bObject.assign(document.createElement('div').style, { width: 'calc(1px / sibling-index())' }).width"calc(1px * (1 / sibling-index()))""calc(1px / sibling-index())"–
Attribute selectors match SVG attribute values case-sensitivelydocument.createRange().createContextualFragment('<svg type=A>').firstChild.matches('[type=a]')truefalse–
CSS Typed OM accepts percentages for opacity and clamps out-of-range computed valuesnew Promise((r) => r(document.createElement('div').attributeStyleMap.set('opacity', CSS.percent(50)))).then(() => 'ok', (e) => e.name)"TypeError""ok"–
Element.pseudo() accepts ::backdrop and ::view-transitiondocument.documentElement.pseudo('::backdrop') !== nullfalsetrue–
Element.pseudo() accepts ::backdrop and ::view-transitiondocument.documentElement.pseudo('::view-transition') !== nullfalsetrue–
Smaller parsing changes: negative overflow-clip-margin, SVG currentTranslate is the same objectCSS.supports('overflow-clip-margin', '-1px')falsetrue–
Smaller parsing changes: negative overflow-clip-margin, SVG currentTranslate is the same object((s) => s.currentTranslate === s.currentTranslate)(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))falsetrue–
Media and GPU
Web Audio FFT moved to a Rust library: oscillator output changes in the last bits((c) => { const o = c.createOscillator(); o.type = 'triangle'; o.frequency.value = 1e4; const k = c.createDynamicsCompressor(); k.threshold.value = -50; k.knee.value = 40; k.ratio.value = 12; k.attack.value = 0; k.release.value = 0.25; o.connect(k); k.connect(c.destination); o.start(0); return c.startRendering().then((b) => b.getChannelData(0).slice(4500, 5e3).reduce((s, x) => s + Math.abs(x), 0)); })(new OfflineAudioContext(1, 44100, 44100))124.04347527516074124.04347776696522n/a
IIRFilterNode normalises its coefficients in double precision(() => { const m = new Float32Array(1); new IIRFilterNode(new OfflineAudioContext(1, 1, 44100), { feedforward: [1], feedback: [1.1, -1.0999] }).getFrequencyResponse(new Float32Array(1), m, new Float32Array(1)); return m[0]; })()9997.616210937510000–
AudioBufferSourceNode honours loopStart when loopEnd is 0(async () => { const c = new OfflineAudioContext(1, 8, 8192), b = c.createBuffer(1, 4, 8192); b.getChannelData(0).set([1, 2, 3, 4]); const s = new AudioBufferSourceNode(c, { buffer: b, loop: true, loopStart: 2 / 8192 }); s.connect(c.destination); s.start(); return [...(await c.startRendering()).getChannelData(0)]; })()[1,2,3,4,1,2,3,4][1,2,3,4,3,4,3,4]–
getDisplayMedia() reads a new audioSelection option(() => { let r = false; navigator.mediaDevices.getDisplayMedia({ get audioSelection() { r = true; } }).catch(() => {}); return r; })()falsetrue–
RTCIceCandidate parses tls candidates that carry a fingerprintnew RTCIceCandidate({ candidate: 'candidate:1 1 tls 1 1.2.3.4 443 typ host fingerprint sha-256;AA', sdpMid: '0' }).addressnull"1.2.3.4"–
WebGL: stricter texSubImage2D type validation and array attribute locations (ANGLE)(() => { const g = document.createElement('canvas').getContext('webgl2'); g.bindTexture(g.TEXTURE_2D, g.createTexture()); g.texImage2D(g.TEXTURE_2D, 0, g.RGBA, 1, 1, 0, g.RGBA, g.UNSIGNED_BYTE, null); g.texSubImage2D(g.TEXTURE_2D, 0, 0, 0, 1, 1, g.RGBA, g.UNSIGNED_SHORT_4_4_4_4, new Uint16Array(1)); return g.getError(); })()01282–
Software WebGL 2 (SwiftShader) reports 8 draw buffers instead of 6(() => { const g = document.createElement('canvas').getContext('webgl2'); return g.getParameter(g.MAX_DRAW_BUFFERS); })()68n/a
WebGPU: subgroup-size-control ships, two feature names are recognisednavigator.gpu.requestAdapter().then((a) => !!a && a.features.has('subgroup-size-control'))falsetruen/a
Automation
DevTools no longer runs Proxy traps in a logged object's prototype chain(() => { let hit = false; console.log(Object.create(new Proxy({}, { ownKeys() { hit = true; return []; } }))); return hit; })()truefalsen/a

A tinted cell differs from the version before it. Values are as the DevTools console prints them. Checks that need a debugger attached are not run live.

TLS, HTTP/2 and request headers in Chrome 152

TLS ClientHello
Changed, the first handshake change in this series. Chrome 152 adds the trust_anchors extension (0xCA34) to every TLS and QUIC ClientHello, listing the 32 trust-anchor IDs of the compiled-in Chrome Root Store (210 bytes; enabled at 152, disabled at 151), and over TCP it prepends a GREASE value to signature_algorithms. JA4 goes from t13d1516h2_8daaf6152771_806a8c22fdea to t13d1517h2_8daaf6152771_cb7bf5808d99 (first visit; a resumed session adds pre_shared_key). Ciphers, groups (X25519MLKEM768 first), ALPN, ALPS, ECH GREASE, certificate compression and extension permutation are unchanged.
HTTP/2
Unchanged: http_network_session.cc and spdy_http_utils.cc send the same SETTINGS, window update, priority and pseudo-header order; HTTP/2 fingerprint 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p. HTTP/2 and QUIC responses with conflicting duplicate Content-Disposition headers now fail, as HTTP/1.1 already did.
Request headers
Default request header names, order and values are unchanged apart from the version-carrying values (User-Agent, Sec-CH-UA, Sec-CH-UA-Full-Version, Sec-CH-UA-Full-Version-List). The Topics and Attribution Reporting request headers are gone, a repeated identical navigation within 3 seconds is dropped instead of re-sent, and the Connection-Allowlist response header is enforced without an origin trial.

Compare your own browser's handshake with these in the JA4 fingerprint checker.

Chrome 152 User-Agent strings and Client Hints

PlatformUser-Agent
WindowsMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
macOSMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
LinuxMozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
AndroidMozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Mobile Safari/537.36
Headless (Linux)Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36

The Linux and headless strings were measured. The others follow chromium's reduced User-Agent format (user_agent_utils.cc, unchanged from 151 apart from an Android desktop platform-version gate): only the major version is real, the rest is frozen, and the platform part is fixed per operating system.

Sec-CH-UA (the low-entropy brand list sent with every request):

"Chromium";v="152", "Not?A_Brand";v="24", "Google Chrome";v="152"

The Chrome user agent page has these for the current version and every recent one; what is my user agent shows your own.

TLS handshake

high impactMeasured on both builds

TLS ClientHello gains the trust_anchors extension and a GREASE signature algorithm

Chrome 152 sends the trust_anchors extension (0xCA34) in every TLS and QUIC ClientHello, carrying the IDs of the Chrome Root Store's trust anchors (32 IDs, 210 bytes on the wire with the compiled-in store); 151 only sent IDs a server had advertised in DNS, which in practice meant none. Over TCP it also prepends a GREASE value to signature_algorithms (11 -> 12 entries). The JA4 extension count goes from 16 to 17 and the JA4 extension hash changes; ciphers, groups and the other extensions are unchanged.

JSON.parse(document.querySelector('pre').textContent).tls.ja4
Chrome 151
"t13d1516h2_8daaf6152771_806a8c22fdea"
Chrome 152
"t13d1517h2_8daaf6152771_cb7bf5808d99"
  • JSON.parse(document.querySelector('pre').textContent).tls.extensions.some((e) => e.name === 'Unknown extension 51764')false→ true

Why it matters: This is the one change in the pair that a server sees before any page code runs: every JA3/JA4-style fingerprint of the handshake moves. Run the checks on tls.peet.ws/api/all in a fresh profile (a resumed connection adds pre_shared_key and reads one extension higher), which is why the live column skips them. JA4 ignores the GREASE value, so only the new extension moves it.

User-Agent and Client Hints

high impactMeasured on both builds

New GREASE brand and brand order in User-Agent Client Hints

Chrome derives the fake "GREASE" brand and the order of the brand list from the major version. On 151 the brand is Not=A?Brand version 99, listed first, followed by Google Chrome and Chromium; on 152 it is Not?A_Brand version 24, and Chromium is listed first. This is what navigator.userAgentData.brands returns and what every request sends in Sec-CH-UA.

navigator.userAgentData.brands.map((b) => `${b.brand};v=${b.version}`).join(', ')
Chrome 151
"Not=A?Brand;v=99, Google Chrome;v=151, Chromium;v=151"
Chrome 152
"Chromium;v=152, Not?A_Brand;v=24, Google Chrome;v=152"

Why it matters: The brand list is on every request a server receives. A browser that changes the version number but keeps the previous brand list or order sends a combination no real Chrome sends. The algorithm is the same code at both tags; only its input, the major, changed.

JavaScript and Web APIs

high impactMeasured on both builds

navigator.cpuPerformance (CPU Performance API)

A new navigator.cpuPerformance attribute returns a coarse CPU tier: 0 unknown, 1 low, 2 mid, 3 high, 4 ultra. The browser computes it once from the CPU model name and core count. It is exposed in secure contexts only, and not in workers. On 151 the Blink flag was experimental (origin trial only); on 152 it is on by default.

'cpuPerformance' in navigator
Chrome 151
false
Chrome 152
true
  • typeof navigator.cpuPerformance"undefined"→ "number"

Why it matters: Presence is a version marker. The value itself is a new hardware signal: it is derived from the host CPU, so it has to agree with hardwareConcurrency, deviceMemory and the GPU a browser reports.

high impactMeasured on both builds

OpaqueRange, NodeRange and createValueRange(): the Range prototype chain changes

Chrome 152 ships OpaqueRange (ranges over the text inside <input> and <textarea>, created with createValueRange()) and a new intermediate interface, NodeRange. Range and StaticRange now inherit from NodeRange, which takes over startContainer and endContainer from AbstractRange. Two new globals.

typeof OpaqueRange
Chrome 151
"undefined"
Chrome 152
"function"
  • Object.getPrototypeOf(Range.prototype).constructor.name"AbstractRange"→ "NodeRange"
  • 'startContainer' in AbstractRange.prototypetrue→ false
  • typeof HTMLInputElement.prototype.createValueRange"undefined"→ "function"

Why it matters: It changes the window's list of globals, the prototype chain of every Range, and where two long-standing properties live. Scripts that walk prototypes see a different shape, not just an extra name.

high impactMeasured on both builds

FeaturePolicy is renamed PermissionsPolicy

The interface behind document.featurePolicy is now called PermissionsPolicy, with FeaturePolicy kept as a legacy alias for the same constructor. A new global appears, and the name and toString tag of the object change.

typeof PermissionsPolicy
Chrome 151
"undefined"
Chrome 152
"function"
  • FeaturePolicy.name"FeaturePolicy"→ "PermissionsPolicy"
  • Object.prototype.toString.call(document.featurePolicy)"[object FeaturePolicy]"→ "[object PermissionsPolicy]"

Why it matters: Constructor names and toString tags are read by scripts that enumerate or stringify browser objects; one line tells the two versions apart.

high impactMeasured on both builds

window.chrome is now a configurable property, created later

chrome.loadTimes() and chrome.csi() used to be installed by a V8 extension script that began var chrome;, which makes window.chrome a non-configurable global created early. Chrome 152 installs them from C++ on a chrome object created with an ordinary property set: it is now configurable (it can be deleted) and it moves after WebAssembly in the window's property list.

Object.getOwnPropertyDescriptor(window, 'chrome').configurable
Chrome 151
false
Chrome 152
true
  • Object.getOwnPropertyNames(window).indexOf('chrome') < Object.getOwnPropertyNames(window).indexOf('WebAssembly')true→ false

Why it matters: window.chrome is among the first things a script checks to decide whether a browser is Chrome, and its descriptor and position are part of that check. Software that defines window.chrome itself has to match the version it claims.

high impactMeasured on both builds

DeviceOrientationEvent.requestPermission() and DeviceMotionEvent.requestPermission()

Both sensor event constructors gain a static requestPermission() that resolves to a permission state. The flag was experimental at the 151 tag and is stable at 152 on every platform, desktop included, although Google's status page lists Chrome 151.

typeof DeviceOrientationEvent.requestPermission
Chrome 151
"undefined"
Chrome 152
"function"
  • typeof DeviceMotionEvent.requestPermission"undefined"→ "function"

Why it matters: Until now this static method was a Safari-only shape; its presence in Chrome is a version marker, and scripts that branch on it to detect iOS take a different path.

high impactMeasured on both builds

console.context() objects get dirxml instead of dirXml

V8's non-standard console.context() returns a console-like object. On 151 its XML-dump method was misnamed dirXml; V8 15.2 names it dirxml, like the global console.

typeof console.context().dirxml
Chrome 151
"undefined"
Chrome 152
"function"
  • typeof console.context().dirXml"function"→ "undefined"

Why it matters: An engine-level one-liner with no setup, the same on every platform, in windows and workers.

high impactMeasured on both builds

V8's internal built-in closures are strict: .caller throws, f.caller is null

Functions V8 creates internally, such as the revoke function of Proxy.revocable() and the closures behind Promise.prototype.finally and promise resolution, are now strict-mode functions. Reading .caller on them throws a TypeError instead of returning null, and a sloppy function they call sees f.caller === null instead of the internal closure.

(() => { try { return typeof Proxy.revocable({}, {}).revoke.caller; } catch (e) { return e.name; } })()
Chrome 151
"object"
Chrome 152
"TypeError"
  • new Promise((r) => Promise.resolve().finally(function f() { r(typeof f.caller); }))"function"→ "object"

Why it matters: Engine behaviour a page cannot configure; the first check is synchronous. Merged into the 152 branch as a security fix. The second check reads f.caller, which only sloppy-mode code may do, so run it in the DevTools console; the live column skips it because this site's scripts are modules, which are always strict.

medium impactMeasured on both builds

Property order moved on window, Document, PointerEvent and the Performance interfaces

Blink installs members behind a runtime flag after all unconditional members, so adding or removing a flag moves a member without adding or removing anything. In 152 the flags of three shipped features were removed (onpagereveal and the PageRevealEvent global, PointerEvent.persistentDeviceId, PerformanceNavigationTiming.confidence and PerformanceTimingConfidence), the paint-timing members lost theirs, document.requestStorageAccessFor gained one, and the Long Animation Frames interfaces became conditionally exposed (for workers), which moves them to the window's tail.

Object.getOwnPropertyNames(window).indexOf('onpagereveal') < Object.getOwnPropertyNames(window).indexOf('onpageswap')
Chrome 151
false
Chrome 152
true
  • Object.getOwnPropertyNames(PointerEvent.prototype).indexOf('persistentDeviceId') < Object.getOwnPropertyNames(PointerEvent.prototype).indexOf('getPredictedEvents')false→ true
  • Object.getOwnPropertyNames(PerformanceNavigationTiming.prototype).indexOf('confidence') < Object.getOwnPropertyNames(PerformanceNavigationTiming.prototype).indexOf('toJSON')false→ true
  • Object.getOwnPropertyNames(PerformancePaintTiming.prototype)[0]"paintTime"→ "toJSON"
  • Object.getOwnPropertyNames(window).indexOf('PerformanceLongAnimationFrameTiming') < Object.getOwnPropertyNames(window).indexOf('Performance')true→ false

Why it matters: Nothing is added or removed, so presence checks see no difference, but every hash of property order changes. The order is a property of the build and cannot be set by a page. The requestStorageAccessFor check reads true again on 154, where the method is removed.

medium impactMeasured on both builds

Sanitizer API: processing instructions in the configuration, streaming sanitizer on

new Sanitizer().get() gains processingInstructions, setHTML() drops processing instructions and <base> by default, and the parser-integrated (streaming) sanitizer is on, which changes the result of Document.parseHTML() for comments.

'processingInstructions' in new Sanitizer().get()
Chrome 151
false
Chrome 152
true
  • Document.parseHTML('a<!--x-->b').body.childNodes.length2→ 1

Why it matters: The Sanitizer's default configuration is a deterministic object that scripts can dump and compare.

medium impactMeasured on both builds

RegExp split with the v flag steps by code point on the spec path

When String.prototype.split uses a RegExp subclass (or an overridden exec), V8 takes the specification's slow path. With the v flag it now advances by whole code points, as with u; on 151 it stepped into the middle of a surrogate pair.

(() => { const v = []; class R extends RegExp { exec() { v.push(this.lastIndex); return null; } } ('a' + String.fromCodePoint(128512) + 'b').split(new R('x', 'v')); return v.join(); })()
Chrome 151
"0,1,2,3"
Chrome 152
"0,1,3"

Why it matters: Pure JavaScript, synchronous and deterministic.

medium impactMeasured on both builds

WebAssembly compile errors name --wasm-* flags instead of --experimental-wasm-*

A module that uses an unshipped WebAssembly feature fails to compile on both versions, but the error message names the flag that would enable it, and V8 15.2 renamed those flags from --experimental-wasm-<feature> to --wasm-<feature>.

(() => { try { new WebAssembly.Module(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 10, 6, 1, 4, 0, 224, 0, 11])); return 'compiled'; } catch (e) { return e.message.slice(e.message.indexOf('(enable')); } })()
Chrome 151
"(enable with --experimental-wasm-wasmfx) @+23"
Chrome 152
"(enable with --wasm-wasmfx) @+23"

Why it matters: Error text comes straight from the engine, so one crafted module identifies it. Throws where WebAssembly is disabled.

low impactMeasured on both builds

Error stacks include WebAssembly frames across JSPI suspensions

An error thrown after a WebAssembly JS Promise Integration (JSPI) suspension now carries the suspended WebAssembly frames and the outer async callers in error.stack; on 151 the stack stopped at the JavaScript import.

(async () => { const b = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 2, 7, 1, 1, 109, 1, 102, 0, 0, 3, 2, 1, 0, 7, 7, 1, 3, 114, 117, 110, 0, 1, 10, 6, 1, 4, 0, 16, 0, 11]); const { instance: i } = await WebAssembly.instantiate(b, { m: { f: new WebAssembly.Suspending(async () => { await 0; throw Error(); }) } }); try { await WebAssembly.promising(i.exports.run)(); } catch (e) { return e.stack.includes('wasm-function'); } })()
Chrome 151
false
Chrome 152
true

Why it matters: Deterministic but asynchronous, and it needs WebAssembly with JSPI.

HTML

medium impactMeasured on both builds

Shadow DOM reference target: ShadowRoot.referenceTarget

ID references such as aria-labelledby and for can now point into a shadow root through referenceTarget. New members: ShadowRoot.prototype.referenceTarget and HTMLTemplateElement.prototype.shadowRootReferenceTarget for declarative shadow DOM.

'referenceTarget' in ShadowRoot.prototype
Chrome 151
false
Chrome 152
true
  • 'shadowRootReferenceTarget' in HTMLTemplateElement.prototypefalse→ true

Why it matters: New prototype members change the property lists that fingerprinting scripts enumerate.

medium impactMeasured on both builds

Declarative shadow DOM: shadowrootslotassignment

A <template shadowrootmode> can now ask for manual slot assignment with the shadowrootslotassignment attribute, reflected as HTMLTemplateElement.prototype.shadowRootSlotAssignment. Google's status page lists Chrome 151; the flag first appears, as stable, at the 152 tag.

'shadowRootSlotAssignment' in HTMLTemplateElement.prototype
Chrome 151
false
Chrome 152
true

Why it matters: A new reflected attribute on a common element prototype.

Removed or deprecated

high impactMeasured on both builds

Protected Audience, Topics and Shared Storage are stubbed out: still exposed, no longer working

The Privacy Sandbox interfaces keep their IDL in 152, but their implementations were replaced by stubs. navigator.adAuctionComponents(1) threw InvalidStateError on an ordinary page in 151 and returns an empty list in 152; document.browsingTopics() rejects with NotSupportedError; navigator.protectedAudience.queryFeatureSupport() reports every feature as unsupported; canLoadAdAuctionFencedFrame() is always false; every sharedStorage call rejects.

(() => { try { return navigator.adAuctionComponents(1).length; } catch (e) { return e.name; } })()
Chrome 151
"InvalidStateError"
Chrome 152
0
  • document.browsingTopics().then(() => 'resolved', (e) => e.name)"resolved"→ "NotSupportedError"
  • navigator.protectedAudience.queryFeatureSupport('sellerNonce')true→ false
  • navigator.canLoadAdAuctionFencedFrame()true→ false

Why it matters: A presence check sees no difference; behaviour does, synchronously and without permissions. The fencing check depends on the page's Content-Security-Policy (a restrictive frame-src makes 151 answer false too), so the live column skips it. Chrome's own status page dates the Protected Audience removal to 153; Shared Storage leaves the page surface in 153.

medium impactMeasured on both builds

The attribution-reporting permissions-policy feature is removed

The attribution-reporting token disappears from the permissions-policy feature list, which shrinks from 81 to 80 entries in a default build. The Attribution Reporting script surface itself (attributionSrc) is still exposed.

document.featurePolicy.features().includes('attribution-reporting')
Chrome 151
true
Chrome 152
false

Why it matters: The feature list and its length are common fingerprint inputs. Chrome's own status page dates the Attribution Reporting removal to 153.

Parsing and rendering behaviour

high impactMeasured on both builds

HTML parser: a truncated <! markup declaration becomes a comment instead of eating the rest

When the parser runs out of input in the middle of a <! markup declaration that is too short to tell <!--, <!DOCTYPE or <![CDATA[ apart, 151 waited for more input and dropped the rest; 152 tokenizes it as a bogus comment, as the HTML specification says.

new DOMParser().parseFromString('a<!d>b', 'text/html').body.innerHTML
Chrome 151
"a"
Chrome 152
"a<!--d-->b"

Why it matters: Pure JavaScript, synchronous and deterministic; any page can run it.

medium impactMeasured on both builds

Processing instructions keep their case

The HTML tokenizer no longer lowercases a processing instruction's target, and the attribute methods of ProcessingInstruction no longer lowercase names.

document.createRange().createContextualFragment('<?Foo bar?>').firstChild.target
Chrome 151
"foo"
Chrome 152
"Foo"
  • document.createProcessingInstruction('x', 'A="1"').getAttribute('A')null→ "1"

Why it matters: Pure JavaScript and deterministic.

medium impactMeasured on both builds

requestAnimationFrame IDs are no longer consumed by internal callbacks

Blink used one counter for page and internal animation-frame callbacks (a <marquee>, paint timing), so internal work could skip the IDs a page receives. 152 keeps a separate counter for internal callbacks.

[requestAnimationFrame(() => {})].map((a) => { const m = document.createElement('marquee'); m.start(); m.stop(); return requestAnimationFrame(() => {}) - a; })[0]
Chrome 151
2
Chrome 152
1

Why it matters: Synchronous and deterministic; a marquee forces an internal callback between the two page requests.

medium impactMeasured on both builds

createImageBitmap resolves one microtask later, and a zero resize has its own error

For synchronous sources such as ImageData, 151 returned an already-resolved promise; 152 resolves it from a queued task, so a callback registered later runs first. A resizeWidth or resizeHeight of 0 is now rejected up front with its own message instead of "could not be allocated".

(async () => { const o = []; createImageBitmap(new ImageData(1, 1)).then(() => o.push(1)); Promise.resolve().then(() => o.push(2)); await new Promise((r) => setTimeout(r, 50)); return o.join(); })()
Chrome 151
"1,2"
Chrome 152
"2,1"
  • createImageBitmap(new ImageData(1, 1), { resizeWidth: 0 }).then(() => 'ok', (e) => e.message.includes('resizeWidth'))false→ true

Why it matters: Promise-ordering and error-text checks are deterministic and need no GPU.

low impactMeasured on both builds

Disabling a focused form control no longer blurs it synchronously

Setting disabled on the focused element used to move focus away at once (and fire blur); 152 leaves it focused until the next focus update, as other engines do.

((i) => { document.body.append(i); i.focus(); i.disabled = true; const r = document.activeElement === i; i.remove(); return r; })(document.createElement('input'))
Chrome 151
false
Chrome 152
true

Why it matters: Synchronous, but it adds an element to the page and moves focus.

CSS

high impactMeasured on both builds

New CSS property: window-drag

A standard replacement for -webkit-app-region: window-drag: move marks a region of an installed web app that drags the window. Values none (initial) and move; inherited. The computed style of every element grows by one property.

CSS.supports('window-drag', 'move')
Chrome 151
false
Chrome 152
true
  • 'windowDrag' in document.body.stylefalse→ true

Why it matters: Scripts that count or list getComputedStyle() properties see one more, on every platform.

high impactMeasured on both builds

corner-shape computed values always serialise as superellipse()

The computed value of corner-shape and its longhands came back as a keyword where one matched (round, bevel, ...); 152 always returns superellipse(n), so the default reads superellipse(1) on every element.

getComputedStyle(document.documentElement).cornerShape
Chrome 151
"round"
Chrome 152
"superellipse(1)"

Why it matters: It changes a default computed value of every element, which computed-style dumps and hashes pick up.

medium impactMeasured on both builds

CSS relative alpha colours: alpha()

The CSS Color 5 alpha(from <color> / <alpha>) function parses, so a colour's transparency can be changed relative to another colour.

CSS.supports('color', 'alpha(from red / 50%)')
Chrome 151
false
Chrome 152
true

Why it matters: A cheap CSS.supports probe with no rendering involved.

medium impactMeasured on both builds

CSS serialisation: background-size keeps its implicit auto, calc() division reads a / b

background-size and mask-size with one length now serialise the implied second value (10px auto), and a division inside calc() is written as a / b instead of a * (1 / b).

Object.assign(document.createElement('div').style, { backgroundSize: '10px' }).backgroundSize
Chrome 151
"10px"
Chrome 152
"10px auto"
  • Object.assign(document.createElement('div').style, { width: 'calc(1px / sibling-index())' }).width"calc(1px * (1 / sibling-index()))"→ "calc(1px / sibling-index())"

Why it matters: Serialisation strings are cheap to read and hard to reproduce without the engine that writes them.

medium impactMeasured on both builds

Attribute selectors match SVG attribute values case-sensitively

HTML treats the values of some attributes (such as type) case-insensitively in selectors. Chrome 151 applied that legacy rule to SVG and other non-HTML elements in HTML documents too; 152 matches them case-sensitively, as the specification says.

document.createRange().createContextualFragment('<svg type=A>').firstChild.matches('[type=a]')
Chrome 151
true
Chrome 152
false

Why it matters: Pure JavaScript, synchronous; HTML elements are unaffected.

medium impactMeasured on both builds

CSS Typed OM accepts percentages for opacity and clamps out-of-range computed values

attributeStyleMap.set('opacity', CSS.percent(50)) threw a TypeError on 151 and works on 152 (likewise outline-style: auto); out-of-range values are wrapped in calc() for more properties and clamped when computed.

new Promise((r) => r(document.createElement('div').attributeStyleMap.set('opacity', CSS.percent(50)))).then(() => 'ok', (e) => e.name)
Chrome 151
"TypeError"
Chrome 152
"ok"

Why it matters: Pure JavaScript and deterministic.

low impactMeasured on both builds

Element.pseudo() accepts ::backdrop and ::view-transition

Element.pseudo() returned null for ::backdrop and the ::view-transition family on 151; on 152 it returns a CSSPseudoElement. (::before, ::after, ::marker and ::scroll-marker already worked.)

document.documentElement.pseudo('::backdrop') !== null
Chrome 151
false
Chrome 152
true
  • document.documentElement.pseudo('::view-transition') !== nullfalse→ true

Why it matters: Deterministic and synchronous; the pseudo-element object exists whether or not the element currently has a backdrop.

low impactMeasured on both builds

Smaller parsing changes: negative overflow-clip-margin, SVG currentTranslate is the same object

overflow-clip-margin accepts negative lengths, and SVGSVGElement.currentTranslate returns the same object on every read, as its [SameObject] annotation requires.

CSS.supports('overflow-clip-margin', '-1px')
Chrome 151
false
Chrome 152
true
  • ((s) => s.currentTranslate === s.currentTranslate)(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))false→ true

Why it matters: Cheap, synchronous probes.

Media and codecs

high impactMeasured on both builds

Web Audio FFT moved to a Rust library: oscillator output changes in the last bits

Blink's Web Audio FFT now uses the Rust rustfft crate instead of PFFFT (Windows, Linux, Android) or Apple's vDSP (macOS). Every OscillatorNode is affected, because its wave tables are built with an inverse FFT, and so are AnalyserNode and ConvolverNode. The classic audio fingerprint, a triangle oscillator into a compressor in an OfflineAudioContext, moves in the eighth significant digit.

((c) => { const o = c.createOscillator(); o.type = 'triangle'; o.frequency.value = 1e4; const k = c.createDynamicsCompressor(); k.threshold.value = -50; k.knee.value = 40; k.ratio.value = 12; k.attack.value = 0; k.release.value = 0.25; o.connect(k); k.connect(c.destination); o.start(0); return c.startRendering().then((b) => b.getChannelData(0).slice(4500, 5e3).reduce((s, x) => s + Math.abs(x), 0)); })(new OfflineAudioContext(1, 44100, 44100))
Chrome 151
124.04347527516074
Chrome 152
124.04347776696522

Why it matters: Audio hashes are among the most widely collected fingerprint values, and they are usually treated as stable per platform. The rustfft planner picks AVX/FMA, SSE4.1 or NEON code at run time, and macOS used vDSP before, so the exact numbers depend on the CPU and the OS: the values shown are Linux x86-64 with AVX2 (a Windows Chrome 152 gave the same 152 value). The live column skips it for that reason.

high impactMeasured on both builds

IIRFilterNode normalises its coefficients in double precision

An IIRFilterNode divides its coefficients by the first feedback coefficient. 151 did that with the divisor rounded to single precision; 152 keeps it in double precision, so filter responses and rendered audio change for coefficients that are not exactly representable as floats.

(() => { const m = new Float32Array(1); new IIRFilterNode(new OfflineAudioContext(1, 1, 44100), { feedforward: [1], feedback: [1.1, -1.0999] }).getFrequencyResponse(new Float32Array(1), m, new Float32Array(1)); return m[0]; })()
Chrome 151
9997.6162109375
Chrome 152
10000

Why it matters: Synchronous, no audio device and no rendering needed; the value is plain arithmetic, so it does not depend on the CPU.

medium impactMeasured on both builds

AudioBufferSourceNode honours loopStart when loopEnd is 0

With loop on and loopEnd left at 0, 151 ignored loopStart and looped the whole buffer; 152 loops from loopStart to the end of the buffer, as the specification says.

(async () => { const c = new OfflineAudioContext(1, 8, 8192), b = c.createBuffer(1, 4, 8192); b.getChannelData(0).set([1, 2, 3, 4]); const s = new AudioBufferSourceNode(c, { buffer: b, loop: true, loopStart: 2 / 8192 }); s.connect(c.destination); s.start(); return [...(await c.startRendering()).getChannelData(0)]; })()
Chrome 151
[1,2,3,4,1,2,3,4]
Chrome 152
[1,2,3,4,3,4,3,4]

Why it matters: Deterministic offline rendering with integer samples, so no CPU dependence.

medium impactMeasured on both builds

getDisplayMedia() reads a new audioSelection option

Screen-capture requests accept audioSelection: 'preferred', a hint that the page wants audio shared with the video. The dictionary member is read on 152 and ignored on 151.

(() => { let r = false; navigator.mediaDevices.getDisplayMedia({ get audioSelection() { r = true; } }).catch(() => {}); return r; })()
Chrome 151
false
Chrome 152
true

Why it matters: A getter on the options object shows whether the engine knows the member, without a permission prompt (the call itself is rejected without a user gesture). Desktop only.

medium impactMeasured on both builds

RTCIceCandidate parses tls candidates that carry a fingerprint

The WebRTC candidate parser rejected the tls transport as unsupported, leaving the parsed fields of an RTCIceCandidate null; WebRTC in 152 accepts it when a fingerprint extension is present.

new RTCIceCandidate({ candidate: 'candidate:1 1 tls 1 1.2.3.4 443 typ host fingerprint sha-256;AA', sdpMid: '0' }).address
Chrome 151
null
Chrome 152
"1.2.3.4"

Why it matters: Synchronous, no permission and no network; WebRTC objects are already part of many bot checks.

WebGL and WebGPU

medium impactMeasured on both builds

WebGL: stricter texSubImage2D type validation and array attribute locations (ANGLE)

ANGLE's front end now rejects a texSubImage2D whose type does not match the texture's sized format (INVALID_OPERATION), and bindAttribLocation(p, 5, 'a[0]') binds the array attribute a. Very long WebGL 2 identifiers that compiled on 151 are rejected.

(() => { const g = document.createElement('canvas').getContext('webgl2'); g.bindTexture(g.TEXTURE_2D, g.createTexture()); g.texImage2D(g.TEXTURE_2D, 0, g.RGBA, 1, 1, 0, g.RGBA, g.UNSIGNED_BYTE, null); g.texSubImage2D(g.TEXTURE_2D, 0, 0, 0, 1, 1, g.RGBA, g.UNSIGNED_SHORT_4_4_4_4, new Uint16Array(1)); return g.getError(); })()
Chrome 151
0
Chrome 152
1282

Why it matters: Validation runs in ANGLE's platform-independent front end, before any GPU backend, so the answer should not depend on the GPU; measured on the software renderer. Throws where WebGL 2 is unavailable.

medium impactMeasured on both builds

Software WebGL 2 (SwiftShader) reports 8 draw buffers instead of 6

ANGLE's Vulkan backend stopped using dynamic rendering on SwiftShader, which removes a cap on colour attachments. On the software renderer (headless without a GPU, virtual machines, blocklisted GPUs) MAX_DRAW_BUFFERS and MAX_COLOR_ATTACHMENTS go from 6 to 8. Hardware GPUs report their driver's limits, unchanged.

(() => { const g = document.createElement('canvas').getContext('webgl2'); return g.getParameter(g.MAX_DRAW_BUFFERS); })()
Chrome 151
6
Chrome 152
8

Why it matters: WebGL parameters are standard fingerprint fields, and SwiftShader's values are a known signature of software rendering; this one now depends on the version. Only meaningful on the software renderer, hence not live.

low impactRead in the source at both tags

WebGPU: subgroup-size-control ships, two feature names are recognised

Dawn marks subgroup-size-control stable, so adapters that support it (D3D12 with Shader Model 6.6, Vulkan with subgroup size control, Apple GPUs) list it in adapter.features. GPUFeatureName also gains texture-compression-unaligned and chromium-experimental-sampling-resource-table (still experimental in Dawn, so not listed by default). The WGSL language feature set is unchanged (10 entries).

navigator.gpu.requestAdapter().then((a) => !!a && a.features.has('subgroup-size-control'))
Chrome 151
false
Chrome 152
true

Why it matters: Adapter features are a common WebGPU fingerprint; this one depends on the GPU and driver, so the live column skips it.

Automation and DevTools protocol

high impactMeasured on both builds

DevTools no longer runs Proxy traps in a logged object's prototype chain

When a debugger is attached with Runtime.enable (DevTools, or a CDP client such as Puppeteer or Playwright), V8 builds a preview of every object passed to console.log. On 151, if the object's prototype was a Proxy, building that preview ran the Proxy's ownKeys trap, so a page could plant one and learn that a debugger was listening. V8 15.2 skips Proxy prototypes while iterating properties for the debugger.

(() => { let hit = false; console.log(Object.create(new Proxy({}, { ownKeys() { hit = true; return []; } }))); return hit; })()
Chrome 151
true
Chrome 152
false

Why it matters: Only meaningful with a debugger attached: without one both versions return false, which is why the live column skips it. Run it from the DevTools console (which attaches one) on each version. A related probe, a Proxy used as a getter, still fires on 152 and was closed in 153.

Network: TLS, HTTP/2 and headers

medium impactRead in the source at both tags

Connection-Allowlist response header is enforced for every site

A document or worker served with Connection-Allowlist: (response-origin "https://cdn.example/*") may only connect to the listed endpoints. In 151 the header was only honoured with a valid origin-trial token in the same response; 152 removed that gate, and the network-service feature was already on by default.

fetch('https://example.com/', { mode: 'no-cors' }).then(() => 'sent', (e) => e.name)
Chrome 151
"sent"
Chrome 152
"TypeError"

Why it matters: A server can probe the claimed version actively: send the header without a token and see whether the next cross-origin request arrives. Run the check on a page served with Connection-Allowlist: (response-origin); on any other page both versions send the request, which is why the live column skips it.

medium impactMeasured on both builds

Topics and Attribution Reporting request options and headers removed

fetch() and Request no longer read the browsingTopics and attributionReporting options, and no code sets Sec-Browsing-Topics, Attribution-Reporting-Eligible, Attribution-Reporting-Support or Sec-Ad-Auction-Fetch any more; the browser-side Attribution Reporting service is gone.

(() => { let read = false; new Request('/', { get attributionReporting() { read = true; } }); return read; })()
Chrome 151
true
Chrome 152
false
  • (() => { let read = false; new Request('/', { get browsingTopics() { read = true; } }); return read; })()true→ false

Why it matters: A dictionary member that is no longer read is a clean, synchronous version marker, and the headers these options produced no longer reach servers. The attributionSrc attributes and document.browsingTopics stay exposed as stubs.

What the docs claim that the source does not show

Release notes and Chrome Platform Status describe intent; the source at the release tag is what shipped. These entries differ, so a version check built on the docs alone would be wrong.

Reproduce these results

  1. 1. Get both versions

    Use the stable Google Chrome packages of the two exact versions. Google keeps every version in its Linux package pool:

    bash
    for v in 151.0.7922.173 152.0.7977.82; do
      curl -sSO https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${v}-1_amd64.deb
      dpkg -x google-chrome-stable_${v}-1_amd64.deb chrome-${v}
    done
  2. 2. Start each one clean

    A new profile directory per run and no automation flags. A fresh profile has not applied Google's field-trial configuration yet, which is the state these values describe. Headless (--headless=new) gives the same values as headful for everything on this page except the WebGL check, which needs a GPU or SwiftShader.

    bash
    ./chrome-152.0.7977.82/opt/google/chrome/chrome --user-data-dir="$(mktemp -d)" --no-first-run https://example.com
  3. 3. Run the checks

    Press "Run the checks in this browser" at the top of this page, or paste the script below into the DevTools console on any https:// page. Several checks are asynchronous; the script awaits them.

  4. 4. Check the network side

    Open tls.peet.ws in a fresh profile of each version and compare the JA4 with the values above. Use the first visit: a resumed TLS session adds pre_shared_key and changes the count. Or compare your own browser in the JA4 fingerprint checker.

  5. 5. Find a change in the source yourself

    Blink's test expectations record the page-visible surface of each release; the runtime feature list records what is on by default:

    bash
    git diff 151.0.7922.176 152.0.7977.85 -- third_party/blink/web_tests/virtual/stable/webexposed/
    git diff 151.0.7922.176 152.0.7977.85 -- third_party/blink/renderer/platform/runtime_enabled_features.json5

The whole check list as one script

Paste it into the DevTools console on Chrome 151 and on Chrome 152: it prints every check on this page with the value your browser returns.

chrome-152-probe.js
// Chrome 151 vs 152: paste into the DevTools console on any https:// page and press Enter.
// Each line prints "<change>  <check> = <value>". Expected values: clearcotelabs.com/chrome-releases/152
(async () => {
  const checks = [
    ["grease-brand", () => navigator.userAgentData.brands.map((b) => `${b.brand};v=${b.version}`).join(', ')],
    ["tls-trust-anchors", () => JSON.parse(document.querySelector('pre').textContent).tls.ja4],
    ["tls-trust-anchors", () => JSON.parse(document.querySelector('pre').textContent).tls.extensions.some((e) => e.name === 'Unknown extension 51764')],
    ["cpu-performance", () => 'cpuPerformance' in navigator],
    ["cpu-performance", () => typeof navigator.cpuPerformance],
    ["opaque-range", () => typeof OpaqueRange],
    ["opaque-range", () => Object.getPrototypeOf(Range.prototype).constructor.name],
    ["opaque-range", () => 'startContainer' in AbstractRange.prototype],
    ["opaque-range", () => typeof HTMLInputElement.prototype.createValueRange],
    ["permissions-policy-interface", () => typeof PermissionsPolicy],
    ["permissions-policy-interface", () => FeaturePolicy.name],
    ["permissions-policy-interface", () => Object.prototype.toString.call(document.featurePolicy)],
    ["window-chrome-configurable", () => Object.getOwnPropertyDescriptor(window, 'chrome').configurable],
    ["window-chrome-configurable", () => Object.getOwnPropertyNames(window).indexOf('chrome') < Object.getOwnPropertyNames(window).indexOf('WebAssembly')],
    ["privacy-sandbox-stubbed", () => (() => { try { return navigator.adAuctionComponents(1).length; } catch (e) { return e.name; } })()],
    ["privacy-sandbox-stubbed", () => document.browsingTopics().then(() => 'resolved', (e) => e.name)],
    ["privacy-sandbox-stubbed", () => navigator.protectedAudience.queryFeatureSupport('sellerNonce')],
    ["privacy-sandbox-stubbed", () => navigator.canLoadAdAuctionFencedFrame()],
    ["webaudio-rustfft", () => ((c) => { const o = c.createOscillator(); o.type = 'triangle'; o.frequency.value = 1e4; const k = c.createDynamicsCompressor(); k.threshold.value = -50; k.knee.value = 40; k.ratio.value = 12; k.attack.value = 0; k.release.value = 0.25; o.connect(k); k.connect(c.destination); o.start(0); return c.startRendering().then((b) => b.getChannelData(0).slice(4500, 5e3).reduce((s, x) => s + Math.abs(x), 0)); })(new OfflineAudioContext(1, 44100, 44100))],
    ["device-orientation-permission", () => typeof DeviceOrientationEvent.requestPermission],
    ["device-orientation-permission", () => typeof DeviceMotionEvent.requestPermission],
    ["window-drag", () => CSS.supports('window-drag', 'move')],
    ["window-drag", () => 'windowDrag' in document.body.style],
    ["html-truncated-markup-declaration", () => new DOMParser().parseFromString('a<!d>b', 'text/html').body.innerHTML],
    ["corner-shape-serialization", () => getComputedStyle(document.documentElement).cornerShape],
    ["console-context-dirxml", () => typeof console.context().dirxml],
    ["console-context-dirxml", () => typeof console.context().dirXml],
    ["v8-strict-builtin-closures", () => (() => { try { return typeof Proxy.revocable({}, {}).revoke.caller; } catch (e) { return e.name; } })()],
    ["v8-strict-builtin-closures", () => new Promise((r) => Promise.resolve().finally(function f() { r(typeof f.caller); }))],
    ["iir-filter-double-precision", () => (() => { const m = new Float32Array(1); new IIRFilterNode(new OfflineAudioContext(1, 1, 44100), { feedforward: [1], feedback: [1.1, -1.0999] }).getFrequencyResponse(new Float32Array(1), m, new Float32Array(1)); return m[0]; })()],
    ["devtools-proxy-prototype", () => (() => { let hit = false; console.log(Object.create(new Proxy({}, { ownKeys() { hit = true; return []; } }))); return hit; })()],
    ["property-order-152", () => Object.getOwnPropertyNames(window).indexOf('onpagereveal') < Object.getOwnPropertyNames(window).indexOf('onpageswap')],
    ["property-order-152", () => Object.getOwnPropertyNames(PointerEvent.prototype).indexOf('persistentDeviceId') < Object.getOwnPropertyNames(PointerEvent.prototype).indexOf('getPredictedEvents')],
    ["property-order-152", () => Object.getOwnPropertyNames(PerformanceNavigationTiming.prototype).indexOf('confidence') < Object.getOwnPropertyNames(PerformanceNavigationTiming.prototype).indexOf('toJSON')],
    ["property-order-152", () => Object.getOwnPropertyNames(PerformancePaintTiming.prototype)[0]],
    ["property-order-152", () => Object.getOwnPropertyNames(window).indexOf('PerformanceLongAnimationFrameTiming') < Object.getOwnPropertyNames(window).indexOf('Performance')],
    ["connection-allowlists", () => fetch('https://example.com/', { mode: 'no-cors' }).then(() => 'sent', (e) => e.name)],
    ["privacy-sandbox-request-options", () => (() => { let read = false; new Request('/', { get attributionReporting() { read = true; } }); return read; })()],
    ["privacy-sandbox-request-options", () => (() => { let read = false; new Request('/', { get browsingTopics() { read = true; } }); return read; })()],
    ["attribution-reporting-policy-removed", () => document.featurePolicy.features().includes('attribution-reporting')],
    ["reference-target", () => 'referenceTarget' in ShadowRoot.prototype],
    ["reference-target", () => 'shadowRootReferenceTarget' in HTMLTemplateElement.prototype],
    ["shadowroot-slot-assignment", () => 'shadowRootSlotAssignment' in HTMLTemplateElement.prototype],
    ["css-alpha-function", () => CSS.supports('color', 'alpha(from red / 50%)')],
    ["css-serialization-152", () => Object.assign(document.createElement('div').style, { backgroundSize: '10px' }).backgroundSize],
    ["css-serialization-152", () => Object.assign(document.createElement('div').style, { width: 'calc(1px / sibling-index())' }).width],
    ["svg-attribute-selector-case", () => document.createRange().createContextualFragment('<svg type=A>').firstChild.matches('[type=a]')],
    ["processing-instruction-case", () => document.createRange().createContextualFragment('<?Foo bar?>').firstChild.target],
    ["processing-instruction-case", () => document.createProcessingInstruction('x', 'A="1"').getAttribute('A')],
    ["sanitizer-processing-instructions", () => 'processingInstructions' in new Sanitizer().get()],
    ["sanitizer-processing-instructions", () => Document.parseHTML('a<!--x-->b').body.childNodes.length],
    ["url-data-scheme-case", () => new URL('DATA:,a' + String.fromCharCode(10) + 'b').href],
    ["raf-callback-ids", () => [requestAnimationFrame(() => {})].map((a) => { const m = document.createElement('marquee'); m.start(); m.stop(); return requestAnimationFrame(() => {}) - a; })[0]],
    ["typed-om-152", () => new Promise((r) => r(document.createElement('div').attributeStyleMap.set('opacity', CSS.percent(50)))).then(() => 'ok', (e) => e.name)],
    ["regexp-split-v-flag", () => (() => { const v = []; class R extends RegExp { exec() { v.push(this.lastIndex); return null; } } ('a' + String.fromCodePoint(128512) + 'b').split(new R('x', 'v')); return v.join(); })()],
    ["wasm-error-flag-names", () => (() => { try { new WebAssembly.Module(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 10, 6, 1, 4, 0, 224, 0, 11])); return 'compiled'; } catch (e) { return e.message.slice(e.message.indexOf('(enable')); } })()],
    ["create-image-bitmap-timing", () => (async () => { const o = []; createImageBitmap(new ImageData(1, 1)).then(() => o.push(1)); Promise.resolve().then(() => o.push(2)); await new Promise((r) => setTimeout(r, 50)); return o.join(); })()],
    ["create-image-bitmap-timing", () => createImageBitmap(new ImageData(1, 1), { resizeWidth: 0 }).then(() => 'ok', (e) => e.message.includes('resizeWidth'))],
    ["audio-buffer-source-loop-start", () => (async () => { const c = new OfflineAudioContext(1, 8, 8192), b = c.createBuffer(1, 4, 8192); b.getChannelData(0).set([1, 2, 3, 4]); const s = new AudioBufferSourceNode(c, { buffer: b, loop: true, loopStart: 2 / 8192 }); s.connect(c.destination); s.start(); return [...(await c.startRendering()).getChannelData(0)]; })()],
    ["get-display-media-audio-selection", () => (() => { let r = false; navigator.mediaDevices.getDisplayMedia({ get audioSelection() { r = true; } }).catch(() => {}); return r; })()],
    ["rtc-ice-candidate-tls", () => new RTCIceCandidate({ candidate: 'candidate:1 1 tls 1 1.2.3.4 443 typ host fingerprint sha-256;AA', sdpMid: '0' }).address],
    ["webgl-angle-validation-152", () => (() => { const g = document.createElement('canvas').getContext('webgl2'); g.bindTexture(g.TEXTURE_2D, g.createTexture()); g.texImage2D(g.TEXTURE_2D, 0, g.RGBA, 1, 1, 0, g.RGBA, g.UNSIGNED_BYTE, null); g.texSubImage2D(g.TEXTURE_2D, 0, 0, 0, 1, 1, g.RGBA, g.UNSIGNED_SHORT_4_4_4_4, new Uint16Array(1)); return g.getError(); })()],
    ["swiftshader-max-draw-buffers", () => (() => { const g = document.createElement('canvas').getContext('webgl2'); return g.getParameter(g.MAX_DRAW_BUFFERS); })()],
    ["css-pseudo-element-types", () => document.documentElement.pseudo('::backdrop') !== null],
    ["css-pseudo-element-types", () => document.documentElement.pseudo('::view-transition') !== null],
    ["css-parsing-152", () => CSS.supports('overflow-clip-margin', '-1px')],
    ["css-parsing-152", () => ((s) => s.currentTranslate === s.currentTranslate)(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))],
    ["disabled-control-blur", () => ((i) => { document.body.append(i); i.focus(); i.disabled = true; const r = document.activeElement === i; i.remove(); return r; })(document.createElement('input'))],
    ["jspi-async-stack", () => (async () => { const b = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 2, 7, 1, 1, 109, 1, 102, 0, 0, 3, 2, 1, 0, 7, 7, 1, 3, 114, 117, 110, 0, 1, 10, 6, 1, 4, 0, 16, 0, 11]); const { instance: i } = await WebAssembly.instantiate(b, { m: { f: new WebAssembly.Suspending(async () => { await 0; throw Error(); }) } }); try { await WebAssembly.promising(i.exports.run)(); } catch (e) { return e.stack.includes('wasm-function'); } })()],
    ["webgpu-subgroup-size-control", () => navigator.gpu.requestAdapter().then((a) => !!a && a.features.has('subgroup-size-control'))],
  ];
  const out = [];
  for (const [id, f] of checks) {
    let v;
    try { v = await f(); } catch (e) { v = "throws " + e.name; }
    const expr = f.toString().replace(/^\(\)\s*=>\s*/, "");
    out.push(id.padEnd(28) + expr + " = " + (typeof v === "string" ? JSON.stringify(v) : String(v)));
  }
  console.log(out.join("\n"));
})();

FAQ

When was Chrome 152 released?

Chrome 152 reached the stable channel on August 25, 2026, four weeks after Chrome 151. It was the last version on the four-week cycle: Chrome 153 followed two weeks later.

What is the Chrome 152 user agent string?

On Windows: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36. Only the major version changes; the full version travels in Client Hints. The table above lists every platform.

What is the Sec-CH-UA header in Chrome 152?

"Chromium";v="152", "Not?A_Brand";v="24", "Google Chrome";v="152". Both the fake GREASE brand and the order change with every major.

Did Chrome 152 change the TLS (JA3/JA4) fingerprint?

Yes, the first change in this series. Chrome 152 adds the trust_anchors extension (0xCA34) and a GREASE signature algorithm, so JA4 goes from t13d1516h2_8daaf6152771_806a8c22fdea to t13d1517h2_8daaf6152771_cb7bf5808d99 on a first visit. HTTP/2 is unchanged.

How can a website tell Chrome 151 from Chrome 152?

The quickest lines are 'cpuPerformance' in navigator, typeof OpaqueRange and FeaturePolicy.name, which is "PermissionsPolicy" from 152. A server sees the new JA4 before any of them.

Why did the Web Audio fingerprint change in Chrome 152?

Chrome 152 computes Web Audio's FFT, and the oscillator wave tables built from it, with a Rust library instead of PFFFT. The output differs only in the last digits, but audio fingerprints hash those digits, so every oscillator-based audio hash moves.

Clearcote puts this into practice

An open-source Chromium with fingerprint control compiled into the engine. A drop-in for Playwright & Puppeteer.

Free for one browser with GitHub. No card.