Skip to content

Commit

Permalink
adopt cookieless ID on dashboard and server
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobhero authored and roboquat committed Dec 12, 2022
1 parent 5e0c93c commit 96a7836
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 15 deletions.
10 changes: 9 additions & 1 deletion components/dashboard/src/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,15 @@ export const identifyUser = async (traits: Traits) => {
});
};

const getAnonymousId = (): string => {
const getCookieConsent = () => {
return Cookies.get("gp-analytical") === "true";
};

const getAnonymousId = (): string | undefined => {
if (!getCookieConsent()) {
//we do not want to read or set the id cookie if we don't have consent
return;
}
let anonymousId = Cookies.get("ajs_anonymous_id");
if (anonymousId) {
return anonymousId.replace(/^"(.+(?="$))"$/, "$1"); //strip enclosing double quotes before returning
Expand Down
44 changes: 36 additions & 8 deletions components/server/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { User } from "@gitpod/gitpod-protocol";
import { Request } from "express";
import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
import { SubscriptionService } from "@gitpod/gitpod-payment-endpoint/lib/accounting";
import * as crypto from "crypto";

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

// track the login
analytics.track({
userId: user.id,
anonymousId: stripCookie(request.cookies.ajs_anonymous_id),
anonymousId: getAnonymousId(request) || createCookielessId(ip, ua),
event: "login",
properties: {
loginContext: authHost,
Expand All @@ -32,11 +35,13 @@ export async function trackLogin(
export async function trackSignup(user: User, request: Request, analytics: IAnalyticsWriter) {
// make new complete identify call for each signup
await fullIdentify(user, request, analytics);
const ip = request.ips[0];
const ua = request.headers["user-agent"];

// track the signup
analytics.track({
userId: user.id,
anonymousId: stripCookie(request.cookies.ajs_anonymous_id),
anonymousId: getAnonymousId(request) || createCookielessId(ip, ua),
event: "signup",
properties: {
auth_provider: user.identities[0].authProviderId,
Expand All @@ -46,6 +51,26 @@ export async function trackSignup(user: User, request: Request, analytics: IAnal
});
}

export function createCookielessId(ip?: string, ua?: string): string | number | undefined {
if (!ip || !ua) {
return "unidentified-user"; //use placeholder if we cannot resolve IP and user agent
}
const date = new Date();
const today = `${date.getDate()}/${date.getMonth()}/${date.getFullYear()}`;
return crypto
.createHash("sha512")
.update(ip + ua + today)
.digest("hex");
}

export function maskIp(ip?: string) {
if (!ip) {
return;
}
const octets = ip.split(".");
return octets?.length == 4 ? octets.slice(0, 3).concat(["0"]).join(".") : undefined;
}

async function fullIdentify(
user: User,
request: Request,
Expand All @@ -54,18 +79,19 @@ async function fullIdentify(
) {
// makes a full identify call for authenticated users
const coords = request.get("x-glb-client-city-lat-long")?.split(", ");
const ip = request.get("x-forwarded-for")?.split(",")[0];
const ip = request.ips[0];
const ua = request.headers["user-agent"];
var subscriptionIDs: string[] = [];
const subscriptions = await subscriptionService?.getNotYetCancelledSubscriptions(user, new Date().toISOString());
if (subscriptions) {
subscriptionIDs = subscriptions.filter((sub) => !!sub.planId).map((sub) => sub.planId!);
}
analytics.identify({
anonymousId: stripCookie(request.cookies.ajs_anonymous_id),
anonymousId: getAnonymousId(request) || createCookielessId(ip, ua),
userId: user.id,
context: {
ip: ip ? maskIp(ip) : undefined,
userAgent: request.get("User-Agent"),
userAgent: ua,
location: {
city: request.get("x-glb-client-city"),
country: request.get("x-glb-client-region"),
Expand All @@ -87,9 +113,11 @@ async function fullIdentify(
});
}

function maskIp(ip: string) {
const octets = ip.split(".");
return octets?.length == 4 ? octets.slice(0, 3).concat(["0"]).join(".") : undefined;
function getAnonymousId(request: Request) {
if (!(request.cookies["gp-analytical"] === "true")) {
return;
}
return stripCookie(request.cookies.ajs_anonymous_id);
}

function resolveIdentities(user: User) {
Expand Down
15 changes: 9 additions & 6 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
import * as grpc from "@grpc/grpc-js";
import { CachingBlobServiceClientProvider } from "../util/content-service-sugar";
import { CostCenterJSON } from "@gitpod/gitpod-protocol/lib/usage";
import { createCookielessId, maskIp } from "../analytics";

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

const userId = this.user?.id;
const anonymousId = event.anonymousId;
const { ip, userAgent } = this.clientHeaderFields;
const anonymousId = event.anonymousId || createCookielessId(ip, userAgent);
const msg = {
event: event.event,
messageId: event.messageId,
Expand All @@ -2893,7 +2895,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {

public async trackLocation(ctx: TraceContext, event: RemotePageMessage): Promise<void> {
const userId = this.user?.id;
const anonymousId = event.anonymousId;
const { ip, userAgent } = this.clientHeaderFields;
const anonymousId = event.anonymousId || createCookielessId(ip, userAgent);
let msg = {
messageId: event.messageId,
context: {},
Expand All @@ -2903,8 +2906,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
//only page if at least one identifier is known
if (userId) {
msg.context = {
ip: this.clientHeaderFields?.ip,
userAgent: this.clientHeaderFields?.userAgent,
ip: maskIp(ip),
userAgent: userAgent,
};
this.analytics.page({
userId: userId,
Expand All @@ -2924,10 +2927,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {

//Identify calls collect user informmation. If the user is unknown, we don't make a call (privacy preservation)
const user = this.checkUser("identifyUser");

const { ip, userAgent } = this.clientHeaderFields;
const identifyMessage: IdentifyMessage = {
userId: user.id,
anonymousId: event.anonymousId,
anonymousId: event.anonymousId || createCookielessId(ip, userAgent),
traits: event.traits,
context: event.context,
};
Expand Down

0 comments on commit 96a7836

Please sign in to comment.