diff --git a/open-api/openapi.json b/open-api/openapi.json index 6da799a..00a4c97 100644 --- a/open-api/openapi.json +++ b/open-api/openapi.json @@ -2451,7 +2451,8 @@ { "schema": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 40 }, "required": true, "name": "query", diff --git a/src/data-access-layer/__test__/teammates.test.ts b/src/data-access-layer/__test__/teammates.test.ts index d1fc191..ebc1950 100644 --- a/src/data-access-layer/__test__/teammates.test.ts +++ b/src/data-access-layer/__test__/teammates.test.ts @@ -7,7 +7,9 @@ cleanupPostgresAfterAll() describe("getPlayer", () => { it("returns the correct shape", async () => { - const data = await getTeammates("4611686018443649478").catch(console.error) + const data = await getTeammates("4611686018443649478", { + count: 10 + }).catch(console.error) const parsed = z.array(zTeammate).safeParse(data) if (!parsed.success) { diff --git a/src/data-access-layer/history.ts b/src/data-access-layer/history.ts index 721920c..4c5a0b4 100644 --- a/src/data-access-layer/history.ts +++ b/src/data-access-layer/history.ts @@ -1,7 +1,7 @@ import { InstanceForPlayer } from "../schema/components/InstanceForPlayer" import { postgres } from "../services/postgres" import { activityHistoryQueryTimer } from "../services/prometheus/metrics" -import { withTimer } from "../services/prometheus/util" +import { withHistogramTimer } from "../services/prometheus/util" export const getActivities = async ( membershipId: bigint | string, @@ -15,7 +15,7 @@ export const getActivities = async ( cursor?: Date } ) => { - return await withTimer( + return await withHistogramTimer( activityHistoryQueryTimer, { count: count, diff --git a/src/data-access-layer/player-search.ts b/src/data-access-layer/player-search.ts index 9260a23..23c7dda 100644 --- a/src/data-access-layer/player-search.ts +++ b/src/data-access-layer/player-search.ts @@ -1,6 +1,8 @@ import { PlayerInfo } from "../schema/components/PlayerInfo" import { DestinyMembershipType } from "../schema/enums/DestinyMembershipType" import { postgres } from "../services/postgres" +import { playerSearchQueryTimer } from "../services/prometheus/metrics" +import { withHistogramTimer } from "../services/prometheus/util" /** * Case insensitive search @@ -18,29 +20,35 @@ export async function searchForPlayer( }> { const searchTerm = query.trim().toLowerCase() - const results = await postgres.queryRows( - `SELECT - membership_id::text AS "membershipId", - membership_type AS "membershipType", - icon_path AS "iconPath", - display_name AS "displayName", - bungie_global_display_name AS "bungieGlobalDisplayName", - bungie_global_display_name_code AS "bungieGlobalDisplayNameCode", - last_seen AS "lastSeen", - is_private AS "isPrivate" - FROM player - WHERE lower(${opts.global ? "bungie_name" : "display_name"}) LIKE $1 - ${opts.membershipType ? "AND membership_type = $3" : ""} - AND last_seen > TIMESTAMP 'epoch' - ORDER BY _search_score DESC - LIMIT $2;`, - { - params: opts.membershipType - ? [searchTerm + "%", opts.count, opts.membershipType] - : [searchTerm + "%", opts.count], - fetchCount: opts.count - } + const results = await withHistogramTimer( + playerSearchQueryTimer, + { prefixLength: searchTerm.split("#")[0]?.length ?? 0 }, + () => + postgres.queryRows( + `SELECT + membership_id::text AS "membershipId", + membership_type AS "membershipType", + icon_path AS "iconPath", + display_name AS "displayName", + bungie_global_display_name AS "bungieGlobalDisplayName", + bungie_global_display_name_code AS "bungieGlobalDisplayNameCode", + last_seen AS "lastSeen", + is_private AS "isPrivate" + FROM player + WHERE lower(${opts.global ? "bungie_name" : "display_name"}) LIKE $1 + ${opts.membershipType ? "AND membership_type = $3" : ""} + AND last_seen > TIMESTAMP 'epoch' + ORDER BY _search_score DESC + LIMIT $2;`, + { + params: opts.membershipType + ? [searchTerm + "%", opts.count, opts.membershipType] + : [searchTerm + "%", opts.count], + fetchCount: opts.count + } + ) ) + return { searchTerm, results diff --git a/src/data-access-layer/player.ts b/src/data-access-layer/player.ts index 7e4f910..94deb54 100644 --- a/src/data-access-layer/player.ts +++ b/src/data-access-layer/player.ts @@ -6,7 +6,7 @@ import { } from "../schema/components/PlayerProfile" import { postgres } from "../services/postgres" import { playerProfileQueryTimer } from "../services/prometheus/metrics" -import { withTimer } from "../services/prometheus/util" +import { withHistogramTimer } from "../services/prometheus/util" export const getPlayer = async (membershipId: bigint | string) => { return await postgres.queryRow( @@ -27,7 +27,7 @@ export const getPlayer = async (membershipId: bigint | string) => { ) } export const getPlayerActivityStats = async (membershipId: bigint | string) => { - return await withTimer( + return await withHistogramTimer( playerProfileQueryTimer, { method: "getPlayerActivityStats" @@ -78,7 +78,7 @@ export const getPlayerActivityStats = async (membershipId: bigint | string) => { } export const getPlayerGlobalStats = async (membershipId: bigint | string) => { - return await withTimer( + return await withHistogramTimer( playerProfileQueryTimer, { method: "getPlayerGlobalStats" @@ -112,7 +112,7 @@ export const getPlayerGlobalStats = async (membershipId: bigint | string) => { } export const getWorldFirstEntries = async (membershipId: bigint | string) => { - return await withTimer( + return await withHistogramTimer( playerProfileQueryTimer, { method: "getWorldFirstEntries" diff --git a/src/data-access-layer/teammates.ts b/src/data-access-layer/teammates.ts index 97bd084..cfeaae2 100644 --- a/src/data-access-layer/teammates.ts +++ b/src/data-access-layer/teammates.ts @@ -1,7 +1,7 @@ import { Teammate } from "../schema/components/Teammate" import { postgres } from "../services/postgres" -export const getTeammates = async (membershipId: bigint | string) => { +export const getTeammates = async (membershipId: bigint | string, { count }: { count: number }) => { return await postgres.queryRows( `WITH self AS ( SELECT @@ -18,6 +18,8 @@ export const getTeammates = async (membershipId: bigint | string) => { JOIN activity_player AS teammate USING (instance_id) WHERE membership_id <> $1::bigint GROUP BY (membership_id) + ORDER BY clears DESC, time_played DESC + LIMIT $2 ) SELECT agg_data.time_played as "estimatedTimePlayedSeconds", @@ -34,12 +36,10 @@ export const getTeammates = async (membershipId: bigint | string) => { 'isPrivate', "is_private" ) AS "playerInfo" FROM agg_data - JOIN player USING (membership_id) - ORDER BY clears DESC, time_played DESC - LIMIT 100;`, + JOIN player USING (membership_id);`, { - params: [membershipId], - fetchCount: 100 + params: [membershipId, count], + fetchCount: count } ) } diff --git a/src/routes/player/membershipId/teamates.ts b/src/routes/player/membershipId/teamates.ts index 58e3a4f..6c25363 100644 --- a/src/routes/player/membershipId/teamates.ts +++ b/src/routes/player/membershipId/teamates.ts @@ -47,7 +47,9 @@ export const playerTeammatesRoute = new RaidHubRoute({ return RaidHubRoute.fail(ErrorCode.PlayerPrivateProfileError, { membershipId }) } - const teamates = await getTeammates(membershipId) + const teamates = await getTeammates(membershipId, { + count: 100 + }) return RaidHubRoute.ok(teamates) } diff --git a/src/routes/player/search.ts b/src/routes/player/search.ts index e14e0c3..8129474 100644 --- a/src/routes/player/search.ts +++ b/src/routes/player/search.ts @@ -13,7 +13,7 @@ Players who have not attempted a raid may not appear in the search results. Results are ordered by a combination of the number of raid completions and last played date.`, query: z.object({ count: z.coerce.number().int().min(1).max(50).default(20), - query: z.string().min(1), + query: z.string().min(1).max(40), membershipType: zDestinyMembershipType.default(-1).openapi({ description: "Filter by Destiny membership type. Defaults to -1 (all). Note that the membership type of an account is determined by the platform the was first created on" diff --git a/src/services/prometheus/metrics.ts b/src/services/prometheus/metrics.ts index 85a8dfd..c6a4dae 100644 --- a/src/services/prometheus/metrics.ts +++ b/src/services/prometheus/metrics.ts @@ -7,16 +7,25 @@ export const httpRequestTimer = new Histogram({ buckets: [0.1, 1, 5, 15, 50, 100, 200, 300, 400, 500, 1000, 2000, 5000, 10000] }) +const QueryBuckets = [0.1, 0.5, 1, 5, 10, 50, 100, 250, 500, 1000, 5000, 10000] + export const activityHistoryQueryTimer = new Histogram({ name: "activity_history_query_duration_ms", help: "Duration of activity history queries in ms", labelNames: ["count", "cutoff", "cursor"], - buckets: [0.1, 0.5, 1, 5, 10, 50, 100, 250, 500, 1000, 5000, 10000] + buckets: QueryBuckets }) export const playerProfileQueryTimer = new Histogram({ name: "player_profile_query_duration_ms", help: "Duration of player profile queries in ms", labelNames: ["method"], - buckets: [0.1, 0.5, 1, 5, 10, 50, 100, 250, 500, 1000, 5000, 10000] + buckets: QueryBuckets +}) + +export const playerSearchQueryTimer = new Histogram({ + name: "search_player_query_duration_ms", + help: "Duration of player search queries in ms", + labelNames: ["prefixLength"], + buckets: QueryBuckets }) diff --git a/src/services/prometheus/registry.ts b/src/services/prometheus/registry.ts index 21c4e85..e99430d 100644 --- a/src/services/prometheus/registry.ts +++ b/src/services/prometheus/registry.ts @@ -1,8 +1,14 @@ import { Registry } from "prom-client" -import { activityHistoryQueryTimer, httpRequestTimer, playerProfileQueryTimer } from "./metrics" +import { + activityHistoryQueryTimer, + httpRequestTimer, + playerProfileQueryTimer, + playerSearchQueryTimer +} from "./metrics" export const prometheusRegistry = new Registry() prometheusRegistry.registerMetric(httpRequestTimer) prometheusRegistry.registerMetric(activityHistoryQueryTimer) prometheusRegistry.registerMetric(playerProfileQueryTimer) +prometheusRegistry.registerMetric(playerSearchQueryTimer) diff --git a/src/services/prometheus/util.ts b/src/services/prometheus/util.ts index 29cec47..2a0a69a 100644 --- a/src/services/prometheus/util.ts +++ b/src/services/prometheus/util.ts @@ -1,6 +1,6 @@ import { Histogram } from "prom-client" -export const withTimer = async ( +export const withHistogramTimer = async ( metric: Histogram, labels: Partial>, fn: () => Promise