Skip to content

Commit 96a7836

Browse files
jakobheroroboquat
authored andcommitted
adopt cookieless ID on dashboard and server
1 parent 5e0c93c commit 96a7836

File tree

3 files changed

+54
-15
lines changed

3 files changed

+54
-15
lines changed

components/dashboard/src/Analytics.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,15 @@ export const identifyUser = async (traits: Traits) => {
173173
});
174174
};
175175

176-
const getAnonymousId = (): string => {
176+
const getCookieConsent = () => {
177+
return Cookies.get("gp-analytical") === "true";
178+
};
179+
180+
const getAnonymousId = (): string | undefined => {
181+
if (!getCookieConsent()) {
182+
//we do not want to read or set the id cookie if we don't have consent
183+
return;
184+
}
177185
let anonymousId = Cookies.get("ajs_anonymous_id");
178186
if (anonymousId) {
179187
return anonymousId.replace(/^"(.+(?="$))"$/, "$1"); //strip enclosing double quotes before returning

components/server/src/analytics.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { User } from "@gitpod/gitpod-protocol";
77
import { Request } from "express";
88
import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
99
import { SubscriptionService } from "@gitpod/gitpod-payment-endpoint/lib/accounting";
10+
import * as crypto from "crypto";
1011

1112
export async function trackLogin(
1213
user: User,
@@ -17,11 +18,13 @@ export async function trackLogin(
1718
) {
1819
// make new complete identify call for each login
1920
await fullIdentify(user, request, analytics, subscriptionService);
21+
const ip = request.ips[0];
22+
const ua = request.headers["user-agent"];
2023

2124
// track the login
2225
analytics.track({
2326
userId: user.id,
24-
anonymousId: stripCookie(request.cookies.ajs_anonymous_id),
27+
anonymousId: getAnonymousId(request) || createCookielessId(ip, ua),
2528
event: "login",
2629
properties: {
2730
loginContext: authHost,
@@ -32,11 +35,13 @@ export async function trackLogin(
3235
export async function trackSignup(user: User, request: Request, analytics: IAnalyticsWriter) {
3336
// make new complete identify call for each signup
3437
await fullIdentify(user, request, analytics);
38+
const ip = request.ips[0];
39+
const ua = request.headers["user-agent"];
3540

3641
// track the signup
3742
analytics.track({
3843
userId: user.id,
39-
anonymousId: stripCookie(request.cookies.ajs_anonymous_id),
44+
anonymousId: getAnonymousId(request) || createCookielessId(ip, ua),
4045
event: "signup",
4146
properties: {
4247
auth_provider: user.identities[0].authProviderId,
@@ -46,6 +51,26 @@ export async function trackSignup(user: User, request: Request, analytics: IAnal
4651
});
4752
}
4853

54+
export function createCookielessId(ip?: string, ua?: string): string | number | undefined {
55+
if (!ip || !ua) {
56+
return "unidentified-user"; //use placeholder if we cannot resolve IP and user agent
57+
}
58+
const date = new Date();
59+
const today = `${date.getDate()}/${date.getMonth()}/${date.getFullYear()}`;
60+
return crypto
61+
.createHash("sha512")
62+
.update(ip + ua + today)
63+
.digest("hex");
64+
}
65+
66+
export function maskIp(ip?: string) {
67+
if (!ip) {
68+
return;
69+
}
70+
const octets = ip.split(".");
71+
return octets?.length == 4 ? octets.slice(0, 3).concat(["0"]).join(".") : undefined;
72+
}
73+
4974
async function fullIdentify(
5075
user: User,
5176
request: Request,
@@ -54,18 +79,19 @@ async function fullIdentify(
5479
) {
5580
// makes a full identify call for authenticated users
5681
const coords = request.get("x-glb-client-city-lat-long")?.split(", ");
57-
const ip = request.get("x-forwarded-for")?.split(",")[0];
82+
const ip = request.ips[0];
83+
const ua = request.headers["user-agent"];
5884
var subscriptionIDs: string[] = [];
5985
const subscriptions = await subscriptionService?.getNotYetCancelledSubscriptions(user, new Date().toISOString());
6086
if (subscriptions) {
6187
subscriptionIDs = subscriptions.filter((sub) => !!sub.planId).map((sub) => sub.planId!);
6288
}
6389
analytics.identify({
64-
anonymousId: stripCookie(request.cookies.ajs_anonymous_id),
90+
anonymousId: getAnonymousId(request) || createCookielessId(ip, ua),
6591
userId: user.id,
6692
context: {
6793
ip: ip ? maskIp(ip) : undefined,
68-
userAgent: request.get("User-Agent"),
94+
userAgent: ua,
6995
location: {
7096
city: request.get("x-glb-client-city"),
7197
country: request.get("x-glb-client-region"),
@@ -87,9 +113,11 @@ async function fullIdentify(
87113
});
88114
}
89115

90-
function maskIp(ip: string) {
91-
const octets = ip.split(".");
92-
return octets?.length == 4 ? octets.slice(0, 3).concat(["0"]).join(".") : undefined;
116+
function getAnonymousId(request: Request) {
117+
if (!(request.cookies["gp-analytical"] === "true")) {
118+
return;
119+
}
120+
return stripCookie(request.cookies.ajs_anonymous_id);
93121
}
94122

95123
function resolveIdentities(user: User) {

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
181181
import * as grpc from "@grpc/grpc-js";
182182
import { CachingBlobServiceClientProvider } from "../util/content-service-sugar";
183183
import { CostCenterJSON } from "@gitpod/gitpod-protocol/lib/usage";
184+
import { createCookielessId, maskIp } from "../analytics";
184185

185186
// shortcut
186187
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
@@ -2868,7 +2869,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
28682869
// handles potentially broken or malicious input, we better err on the side of caution.
28692870

28702871
const userId = this.user?.id;
2871-
const anonymousId = event.anonymousId;
2872+
const { ip, userAgent } = this.clientHeaderFields;
2873+
const anonymousId = event.anonymousId || createCookielessId(ip, userAgent);
28722874
const msg = {
28732875
event: event.event,
28742876
messageId: event.messageId,
@@ -2893,7 +2895,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
28932895

28942896
public async trackLocation(ctx: TraceContext, event: RemotePageMessage): Promise<void> {
28952897
const userId = this.user?.id;
2896-
const anonymousId = event.anonymousId;
2898+
const { ip, userAgent } = this.clientHeaderFields;
2899+
const anonymousId = event.anonymousId || createCookielessId(ip, userAgent);
28972900
let msg = {
28982901
messageId: event.messageId,
28992902
context: {},
@@ -2903,8 +2906,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
29032906
//only page if at least one identifier is known
29042907
if (userId) {
29052908
msg.context = {
2906-
ip: this.clientHeaderFields?.ip,
2907-
userAgent: this.clientHeaderFields?.userAgent,
2909+
ip: maskIp(ip),
2910+
userAgent: userAgent,
29082911
};
29092912
this.analytics.page({
29102913
userId: userId,
@@ -2924,10 +2927,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
29242927

29252928
//Identify calls collect user informmation. If the user is unknown, we don't make a call (privacy preservation)
29262929
const user = this.checkUser("identifyUser");
2927-
2930+
const { ip, userAgent } = this.clientHeaderFields;
29282931
const identifyMessage: IdentifyMessage = {
29292932
userId: user.id,
2930-
anonymousId: event.anonymousId,
2933+
anonymousId: event.anonymousId || createCookielessId(ip, userAgent),
29312934
traits: event.traits,
29322935
context: event.context,
29332936
};

0 commit comments

Comments
 (0)