{"id":100426,"date":"2025-02-21T00:18:52","date_gmt":"2025-02-21T00:18:52","guid":{"rendered":"https:\/\/peraltafinancing.com\/analytics\/itp-2-1-and-web-analytics\/"},"modified":"2025-02-21T00:18:52","modified_gmt":"2025-02-21T00:18:52","slug":"itp-2-1-and-web-analytics","status":"publish","type":"post","link":"https:\/\/fivemor.com\/?p=100426","title":{"rendered":"ITP 2.1 And Web Analytics"},"content":{"rendered":"<p> <br \/>\n<\/p>\n<div>\n<blockquote>\n<p><strong>Updated 1 October 2019<\/strong> With <a href=\"https:\/\/webkit.org\/blog\/9521\/intelligent-tracking-prevention-2-3\/\">ITP 2.3<\/a> it looks like Safari is reducing the usefulness of <code>localStorage<\/code> as well, so using that as an alternative fix to persistence issues should not be considered future-proof. this solution should <strong>not<\/strong> be considered future-proof.<\/p>\n<\/blockquote>\n<blockquote>\n<p>Updated <strong>12 March 2019<\/strong> with some minor clarifications..<\/p>\n<\/blockquote>\n<p>On 21st February 2019, <a href=\"https:\/\/webkit.org\/\">WebKit<\/a> announced the release of the latest iteration of Safari\u2019s <strong>Intelligent Tracking Prevention<\/strong> (ITP), known as <a href=\"https:\/\/webkit.org\/blog\/8613\/intelligent-tracking-prevention-2-1\/\">ITP 2.1<\/a>. For a while now, Safari has been targeting cross-site tracking with ITP, first starting with <a href=\"https:\/\/webkit.org\/blog\/7675\/intelligent-tracking-prevention\/\">cookies in third-party contexts<\/a>, then <a href=\"https:\/\/webkit.org\/blog\/8311\/intelligent-tracking-prevention-2-0\/\">tightening the noose<\/a> after a number of workarounds emerged, and finally with the latest iteration targeting cookies that were moved from a third-party context to a first-party context.<\/p>\n<div style=\"aspect-ratio: 3068 \/ 2136;\" class=\"figure nocaption\">\n<p>    <a href=\"https:\/\/www.simoahava.com\/images\/2019\/02\/itp-2-1-web-analytics.jpg\" title=\"ITP 2.1 and web analytics\"><\/p>\n<p>    <img decoding=\"async\" class=\"fig-img\" height=\"2136\" width=\"3068\" loading=\"lazy\" src=\"https:\/\/www.simoahava.com\/images\/2019\/02\/itp-2-1-web-analytics.jpg#ZgotmplZ\" alt=\"ITP 2.1 and web analytics\"\/><\/p>\n<p>    <\/a><\/p>\n<\/div>\n<p>ITP 2.1 has one specific feature that will make us web analytics folks tremble in our boots:<\/p>\n<blockquote>\n<p>With ITP 2.1, all persistent client-side cookies, i.e. persistent cookies created through document.cookie, are capped to a <strong>seven day expiry<\/strong>.<\/p>\n<\/blockquote>\n<p>From <a href=\"https:\/\/webkit.org\/blog\/8613\/intelligent-tracking-prevention-2-1\/\">the WebKit blog<\/a>, emphasis mine.<\/p>\n<p>This means that any <strong>JavaScript library<\/strong> wanting to store a cookie in the web browser will have that cookie capped to a seven day lifetime. After seven days since cookie creation, the cookie will expire and will be removed from the browser.<\/p>\n<p>In this article, I want to explore the implications this has on web analytics, and what we can do to avoid losing the integrity of our data.<\/p>\n<p>                <span class=\"simmer\"><br \/>\n  <span class=\"close\">X<\/span><\/p>\n<p>\n    <span class=\"fa fa-md fa-bell\"\/><br \/>\n    <strong>The Simmer Newsletter<\/strong>\n  <\/p>\n<p>\n    Subscribe to the <a href=\"https:\/\/www.simoahava.com\/newsletter\/\">Simmer newsletter<\/a> to get the latest news and content from Simo Ahava into your email inbox!\n  <\/p>\n<p>  <\/span><\/p>\n<h2 id=\"brief-introduction-to-itp\">Brief introduction to ITP<\/h2>\n<p>In the following chapters, I will use these two terms:<\/p>\n<ul>\n<li>\n<p><strong>Third-party context<\/strong> means that a resource is requested from a domain external from the one the user is currently on (i.e. they don\u2019t <strong>share<\/strong> the same <a href=\"https:\/\/godoc.org\/golang.org\/x\/net\/publicsuffix\">eTLD + 1<\/a>), and this request tries to access cookies set on this external domain. Since the domain differs from the one the user is on (has to be a different root domain, basically), the interaction with this external domain happens in a <strong>third-party context<\/strong>.<\/p>\n<\/li>\n<li>\n<p><strong>First-party context<\/strong> means that a resource is requested from the current (parent) domain (eTLD + 1) the user is on. Since the user is in the same domain environment, any requests made happen in a <strong>first-party context<\/strong>.<\/p>\n<\/li>\n<\/ul>\n<h3 id=\"itp-10\">ITP 1.0<\/h3>\n<p>When Safari <a href=\"https:\/\/webkit.org\/blog\/7675\/intelligent-tracking-prevention\/\">first introduced<\/a> Intelligent Tracking Prevention, its mission was fairly clear. Safari wanted to prevent domains classified as having tracking capabilities from tracking users across different sites using <strong>third-party cookies<\/strong>.<\/p>\n<p>A classic example is how advertising technology services might build an audience profile of their users by observing on which sites the user visits. This is done by having those sites call the AdTech domain with a pixel request or something similar, and a third-party cookie stored on the AdTech domain will thus be able to build a profile of the user based on the sites where the pixel was requested on.<\/p>\n<div style=\"aspect-ratio: 2196 \/ 982;\" class=\"figure nocaption\">\n<p>    <a href=\"https:\/\/www.simoahava.com\/images\/2019\/02\/ad-tech-third-party.jpg\" title=\"Ad Tech third party cookie\"><\/p>\n<p>    <img decoding=\"async\" class=\"fig-img\" height=\"982\" width=\"2196\" loading=\"lazy\" src=\"https:\/\/www.simoahava.com\/images\/2019\/02\/ad-tech-third-party.jpg#ZgotmplZ\" alt=\"Ad Tech third party cookie\"\/><\/p>\n<p>    <\/a><\/p>\n<\/div>\n<p>ITP made this more difficult by requiring that users actually <strong>interact<\/strong> with the third-party domain in a first-party context in order for the domain to be allowed to harvest their data in a third-party context. If there was no <strong>meaningful interaction<\/strong> such as loading the page and clicking on a button, a machine learning algorithm would classify the domain as having cross-site tracking capabilities, and as a result <strong>partition<\/strong> the cookies on that domain, preventing them from being used for cross-site tracking.<\/p>\n<p>Remember: ITP\u2019s main <em>modus operandi<\/em> is <strong>preventing cross-site tracking<\/strong>. It\u2019s Intelligent <strong>Tracking<\/strong> Prevention, not Intelligent <strong>Cookie<\/strong> Prevention.<\/p>\n<p>Naturally, for AdTech this is awkward. The whole point of pixel tracking is that it\u2019s transparent and unobtrusive. What would you think if you were suddenly redirected to e.g. <strong>doubleclick.net<\/strong> and asked if it\u2019s ok that Google continues to build an audience profile out of your web browsing behavior?<\/p>\n<div style=\"aspect-ratio: 1037 \/ 315;\" class=\"figure \">\n<p>    <a href=\"https:\/\/www.simoahava.com\/images\/2019\/02\/itp-original-tracking-prevention-timeline.jpg\" title=\"From https:\/\/webkit.org\/blog\/7675\/intelligent-tracking-prevention\/\"><\/p>\n<p>    <img decoding=\"async\" class=\"fig-img\" height=\"315\" width=\"1037\" loading=\"lazy\" src=\"https:\/\/www.simoahava.com\/images\/2019\/02\/itp-original-tracking-prevention-timeline.jpg#ZgotmplZ\" alt=\"From https:\/\/webkit.org\/blog\/7675\/intelligent-tracking-prevention\/\"\/><\/p>\n<p>    <\/a><\/p>\n<p>    <span class=\"caption\">From https:\/\/webkit.org\/blog\/7675\/intelligent-tracking-prevention\/<\/span><\/p>\n<\/div>\n<p>But, third-party cookies can also be used for <strong>non-tracking purposes<\/strong>, such as maintaining a single sign-on (SSO) session. This is why the original ITP introduced a 24-hour grace period during which time the SSO cookie could be used in a third-party context. After that, the cookies should be stored in a first-party context or, preferably, by moving to <strong>HTTP cookies<\/strong> (more on this later) to avoid these issues altogether.<\/p>\n<p>The cookies would be <strong>partitioned<\/strong> for 30 days. This meant that cookies used for tracking purposes get a unique storage double-keyed to the domain requesting the cookie (the first-party domain), and the domain setting the cookie (third-party domain). So, for example, a third-party cookie set by <strong>google.com<\/strong> while on <strong>simoahava.com<\/strong> would only be accessible when browsing simoahava.com. If the user went to <strong>younggoodmanahava.com<\/strong>, a separate cookie store would be created for that origin.<\/p>\n<p>Partitioning the cookies this way meant that third-party cookies could still be used for things like login state management. Since the partitions are unique to each origin combination, cross-site tracking would effectively be neutered.<\/p>\n<p>If the user visited the tracking domain within 30 days of the last interaction, both the 24-hour grace period and the 30-day partition timer would be reset.<\/p>\n<p>If the 30-day limit expired without interaction with the tracking domain, all cookies would be purged from the tracking domain.<\/p>\n<p>ITP 1.0 wasn\u2019t very impactful for us <a href=\"https:\/\/analytics.google.com\/\">Google Analytics<\/a> users. GA works in a first-party context, so ITP\u2019s updates did not concern us.<\/p>\n<h3 id=\"itp-11-and-storage-access-api\">ITP 1.1 and Storage Access API<\/h3>\n<p>With <a href=\"https:\/\/webkit.org\/blog\/8142\/intelligent-tracking-prevention-1-1\/\"><strong>ITP 1.1<\/strong><\/a> and the <a href=\"https:\/\/webkit.org\/blog\/8124\/introducing-storage-access-api\/\">Storage Access API<\/a>, ITP made some concessions to third-party services serving embedded content, such as social logins or video services. It would be weird to have the user visit these services in a first-party context, because the whole purpose of embedding content is to do it smoothly while staying on the same site. The <strong>Storage Access API<\/strong> was the solution, where embedded content would be given access to the cookies stored on the third-party domain as long as the embed followed certain rules:<\/p>\n<div style=\"aspect-ratio: 1602 \/ 1220;\" class=\"figure \">\n<p>    <a href=\"https:\/\/www.simoahava.com\/images\/2019\/02\/storage-access-embed.jpg\" title=\"From https:\/\/webkit.org\/blog\/8124\/introducing-storage-access-api\/\"><\/p>\n<p>    <img decoding=\"async\" class=\"fig-img\" height=\"1220\" width=\"1602\" loading=\"lazy\" src=\"https:\/\/www.simoahava.com\/images\/2019\/02\/storage-access-embed.jpg#ZgotmplZ\" alt=\"From https:\/\/webkit.org\/blog\/8124\/introducing-storage-access-api\/\"\/><\/p>\n<p>    <\/a><\/p>\n<p>    <span class=\"caption\">From https:\/\/webkit.org\/blog\/8124\/introducing-storage-access-api\/<\/span><\/p>\n<\/div>\n<p>The Storage Access API did not prompt the user for anything &#8211; this was the major concession from WebKit.<\/p>\n<p>Since the Storage Access API allowed access to the third-party domain\u2019s own, non-partitioned cookies, the <em>partitioned<\/em> cookies preserved for things like login state management were demoted to <em>session cookies<\/em>, so they would get purged once the browser was closed. This meant that the Storage Access API was the main way to access persistent data in a third-party context.<\/p>\n<p>Still no issues with first-party web analytics, phew!<\/p>\n<h3 id=\"itp-20\">ITP 2.0<\/h3>\n<p>Mid-2018, WebKit introduced <a href=\"https:\/\/webkit.org\/blog\/8311\/intelligent-tracking-prevention-2-0\/\"><strong>ITP 2.0<\/strong><\/a>.<\/p>\n<p>ITP 2.0 removed the 24-hour grace period altogether. Now cookies would be partitioned immediately after creation, if the domain was classified as having cross-site tracking capabilities.<\/p>\n<div style=\"aspect-ratio: 2340 \/ 1077;\" class=\"figure \">\n<p>    <a href=\"https:\/\/www.simoahava.com\/images\/2019\/02\/itp-2-diagram.jpg\" title=\"From https:\/\/webkit.org\/blog\/8311\/intelligent-tracking-prevention-2-0\/\"><\/p>\n<p>    <img decoding=\"async\" class=\"fig-img\" height=\"1077\" width=\"2340\" loading=\"lazy\" src=\"https:\/\/www.simoahava.com\/images\/2019\/02\/itp-2-diagram.jpg#ZgotmplZ\" alt=\"From https:\/\/webkit.org\/blog\/8311\/intelligent-tracking-prevention-2-0\/\"\/><\/p>\n<p>    <\/a><\/p>\n<p>    <span class=\"caption\">From https:\/\/webkit.org\/blog\/8311\/intelligent-tracking-prevention-2-0\/<\/span><\/p>\n<\/div>\n<p>Another major thing ITP 2.0 did was make Storage Access API prompt-based. Instead of allowing embedded content to access cookies set in a third-party context by virtue of following the rules established in the previous chapter, Safari would now explicitly prompt the user for access to the third-party context. If the user gave access, the consent would persist and the 30-day timeline would be refreshed.<\/p>\n<p>In other words, the <strong>Storage Access API<\/strong> was now the main way for domains classified as having cross-site tracking capabilities to access their cookies when embedded or requested in a third-party context.<\/p>\n<p>First-party web analytics is still safe. But not for long.<\/p>\n<h2 id=\"itp-21\">ITP 2.1<\/h2>\n<p><a href=\"https:\/\/webkit.org\/blog\/8613\/intelligent-tracking-prevention-2-1\/\">ITP 2.1<\/a> was announced on February 21st, 2019, and it will come to effect as soon as <strong>iOS 12.2<\/strong> and <strong>Safari 12.1<\/strong> come out of beta.<\/p>\n<blockquote>\n<p><strong>Update 8 March 2019<\/strong>: It looks like features that are now bundled as ITP 2.1 have been quitely rolling out since the beginning of this year already.<\/p>\n<\/blockquote>\n<p>ITP 2.1 removes partitioned cookies altogether. Now if a domain classified as having cross-site tracking capabilities needs to have access to its cookies in a third-party context, the <strong>Storage Access API<\/strong> <em>must<\/em> be used, even if used for things like login state management. The main purpose of this change is to reduce the amount of memory overhead that partitioned (session) cookies introduce on any given site.<\/p>\n<p>But by far the biggest change in ITP 2.1, one that has <strong>direct consequences on first-party web analytics<\/strong> are the new measures placed on <strong>first-party cookies<\/strong> set with client-side JavaScript.<\/p>\n<p>No longer is ITP just attacking AdTech companies who rely on cross-site tracking to build audience profiles. Now cookies set with <code>document.cookie<\/code> will also be targeted to prevent them from being harvested on different subdomains than the one on which they were set.<\/p>\n<div style=\"aspect-ratio: 2826 \/ 974;\" class=\"figure nocaption\">\n<p>    <a href=\"https:\/\/www.simoahava.com\/images\/2019\/02\/first-party-cookies.jpg\" title=\"First party cookies\"><\/p>\n<p>    <img decoding=\"async\" class=\"fig-img\" height=\"974\" width=\"2826\" loading=\"lazy\" src=\"https:\/\/www.simoahava.com\/images\/2019\/02\/first-party-cookies.jpg#ZgotmplZ\" alt=\"First party cookies\"\/><\/p>\n<p>    <\/a><\/p>\n<\/div>\n<p>As in the above example, any one of the subdomains of <strong>simoahava.com<\/strong> can write a first-party cookie on that root domain, after which <strong>any<\/strong> subdomain of simoahava.com can access that cookie. This is nothing new &#8211; this is how cookies set on a root domain have always worked.<\/p>\n<p>However, ITP now targets cookies set with JavaScript\u2019s <code>document.cookie<\/code> because it has become apparent that some vendors (e.g. one whose name rhymes with <em>lacebook<\/em>) have begun repurposing first-party cookies to help with their cross-site tracking intentions. They are also taking advantage of the scenario depicted in the image above. Even if you limited these vendors\u2019 pixels to fire only on a specific subdomain, they could still access cookies on higher-level domain names.<\/p>\n<p>The impact on web analytics is brutal, depending of course on Safari\u2019s share of your traffic. Take the following example:<\/p>\n<ul>\n<li>\n<p><strong>Day 1<\/strong>: User visits <code>www.simoahava.com<\/code>, the <code>_ga<\/code> cookie is written on simoahava.com. It is set at a 7-day expiry (rather than the 2 years that analytics.js defaults to).<\/p>\n<\/li>\n<li>\n<p><strong>Day 3<\/strong>: User visits <code>blog.simoahava.com<\/code>. The <code>_ga<\/code> cookie is found on simoahava.com, so its value is available to blog.simoahava.com, and the 7-day expiry is reset.<\/p>\n<\/li>\n<li>\n<p><strong>Day 13<\/strong>: User visits <code>www.simoahava.com<\/code>. The <code>_ga<\/code> cookie has expired, so a new Client ID is generated in a new <code>_ga<\/code> cookie, and the visitor is <strong>treated as a new user<\/strong> in Google Analytics.<\/p>\n<\/li>\n<\/ul>\n<p>Google Analytics uses <code>document.cookie<\/code> to set the cookie in the browser. It really has no other choice. It\u2019s a vendored JavaScript library, downloaded from Google\u2019s servers, so it has no capability of setting e.g. HTTP cookies on simoahava.com, since they would be set in a third-party context.<\/p>\n<p>Funnily enough, ITP 2.1 removes support for the <strong>Do Not Track<\/strong> signal in Safari, denoting the end to this miserable experiment in WebKit. Had more sites respected DNT when determining should visitors be tracked or not, perhaps we wouldn\u2019t have seen ITP 2.1 in its current shape.<\/p>\n<p>But because sites can\u2019t be trusted to handle privacy and tracking prevention on their own, Safari must now take the reins and do it for them.<\/p>\n<h2 id=\"technical-implications-and-solutions\">Technical implications and solutions<\/h2>\n<p>First of all, ITP 2.1 really only impacts <strong>browser cookies<\/strong> set with <code>document.cookie<\/code>. It will <strong>not<\/strong> impact other DOM storage (such as <code>localStorage<\/code>), because those are same-origin only, so lacking the capability of working across sub-domains. Similarly, it will <strong>not<\/strong> impact cookies set with HTTP responses, i.e. using the <code>Set-Cookie<\/code> header in the HTTP response.<\/p>\n<p>HTTP cookies require a server-side script (or an edge cache \/ serverless solution) to modify the HTTP response, so it\u2019s a deliberate decision from the site owner to set the cookies in the first-party context rather than a JavaScript library downloaded from a CDN being able to, at whim, set and get any first-party cookies it wants to. I suppose this is why they are not (yet) impacted by ITP.<\/p>\n<h3 id=\"localstorage-for-same-domain-browsing\">localStorage for same-domain browsing<\/h3>\n<blockquote>\n<p><strong>Update 1 October 2019<\/strong>: With <a href=\"https:\/\/webkit.org\/blog\/9521\/intelligent-tracking-prevention-2-3\/\">ITP 2.3<\/a>, <code>localStorage<\/code> should <strong>not<\/strong> be considered a viable solution for persisting client-side state anymore.<\/p>\n<\/blockquote>\n<p>Many suggested using <code>localStorage<\/code> to persist these anonymous identifiers used by Google Analytics, for example. Instead of dropping a cookie named <code>_ga<\/code>, use <code>window.localStorage.setItem()<\/code> instead.<\/p>\n<p>I was excited about this option, and even <a href=\"https:\/\/www.simoahava.com\/analytics\/use-localstorage-client-id-persistence-google-analytics\/\">wrote an article<\/a> that describes this possibility in detail.<\/p>\n<p>However, the main issue is that <code>localStorage<\/code> is same-origin only. The storage in <code>www.simoahava.com<\/code> will not be available on blog.simoahava.com.<\/p>\n<p>Thus the only thing that <code>localStorage<\/code> actually solves is not having the 7-day expiration cap on data stored on a single domain. Since all my web traffic happens on <code>www.simoahava.com<\/code>, <code>localStorage<\/code> is a good option for me, since it persists the Client ID nicely (though with <a href=\"https:\/\/www.simoahava.com\/analytics\/use-localstorage-client-id-persistence-google-analytics\/#caveats\">some caveats<\/a> nevertheless).<\/p>\n<h3 id=\"localstorage-for-cross-subdomain-browsing\">localStorage for cross-subdomain browsing<\/h3>\n<blockquote>\n<p>Thanks to <a href=\"https:\/\/www.linkedin.com\/in\/eike-pierstorff-77a4a166\/\">Eike Pierstorff<\/a> and <a href=\"https:\/\/twitter.com\/fujii0\">Fujii Hironori<\/a> for suggesting this solution.<\/p>\n<\/blockquote>\n<p>An alternative option to same-domain <code>localStorage<\/code> is to create a <strong>store<\/strong> on a page in one of your subdomains, and then load that page in an <code>iframe<\/code> element, utilizing the  <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Window\/postMessage\"><code>postMessage<\/code> API<\/a> to get and set persistent cookie values on your site.<\/p>\n<div style=\"aspect-ratio: 2478 \/ 964;\" class=\"figure nocaption\">\n<p>    <a href=\"https:\/\/www.simoahava.com\/images\/2019\/02\/postmessage-localstorage-store.jpg\" title=\"localStorage postMessage store\"><\/p>\n<p>    <img decoding=\"async\" class=\"fig-img\" height=\"964\" width=\"2478\" loading=\"lazy\" src=\"https:\/\/www.simoahava.com\/images\/2019\/02\/postmessage-localstorage-store.jpg#ZgotmplZ\" alt=\"localStorage postMessage store\"\/><\/p>\n<p>    <\/a><\/p>\n<\/div>\n<p>Since the <code>localStorage<\/code> store shares the same parent domain <a href=\"https:\/\/godoc.org\/golang.org\/x\/net\/publicsuffix\">eTLD + 1<\/a> with the pages making the requests through the iframe, the interaction happens in a first-party context, and the <code>localStorage<\/code> store can be accessed without the Storage Access API. Also, in <strong>third-party contexts<\/strong>, <code>localStorage<\/code> is <strong>transient<\/strong> in Safari, meaning it is purged after the browser is closed. In first-party context this is not the case.<\/p>\n<p>The process would look like this.<\/p>\n<ol>\n<li>\n<p>The page wanting to access the store would load the tracker page in an iframe, and send a message to it requesting a stored Client ID.<\/p>\n<\/li>\n<li>\n<p>The iframe checks if it has the Client ID stored. If it does, it returns it in a message back to the origin page. If it doesn\u2019t, it returns a string indicating this.<\/p>\n<\/li>\n<li>\n<p>On the origin page, a listener listens for the response. If the response has a Client ID, that is used in the trackers running on the page. If the response has a \u201cnull\u201d indicator, the origin page builds the Client ID, and finally sends it with another message back into the iframe for storage.<\/p>\n<\/li>\n<\/ol>\n<p>Here\u2019s what the parent page script would look like:<\/p>\n<div class=\"highlight\">\n<pre style=\"background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-javascript\" data-lang=\"javascript\"><span style=\"color:#aaa;font-style:italic\">\/\/ Create iframe\n<\/span><span style=\"color:#aaa;font-style:italic\"\/><span style=\"color:#00a\">var<\/span> el = <span style=\"color:#0aa\">document<\/span>.createElement(<span style=\"color:#a50\">'iframe'<\/span>);\nel.src = <span style=\"color:#a50\">'https:\/\/www.maindomain.com\/tracker.html'<\/span>;\nel.setAttribute(<span style=\"color:#a50\">'style'<\/span>, <span style=\"color:#a50\">'width:0;height:0;border:0;border:none'<\/span>);\n<span style=\"color:#0aa\">document<\/span>.body.appendChild(el);\n\n<span style=\"color:#aaa;font-style:italic\">\/\/ Add listener to get the stored Client ID\n<\/span><span style=\"color:#aaa;font-style:italic\"\/><span style=\"color:#0aa\">window<\/span>.addEventListener(<span style=\"color:#a50\">'message'<\/span>, <span style=\"color:#00a\">function<\/span>(e) {\n  <span style=\"color:#00a\">if<\/span> (e.origin !== <span style=\"color:#a50\">'https:\/\/www.maindomain.com'<\/span>) { <span style=\"color:#00a\">return<\/span>; }\n  <span style=\"color:#00a\">if<\/span> (e.data === <span style=\"color:#a50\">'null'<\/span>) { generateTracker(<span style=\"color:#00a\">null<\/span>); }\n  <span style=\"color:#00a\">else<\/span> { generateTracker(e.data); }\n});\n\n<span style=\"color:#aaa;font-style:italic\">\/\/ Send request to get the Client ID\n<\/span><span style=\"color:#aaa;font-style:italic\"\/>el.contentWindow.postMessage(<span style=\"color:#a50\">'get'<\/span>, <span style=\"color:#a50\">'https:\/\/www.maindomain.com'<\/span>);\n\n<span style=\"color:#aaa;font-style:italic\">\/\/ Send message to set the Client ID after the tracker has generated one\n<\/span><span style=\"color:#aaa;font-style:italic\"\/><span style=\"color:#00a\">function<\/span> trackerGeneratedCallback(clientId) {\n  el.contentWindow.postMessage(clientId, <span style=\"color:#a50\">'https:\/\/www.maindomain.com'<\/span>);\n}\n<\/code><\/pre>\n<\/div>\n<p>Here, the iframe is generated, after which a message is sent to the iframe to get the Client ID. A listener on the page then waits for the iframe to respond. If the iframe responds with a <code>'null'<\/code> message, a new tracker is generated (<code>generateTracker<\/code>) with its default Client ID generation mechanism.<\/p>\n<p>If the iframe responds with the stored Client ID, then a new tracker is generated with this stored Client ID instead.<\/p>\n<p>If the tracker <strong>did<\/strong> generate a new Client ID, then the <code>trackerGeneratedCallback<\/code> is called afterwards, and this sends the Client ID to the tracker page for storage.<\/p>\n<p>And this is what the iframe page would look like:<\/p>\n<div class=\"highlight\">\n<pre style=\"background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-html\" data-lang=\"html\">html&gt;\nhead&gt;\nmeta <span style=\"color:#1e90ff\">name<\/span>=<span style=\"color:#a50\">\"robots\"<\/span> <span style=\"color:#1e90ff\">content<\/span>=<span style=\"color:#a50\">\"noindex,nofollow\"<\/span>&gt;\nscript&gt;\n  <span style=\"color:#aaa;font-style:italic\">\/\/ Check the request comes from *.maindomain.com\n<\/span><span style=\"color:#aaa;font-style:italic\"\/>  <span style=\"color:#00a\">var<\/span> hostNameRegex = <span style=\"color:#099\">\/^https:\\\/\\\/([^.]+\\.)maindomain\\.com\/<\/span>;\n  \n  <span style=\"color:#00a\">function<\/span> processMessage(event) {\n    <span style=\"color:#00a\">if<\/span> (!hostnameRegex.test(event.origin)) {\n      <span style=\"color:#00a\">return<\/span>;\n    }\n    \n    <span style=\"color:#aaa;font-style:italic\">\/\/ If request is to get the clientId, send it as a message back to the source page\n<\/span><span style=\"color:#aaa;font-style:italic\"\/>    <span style=\"color:#aaa;font-style:italic\">\/\/ or send 'null' if no Client ID is found.\n<\/span><span style=\"color:#aaa;font-style:italic\"\/>    <span style=\"color:#00a\">if<\/span> (event.data === <span style=\"color:#a50\">'get'<\/span>) {\n      event.source.postMessage(<span style=\"color:#0aa\">window<\/span>.localStorage.getItem(<span style=\"color:#a50\">'ga_client_id'<\/span>) || <span style=\"color:#a50\">'null'<\/span>, event.origin);\n      <span style=\"color:#00a\">return<\/span>;\n    }\n    \n    <span style=\"color:#aaa;font-style:italic\">\/\/ Otherwise, set the Client ID in localStorage using the message content\n<\/span><span style=\"color:#aaa;font-style:italic\"\/>    <span style=\"color:#0aa\">window<\/span>.localStorage.setItem(<span style=\"color:#a50\">'ga_client_id'<\/span>, event.data);\n    <span style=\"color:#00a\">return<\/span>,\n  }\n  \n  <span style=\"color:#aaa;font-style:italic\">\/\/ Add a listener to listen for postMessage messages\n<\/span><span style=\"color:#aaa;font-style:italic\"\/>  <span style=\"color:#0aa\">window<\/span>.addEventListener(<span style=\"color:#a50\">'message'<\/span>, processMessage);\n<span style=\"color:#1e90ff;font-weight:bold\">script<\/span>&gt;\n<span style=\"color:#1e90ff;font-weight:bold\">head<\/span>&gt;\nbody&gt;\n<span style=\"color:#1e90ff;font-weight:bold\">body<\/span>&gt;\n<span style=\"color:#1e90ff;font-weight:bold\">html<\/span>&gt;<\/code><\/pre>\n<\/div>\n<p>This way you could persist any client-side cookie values in the <code>localStorage<\/code> store of the domain where the iframe rests. This example is just for an arbitrary Client ID, but you could extend it for any value passed in the message.<\/p>\n<p>Just remember the idiosyncracies of each platform. With Google Analytics, for example, additional measures need to be taken to handle cross-domain tracking, cookie expiration, etc.<\/p>\n<blockquote>\n<p><strong>Update 1 October 2019<\/strong>: This is definitely the most effective way to persist client-side state. See <a href=\"https:\/\/www.simoahava.com\/google-cloud\/create-cookie-rewrite-web-service-google-cloud\/\">this article<\/a> for inspiration.<\/p>\n<\/blockquote>\n<p>Instead of setting the <code>_ga<\/code> cookie with <code>document.cookie<\/code>, you could instead set it with the HTTP response. For example, if running a <strong>node.js<\/strong> Express server, you could have the following lines in your code:<\/p>\n<div class=\"highlight\">\n<pre style=\"background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-javascript\" data-lang=\"javascript\">app.get(<span style=\"color:#a50\">'\/'<\/span>, (req, res) =&gt; {\n  <span style=\"color:#aaa;font-style:italic\">\/\/ Check for existing cookie and use that if found\n<\/span><span style=\"color:#aaa;font-style:italic\"\/>  <span style=\"color:#00a\">let<\/span> ga = req.cookies[<span style=\"color:#a50\">'_ga'<\/span>];\n  \n  <span style=\"color:#aaa;font-style:italic\">\/\/ Check for linker and use that if valid\n<\/span><span style=\"color:#aaa;font-style:italic\"\/>  <span style=\"color:#00a\">if<\/span> (req.query[<span style=\"color:#a50\">'_ga'<\/span>]) {\n    ga = getClientIdFromLinker(req.query[<span style=\"color:#a50\">'_ga'<\/span>]) || ga;\n  }\n  \n  <span style=\"color:#aaa;font-style:italic\">\/\/ Otherwise generate a new Client ID\n<\/span><span style=\"color:#aaa;font-style:italic\"\/>  <span style=\"color:#00a\">if<\/span> (!ga) {\n    ga = generateGAClientId();\n  }\n  \n  res.cookie(<span style=\"color:#a50\">'_ga'<\/span>, ga, {domain: <span style=\"color:#a50\">'simoahava.com'<\/span>, path: <span style=\"color:#a50\">'\/'<\/span>, secure: <span style=\"color:#00a\">true<\/span>, expires: <span style=\"color:#00a\">new<\/span> <span style=\"color:#0aa\">Date<\/span>(<span style=\"color:#0aa\">Date<\/span>.now() + <span style=\"color:#099\">1000<\/span>*<span style=\"color:#099\">60<\/span>*<span style=\"color:#099\">60<\/span>*<span style=\"color:#099\">24<\/span>*<span style=\"color:#099\">365<\/span>*<span style=\"color:#099\">2<\/span>), });\n  \n  res.render(<span style=\"color:#a50\">'index'<\/span>);\n});\n<\/code><\/pre>\n<\/div>\n<p>In this example, when the home page is requested from the server, its cookies are parsed. If the <code>_ga<\/code> cookie is found, its value is stored as a local variable. Then, if the request also has the cross-domain linker parameter, the parameter validity is verified (using e.g. my <a href=\"https:\/\/gist.github.com\/sahava\/f3718f981bb01768c0eba714ee94e2d2\">Gist<\/a>) and if it\u2019s a valid parameter, the local variable is overwritten with the Client ID from the linker.<\/p>\n<blockquote>\n<p><strong>NOTE!<\/strong> The <code>allowLinker<\/code> script builds the fingerprint using, among others, the browser-based APIs <code>window.navigator.plugins<\/code> and <code>window.navigator.language<\/code>. As these are not available in the HTTP headers, you\u2019d need to pass these to the server with the payload of the request you\u2019d use to route the cookies, and then modify the <code>allowLinker<\/code> script to use these payload parameters rather than the <code>window.navigator<\/code> APIs. Alternatively, you could build your own cross-domain linker solution.<\/p>\n<\/blockquote>\n<p>Finally, if the <code>_ga<\/code> cookie didn\u2019t exist and if the cross-domain linker was missing or broken, a new Client ID is generated.<\/p>\n<p>In the HTTP response, the <code>_ga<\/code> cookie is set with this value, and its expiration is reset to two years.<\/p>\n<p>This type of server-side setup should be trivial to do in almost any web server environment (<a href=\"http:\/\/peter.nikolow.me\/safari-itp-2-1-demo\/\">PHP\/Apache<\/a>, React, Node.js, IIS, etc.). But you do need access to the web server and its request handlers to be able to do this.<\/p>\n<p>If you\u2019re using a service such as <a href=\"https:\/\/www.cloudflare.com\/\">Cloudflare<\/a>, which caches your server content on \u201cthe edge\u201d, you might also be able to run JavaScript to intercept and handle the HTTP requests between the browser and Cloudflare. On Cloudflare, this technology is called <a href=\"https:\/\/www.cloudflare.com\/products\/cloudflare-workers\/\">Cloudflare Workers<\/a>. Amazon has a similar solution called <a href=\"https:\/\/docs.aws.amazon.com\/AmazonCloudFront\/latest\/DeveloperGuide\/lambda-at-the-edge.html\">Lambda@Edge<\/a>.<\/p>\n<p>The idea is that when the HTTP request for content comes in, a script running in the edge \u201crewrites\u201d any cookies in the HTTP header with <code>Set-Cookie<\/code>, thus avoiding ITP 2.1 limitations.<\/p>\n<p>Dustin Recko tackled this in a <a href=\"https:\/\/omr.ruhr\/google-analytics-itp-2-1-prevention-http-set-cookie-snippet-182092779d40\">recent article on ITP 2.1<\/a>.<\/p>\n<p>Since many sites are already leveraging Cloudflare or Amazon\u2019s Cloudfront, this would be a fairly lightweight way to handle the cookie routing.<\/p>\n<h3 id=\"shared-web-service-referenced-with-a-cname-record\">Shared web service referenced with a CNAME record<\/h3>\n<blockquote>\n<p>Thanks to <a href=\"https:\/\/www.linkedin.com\/in\/larsgundersen\/\">Lars Gundersen<\/a> for nudging me about this idea.<\/p>\n<\/blockquote>\n<p>This is super interesting. Apparently, it\u2019s what <a href=\"https:\/\/helpx.adobe.com\/analytics\/kb\/adobe-analytics-anditp.html\">Adobe<\/a> has already been doing for a while.<\/p>\n<p>The logic is similar to the <a href=\"#set-cookie-headers-in-a-server-side-script\">server-side cookie router<\/a> and\/or the <a href=\"#localstorage-for-cross-subdomain-browsing\"><code>localStorage<\/code> store<\/a> described above. But instead of hosting the content on your own domain, you would create the endpoint in a virtual machine running in the Google Cloud, for example, and then create a <a href=\"https:\/\/en.wikipedia.org\/wiki\/CNAME_record\">CNAME record<\/a> in your DNS to point <code>tracker.mydomain.com<\/code> to that virtual machine.<\/p>\n<p>Then, when a page is loaded, the first thing it does is call this endpoint (<code>tracker.mydomain.com<\/code>). The endpoint would grab the cookies from the HTTP headers and use <code>Set-Cookie<\/code> in the response to write the cookies on <code>mydomain.com<\/code>, thus avoiding the ITP 2.1 ban on client-side cookies.<\/p>\n<p>Alternatively, you could have <code>tracker.html<\/code> running in the service, and you\u2019d load <code>tracker.mydomain.com\/tracker.html<\/code> in an iframe, and use that as the <code>localStorage<\/code> store.<\/p>\n<p>Naturally, this incurs some extra costs, because you would have to route a lot of calls to this cloud endpoint to make sure all the relevant cookies get their expiration extended. This is why, I guess, Adobe offers it as a service.<\/p>\n<p>I\u2019d be curious to see if this is something Google is looking at, too. Seems like it would be a somewhat elegant way to handle the problem with the <code>_ga<\/code> cookie. They could, for example, have <code>analytics.js<\/code> fetched from this CNAME redirected domain, but in addition to serving the library, it would also write the <code>_ga<\/code> cookie in a <code>Set-Cookie<\/code> response.<\/p>\n<p>I verified with <a href=\"https:\/\/twitter.com\/johnwilander\">John Wilander<\/a> (a WebKit engineer working on ITP) that this should be OK.<\/p>\n<p>The <strong>problem<\/strong> here is that it adds quite a bit of overhead to the web service providing the response. For one, it would need to do the calculations for the <code>_ga<\/code> ID, but also it would need to process the request and customize the response. It\u2019s possible that even if Google did do this, they wouldn\u2019t offer it as part of the <strong>free Google Analytics<\/strong> service.<\/p>\n<h3 id=\"reverse-proxy-to-third-party-service\">Reverse proxy to third party service<\/h3>\n<blockquote>\n<p>Thanks to <a href=\"https:\/\/www.linkedin.com\/in\/plamenarnaudov\/\">Plamen Arnaudov<\/a> for this suggestion.<\/p>\n<\/blockquote>\n<p>A <a href=\"https:\/\/www.cloudflare.com\/learning\/cdn\/glossary\/reverse-proxy\/\">reverse proxy<\/a> stands in front of web servers, masking their existence when the user requests for resources from a site. For example, a reverse proxy could forward a request for content to <a href=\"https:\/\/www.google.com\/,\">https:\/\/www.google.com\/,<\/a> but show <a href=\"https:\/\/www.simoahava.com\/search-page\">https:\/\/www.simoahava.com\/search-page<\/a> in the URL.<\/p>\n<p>It\u2019s thus conceptually similar to the CNAME redirect, where the user is exploring content in their own domain namespace but the content is delivered from another origin.<\/p>\n<div style=\"aspect-ratio: 1468 \/ 720;\" class=\"figure \">\n<p>    <a href=\"https:\/\/www.simoahava.com\/images\/2019\/02\/reverse-proxy-cloudflare.jpg\" title=\"From https:\/\/www.cloudflare.com\/learning\/cdn\/glossary\/reverse-proxy\/\"><\/p>\n<p>    <img decoding=\"async\" class=\"fig-img\" height=\"720\" width=\"1468\" loading=\"lazy\" src=\"https:\/\/www.simoahava.com\/images\/2019\/02\/reverse-proxy-cloudflare.jpg#ZgotmplZ\" alt=\"From https:\/\/www.cloudflare.com\/learning\/cdn\/glossary\/reverse-proxy\/\"\/><\/p>\n<p>    <\/a><\/p>\n<p>    <span class=\"caption\">From https:\/\/www.cloudflare.com\/learning\/cdn\/glossary\/reverse-proxy\/<\/span><\/p>\n<\/div>\n<p>With the reverse proxy, a service like Google Analytics should need to modify some endpoint to either rewrite the <code>_ga<\/code> cookie in the request header with a <code>Set-Cookie<\/code> response, or create the cookie upon the request and serve it with the HTTP header. It might make sense, for example, to have the request for <code>analytics.js<\/code> respond with the <code>Set-Cookie<\/code> header.<\/p>\n<p>We have the same limitations here as with the CNAME option above. Google would need to process the request before returning the header. Considering how many millions of requests to their services are made daily, any type of extra processing they\u2019d need to do would reflect in costs.<\/p>\n<p>The main difference between this and the CNAME option is that a reverse proxy requires the site owners and developers to add the server-side logic for the proxy, whereas CNAME requires just a DNS record change.<\/p>\n<p>Setting up a reverse proxy isn\u2019t complicated at all &#8211; with a Node.js server, you could do it with <code>http-proxy-middleware<\/code> in just a couple of lines:<\/p>\n<div class=\"highlight\">\n<pre style=\"background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-javascript\" data-lang=\"javascript\"><span style=\"color:#00a\">const<\/span> proxy = require(<span style=\"color:#a50\">'http-proxy-middleware'<\/span>);\n\n...\n\napp.get(<span style=\"color:#a50\">'\/analytics.js'<\/span>, proxy({\n  target: <span style=\"color:#a50\">'https:\/\/www.google-analytics.com\/'<\/span>,\n  changeOrigin: <span style=\"color:#00a\">true<\/span>\n});\n<\/code><\/pre>\n<\/div>\n<p>This would essentially perform the GET request for the <strong>analytics.js<\/strong> library on Google\u2019s domain, but serve its content under your domain\u2019s <code>\/analytics.js<\/code> URL. If Google plays along, it could serve as a valid solution especially in cases where you can\u2019t add any more CNAME records to your DNS.<\/p>\n<h3 id=\"server-side-analytics\">Server-side analytics<\/h3>\n<p>There have been an array of suggestions using terms like <strong>server-side analytics<\/strong>, but I think these have misunderstood what ITP 2.1 does.<\/p>\n<p>If there is no authentication against a backend, the user\u2019s browser is identified with an anonymous identifier set in a cookie. No matter what type of analytics server is used &#8211; third-party or first-party &#8211; it\u2019s this browser cookie that tells the service the user is the same as the one who visited some other page 5 minutes ago.<\/p>\n<p>Changing the endpoint from <code>www.google-analytics.com\/collect<\/code> to <code>proxy.simoahava.com\/collect<\/code>, or changing from <code>www.google-analytics.com\/collect<\/code> to <code>snowplow.simoahava.com\/track<\/code> doesn\u2019t change this dynamic. There\u2019s no way to really align requests sent from a browser with requests sent by the same browser without having this identifier binding those two together somehow.<\/p>\n<p>And if that cookie is set with <code>document.cookie<\/code>, it will fall under the 7-day expiration schedule, meaning two requests spread more than 7 days apart will not be able to enjoy the same cookie value any more.<\/p>\n<p>The only way any server-side solution would work is if cookies were set with HTTP requests instead of with <code>document.cookie<\/code>. At this point, it might be easier to setup a <a href=\"#set-cookie-headers-in-a-server-side-script\">cookie router<\/a>, a <a href=\"#shared-web-service-referenced-with-a-cname-record\">CNAME<\/a> redirect, or with a <a href=\"#reverse-proxy-to-third-party-service\">reverse proxy<\/a>.<\/p>\n<h2 id=\"final-thoughts\">Final thoughts<\/h2>\n<p>I have many thoughts on this topic. Many, indeed. So let me list them here:<\/p>\n<h3 id=\"this-is-a-good-thing\">This is a good thing\u2026<\/h3>\n<p>I think anything that questions the validity of browser cookies is a welcome disruption.<\/p>\n<p>It\u2019s not just those set with <code>document.cookie<\/code>. An overwhelming amount of sensitive information is still stored in cookies that do not use the <code>HttpOnly<\/code> and <code>Secure<\/code> attributes. See <a href=\"https:\/\/github.com\/mikewest\/http-state-tokens\">this piece by Mike West from Google<\/a> if you want to be depressed (and there\u2019s some cool suggestions for going forward, too).<\/p>\n<p>Anything that makes life even more difficult for AdTech gets a solid thumbs up from me. Turning third-party data leeching into a consent-based prompt via Storage Access API is a great way to give users the reins.<\/p>\n<p>For sites with authentication, this makes it even more prudent to incentivize a login. If a user logs in to the service, no cookies are even needed, since a persistent User ID could be used as the user\u2019s identifier sent to analytics platforms (as long as the user\u2019s consent for this is requested and as long as the tracking only happens when they\u2019re logged in).<\/p>\n<h3 id=\"but-perhaps-the-baby-was-thrown-out-with-the-bathwater\">\u2026but perhaps the baby was thrown out with the bathwater<\/h3>\n<p>I can\u2019t help thinking that <em>benevolent<\/em> first-party analytics gets hit with collateral damage here.<\/p>\n<p>I had some exchanges in Twitter with the WebKit engineer working on ITP (<a href=\"https:\/\/twitter.com\/johnwilander\">John Wilander<\/a>), and this is one of the things he responded to me with.<\/p>\n<p>I totally agree with cross-site tracking being <em>generally<\/em> bad. But there are ways to do web analytics without malicious intent. It would have made more sense to prevent cookies from being accessed outside the domain they were set on, thus forcing sites to append linker parameters to internal links, but expiring all <code>document.cookie<\/code> setters with a 7-day cap is pretty drastic.<\/p>\n<p>Having Google Analytics running on your site does not make you a bad person &#8211; you are using it to optimize the performance and usability of your site, and for creating better landing pages and marketing campaigns. But now that the cookie is capped at 7 days, you basically lose all ability to build cohorts of users who only visit your site once a month, for example, unless you invest in setting HTTP cookies instead.<\/p>\n<p>John\u2019s later tweet clarified this.<\/p>\n<p>So it does seem that ITP is taking blanket measures to best target the bad actors in the industry.<\/p>\n<h3 id=\"and-this-is-definitely-not-the-last-of-it\">And this is definitely not the last of it<\/h3>\n<p>It\u2019s not just ITP. Firefox will <a href=\"https:\/\/www.fastcompany.com\/90299092\/mozilla-most-innovative-companies-2019\">also start blocking cross-site third-party trackers<\/a>, no doubt taking a leaf out of ITP\u2019s book. They\u2019ve already had a very <a href=\"https:\/\/support.mozilla.org\/en-US\/kb\/content-blocking\">strong-armed approach<\/a> to blocking trackers (though in private browsing mode only, for now), so this doesn\u2019t come as a big surprise.<\/p>\n<blockquote>\n<p><strong>Update 8 March 2019<\/strong>: Firefox announced they will start experimenting with the <a href=\"https:\/\/groups.google.com\/forum\/m\/#!topic\/mozilla.dev.platform\/lECBPeiGTy4\">7-day expiration of JavaScript cookies, too<\/a>.<\/p>\n<\/blockquote>\n<p>As workarounds for ITP are invented, new iterations of ITP will be introduced. Until now, each iteration has made ITP stricter. Even if they made a concession (Storage Access API without user prompt), they seem to not hesitate to pull it back in a later version.<\/p>\n<p>I don\u2019t think they\u2019ll let go of the 7-day cookie expiration for <code>document.cookie<\/code>. If anything, they\u2019ll reduce it to just one day, or only allow session cookies. Or, perhaps eradicate JavaScript cookies altogethe. In any case, it\u2019s up to vendors and their enterprise clients (since money talks) to consider how big a deal this is, and how to solve this going forward.<\/p>\n<p>Adobe has already introduced their own ways of <a href=\"https:\/\/helpx.adobe.com\/analytics\/kb\/adobe-analytics-anditp.html\">tackling ITP<\/a> but with the ban on <code>document.cookie<\/code> it remains to be see how they\u2019ll react to ITP 2.1.<\/p>\n<p>Vendors might be hesitant to take action because nothing is future-proofed. Any solution that caters to web analytics users could be misappropriated for cross-site tracking purposes, and at that point a future iteration of ITP would most certainly neuter it. And if that happens, the analytics vendor who encouraged enterprise clients to spend thousands of dollars in implementing the solution will now be blamed for not having enough foresight.<\/p>\n<h3 id=\"going-forward\">Going forward<\/h3>\n<p>To sum up this article, here are the key takeaways:<\/p>\n<ol>\n<li>\n<p>If you are tracking just root domains, and do not need cross-subdomain tracking to work without explicit linker parameters, you can use the <a href=\"https:\/\/www.simoahava.com\/analytics\/use-localstorage-client-id-persistence-google-analytics\/\"><code>localStorage<\/code> workaround<\/a>. See <a href=\"#localstorage-isn-t-a-solid-workaround\">this chapter<\/a> for more information.<\/p>\n<\/li>\n<li>\n<p>If you think Safari has a big enough share of your traffic to seriously impact your data quality, you should look into writing the <code>_ga<\/code> cookie using HTTP cookies. See <a href=\"#set-cookie-headers-in-a-server-side-script\">here<\/a> for a recap.<\/p>\n<\/li>\n<li>\n<p>Follow <a href=\"https:\/\/twitter.com\/johnwilander\">John Wilander<\/a> on Twitter.<\/p>\n<\/li>\n<li>\n<p>Await an official announcement from Google (or whatever your favorite analytics vendor is) on how they intend to tackle ITP\u2019s stranglehold on cookies written with <code>document.cookie<\/code>.<\/p>\n<\/li>\n<\/ol>\n<p>For a geeky tech dude like me, we\u2019re living in some pretty cool and exciting times. Having to figure out workarounds for this concentrated attack on cookies by (most) web browsers is inspiring!<\/p>\n<\/p><\/div>\n<p><script async src=\"\/\/platform.twitter.com\/widgets.js\" charset=\"utf-8\"><\/script><br \/>\n<br \/><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Updated 1 October 2019 With ITP 2.3 it looks like Safari is reducing the usefulness of localStorage as well, so using that as an alternative fix to persistence issues should not be considered future-proof. this solution should not be considered future-proof. Updated 12 March 2019 with some minor clarifications.. On 21st February 2019, WebKit announced [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":100427,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12033],"tags":[12765,39474,5393],"dealstore":[],"offerexpiration":[],"class_list":["post-100426","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-analytics","tag-analytics","tag-itp","tag-web"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>ITP 2.1 And Web Analytics - Som2ny Network<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/fivemor.com\/?p=100426\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"ITP 2.1 And Web Analytics - Som2ny Network\" \/>\n<meta property=\"og:description\" content=\"Updated 1 October 2019 With ITP 2.3 it looks like Safari is reducing the usefulness of localStorage as well, so using that as an alternative fix to persistence issues should not be considered future-proof. this solution should not be considered future-proof. Updated 12 March 2019 with some minor clarifications.. On 21st February 2019, WebKit announced [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/fivemor.com\/?p=100426\" \/>\n<meta property=\"og:site_name\" content=\"Som2ny Network\" \/>\n<meta property=\"article:published_time\" content=\"2025-02-21T00:18:52+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/02\/itp-2-1-web-analytics-scaled.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"2560\" \/>\n\t<meta property=\"og:image:height\" content=\"1782\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"admin\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"admin\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"25 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/fivemor.com\/?p=100426#article\",\"isPartOf\":{\"@id\":\"https:\/\/fivemor.com\/?p=100426\"},\"author\":{\"name\":\"admin\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371\"},\"headline\":\"ITP 2.1 And Web Analytics\",\"datePublished\":\"2025-02-21T00:18:52+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/fivemor.com\/?p=100426\"},\"wordCount\":4472,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/fivemor.com\/#organization\"},\"image\":{\"@id\":\"https:\/\/fivemor.com\/?p=100426#primaryimage\"},\"thumbnailUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/02\/itp-2-1-web-analytics-scaled.jpg\",\"keywords\":[\"Analytics\",\"ITP\",\"Web\"],\"articleSection\":[\"Analytics\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/fivemor.com\/?p=100426#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/fivemor.com\/?p=100426\",\"url\":\"https:\/\/fivemor.com\/?p=100426\",\"name\":\"ITP 2.1 And Web Analytics - Som2ny Network\",\"isPartOf\":{\"@id\":\"https:\/\/fivemor.com\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/fivemor.com\/?p=100426#primaryimage\"},\"image\":{\"@id\":\"https:\/\/fivemor.com\/?p=100426#primaryimage\"},\"thumbnailUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/02\/itp-2-1-web-analytics-scaled.jpg\",\"datePublished\":\"2025-02-21T00:18:52+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/fivemor.com\/?p=100426#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/fivemor.com\/?p=100426\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/fivemor.com\/?p=100426#primaryimage\",\"url\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/02\/itp-2-1-web-analytics-scaled.jpg\",\"contentUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/02\/itp-2-1-web-analytics-scaled.jpg\",\"width\":2560,\"height\":1782},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/fivemor.com\/?p=100426#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/fivemor.com\/?bp_activities=1\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"ITP 2.1 And Web Analytics\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/fivemor.com\/#website\",\"url\":\"https:\/\/fivemor.com\/\",\"name\":\"Som2ny Network\",\"description\":\"Daily Deals\",\"publisher\":{\"@id\":\"https:\/\/fivemor.com\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/fivemor.com\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/fivemor.com\/#organization\",\"name\":\"Som2ny Network\",\"url\":\"https:\/\/fivemor.com\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png\",\"contentUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png\",\"width\":300,\"height\":86,\"caption\":\"Som2ny Network\"},\"image\":{\"@id\":\"https:\/\/fivemor.com\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371\",\"name\":\"admin\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png\",\"caption\":\"admin\"},\"sameAs\":[\"https:\/\/fivemor.com\"],\"url\":\"https:\/\/fivemor.com\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"ITP 2.1 And Web Analytics - Som2ny Network","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/fivemor.com\/?p=100426","og_locale":"en_US","og_type":"article","og_title":"ITP 2.1 And Web Analytics - Som2ny Network","og_description":"Updated 1 October 2019 With ITP 2.3 it looks like Safari is reducing the usefulness of localStorage as well, so using that as an alternative fix to persistence issues should not be considered future-proof. this solution should not be considered future-proof. Updated 12 March 2019 with some minor clarifications.. On 21st February 2019, WebKit announced [&hellip;]","og_url":"https:\/\/fivemor.com\/?p=100426","og_site_name":"Som2ny Network","article_published_time":"2025-02-21T00:18:52+00:00","og_image":[{"width":2560,"height":1782,"url":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/02\/itp-2-1-web-analytics-scaled.jpg","type":"image\/jpeg"}],"author":"admin","twitter_card":"summary_large_image","twitter_misc":{"Written by":"admin","Est. reading time":"25 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/fivemor.com\/?p=100426#article","isPartOf":{"@id":"https:\/\/fivemor.com\/?p=100426"},"author":{"name":"admin","@id":"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371"},"headline":"ITP 2.1 And Web Analytics","datePublished":"2025-02-21T00:18:52+00:00","mainEntityOfPage":{"@id":"https:\/\/fivemor.com\/?p=100426"},"wordCount":4472,"commentCount":0,"publisher":{"@id":"https:\/\/fivemor.com\/#organization"},"image":{"@id":"https:\/\/fivemor.com\/?p=100426#primaryimage"},"thumbnailUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/02\/itp-2-1-web-analytics-scaled.jpg","keywords":["Analytics","ITP","Web"],"articleSection":["Analytics"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/fivemor.com\/?p=100426#respond"]}]},{"@type":"WebPage","@id":"https:\/\/fivemor.com\/?p=100426","url":"https:\/\/fivemor.com\/?p=100426","name":"ITP 2.1 And Web Analytics - Som2ny Network","isPartOf":{"@id":"https:\/\/fivemor.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/fivemor.com\/?p=100426#primaryimage"},"image":{"@id":"https:\/\/fivemor.com\/?p=100426#primaryimage"},"thumbnailUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/02\/itp-2-1-web-analytics-scaled.jpg","datePublished":"2025-02-21T00:18:52+00:00","breadcrumb":{"@id":"https:\/\/fivemor.com\/?p=100426#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/fivemor.com\/?p=100426"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/fivemor.com\/?p=100426#primaryimage","url":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/02\/itp-2-1-web-analytics-scaled.jpg","contentUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/02\/itp-2-1-web-analytics-scaled.jpg","width":2560,"height":1782},{"@type":"BreadcrumbList","@id":"https:\/\/fivemor.com\/?p=100426#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/fivemor.com\/?bp_activities=1"},{"@type":"ListItem","position":2,"name":"ITP 2.1 And Web Analytics"}]},{"@type":"WebSite","@id":"https:\/\/fivemor.com\/#website","url":"https:\/\/fivemor.com\/","name":"Som2ny Network","description":"Daily Deals","publisher":{"@id":"https:\/\/fivemor.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/fivemor.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/fivemor.com\/#organization","name":"Som2ny Network","url":"https:\/\/fivemor.com\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/fivemor.com\/#\/schema\/logo\/image\/","url":"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png","contentUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png","width":300,"height":86,"caption":"Som2ny Network"},"image":{"@id":"https:\/\/fivemor.com\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371","name":"admin","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/fivemor.com\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png","caption":"admin"},"sameAs":["https:\/\/fivemor.com"],"url":"https:\/\/fivemor.com\/?author=1"}]}},"_links":{"self":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/posts\/100426","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=100426"}],"version-history":[{"count":0,"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/posts\/100426\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/media\/100427"}],"wp:attachment":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=100426"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=100426"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=100426"},{"taxonomy":"dealstore","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fdealstore&post=100426"},{"taxonomy":"offerexpiration","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fofferexpiration&post=100426"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}