Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions scripts/sentry/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ const METRICS = {
"video.upload.duration": MS,
// a video edited in the composer's editor (path: convert = trim/crop/turn applied by the converter | render = frames rendered; reason: pixels | angle)
"video.edit": C,
// cosmetics (action: equip | unequip; kind: the server's kind key; error: a CosmeticError name or errorKind)
"cosmetic.equip": C,
// a worn cosmetic this build could not draw (reason: missing_row | unknown_kind | unknown_entry | unimplemented_slug | payload_rejected | option_rejected)
"cosmetic.render.failed": C,
} as const satisfies Record<string, MetricSpec>;

type MetricName = keyof typeof METRICS;
Expand Down Expand Up @@ -344,6 +348,9 @@ const PRODUCT: WidgetDraft[] = [
by("Video plays by source", M("video.play", "result:ok"), "source", { w: 3, description: "inline: silent autoplay in the chat; viewer: opened full size" }),
by("Video edits by path", M("video.edit"), "path", { w: 3, description: "convert: trim/crop/turn applied by the converter, no frame rendered; render: drawn on, frames rendered" }),
bars("Video edits rendered by reason", M("video.edit", "path:render"), "reason", "sum", { w: 3, description: "pixels: drawing, text, stickers, adjustments; angle: a free rotation" }),
by("Cosmetics equipped by kind", M("cosmetic.equip", "result:ok"), "kind", { w: 2, description: "Successful equips and unequips, per kind" }),
bars("Cosmetic equip refusals by error", M("cosmetic.equip", "result:failed"), "error", "sum", { w: 2, description: "CosmeticError names from the server, or a transport errorKind" }),
bars("Cosmetics not drawn by reason", M("cosmetic.render.failed"), "reason", "sum", { w: 2, description: "Worn cosmetics this build skipped; counted once per item per session" }),
];

const CALLS: WidgetDraft[] = [
Expand Down
5 changes: 5 additions & 0 deletions src/cosmetics/defineCosmeticKind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { CosmeticKindModule } from "@/cosmetics/types";

export function defineCosmeticKind<TPayload>(module: CosmeticKindModule<TPayload>): CosmeticKindModule<TPayload> {
return module;
}
13 changes: 13 additions & 0 deletions src/cosmetics/kinds/avatar-decoration.kind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { AvatarDecorationPayload } from "@/cosmetics/kinds/avatar-decoration";
import { defineCosmeticKind } from "@/cosmetics/defineCosmeticKind";
import { avatarDecorationOf } from "@/cosmetics/wire";

export default defineCosmeticKind<AvatarDecorationPayload>({
key: "avatar.decoration",
surfaces: ["avatar"],
primitive: "imageLayer",
layer: 200,
labelKey: "cosmetic_kind_avatar_decoration",

parsePayload: avatarDecorationOf,
});
22 changes: 22 additions & 0 deletions src/cosmetics/kinds/nickname-style.kind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
NICKNAME_STYLE_KIND,
type NicknameStyleTuning,
} from "@/cosmetics/kinds/nickname-style";
import { defineCosmeticKind } from "@/cosmetics/defineCosmeticKind";

/** Bare: there are no rows of it. A worn look is its two axes plus its wearer's own tuning. */
export default defineCosmeticKind<NicknameStyleTuning>({
key: NICKNAME_STYLE_KIND,
surfaces: ["profileCard", "ownProfile", "nicknameInMessages", "memberListRow"],
primitive: "textStyle",
layer: 400,
labelKey: "cosmetic_kind_nickname_style",
bare: true,

facets: [
{ id: "font", labelKey: "cosmetic_facet_font", optionKindKey: "option.font" },
{ id: "effect", labelKey: "cosmetic_facet_effect", optionKindKey: "option.text-effect" },
],

parsePayload: () => null,
});
2 changes: 2 additions & 0 deletions src/cosmetics/kinds/nickname-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* of it. Everything anybody sees comes from its two axes, the font and the treatment, and from
* this.
*/
export const NICKNAME_STYLE_KIND = "nickname.style";

