ITP Mitigation

Info

Available since Jitsu v2.8.0 and npm packages v1.9.7

Safari Intelligent Tracking Prevention

Intelligent Tracking Prevention (ITP) is an automatic feature of the Safari web browser designed to limit user tracking.

ITP employs multiple measures to achieve this, one of which is the limited lifespan of first-party cookies

Info

In Safari browser first-party cookies will be deleted after 7 days without access.

Since Jitsu relies on first-party cookies to store anonymous user IDs, ITP may negatively affect the quality of data collected by Jitsu.

For example, if some user returns to a particular site after a pause of longer than 7 days, he will receive a new anonymous ID and may appear as a new user (at least unless identify is call is used).

Mitigation

Not all first-party cookies are subject to the ITP's 7-day expiry limit.

Cookies with the HttpOnly flag, served directly from the customer’s website server, will not be removed by ITP.

To enable ITP mitigation, customers need to add a simple service (endpoint) to their website that adheres to Jitsu’s specifications.

Server setup

Jitsu ID endpoint must be added to customer's website.

Endpoint requirements:

  1. ID endpoint must respond on the same domain as the website. (That can be a subdomain only if it resolves to the same IP address as the main domain)
  2. Respond to GET HTTP method.
  3. Read domain parameter from request query string.
  4. Read __eventn_id or __eventn_id_srvr HTTP request cookie as a source of anonymousId value
  5. If it couldn't acquire value for anonymousId from either of these cookies, generate a new one (UUID string)
  6. Add __eventn_id and __eventn_id_srvr cookies to "Set-Cookie" headers with the following parameters:
    Set-Cookie: __eventn_id=anonymousId; Domain=domain; Max-Age=157680000; Path=/; SameSite=None; Secure;
    Set-Cookie: __eventn_id_srvr=anonymousId; Domain=domain; Max-Age=157680000; Path=/; SameSite=None; Secure; httpOnly=true;
    where domain was acquired from query string and anonymousId was set previously
  7. Send response with status 200 and JSON payload: { "anonymousId": anonymousId }

Optional part:

It is also possible to protect the userId cookie from expiration, but a better approach is to identify the user on every new session.

  1. Read __eventn_uid or __eventn_uid_srvr HTTP request cookie as a source of userId value
  2. If it couldn't acquire value for userId skip the rest and do nothing
  3. Add __eventn_uid and __eventn_uid_srvr cookies to "Set-Cookie" headers with the following parameters:
    Set-Cookie: __eventn_uid=userId; Domain=domain; Max-Age=157680000; Path=/; SameSite=None; Secure;
    Set-Cookie: __eventn_uid=userId_srvr; Domain=domain; Max-Age=157680000; Path=/; SameSite=None; Secure; httpOnly=true;
    where domain was acquired from query string and userId was set previously
  4. Change response with status 200 and JSON payload: { "anonymousId": anonymousId, "userId": userId }

Client setup

Jitsu client library must be configured to use ID endpoint with idEndpoint parameter:

<script async src="https://your-jitsu-domain.com/p.js" data-id-endpoint="/api/jitsu-id"></script>

Server code example

Here is an example of ID endpoint implemented in Typescript language for Next.js 14 framework:

import {NextRequest, NextResponse} from 'next/server'
import {randomUUID} from "crypto";
 
const USER_COOKIE = "__eventn_uid";
const ANON_COOKIE = "__eventn_id";
 
function getDomain(request: NextRequest) {
   let domain = request.nextUrl.searchParams.get("domain");
   if (domain) {
      return domain;
   }
   domain = request.headers.get("host")?.toString() ?? "";
   if (domain.startsWith("localhost")) return "localhost";
   return domain;
}
 
function renewCookies(request: NextRequest, headers: Headers, browserName: string, serverName: string, generateNew: boolean) {
   const cookie = request.cookies.get(browserName) || request.cookies.get(serverName);
   let cookieValue = cookie?.value
   if (!cookieValue) {
      if (!generateNew) return;
      cookieValue = randomUUID();
   }
   const secure = request.headers.get("x-forwarded-proto") === "https" ? " Secure;" : ""
   const sameSite = ` SameSite=${secure ? "None" : "Lax"};`
   const maxAge = 31_536_000 * 5; // 5 years in seconds
   const domain = getDomain(request);
   headers.append("Set-Cookie", `${browserName}=${cookieValue}; Max-Age=${maxAge}; Domain=${domain}; Path=/;${sameSite}${secure}`)
   headers.append("Set-Cookie", `${serverName}=${cookieValue}; Max-Age=${maxAge}; Domain=${domain}; Path=/;${sameSite}${secure} httpOnly=true;`)
   return cookieValue;
}
 
export async function GET(request: NextRequest) {
   const headers = new Headers({
      'Cache-Control': "must-revalidate,no-cache,no-store",
      'Content-Type': "application/json"
   })
   const anonymousId = renewCookies(request, headers, ANON_COOKIE, `${ANON_COOKIE}_srvr`, true);
   const userId = renewCookies(request, headers, USER_COOKIE, `${USER_COOKIE}_srvr`, false);
   const payload = {
      anonymousId: anonymousId,
      userId: userId
   }
   return new NextResponse(JSON.stringify(payload), {
      status: 200,
      headers: headers
   });
}