feat(experiments): ship seven treatments to everyone (#2798)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-15 18:03:35 +02:00
1 parent 9e614026d7
commit 910db6734b
167 files changed
+4608 -58147

No files matched your search

@@ -19,38 +19,10 @@ import {
VoiceNoiseSuppressionConfigResponse,
VoiceNoiseSuppressionConfigUpdateRequest,
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
import {
BlockedMessageGroupsConfigResponse,
BlockedMessageGroupsConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/BlockedMessageGroupsSchemas';
import {
ExperimentDeliveryConfigResponse,
ExperimentDeliveryConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
import {
ExpressionInfoCardConfigResponse,
ExpressionInfoCardConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/ExpressionInfoCardSchemas';
import {
GuildActivityLogPresentationConfigResponse,
GuildActivityLogPresentationConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/GuildActivityLogPresentationSchemas';
import {
GuildHeaderCollapseConfigResponse,
GuildHeaderCollapseConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/GuildHeaderCollapseSchemas';
import {
MessageHoverTrackingConfigResponse,
MessageHoverTrackingConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/MessageHoverTrackingSchemas';
import {
MessageKeyboardFocusConfigResponse,
MessageKeyboardFocusConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/MessageKeyboardFocusSchemas';
import {
TypingIndicatorReworkConfigResponse,
TypingIndicatorReworkConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/TypingIndicatorReworkSchemas';
import {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import {
InstanceCaptchaProviderSchema,
@@ -657,14 +629,7 @@ export const InstanceConfigResponse = z.object({
sso: SsoConfigResponse,
gateway_rollout: GatewayRolloutConfigResponse,
voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
guild_activity_log_presentation: GuildActivityLogPresentationConfigResponse,
experiment_delivery: ExperimentDeliveryConfigResponse,
message_hover_tracking: MessageHoverTrackingConfigResponse,
message_keyboard_focus: MessageKeyboardFocusConfigResponse,
blocked_message_groups: BlockedMessageGroupsConfigResponse,
expression_info_card: ExpressionInfoCardConfigResponse,
guild_header_collapse: GuildHeaderCollapseConfigResponse,
typing_indicator_rework: TypingIndicatorReworkConfigResponse,
registration: InstanceRegistrationResponse,
self_hosted: z.boolean(),
app_public: AppPublicConfigResponse,
@@ -700,14 +665,7 @@ const InstancePolicyUpdateSchema = z.object({
export const InstanceConfigUpdateRequest = z.object({
gateway_rollout: GatewayRolloutConfigUpdateRequest.nullish(),
voice_noise_suppression: VoiceNoiseSuppressionConfigUpdateRequest.nullish(),
guild_activity_log_presentation: GuildActivityLogPresentationConfigUpdateRequest.nullish(),
experiment_delivery: ExperimentDeliveryConfigUpdateRequest.nullish(),
message_hover_tracking: MessageHoverTrackingConfigUpdateRequest.nullish(),
message_keyboard_focus: MessageKeyboardFocusConfigUpdateRequest.nullish(),
blocked_message_groups: BlockedMessageGroupsConfigUpdateRequest.nullish(),
expression_info_card: ExpressionInfoCardConfigUpdateRequest.nullish(),
guild_header_collapse: GuildHeaderCollapseConfigUpdateRequest.nullish(),
typing_indicator_rework: TypingIndicatorReworkConfigUpdateRequest.nullish(),
registration: z
.object({
mode: InstanceRegistrationModeSchema.optional(),
@@ -1,34 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createClientBehaviorExperiment} from '@fluxer/schema/src/domains/experiment/ClientBehaviorExperiment';
import type {z} from 'zod';
const blockedMessageGroups = createClientBehaviorExperiment({defaultRolloutSalt: 'blocked-message-groups-v1'});
export const BlockedMessageGroupsConfigSchema = blockedMessageGroups.ConfigSchema;
export type BlockedMessageGroupsConfig = z.infer<typeof BlockedMessageGroupsConfigSchema>;
export const DEFAULT_BLOCKED_MESSAGE_GROUPS_CONFIG: BlockedMessageGroupsConfig = blockedMessageGroups.DEFAULT_CONFIG;
export const BlockedMessageGroupsConfigUpdateRequest = blockedMessageGroups.ConfigUpdateRequest;
export type BlockedMessageGroupsConfigUpdateRequest = z.infer<typeof BlockedMessageGroupsConfigUpdateRequest>;
export const BlockedMessageGroupsConfigResponse = blockedMessageGroups.ConfigSchema;
export type BlockedMessageGroupsConfigResponse = BlockedMessageGroupsConfig;
export const BlockedMessageGroupsAssignmentResponse = blockedMessageGroups.AssignmentResponse;
export type BlockedMessageGroupsAssignmentResponse = z.infer<typeof BlockedMessageGroupsAssignmentResponse>;
export const INERT_BLOCKED_MESSAGE_GROUPS_ASSIGNMENT: BlockedMessageGroupsAssignmentResponse =
blockedMessageGroups.INERT_ASSIGNMENT;
export function resolveBlockedMessageGroupsAssignment(
config: BlockedMessageGroupsConfig,
userId: string,
): BlockedMessageGroupsAssignmentResponse {
return blockedMessageGroups.resolveAssignment(config, userId);
}
@@ -1,59 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
import {z} from 'zod';
const CLIENT_BEHAVIOR_MAX_TARGETED_USERS = 1000;
const CLIENT_BEHAVIOR_ASSIGNMENT_SOURCES = ['user_rule', 'canary'] as const;
export function createClientBehaviorExperiment({defaultRolloutSalt}: {defaultRolloutSalt: string}) {
const targetIdSchema = z.string().regex(/^\d{1,20}$/u);
const targetedUserIdsSchema = z.array(targetIdSchema).max(CLIENT_BEHAVIOR_MAX_TARGETED_USERS);
const configFields = {
enabled: z.boolean(),
config_version: z.number().int().min(0),
rollout_basis_points: z.number().int().min(0).max(EXPERIMENT_BUCKET_RESOLUTION),
rollout_salt: z.string().trim().min(1).max(64),
included_user_ids: targetedUserIdsSchema,
excluded_user_ids: targetedUserIdsSchema,
};
const ConfigSchema = z.object({
enabled: configFields.enabled.default(false),
config_version: configFields.config_version.default(0),
rollout_basis_points: configFields.rollout_basis_points.default(0),
rollout_salt: configFields.rollout_salt.default(defaultRolloutSalt),
included_user_ids: configFields.included_user_ids.default([]),
excluded_user_ids: configFields.excluded_user_ids.default([]),
});
const ConfigUpdateRequest = z.object(configFields).omit({config_version: true}).partial();
const AssignmentResponse = z.object({
enabled: configFields.enabled,
config_version: z.number().int(),
user_targeted: z.boolean(),
source: z.enum(CLIENT_BEHAVIOR_ASSIGNMENT_SOURCES).nullable(),
});
type Config = z.infer<typeof ConfigSchema>;
type Assignment = z.infer<typeof AssignmentResponse>;
const DEFAULT_CONFIG: Config = ConfigSchema.parse({});
const INERT_ASSIGNMENT: Assignment = {
enabled: false,
config_version: 0,
user_targeted: false,
source: null,
};
const resolveAssignment = (config: Config, userId: string): Assignment => {
if (!config.enabled) {
return {...INERT_ASSIGNMENT, config_version: config.config_version};
}
const shared = {enabled: true, config_version: config.config_version};
if (config.excluded_user_ids.includes(userId)) {
return {...shared, user_targeted: false, source: null};
}
if (config.included_user_ids.includes(userId)) {
return {...shared, user_targeted: true, source: 'user_rule'};
}
const inCanary = experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points;
return {...shared, user_targeted: inCanary, source: inCanary ? 'canary' : null};
};
return {ConfigSchema, ConfigUpdateRequest, AssignmentResponse, DEFAULT_CONFIG, INERT_ASSIGNMENT, resolveAssignment};
}
@@ -4,34 +4,6 @@ import {
INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
VoiceNoiseSuppressionAssignmentResponse,
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
import {
BlockedMessageGroupsAssignmentResponse,
INERT_BLOCKED_MESSAGE_GROUPS_ASSIGNMENT,
} from '@fluxer/schema/src/domains/experiment/BlockedMessageGroupsSchemas';
import {
ExpressionInfoCardAssignmentResponse,
INERT_EXPRESSION_INFO_CARD_ASSIGNMENT,
} from '@fluxer/schema/src/domains/experiment/ExpressionInfoCardSchemas';
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,
} from '@fluxer/schema/src/domains/experiment/MessageHoverTrackingSchemas';
import {
INERT_MESSAGE_KEYBOARD_FOCUS_ASSIGNMENT,
MessageKeyboardFocusAssignmentResponse,
} from '@fluxer/schema/src/domains/experiment/MessageKeyboardFocusSchemas';
import {
INERT_TYPING_INDICATOR_REWORK_ASSIGNMENT,
TypingIndicatorReworkAssignmentResponse,
} from '@fluxer/schema/src/domains/experiment/TypingIndicatorReworkSchemas';
import {z} from 'zod';
export const EXPERIMENT_MIN_POLL_INTERVAL_SECONDS = 60;
@@ -70,13 +42,6 @@ export type ExperimentDeliveryConfigResponse = z.infer<typeof ExperimentDelivery
const ExperimentAssignmentsSchema = z.object({
voice_noise_suppression: VoiceNoiseSuppressionAssignmentResponse.optional(),
message_hover_tracking: MessageHoverTrackingAssignmentResponse.optional(),
message_keyboard_focus: MessageKeyboardFocusAssignmentResponse.optional(),
blocked_message_groups: BlockedMessageGroupsAssignmentResponse.optional(),
guild_activity_log_presentation: GuildActivityLogPresentationAssignmentResponse.optional(),
expression_info_card: ExpressionInfoCardAssignmentResponse.optional(),
guild_header_collapse: GuildHeaderCollapseAssignmentResponse.optional(),
typing_indicator_rework: TypingIndicatorReworkAssignmentResponse.optional(),
});
export const ExperimentAssignmentsResponse = z.object({
@@ -98,45 +63,3 @@ export function readVoiceNoiseSuppressionAssignment(
): VoiceNoiseSuppressionAssignmentResponse {
return response.assignments.voice_noise_suppression ?? INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT;
}
export function readMessageHoverTrackingAssignment(
response: ExperimentAssignmentsResponse,
): MessageHoverTrackingAssignmentResponse {
return response.assignments.message_hover_tracking ?? INERT_MESSAGE_HOVER_TRACKING_ASSIGNMENT;
}
export function readMessageKeyboardFocusAssignment(
response: ExperimentAssignmentsResponse,
): MessageKeyboardFocusAssignmentResponse {
return response.assignments.message_keyboard_focus ?? INERT_MESSAGE_KEYBOARD_FOCUS_ASSIGNMENT;
}
export function readBlockedMessageGroupsAssignment(
response: ExperimentAssignmentsResponse,
): BlockedMessageGroupsAssignmentResponse {
return response.assignments.blocked_message_groups ?? INERT_BLOCKED_MESSAGE_GROUPS_ASSIGNMENT;
}
export function readGuildActivityLogPresentationAssignment(
response: ExperimentAssignmentsResponse,
): GuildActivityLogPresentationAssignmentResponse {
return response.assignments.guild_activity_log_presentation ?? INERT_GUILD_ACTIVITY_LOG_PRESENTATION_ASSIGNMENT;
}
export function readExpressionInfoCardAssignment(
response: ExperimentAssignmentsResponse,
): 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;
}
export function readTypingIndicatorReworkAssignment(
response: ExperimentAssignmentsResponse,
): TypingIndicatorReworkAssignmentResponse {
return response.assignments.typing_indicator_rework ?? INERT_TYPING_INDICATOR_REWORK_ASSIGNMENT;
}
@@ -1,34 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createClientBehaviorExperiment} from '@fluxer/schema/src/domains/experiment/ClientBehaviorExperiment';
import type {z} from 'zod';
const expressionInfoCard = createClientBehaviorExperiment({defaultRolloutSalt: 'expression-info-card-v1'});
export const ExpressionInfoCardConfigSchema = expressionInfoCard.ConfigSchema;
export type ExpressionInfoCardConfig = z.infer<typeof ExpressionInfoCardConfigSchema>;
export const DEFAULT_EXPRESSION_INFO_CARD_CONFIG: ExpressionInfoCardConfig = expressionInfoCard.DEFAULT_CONFIG;
export const ExpressionInfoCardConfigUpdateRequest = expressionInfoCard.ConfigUpdateRequest;
export type ExpressionInfoCardConfigUpdateRequest = z.infer<typeof ExpressionInfoCardConfigUpdateRequest>;
export const ExpressionInfoCardConfigResponse = expressionInfoCard.ConfigSchema;
export type ExpressionInfoCardConfigResponse = ExpressionInfoCardConfig;
export const ExpressionInfoCardAssignmentResponse = expressionInfoCard.AssignmentResponse;
export type ExpressionInfoCardAssignmentResponse = z.infer<typeof ExpressionInfoCardAssignmentResponse>;
export const INERT_EXPRESSION_INFO_CARD_ASSIGNMENT: ExpressionInfoCardAssignmentResponse =
expressionInfoCard.INERT_ASSIGNMENT;
export function resolveExpressionInfoCardAssignment(
config: ExpressionInfoCardConfig,
userId: string,
): ExpressionInfoCardAssignmentResponse {
return expressionInfoCard.resolveAssignment(config, userId);
}
@@ -1,41 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createClientBehaviorExperiment} from '@fluxer/schema/src/domains/experiment/ClientBehaviorExperiment';
import type {z} from 'zod';
const guildActivityLogPresentation = createClientBehaviorExperiment({
defaultRolloutSalt: 'guild-activity-log-presentation-v1',
});
export const GuildActivityLogPresentationConfigSchema = guildActivityLogPresentation.ConfigSchema;
export type GuildActivityLogPresentationConfig = z.infer<typeof GuildActivityLogPresentationConfigSchema>;
export const DEFAULT_GUILD_ACTIVITY_LOG_PRESENTATION_CONFIG: GuildActivityLogPresentationConfig =
guildActivityLogPresentation.DEFAULT_CONFIG;
export const GuildActivityLogPresentationConfigUpdateRequest = guildActivityLogPresentation.ConfigUpdateRequest;
export type GuildActivityLogPresentationConfigUpdateRequest = z.infer<
typeof GuildActivityLogPresentationConfigUpdateRequest
>;
export const GuildActivityLogPresentationConfigResponse = guildActivityLogPresentation.ConfigSchema;
export type GuildActivityLogPresentationConfigResponse = GuildActivityLogPresentationConfig;
export const GuildActivityLogPresentationAssignmentResponse = guildActivityLogPresentation.AssignmentResponse;
export type GuildActivityLogPresentationAssignmentResponse = z.infer<
typeof GuildActivityLogPresentationAssignmentResponse
>;
export const INERT_GUILD_ACTIVITY_LOG_PRESENTATION_ASSIGNMENT: GuildActivityLogPresentationAssignmentResponse =
guildActivityLogPresentation.INERT_ASSIGNMENT;
export function resolveGuildActivityLogPresentationAssignment(
config: GuildActivityLogPresentationConfig,
userId: string,
): GuildActivityLogPresentationAssignmentResponse {
return guildActivityLogPresentation.resolveAssignment(config, userId);
}
@@ -1,38 +0,0 @@
// 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;
}
@@ -1,34 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createClientBehaviorExperiment} from '@fluxer/schema/src/domains/experiment/ClientBehaviorExperiment';
import type {z} from 'zod';
const messageHoverTracking = createClientBehaviorExperiment({defaultRolloutSalt: 'message-hover-tracking-v1'});
export const MessageHoverTrackingConfigSchema = messageHoverTracking.ConfigSchema;
export type MessageHoverTrackingConfig = z.infer<typeof MessageHoverTrackingConfigSchema>;
export const DEFAULT_MESSAGE_HOVER_TRACKING_CONFIG: MessageHoverTrackingConfig = messageHoverTracking.DEFAULT_CONFIG;
export const MessageHoverTrackingConfigUpdateRequest = messageHoverTracking.ConfigUpdateRequest;
export type MessageHoverTrackingConfigUpdateRequest = z.infer<typeof MessageHoverTrackingConfigUpdateRequest>;
export const MessageHoverTrackingConfigResponse = messageHoverTracking.ConfigSchema;
export type MessageHoverTrackingConfigResponse = MessageHoverTrackingConfig;
export const MessageHoverTrackingAssignmentResponse = messageHoverTracking.AssignmentResponse;
export type MessageHoverTrackingAssignmentResponse = z.infer<typeof MessageHoverTrackingAssignmentResponse>;
export const INERT_MESSAGE_HOVER_TRACKING_ASSIGNMENT: MessageHoverTrackingAssignmentResponse =
messageHoverTracking.INERT_ASSIGNMENT;
export function resolveMessageHoverTrackingAssignment(
config: MessageHoverTrackingConfig,
userId: string,
): MessageHoverTrackingAssignmentResponse {
return messageHoverTracking.resolveAssignment(config, userId);
}
@@ -1,36 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createClientBehaviorExperiment} from '@fluxer/schema/src/domains/experiment/ClientBehaviorExperiment';
import type {z} from 'zod';
const messageKeyboardFocus = createClientBehaviorExperiment({
defaultRolloutSalt: 'message-keyboard-focus-v1',
});
export const MessageKeyboardFocusConfigSchema = messageKeyboardFocus.ConfigSchema;
export type MessageKeyboardFocusConfig = z.infer<typeof MessageKeyboardFocusConfigSchema>;
export const DEFAULT_MESSAGE_KEYBOARD_FOCUS_CONFIG: MessageKeyboardFocusConfig = messageKeyboardFocus.DEFAULT_CONFIG;
export const MessageKeyboardFocusConfigUpdateRequest = messageKeyboardFocus.ConfigUpdateRequest;
export type MessageKeyboardFocusConfigUpdateRequest = z.infer<typeof MessageKeyboardFocusConfigUpdateRequest>;
export const MessageKeyboardFocusConfigResponse = messageKeyboardFocus.ConfigSchema;
export type MessageKeyboardFocusConfigResponse = MessageKeyboardFocusConfig;
export const MessageKeyboardFocusAssignmentResponse = messageKeyboardFocus.AssignmentResponse;
export type MessageKeyboardFocusAssignmentResponse = z.infer<typeof MessageKeyboardFocusAssignmentResponse>;
export const INERT_MESSAGE_KEYBOARD_FOCUS_ASSIGNMENT: MessageKeyboardFocusAssignmentResponse =
messageKeyboardFocus.INERT_ASSIGNMENT;
export function resolveMessageKeyboardFocusAssignment(
config: MessageKeyboardFocusConfig,
userId: string,
): MessageKeyboardFocusAssignmentResponse {
return messageKeyboardFocus.resolveAssignment(config, userId);
}
@@ -1,38 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createClientBehaviorExperiment} from '@fluxer/schema/src/domains/experiment/ClientBehaviorExperiment';
import type {z} from 'zod';
const typingIndicatorRework = createClientBehaviorExperiment({defaultRolloutSalt: 'typing-indicator-rework-v1'});
export const TypingIndicatorReworkConfigSchema = typingIndicatorRework.ConfigSchema;
export type TypingIndicatorReworkConfig = z.infer<typeof TypingIndicatorReworkConfigSchema>;
export const DEFAULT_TYPING_INDICATOR_REWORK_CONFIG: TypingIndicatorReworkConfig = typingIndicatorRework.DEFAULT_CONFIG;
export const TypingIndicatorReworkConfigUpdateRequest = typingIndicatorRework.ConfigUpdateRequest;
export type TypingIndicatorReworkConfigUpdateRequest = z.infer<typeof TypingIndicatorReworkConfigUpdateRequest>;
export const TypingIndicatorReworkConfigResponse = typingIndicatorRework.ConfigSchema;
export type TypingIndicatorReworkConfigResponse = TypingIndicatorReworkConfig;
export const TypingIndicatorReworkAssignmentResponse = typingIndicatorRework.AssignmentResponse;
export type TypingIndicatorReworkAssignmentResponse = z.infer<typeof TypingIndicatorReworkAssignmentResponse>;
export const INERT_TYPING_INDICATOR_REWORK_ASSIGNMENT: TypingIndicatorReworkAssignmentResponse =
typingIndicatorRework.INERT_ASSIGNMENT;
export function resolveTypingIndicatorReworkAssignment(
config: TypingIndicatorReworkConfig,
userId: string,
): TypingIndicatorReworkAssignmentResponse {
return typingIndicatorRework.resolveAssignment(config, userId);
}
export function isTypingIndicatorReworkTargeted(assignment: TypingIndicatorReworkAssignmentResponse): boolean {
return assignment.enabled && assignment.user_targeted;
}
@@ -30,8 +30,6 @@ export const AuditLogChangeSchema = z.object({
new_value: AuditLogChangeValueSchema.optional().describe('Value after the change'),
});
export type AuditLogChange = z.infer<typeof AuditLogChangeSchema>;
const AuditLogOptionsSchema = z.object({
channel_id: z.string().optional().describe('Channel ID for relevant actions'),
count: z.number().optional().describe('Count of items affected'),
@@ -1,98 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
BlockedMessageGroupsConfigSchema,
DEFAULT_BLOCKED_MESSAGE_GROUPS_CONFIG,
resolveBlockedMessageGroupsAssignment,
} from '@fluxer/schema/src/domains/experiment/BlockedMessageGroupsSchemas';
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
import {describe, expect, it} from 'vitest';
const USER_ID = '1485064866382176262';
const OTHER_USER_ID = '1485064866382176263';
function config(overrides: Partial<ReturnType<typeof BlockedMessageGroupsConfigSchema.parse>> = {}) {
return BlockedMessageGroupsConfigSchema.parse({...overrides});
}
describe('blocked message groups configuration', () => {
it('defaults to a disabled rollout that targets nobody', () => {
expect(DEFAULT_BLOCKED_MESSAGE_GROUPS_CONFIG).toEqual({
enabled: false,
config_version: 0,
rollout_basis_points: 0,
rollout_salt: 'blocked-message-groups-v1',
included_user_ids: [],
excluded_user_ids: [],
});
});
it('refuses a rollout wider than the bucket resolution', () => {
expect(() => config({rollout_basis_points: EXPERIMENT_BUCKET_RESOLUTION + 1})).toThrow();
});
it('refuses a target that is not a snowflake', () => {
expect(() => config({included_user_ids: ['not-a-snowflake']})).toThrow();
});
});
describe('blocked message groups resolution', () => {
it('reports the stored revision while the rollout is disabled', () => {
const assignment = resolveBlockedMessageGroupsAssignment(config({config_version: 3}), USER_ID);
expect(assignment).toEqual({enabled: false, config_version: 3, user_targeted: false, source: null});
});
it('targets an allowlisted account regardless of its bucket', () => {
const assignment = resolveBlockedMessageGroupsAssignment(
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('keeps a blocklisted account out of a full rollout', () => {
const assignment = resolveBlockedMessageGroupsAssignment(
config({
enabled: true,
rollout_basis_points: EXPERIMENT_BUCKET_RESOLUTION,
excluded_user_ids: [USER_ID],
}),
USER_ID,
);
expect(assignment).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('prefers the blocklist over the allowlist', () => {
const assignment = resolveBlockedMessageGroupsAssignment(
config({enabled: true, included_user_ids: [USER_ID], excluded_user_ids: [USER_ID]}),
USER_ID,
);
expect(assignment.user_targeted).toBe(false);
});
it('targets an account whose bucket falls inside the rollout', () => {
const salt = DEFAULT_BLOCKED_MESSAGE_GROUPS_CONFIG.rollout_salt;
const bucket = experimentBucket(USER_ID, salt);
const inside = resolveBlockedMessageGroupsAssignment(
config({enabled: true, rollout_basis_points: bucket + 1}),
USER_ID,
);
const outside = resolveBlockedMessageGroupsAssignment(
config({enabled: true, rollout_basis_points: bucket}),
USER_ID,
);
expect(inside).toEqual({enabled: true, config_version: 0, user_targeted: true, source: 'canary'});
expect(outside).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('separates two accounts under the same salt', () => {
const salt = DEFAULT_BLOCKED_MESSAGE_GROUPS_CONFIG.rollout_salt;
expect(experimentBucket(USER_ID, salt)).not.toBe(experimentBucket(OTHER_USER_ID, salt));
});
it('moves an account when the salt changes', () => {
expect(experimentBucket(USER_ID, 'blocked-message-groups-v1')).not.toBe(
experimentBucket(USER_ID, 'blocked-message-groups-v2'),
);
});
});
@@ -1,34 +0,0 @@
// 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();
});
});
@@ -1,95 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
import {
DEFAULT_EXPRESSION_INFO_CARD_CONFIG,
ExpressionInfoCardConfigSchema,
resolveExpressionInfoCardAssignment,
} from '@fluxer/schema/src/domains/experiment/ExpressionInfoCardSchemas';
import {describe, expect, it} from 'vitest';
const USER_ID = '1485064866382176262';
const OTHER_USER_ID = '1485064866382176263';
function config(overrides: Partial<ReturnType<typeof ExpressionInfoCardConfigSchema.parse>> = {}) {
return ExpressionInfoCardConfigSchema.parse({...overrides});
}
describe('expression info card configuration', () => {
it('defaults to a disabled rollout that targets nobody', () => {
expect(DEFAULT_EXPRESSION_INFO_CARD_CONFIG).toEqual({
enabled: false,
config_version: 0,
rollout_basis_points: 0,
rollout_salt: 'expression-info-card-v1',
included_user_ids: [],
excluded_user_ids: [],
});
});
it('refuses a rollout wider than the bucket resolution', () => {
expect(() => config({rollout_basis_points: EXPERIMENT_BUCKET_RESOLUTION + 1})).toThrow();
});
it('refuses a target that is not a snowflake', () => {
expect(() => config({included_user_ids: ['not-a-snowflake']})).toThrow();
});
});
describe('expression info card resolution', () => {
it('reports the stored revision while the rollout is disabled', () => {
const assignment = resolveExpressionInfoCardAssignment(config({config_version: 3}), USER_ID);
expect(assignment).toEqual({enabled: false, config_version: 3, user_targeted: false, source: null});
});
it('targets an allowlisted account regardless of its bucket', () => {
const assignment = resolveExpressionInfoCardAssignment(
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('keeps a blocklisted account out of a full rollout', () => {
const assignment = resolveExpressionInfoCardAssignment(
config({
enabled: true,
rollout_basis_points: EXPERIMENT_BUCKET_RESOLUTION,
excluded_user_ids: [USER_ID],
}),
USER_ID,
);
expect(assignment).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('prefers the blocklist over the allowlist', () => {
const assignment = resolveExpressionInfoCardAssignment(
config({enabled: true, included_user_ids: [USER_ID], excluded_user_ids: [USER_ID]}),
USER_ID,
);
expect(assignment.user_targeted).toBe(false);
});
it('targets an account whose bucket falls inside the rollout', () => {
const salt = DEFAULT_EXPRESSION_INFO_CARD_CONFIG.rollout_salt;
const bucket = experimentBucket(USER_ID, salt);
const inside = resolveExpressionInfoCardAssignment(
config({enabled: true, rollout_basis_points: bucket + 1}),
USER_ID,
);
const outside = resolveExpressionInfoCardAssignment(config({enabled: true, rollout_basis_points: bucket}), USER_ID);
expect(inside).toEqual({enabled: true, config_version: 0, user_targeted: true, source: 'canary'});
expect(outside).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('separates two accounts under the same salt', () => {
const salt = DEFAULT_EXPRESSION_INFO_CARD_CONFIG.rollout_salt;
expect(experimentBucket(USER_ID, salt)).not.toBe(experimentBucket(OTHER_USER_ID, salt));
});
it('moves an account when the salt changes', () => {
expect(experimentBucket(USER_ID, 'expression-info-card-v1')).not.toBe(
experimentBucket(USER_ID, 'expression-info-card-v2'),
);
});
});
@@ -1,98 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
import {
DEFAULT_GUILD_ACTIVITY_LOG_PRESENTATION_CONFIG,
GuildActivityLogPresentationConfigSchema,
resolveGuildActivityLogPresentationAssignment,
} from '@fluxer/schema/src/domains/experiment/GuildActivityLogPresentationSchemas';
import {describe, expect, it} from 'vitest';
const USER_ID = '1485064866382176262';
const OTHER_USER_ID = '1485064866382176263';
function config(overrides: Partial<ReturnType<typeof GuildActivityLogPresentationConfigSchema.parse>> = {}) {
return GuildActivityLogPresentationConfigSchema.parse({...overrides});
}
describe('guild activity log presentation configuration', () => {
it('defaults to a disabled rollout that targets nobody', () => {
expect(DEFAULT_GUILD_ACTIVITY_LOG_PRESENTATION_CONFIG).toEqual({
enabled: false,
config_version: 0,
rollout_basis_points: 0,
rollout_salt: 'guild-activity-log-presentation-v1',
included_user_ids: [],
excluded_user_ids: [],
});
});
it('refuses a rollout wider than the bucket resolution', () => {
expect(() => config({rollout_basis_points: EXPERIMENT_BUCKET_RESOLUTION + 1})).toThrow();
});
it('refuses a target that is not a snowflake', () => {
expect(() => config({included_user_ids: ['not-a-snowflake']})).toThrow();
});
});
describe('guild activity log presentation resolution', () => {
it('reports the stored revision while the rollout is disabled', () => {
const assignment = resolveGuildActivityLogPresentationAssignment(config({config_version: 3}), USER_ID);
expect(assignment).toEqual({enabled: false, config_version: 3, user_targeted: false, source: null});
});
it('targets an allowlisted account regardless of its bucket', () => {
const assignment = resolveGuildActivityLogPresentationAssignment(
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('keeps a blocklisted account out of a full rollout', () => {
const assignment = resolveGuildActivityLogPresentationAssignment(
config({
enabled: true,
rollout_basis_points: EXPERIMENT_BUCKET_RESOLUTION,
excluded_user_ids: [USER_ID],
}),
USER_ID,
);
expect(assignment).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('prefers the blocklist over the allowlist', () => {
const assignment = resolveGuildActivityLogPresentationAssignment(
config({enabled: true, included_user_ids: [USER_ID], excluded_user_ids: [USER_ID]}),
USER_ID,
);
expect(assignment.user_targeted).toBe(false);
});
it('targets an account whose bucket falls inside the rollout', () => {
const salt = DEFAULT_GUILD_ACTIVITY_LOG_PRESENTATION_CONFIG.rollout_salt;
const bucket = experimentBucket(USER_ID, salt);
const inside = resolveGuildActivityLogPresentationAssignment(
config({enabled: true, rollout_basis_points: bucket + 1}),
USER_ID,
);
const outside = resolveGuildActivityLogPresentationAssignment(
config({enabled: true, rollout_basis_points: bucket}),
USER_ID,
);
expect(inside).toEqual({enabled: true, config_version: 0, user_targeted: true, source: 'canary'});
expect(outside).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('separates two accounts under the same salt', () => {
const salt = DEFAULT_GUILD_ACTIVITY_LOG_PRESENTATION_CONFIG.rollout_salt;
expect(experimentBucket(USER_ID, salt)).not.toBe(experimentBucket(OTHER_USER_ID, salt));
});
it('moves an account when the salt changes', () => {
expect(experimentBucket(USER_ID, 'guild-activity-log-presentation-v1')).not.toBe(
experimentBucket(USER_ID, 'guild-activity-log-presentation-v2'),
);
});
});
@@ -1,122 +0,0 @@
// 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);
});
});
@@ -1,98 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
import {
DEFAULT_MESSAGE_HOVER_TRACKING_CONFIG,
MessageHoverTrackingConfigSchema,
resolveMessageHoverTrackingAssignment,
} from '@fluxer/schema/src/domains/experiment/MessageHoverTrackingSchemas';
import {describe, expect, it} from 'vitest';
const USER_ID = '1485064866382176262';
const OTHER_USER_ID = '1485064866382176263';
function config(overrides: Partial<ReturnType<typeof MessageHoverTrackingConfigSchema.parse>> = {}) {
return MessageHoverTrackingConfigSchema.parse({...overrides});
}
describe('message hover tracking configuration', () => {
it('defaults to a disabled rollout that targets nobody', () => {
expect(DEFAULT_MESSAGE_HOVER_TRACKING_CONFIG).toEqual({
enabled: false,
config_version: 0,
rollout_basis_points: 0,
rollout_salt: 'message-hover-tracking-v1',
included_user_ids: [],
excluded_user_ids: [],
});
});
it('refuses a rollout wider than the bucket resolution', () => {
expect(() => config({rollout_basis_points: EXPERIMENT_BUCKET_RESOLUTION + 1})).toThrow();
});
it('refuses a target that is not a snowflake', () => {
expect(() => config({included_user_ids: ['not-a-snowflake']})).toThrow();
});
});
describe('message hover tracking resolution', () => {
it('reports the stored revision while the rollout is disabled', () => {
const assignment = resolveMessageHoverTrackingAssignment(config({config_version: 3}), USER_ID);
expect(assignment).toEqual({enabled: false, config_version: 3, user_targeted: false, source: null});
});
it('targets an allowlisted account regardless of its bucket', () => {
const assignment = resolveMessageHoverTrackingAssignment(
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('keeps a blocklisted account out of a full rollout', () => {
const assignment = resolveMessageHoverTrackingAssignment(
config({
enabled: true,
rollout_basis_points: EXPERIMENT_BUCKET_RESOLUTION,
excluded_user_ids: [USER_ID],
}),
USER_ID,
);
expect(assignment).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('prefers the blocklist over the allowlist', () => {
const assignment = resolveMessageHoverTrackingAssignment(
config({enabled: true, included_user_ids: [USER_ID], excluded_user_ids: [USER_ID]}),
USER_ID,
);
expect(assignment.user_targeted).toBe(false);
});
it('targets an account whose bucket falls inside the rollout', () => {
const salt = DEFAULT_MESSAGE_HOVER_TRACKING_CONFIG.rollout_salt;
const bucket = experimentBucket(USER_ID, salt);
const inside = resolveMessageHoverTrackingAssignment(
config({enabled: true, rollout_basis_points: bucket + 1}),
USER_ID,
);
const outside = resolveMessageHoverTrackingAssignment(
config({enabled: true, rollout_basis_points: bucket}),
USER_ID,
);
expect(inside).toEqual({enabled: true, config_version: 0, user_targeted: true, source: 'canary'});
expect(outside).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('separates two accounts under the same salt', () => {
const salt = DEFAULT_MESSAGE_HOVER_TRACKING_CONFIG.rollout_salt;
expect(experimentBucket(USER_ID, salt)).not.toBe(experimentBucket(OTHER_USER_ID, salt));
});
it('moves an account when the salt changes', () => {
expect(experimentBucket(USER_ID, 'message-hover-tracking-v1')).not.toBe(
experimentBucket(USER_ID, 'message-hover-tracking-v2'),
);
});
});
@@ -1,98 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
import {
DEFAULT_MESSAGE_KEYBOARD_FOCUS_CONFIG,
MessageKeyboardFocusConfigSchema,
resolveMessageKeyboardFocusAssignment,
} from '@fluxer/schema/src/domains/experiment/MessageKeyboardFocusSchemas';
import {describe, expect, it} from 'vitest';
const USER_ID = '1485064866382176262';
const OTHER_USER_ID = '1485064866382176263';
function config(overrides: Partial<ReturnType<typeof MessageKeyboardFocusConfigSchema.parse>> = {}) {
return MessageKeyboardFocusConfigSchema.parse({...overrides});
}
describe('message keyboard focus configuration', () => {
it('defaults to a disabled rollout that targets nobody', () => {
expect(DEFAULT_MESSAGE_KEYBOARD_FOCUS_CONFIG).toEqual({
enabled: false,
config_version: 0,
rollout_basis_points: 0,
rollout_salt: 'message-keyboard-focus-v1',
included_user_ids: [],
excluded_user_ids: [],
});
});
it('refuses a rollout wider than the bucket resolution', () => {
expect(() => config({rollout_basis_points: EXPERIMENT_BUCKET_RESOLUTION + 1})).toThrow();
});
it('refuses a target that is not a snowflake', () => {
expect(() => config({included_user_ids: ['not-a-snowflake']})).toThrow();
});
});
describe('message keyboard focus resolution', () => {
it('reports the stored revision while the rollout is disabled', () => {
const assignment = resolveMessageKeyboardFocusAssignment(config({config_version: 3}), USER_ID);
expect(assignment).toEqual({enabled: false, config_version: 3, user_targeted: false, source: null});
});
it('targets an allowlisted account regardless of its bucket', () => {
const assignment = resolveMessageKeyboardFocusAssignment(
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('keeps a blocklisted account out of a full rollout', () => {
const assignment = resolveMessageKeyboardFocusAssignment(
config({
enabled: true,
rollout_basis_points: EXPERIMENT_BUCKET_RESOLUTION,
excluded_user_ids: [USER_ID],
}),
USER_ID,
);
expect(assignment).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('prefers the blocklist over the allowlist', () => {
const assignment = resolveMessageKeyboardFocusAssignment(
config({enabled: true, included_user_ids: [USER_ID], excluded_user_ids: [USER_ID]}),
USER_ID,
);
expect(assignment.user_targeted).toBe(false);
});
it('targets an account whose bucket falls inside the rollout', () => {
const salt = DEFAULT_MESSAGE_KEYBOARD_FOCUS_CONFIG.rollout_salt;
const bucket = experimentBucket(USER_ID, salt);
const inside = resolveMessageKeyboardFocusAssignment(
config({enabled: true, rollout_basis_points: bucket + 1}),
USER_ID,
);
const outside = resolveMessageKeyboardFocusAssignment(
config({enabled: true, rollout_basis_points: bucket}),
USER_ID,
);
expect(inside).toEqual({enabled: true, config_version: 0, user_targeted: true, source: 'canary'});
expect(outside).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
});
it('separates two accounts under the same salt', () => {
const salt = DEFAULT_MESSAGE_KEYBOARD_FOCUS_CONFIG.rollout_salt;
expect(experimentBucket(USER_ID, salt)).not.toBe(experimentBucket(OTHER_USER_ID, salt));
});
it('moves an account when the salt changes', () => {
expect(experimentBucket(USER_ID, 'message-keyboard-focus-v1')).not.toBe(
experimentBucket(USER_ID, 'message-keyboard-focus-v2'),
);
});
});
@@ -1,102 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
DEFAULT_TYPING_INDICATOR_REWORK_CONFIG,
INERT_TYPING_INDICATOR_REWORK_ASSIGNMENT,
isTypingIndicatorReworkTargeted,
resolveTypingIndicatorReworkAssignment,
type TypingIndicatorReworkConfig,
TypingIndicatorReworkConfigSchema,
} from '@fluxer/schema/src/domains/experiment/TypingIndicatorReworkSchemas';
import {describe, expect, it} from 'vitest';
const USER_ID = '1485064866382176262';
const OTHER_USER_ID = '1485064866382176263';
function config(overrides: Partial<TypingIndicatorReworkConfig> = {}): TypingIndicatorReworkConfig {
return {...DEFAULT_TYPING_INDICATOR_REWORK_CONFIG, ...overrides};
}
describe('TypingIndicatorReworkConfigSchema', () => {
it('defaults to a disabled rollout that targets nobody', () => {
expect(DEFAULT_TYPING_INDICATOR_REWORK_CONFIG).toEqual({
enabled: false,
config_version: 0,
rollout_basis_points: 0,
rollout_salt: 'typing-indicator-rework-v1',
included_user_ids: [],
excluded_user_ids: [],
});
});
it('rejects a rollout share above the bucket resolution', () => {
expect(() => TypingIndicatorReworkConfigSchema.parse({rollout_basis_points: 10001})).toThrow();
});
it('rejects a target that is not a snowflake', () => {
expect(() => TypingIndicatorReworkConfigSchema.parse({included_user_ids: ['not-a-snowflake']})).toThrow();
});
});
describe('resolveTypingIndicatorReworkAssignment', () => {
it('reports the inert assignment with the stored revision while the rollout is off', () => {
const assignment = resolveTypingIndicatorReworkAssignment(
config({enabled: false, config_version: 4, rollout_basis_points: 10000}),
USER_ID,
);
expect(assignment).toEqual({...INERT_TYPING_INDICATOR_REWORK_ASSIGNMENT, config_version: 4});
expect(isTypingIndicatorReworkTargeted(assignment)).toBe(false);
});
it('targets every account at the full rollout share', () => {
const assignment = resolveTypingIndicatorReworkAssignment(
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(isTypingIndicatorReworkTargeted(assignment)).toBe(true);
});
it('targets no account at a zero rollout share', () => {
const assignment = resolveTypingIndicatorReworkAssignment(config({enabled: true}), USER_ID);
expect(assignment).toEqual({enabled: true, config_version: 0, user_targeted: false, source: null});
expect(isTypingIndicatorReworkTargeted(assignment)).toBe(false);
});
it('targets an allowlisted account outside the sampled share', () => {
const assignment = resolveTypingIndicatorReworkAssignment(
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 = resolveTypingIndicatorReworkAssignment(
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});
expect(
resolveTypingIndicatorReworkAssignment(config({enabled: true, rollout_basis_points: 10000}), OTHER_USER_ID),
).toEqual({enabled: true, config_version: 0, user_targeted: true, source: 'canary'});
});
it('resolves the same account the same way on every call', () => {
const rollout = config({enabled: true, rollout_basis_points: 5000});
const first = resolveTypingIndicatorReworkAssignment(rollout, USER_ID);
const second = resolveTypingIndicatorReworkAssignment(rollout, USER_ID);
expect(first).toEqual(second);
});
});