export type NicknameGradientShape = "linear" | "radial" | "conic";

export interface NicknameStyleTuning {
Expand Down
13 changes: 13 additions & 0 deletions src/cosmetics/kinds/option-font.kind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { FontOptionPayload } from "@/cosmetics/kinds/option-font";
import { defineCosmeticKind } from "@/cosmetics/defineCosmeticKind";
import { fontOptionOf } from "@/cosmetics/wire";

export default defineCosmeticKind<FontOptionPayload>({
key: "option.font",
surfaces: [],
primitive: "textStyle",
layer: 0,
labelKey: "cosmetic_kind_option_font",

parsePayload: fontOptionOf,
});
19 changes: 19 additions & 0 deletions src/cosmetics/kinds/option-text-effect.kind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
textEffect,
type TextEffectOptionPayload,
} from "@/cosmetics/kinds/option-text-effect";
import { defineCosmeticKind } from "@/cosmetics/defineCosmeticKind";
import { textEffectOptionOf } from "@/cosmetics/wire";

/** A row is drawable only when this build ships the effect module named by its slug. */
export default defineCosmeticKind<TextEffectOptionPayload>({
key: "option.text-effect",
surfaces: [],
primitive: "textStyle",
layer: 0,
labelKey: "cosmetic_kind_option_text_effect",

parsePayload: textEffectOptionOf,

implementsSlug: slug => textEffect(slug) !== undefined,
});
22 changes: 22 additions & 0 deletions src/cosmetics/kinds/profile-frame.kind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
frameHidesEdges,
frameInsets,
frameOutsets,
type ProfileFramePayload,
} from "@/cosmetics/kinds/profile-frame";
import { defineCosmeticKind } from "@/cosmetics/defineCosmeticKind";
import { profileFrameOf } from "@/cosmetics/wire";

export default defineCosmeticKind<ProfileFramePayload>({
key: "profile.frame",
surfaces: ["profileCard", "ownProfile"],
primitive: "frameAssembly",
layer: 600,
labelKey: "cosmetic_kind_profile_frame",

parsePayload: profileFrameOf,

insetsOf: item => frameInsets(item.payload),
outsetsOf: item => frameOutsets(item.payload),
hidesEdges: item => frameHidesEdges(item.payload),
});
9 changes: 9 additions & 0 deletions src/cosmetics/optionOn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type {
CosmeticFacet,
ResolvedCosmetic,
ResolvedOption,
} from "@/cosmetics/types";

export function optionOn(cosmetic: ResolvedCosmetic, facetId: CosmeticFacet["id"]): ResolvedOption | undefined {
return cosmetic.options.find(option => option.facetId === facetId);
}
38 changes: 38 additions & 0 deletions src/cosmetics/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { logger } from "@argon/core";

import type {
CosmeticKindModule,
CosmeticSurface,
} from "@/cosmetics/types";

const kindModules = import.meta.glob("./kinds/*.kind.ts", {
eager: true,
import: "default",
}) as Record<string, CosmeticKindModule>;

const byKey = new Map<string, CosmeticKindModule>();

for (const [path, module] of Object.entries(kindModules)) {
if (!module?.key) {
logger.error("Cosmetic kind file exports no key", path);
continue;
}

if (byKey.has(module.key)) {
logger.error("Two cosmetic kinds claim the same key", module.key, path);
continue;
}

byKey.set(module.key, module);
}

/** Every kind this build ships: the directory listing of `kinds/*.kind.ts`. */
export const cosmeticKinds: readonly CosmeticKindModule[] = [...byKey.values()];

export function resolveKind(kindKey: string): CosmeticKindModule | undefined {
return byKey.get(kindKey);
}

export function kindsForSurface(surface: CosmeticSurface): CosmeticKindModule[] {
return cosmeticKinds.filter(kind => kind.surfaces.includes(surface));
}
Loading
Loading