Skip to content

Commit

Permalink
feat: indicate premium users (fehmer) (#5092)
Browse files Browse the repository at this point in the history
* feat: indicate premium users

* frontend

* Test multiple userFlags, remove later

* cleanup

* fix flag alignment on profile and leaderboards

* fix name auto scaling

* update screenshot watermark

* update header text

* use userFlags for lbOptOut

* use flex end

* removeo unused code, increase margin

---------

Co-authored-by: Miodec <jack@monkeytype.com>
  • Loading branch information
fehmer and Miodec authored Mar 5, 2024
1 parent 7e957fb commit c95e3b2
Show file tree
Hide file tree
Showing 23 changed files with 373 additions and 91 deletions.
3 changes: 3 additions & 0 deletions backend/__tests__/api/controllers/user.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ describe("user controller test", () => {
enabled: false,
maxMail: 0,
},
premium: {
enabled: true,
},
},
} as any);

Expand Down
161 changes: 145 additions & 16 deletions backend/__tests__/dal/leaderboards.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ObjectId } from "mongodb";
import * as UserDal from "../../src/dal/user";
import * as LeaderboardsDal from "../../src/dal/leaderboards";
import * as PublicDal from "../../src/dal/public";
import * as Configuration from "../../src/init/configuration";
const configuration = Configuration.getCachedConfiguration();

import * as DB from "../../src/init/db";

Expand Down Expand Up @@ -50,10 +52,10 @@ describe("LeaderboardsDal", () => {
const lb = result.map((it) => _.omit(it, ["_id"]));

expect(lb).toEqual([
expectedLbEntry(1, rank1, "15"),
expectedLbEntry(2, rank2, "15"),
expectedLbEntry(3, rank3, "15"),
expectedLbEntry(4, rank4, "15"),
expectedLbEntry("15", { rank: 1, user: rank1 }),
expectedLbEntry("15", { rank: 2, user: rank2 }),
expectedLbEntry("15", { rank: 3, user: rank3 }),
expectedLbEntry("15", { rank: 4, user: rank4 }),
]);
});
it("should create leaderboard time english 60", async () => {
Expand All @@ -76,10 +78,10 @@ describe("LeaderboardsDal", () => {
const lb = result.map((it) => _.omit(it, ["_id"]));

expect(lb).toEqual([
expectedLbEntry(1, rank1, "60"),
expectedLbEntry(2, rank2, "60"),
expectedLbEntry(3, rank3, "60"),
expectedLbEntry(4, rank4, "60"),
expectedLbEntry("60", { rank: 1, user: rank1 }),
expectedLbEntry("60", { rank: 2, user: rank2 }),
expectedLbEntry("60", { rank: 3, user: rank3 }),
expectedLbEntry("60", { rank: 4, user: rank4 }),
]);
});
it("should not include discord properties for users without discord connection", async () => {
Expand Down Expand Up @@ -154,10 +156,113 @@ describe("LeaderboardsDal", () => {
//THEN
expect(result).toEqual({ "20": 2, "110": 2 });
});

it("should create leaderboard with badges", async () => {
//GIVEN
const noBadge = await createUser(lbBests(pb(4)));
const oneBadgeSelected = await createUser(lbBests(pb(3)), {
inventory: { badges: [{ id: 1, selected: true }] },
});
const oneBadgeNotSelected = await createUser(lbBests(pb(2)), {
inventory: { badges: [{ id: 1, selected: false }] },
});
const multipleBadges = await createUser(lbBests(pb(1)), {
inventory: {
badges: [
{ id: 1, selected: false },
{ id: 2, selected: true },
{ id: 3, selected: true },
],
},
});

//WHEN
await LeaderboardsDal.update("time", "15", "english");
const result = (await LeaderboardsDal.get(
"time",
"15",
"english",
0
)) as SharedTypes.LeaderboardEntry[];

//THEN
const lb = result.map((it) => _.omit(it, ["_id"]));

expect(lb).toEqual([
expectedLbEntry("15", { rank: 1, user: noBadge }),
expectedLbEntry("15", {
rank: 2,
user: oneBadgeSelected,
badgeId: 1,
}),
expectedLbEntry("15", { rank: 3, user: oneBadgeNotSelected }),
expectedLbEntry("15", {
rank: 4,
user: multipleBadges,
badgeId: 2,
}),
]);
});

it("should create leaderboard with premium", async () => {
await enablePremiumFeatures(true);
//GIVEN
const noPremium = await createUser(lbBests(pb(4)));
const lifetime = await createUser(lbBests(pb(3)), premium(-1));
const validPremium = await createUser(lbBests(pb(2)), premium(10));
const expiredPremium = await createUser(lbBests(pb(1)), premium(-10));

//WHEN
await LeaderboardsDal.update("time", "15", "english");
const result = (await LeaderboardsDal.get(
"time",
"15",
"english",
0
)) as SharedTypes.LeaderboardEntry[];

//THEN
const lb = result.map((it) => _.omit(it, ["_id"]));

expect(lb).toEqual([
expectedLbEntry("15", { rank: 1, user: noPremium }),
expectedLbEntry("15", {
rank: 2,
user: lifetime,
isPremium: true,
}),
expectedLbEntry("15", {
rank: 3,
user: validPremium,
isPremium: true,
}),
expectedLbEntry("15", { rank: 4, user: expiredPremium }),
]);
});
it("should create leaderboard without premium if feature disabled", async () => {
await enablePremiumFeatures(false);
//GIVEN
const lifetime = await createUser(lbBests(pb(3)), premium(-1));

//WHEN
await LeaderboardsDal.update("time", "15", "english");
const result = (await LeaderboardsDal.get(
"time",
"15",
"english",
0
)) as SharedTypes.LeaderboardEntry[];

//THEN
expect(result[0]?.isPremium).toBeUndefined();
});
});
});

