feat(app): gate a collapsing guild header behind an experiment (#2779)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-14 21:15:31 +02:00
1 parent 550e6b05a1
commit d0c6146429
29 files changed
+1449 -115

No files matched your search

@@ -35,6 +35,10 @@ import {
GuildActivityLogPresentationConfigResponse,
GuildActivityLogPresentationConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/GuildActivityLogPresentationSchemas';
import {
GuildHeaderCollapseConfigResponse,
GuildHeaderCollapseConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/GuildHeaderCollapseSchemas';
import {
MessageHoverTrackingConfigResponse,
MessageHoverTrackingConfigUpdateRequest,
@@ -655,6 +659,7 @@ export const InstanceConfigResponse = z.object({
message_keyboard_focus: MessageKeyboardFocusConfigResponse,
blocked_message_groups: BlockedMessageGroupsConfigResponse,
expression_info_card: ExpressionInfoCardConfigResponse,
guild_header_collapse: GuildHeaderCollapseConfigResponse,
registration: InstanceRegistrationResponse,
self_hosted: z.boolean(),
app_public: AppPublicConfigResponse,
@@ -696,6 +701,7 @@ export const InstanceConfigUpdateRequest = z.object({
message_keyboard_focus: MessageKeyboardFocusConfigUpdateRequest.nullish(),
blocked_message_groups: BlockedMessageGroupsConfigUpdateRequest.nullish(),
expression_info_card: ExpressionInfoCardConfigUpdateRequest.nullish(),
guild_header_collapse: GuildHeaderCollapseConfigUpdateRequest.nullish(),
registration: z
.object({
mode: InstanceRegistrationModeSchema.optional(),
@@ -16,6 +16,10 @@ import {
GuildActivityLogPresentationAssignmentResponse,
INERT_GUILD_ACTIVITY_LOG_PRESENTATION_ASSIGNMENT,
} from '@fluxer/schema/src/domains/experiment/GuildActivityLogPresentationSchemas';
import {
GuildHeaderCollapseAssignmentResponse,
INERT_GUILD_HEADER_COLLAPSE_ASSIGNMENT,
} from '@fluxer/schema/src/domains/experiment/GuildHeaderCollapseSchemas';
import {
INERT_MESSAGE_HOVER_TRACKING_ASSIGNMENT,
MessageHoverTrackingAssignmentResponse,
@@ -67,6 +71,7 @@ const ExperimentAssignmentsSchema = z.object({
blocked_message_groups: BlockedMessageGroupsAssignmentResponse.optional(),
guild_activity_log_presentation: GuildActivityLogPresentationAssignmentResponse.optional(),
expression_info_card: ExpressionInfoCardAssignmentResponse.optional(),
guild_header_collapse: GuildHeaderCollapseAssignmentResponse.optional(),
});
export const ExperimentAssignmentsResponse = z.object({
@@ -118,3 +123,9 @@ export function readExpressionInfoCardAssignment(
): ExpressionInfoCardAssignmentResponse {
return response.assignments.expression_info_card ?? INERT_EXPRESSION_INFO_CARD_ASSIGNMENT;
}
export function readGuildHeaderCollapseAssignment(
response: ExperimentAssignmentsResponse,
): GuildHeaderCollapseAssignmentResponse {
return response.assignments.guild_header_collapse ?? INERT_GUILD_HEADER_COLLAPSE_ASSIGNMENT;
}
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createClientBehaviorExperiment} from '@fluxer/schema/src/domains/experiment/ClientBehaviorExperiment';
import type {z} from 'zod';
const guildHeaderCollapse = createClientBehaviorExperiment({defaultRolloutSalt: 'guild-header-collapse-v1'});
export const GuildHeaderCollapseConfigSchema = guildHeaderCollapse.ConfigSchema;
export type GuildHeaderCollapseConfig = z.infer<typeof GuildHeaderCollapseConfigSchema>;
export const DEFAULT_GUILD_HEADER_COLLAPSE_CONFIG: GuildHeaderCollapseConfig = guildHeaderCollapse.DEFAULT_CONFIG;
export const GuildHeaderCollapseConfigUpdateRequest = guildHeaderCollapse.ConfigUpdateRequest;
export type GuildHeaderCollapseConfigUpdateRequest = z.infer<typeof GuildHeaderCollapseConfigUpdateRequest>;
export const GuildHeaderCollapseConfigResponse = guildHeaderCollapse.ConfigSchema;
export type GuildHeaderCollapseConfigResponse = GuildHeaderCollapseConfig;
export const GuildHeaderCollapseAssignmentResponse = guildHeaderCollapse.AssignmentResponse;
export type GuildHeaderCollapseAssignmentResponse = z.infer<typeof GuildHeaderCollapseAssignmentResponse>;
export const INERT_GUILD_HEADER_COLLAPSE_ASSIGNMENT: GuildHeaderCollapseAssignmentResponse =
guildHeaderCollapse.INERT_ASSIGNMENT;
export function resolveGuildHeaderCollapseAssignment(
config: GuildHeaderCollapseConfig,
userId: string,
): GuildHeaderCollapseAssignmentResponse {
return guildHeaderCollapse.resolveAssignment(config, userId);
}
export function isGuildHeaderCollapseTargeted(assignment: GuildHeaderCollapseAssignmentResponse): boolean {
return assignment.enabled && assignment.user_targeted;
}
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createClientBehaviorExperiment} from '@fluxer/schema/src/domains/experiment/ClientBehaviorExperiment';
import {describe, expect, it} from 'vitest';
const TARGETED_USER_CAP = 1000;
function userIds(count: number): Array<string> {
return Array.from({length: count}, (_, index) => String(index + 1));
}
const rollout = createClientBehaviorExperiment({defaultRolloutSalt: 'client-behaviour-cap-test'});
describe('createClientBehaviorExperiment targeted user lists', () => {
it('accepts an included list at the thousand entry cap', () => {
const config = rollout.ConfigSchema.parse({included_user_ids: userIds(TARGETED_USER_CAP)});
expect(config.included_user_ids).toHaveLength(TARGETED_USER_CAP);
});
it('rejects an included list above the thousand entry cap', () => {
expect(() => rollout.ConfigSchema.parse({included_user_ids: userIds(TARGETED_USER_CAP + 1)})).toThrow();
});
it('accepts an excluded list at the thousand entry cap', () => {
const config = rollout.ConfigSchema.parse({excluded_user_ids: userIds(TARGETED_USER_CAP)});
expect(config.excluded_user_ids).toHaveLength(TARGETED_USER_CAP);
});
it('rejects an excluded list above the thousand entry cap', () => {
expect(() => rollout.ConfigSchema.parse({excluded_user_ids: userIds(TARGETED_USER_CAP + 1)})).toThrow();
});
});
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
import {
DEFAULT_GUILD_HEADER_COLLAPSE_CONFIG,
type GuildHeaderCollapseConfig,
GuildHeaderCollapseConfigSchema,
INERT_GUILD_HEADER_COLLAPSE_ASSIGNMENT,
isGuildHeaderCollapseTargeted,
resolveGuildHeaderCollapseAssignment,
} from '@fluxer/schema/src/domains/experiment/GuildHeaderCollapseSchemas';
import {describe, expect, it} from 'vitest';
const USER_ID = '1485064866382176262';
const OTHER_USER_ID = '1485064866382176263';
function config(overrides: Partial<GuildHeaderCollapseConfig> = {}): GuildHeaderCollapseConfig {
return {...DEFAULT_GUILD_HEADER_COLLAPSE_CONFIG, ...overrides};
}
describe('GuildHeaderCollapseConfigSchema', () => {
it('defaults to a disabled rollout that targets nobody', () => {
expect(DEFAULT_GUILD_HEADER_COLLAPSE_CONFIG).toEqual({
enabled: false,
config_version: 0,
rollout_basis_points: 0,
rollout_salt: 'guild-header-collapse-v1',
included_user_ids: [],
excluded_user_ids: [],
});
});
it('rejects a rollout share above the bucket resolution', () => {
expect(() => GuildHeaderCollapseConfigSchema.parse({rollout_basis_points: 10001})).toThrow();
});
it('rejects a target that is not a snowflake', () => {
expect(() => GuildHeaderCollapseConfigSchema.parse({included_user_ids: ['not-a-snowflake']})).toThrow();
});
});
describe('resolveGuildHeaderCollapseAssignment', () => {
it('reports the inert assignment with the stored revision while the rollout is off', () => {
const assignment = resolveGuildHeaderCollapseAssignment(
config({enabled: false, config_version: 4, rollout_basis_points: 10000}),
USER_ID,
);
expect(assignment).toEqual({...INERT_GUILD_HEADER_COLLAPSE_ASSIGNMENT, config_version: 4});
expect(isGuildHeaderCollapseTargeted(assignment)).toBe(false);
});
it('targets every account at the full rollout share', () => {
const assignment = resolveGuildHeaderCollapseAssignment(
config({enabled: true, config_version: 2, rollout_basis_points: 10000}),
USER_ID,
);
expect(assignment).toEqual({enabled: true, config_version: 2, user_targeted: true, source: 'canary'});
expect(isGuildHeaderCollapseTargeted(assignment)).toBe(true);
});
it('targets no account at a zero rollout share', () => {
const assignment = resolveGuildHeaderCollapseAssignment(config({enabled: true}), USER_ID);
expect(assignment).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
expect(isGuildHeaderCollapseTargeted(assignment)).toBe(false);
});
it('targets an allowlisted account outside the sampled share', () => {
const assignment = resolveGuildHeaderCollapseAssignment(
config({enabled: true, included_user_ids: [USER_ID]}),
USER_ID,
);
expect(assignment).toEqual({enabled: true, config_version: 0, user_targeted: true, source: 'user_rule'});
});
it('excludes an account even when it is allowlisted and inside the share', () => {
const assignment = resolveGuildHeaderCollapseAssignment(
config({
enabled: true,
rollout_basis_points: 10000,
included_user_ids: [USER_ID],
excluded_user_ids: [USER_ID],
}),
USER_ID,
);
expect(assignment).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('resolves the same account the same way on every call', () => {
const rollout = config({enabled: true, rollout_basis_points: 5000});
const first = resolveGuildHeaderCollapseAssignment(rollout, USER_ID);
const second = resolveGuildHeaderCollapseAssignment(rollout, USER_ID);
expect(first).toEqual(second);
});
it('draws a caller inside the share and not one exactly at the boundary', () => {
const bucket = experimentBucket(USER_ID, DEFAULT_GUILD_HEADER_COLLAPSE_CONFIG.rollout_salt);
const inside = resolveGuildHeaderCollapseAssignment(
config({enabled: true, rollout_basis_points: bucket + 1}),
USER_ID,
);
const atBoundary = resolveGuildHeaderCollapseAssignment(
config({enabled: true, rollout_basis_points: bucket}),
USER_ID,
);
expect([inside.user_targeted, inside.source]).toEqual([true, 'canary']);
expect([atBoundary.user_targeted, atBoundary.source]).toEqual([false, null]);
});
it('excludes only the named account', () => {
const rollout = config({enabled: true, rollout_basis_points: 10000, excluded_user_ids: [USER_ID]});
expect(resolveGuildHeaderCollapseAssignment(rollout, USER_ID).user_targeted).toBe(false);
expect(resolveGuildHeaderCollapseAssignment(rollout, OTHER_USER_ID).user_targeted).toBe(true);
});
});