ITP Mitigation
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
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:
- 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)
- Respond to
GETHTTP method. - Read domain parameter from request query string.
- Read
__eventn_idor__eventn_id_srvrHTTP request cookie as a source of anonymousId value - If it couldn't acquire value for anonymousId from either of these cookies, generate a new one (UUID string)
- Add
__eventn_idand__eventn_id_srvrcookies 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 - Send response with status
200and 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.
- Read
__eventn_uidor__eventn_uid_srvrHTTP request cookie as a source of userId value - If it couldn't acquire value for userId skip the rest and do nothing
- Add
__eventn_uidand__eventn_uid_srvrcookies 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 - Change response with status
200and 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> const analytics = jitsuAnalytics({
host: "https://your-jitsu-domain.com",
// path or full URL of ID endpoint
idEndpoint: "/api/jitsu-id",
}); <JitsuProvider options={{host: "https://your-jitsu-domain.com", idEndpoint: "/api/jitsu-id"}}>
<ChildComponent/>
</JitsuProvider>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
});
}