function expectedLbEntry(rank: number, user: MonkeyTypes.DBUser, time: string) {
function expectedLbEntry(
time: string,
{ rank, user, badgeId, isPremium }: ExpectedLbEntry
) {
const lbBest: SharedTypes.PersonalBest =
user.lbPersonalBests?.time[time].english;

Expand All @@ -172,7 +277,8 @@ function expectedLbEntry(rank: number, user: MonkeyTypes.DBUser, time: string) {
consistency: lbBest.consistency,
discordId: user.discordId,
discordAvatar: user.discordAvatar,
badgeId: 2,
badgeId,
isPremium,
};
}

Expand All @@ -192,12 +298,6 @@ async function createUser(
timeTyping: 7200,
discordId: "discord " + uid,
discordAvatar: "avatar " + uid,
inventory: {
badges: [
{ id: 1, selected: false },
{ id: 2, selected: true },
],
},
...userProperties,
lbPersonalBests,
},
Expand Down Expand Up @@ -234,3 +334,32 @@ function pb(
timestamp,
};
}

function premium(expirationDeltaSeconds) {
return {
premium: {
startTimestamp: 0,
expirationTimestamp:
expirationDeltaSeconds === -1
? -1
: Date.now() + expirationDeltaSeconds * 1000,
},
};
}

interface ExpectedLbEntry {
rank: number;
user: MonkeyTypes.DBUser;
badgeId?: number;
isPremium?: boolean;
}

