diff --git a/components/dashboard/src/settings/Account.tsx b/components/dashboard/src/settings/Account.tsx index 235b3f9d9a9516..9ace37ed871319 100644 --- a/components/dashboard/src/settings/Account.tsx +++ b/components/dashboard/src/settings/Account.tsx @@ -20,7 +20,7 @@ export default function Account() { const [modal, setModal] = useState(false); const primaryEmail = User.getPrimaryEmail(user!) || ""; const [typedEmail, setTypedEmail] = useState(""); - const original = ProfileState.getProfileState(user!); + const original = User.getProfile(user!); const [profileState, setProfileState] = useState(original); const [errorMessage, setErrorMessage] = useState(""); const [updated, setUpdated] = useState(false); @@ -31,7 +31,7 @@ export default function Account() { if (error) { return; } - const updatedUser = ProfileState.setProfileState(user!, profileState); + const updatedUser = User.setProfile(user!, profileState); setUser(updatedUser); getGitpodService().server.updateLoggedInUser(updatedUser); setUpdated(true); diff --git a/components/dashboard/src/settings/ProfileInformation.tsx b/components/dashboard/src/settings/ProfileInformation.tsx index 2f2eadfc113e33..bfa010c3641cbd 100644 --- a/components/dashboard/src/settings/ProfileInformation.tsx +++ b/components/dashboard/src/settings/ProfileInformation.tsx @@ -15,48 +15,6 @@ import { UserContext } from "../user-context"; import { isGitpodIo } from "../utils"; export namespace ProfileState { - export interface ProfileState { - name: string; - email: string; - company?: string; - avatarURL?: string; - } - - export function getProfileState(user: User): ProfileState { - return { - name: User.getName(user!) || "", - email: User.getPrimaryEmail(user!) || "", - company: user?.additionalData?.profile?.companyName, - avatarURL: user?.avatarUrl, - }; - } - - export function setProfileState(user: User, profileState: ProfileState): User { - user.fullName = profileState.name; - user.avatarUrl = profileState.avatarURL; - - if (!user.additionalData) { - user.additionalData = {}; - } - if (!user.additionalData.profile) { - user.additionalData.profile = {}; - } - user.additionalData.profile.emailAddress = profileState.email; - user.additionalData.profile.companyName = profileState.company; - user.additionalData.profile.lastUpdatedDetailsNudge = new Date().toISOString(); - - return user; - } - - export function hasChanges(before: ProfileState, after: ProfileState) { - return ( - before.name !== after.name || - before.email !== after.email || - before.company !== after.company || - before.avatarURL !== after.avatarURL - ); - } - function shouldNudgeForUpdate(user: User): boolean { if (!isGitpodIo()) { return false; @@ -79,7 +37,7 @@ export namespace ProfileState { * @param state * @returns error message or empty string when valid */ - export function validate(state: ProfileState): string { + export function validate(state: User.Profile): string { if (state.name.trim() === "") { return "Name must not be empty."; } @@ -98,7 +56,7 @@ export namespace ProfileState { export function NudgeForProfileUpdateModal() { const { user, setUser } = useContext(UserContext); - const original = ProfileState.getProfileState(user!); + const original = User.getProfile(user!); const [profileState, setProfileState] = useState(original); const [errorMessage, setErrorMessage] = useState(""); const [visible, setVisible] = useState(shouldNudgeForUpdate(user!)); @@ -109,7 +67,7 @@ export namespace ProfileState { if (error) { return; } - const updatedUser = ProfileState.setProfileState(user!, profileState); + const updatedUser = User.setProfile(user!, profileState); setUser(updatedUser); getGitpodService().server.updateLoggedInUser(updatedUser); setVisible(shouldNudgeForUpdate(updatedUser!)); @@ -150,8 +108,8 @@ export namespace ProfileState { } export default function ProfileInformation(props: { - profileState: ProfileState.ProfileState; - setProfileState: (newState: ProfileState.ProfileState) => void; + profileState: User.Profile; + setProfileState: (newState: User.Profile) => void; errorMessage: string; updated: boolean; children?: React.ReactChild[] | React.ReactChild; diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 35d8800564e755..1197f70daad883 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -139,6 +139,50 @@ export namespace User { } user.additionalData.ideSettings = newIDESettings; } + + export function getProfile(user: User): Profile { + return { + name: User.getName(user!) || "", + email: User.getPrimaryEmail(user!) || "", + company: user?.additionalData?.profile?.companyName, + avatarURL: user?.avatarUrl, + }; + } + + export function setProfile(user: User, profile: Profile): User { + user.fullName = profile.name; + user.avatarUrl = profile.avatarURL; + + if (!user.additionalData) { + user.additionalData = {}; + } + if (!user.additionalData.profile) { + user.additionalData.profile = {}; + } + user.additionalData.profile.emailAddress = profile.email; + user.additionalData.profile.companyName = profile.company; + user.additionalData.profile.lastUpdatedDetailsNudge = new Date().toISOString(); + + return user; + } + + // The actual Profile of a User + export interface Profile { + name: string; + email: string; + company?: string; + avatarURL?: string; + } + export namespace Profile { + export function hasChanges(before: Profile, after: Profile) { + return ( + before.name !== after.name || + before.email !== after.email || + before.company !== after.company || + before.avatarURL !== after.avatarURL + ); + } + } } export interface AdditionalUserData { @@ -163,6 +207,7 @@ export interface AdditionalUserData { profile?: ProfileDetails; } +// The format in which we store User Profiles in export interface ProfileDetails { // when was the last time the user updated their profile information or has been nudged to do so. lastUpdatedDetailsNudge?: string; diff --git a/components/server/src/user/user-deletion-service.ts b/components/server/src/user/user-deletion-service.ts index 2edafcd29fa501..ec1068498eafab 100644 --- a/components/server/src/user/user-deletion-service.ts +++ b/components/server/src/user/user-deletion-service.ts @@ -100,6 +100,7 @@ export class UserDeletionService { bitbucket_slug: "deleted-user", email: "deleted-user", full_name: "deleted-user", + name: "deleted-user", }, }); } diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 90bdd6a1a901b9..17c0086bd644ff 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -421,6 +421,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = this.checkUser("updateLoggedInUser"); await this.guardAccess({ kind: "user", subject: user }, "update"); + //hang on to user profile before it's overwritten for analytics below + const oldProfile = User.getProfile(user); + const allowedFields: (keyof User)[] = ["avatarUrl", "fullName", "additionalData"]; for (const p of allowedFields) { if (p in partialUser) { @@ -429,6 +432,21 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } await this.userDB.updateUserPartial(user); + + //track event and user profile if profile of partialUser changed + const newProfile = User.getProfile(user); + if (User.Profile.hasChanges(oldProfile, newProfile)) { + this.analytics.track({ + userId: user.id, + event: "profile_changed", + properties: { new: newProfile, old: oldProfile }, + }); + this.analytics.identify({ + userId: user.id, + traits: { email: newProfile.email, company: newProfile.company, name: newProfile.name }, + }); + } + return user; }