async function enablePremiumFeatures(premium: boolean): Promise<void> {
const mockConfig = _.merge(await configuration, {
users: { premium: { enabled: premium } },
});

jest
.spyOn(Configuration, "getCachedConfiguration")
.mockResolvedValue(mockConfig);
}
3 changes: 2 additions & 1 deletion backend/src/api/controllers/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ export async function getDailyLeaderboard(
const topResults = await dailyLeaderboard.getResults(
minRank,
maxRank,
req.ctx.configuration.dailyLeaderboards
req.ctx.configuration.dailyLeaderboards,
req.ctx.configuration.users.premium.enabled
);

return new MonkeyResponse("Daily leaderboard retrieved", topResults);
Expand Down
3 changes: 3 additions & 0 deletions backend/src/api/controllers/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,8 @@ export async function addResult(
(isDevEnvironment() || (user.timeTyping ?? 0) > 7200);

const selectedBadgeId = user.inventory?.badges?.find((b) => b.selected)?.id;
const isPremium =
(await UserDAL.checkIfUserIsPremium(user.uid, user)) || undefined;

if (dailyLeaderboard && validResultCriteria) {
incrementDailyLeaderboard(
Expand All @@ -508,6 +510,7 @@ export async function addResult(
discordAvatar: user.discordAvatar,
discordId: user.discordId,
badgeId: selectedBadgeId,
isPremium,
},
dailyLeaderboardsConfig
);
Expand Down
1 change: 1 addition & 0 deletions backend/src/api/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,7 @@ export async function getProfile(
streak: streak?.length ?? 0,
maxStreak: streak?.maxLength ?? 0,
lbOptOut,
isPremium: await UserDAL.checkIfUserIsPremium(user.uid, user),
};

if (banned) {
Expand Down
61 changes: 39 additions & 22 deletions backend/src/dal/leaderboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Logger from "../utils/logger";
import { performance } from "perf_hooks";
import { setLeaderboard } from "../utils/prometheus";
import { isDevEnvironment } from "../utils/misc";
import { getCachedConfiguration } from "../init/configuration";

const leaderboardUpdating: Record<string, boolean> = {};

Expand All @@ -27,6 +28,13 @@ export async function get(
.skip(skip)
.limit(limit)
.toArray();

const premiumFeaturesEnabled = (await getCachedConfiguration(true)).users
.premium.enabled;

if (!premiumFeaturesEnabled) {
preset.forEach((it) => (it.isPremium = undefined));
}
return preset;
} catch (e) {
if (e.error === 175) {
Expand Down Expand Up @@ -77,7 +85,6 @@ export async function update(
const key = `lbPersonalBests.${mode}.${mode2}.${language}`;
const lbCollectionName = `leaderboards.${language}.${mode}.${mode2}`;
leaderboardUpdating[`${language}_${mode}_${mode2}`] = true;
const start1 = performance.now();
const lb = db
.collection<MonkeyTypes.DBUser>("users")
.aggregate<SharedTypes.LeaderboardEntry>(
Expand Down Expand Up @@ -127,48 +134,51 @@ export async function update(
discordId: 1,
discordAvatar: 1,
inventory: 1,
premium: 1,
},
},

{
$addFields: {
[`${key}.uid`]: "$uid",
[`${key}.name`]: "$name",
[`${key}.discordId`]: {
$ifNull: ["$discordId", "$$REMOVE"],
},
[`${key}.discordAvatar`]: {
$ifNull: ["$discordAvatar", "$$REMOVE"],
},
"user.uid": "$uid",
"user.name": "$name",
"user.discordId": { $ifNull: ["$discordId", "$$REMOVE"] },
"user.discordAvatar": { $ifNull: ["$discordAvatar", "$$REMOVE"] },
[`${key}.consistency`]: {
$ifNull: [`$${key}.consistency`, "$$REMOVE"],
},
[`${key}.rank`]: {
calculated: {
$function: {
body: "function() {try {row_number+= 1;} catch (e) {row_number= 1;}return row_number;}",
args: [],
lang: "js",
},
},
[`${key}.badgeId`]: {
$function: {
body: "function(badges) {if (!badges) return null; for(let i=0;i<badges.length;i++){ if(badges[i].selected) return badges[i].id;}return null;}",
args: ["$inventory.badges"],
lang: "js",
args: [
"$premium.expirationTimestamp",
"$$NOW",
"$inventory.badges",
],
body: `function(expiration, currentTime, badges) {
try {row_number+= 1;} catch (e) {row_number= 1;}
var badgeId = undefined;
if(badges)for(let i=0; i<badges.length; i++){
if(badges[i].selected){ badgeId = badges[i].id; break}
}
var isPremium = expiration !== undefined && (expiration === -1 || new Date(expiration)>currentTime) || undefined;
return {rank:row_number,badgeId, isPremium};
}`,
},
},
},
},
{
$replaceRoot: {
newRoot: `$${key}`,
$replaceWith: {
$mergeObjects: [`$${key}`, "$user", "$calculated"],
},
},
{ $out: lbCollectionName },
],
{ allowDiskUse: true }
);

const start1 = performance.now();
await lb.toArray();
const end1 = performance.now();

Expand All @@ -179,7 +189,6 @@ export async function update(
const end2 = performance.now();

//update speedStats
const start3 = performance.now();
const boundaries = [...Array(32).keys()].map((it) => it * 10);
const statsKey = `${language}_${mode}_${mode2}`;
const src = await db.collection(lbCollectionName);
Expand Down Expand Up @@ -218,6 +227,7 @@ export async function update(
],
{ allowDiskUse: true }
);
const start3 = performance.now();
await histogram.toArray();
const end3 = performance.now();

Expand Down Expand Up @@ -258,6 +268,7 @@ async function createIndex(key: string): Promise<void> {
discordId: 1,
discordAvatar: 1,
inventory: 1,
premium: 1,
};
const partial = {
partialFilterExpression: {
Expand All @@ -275,4 +286,10 @@ async function createIndex(key: string): Promise<void> {
export async function createIndicies(): Promise<void> {
await createIndex("lbPersonalBests.time.15.english");
await createIndex("lbPersonalBests.time.60.english");

if (isDevEnvironment()) {
Logger.info("Updating leaderboards in dev mode...");
await update("time", "15", "english");
await update("time", "60", "english");
}
}
Loading

0 comments on commit c95e3b2

Please sign in to comment.