mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(experiments): ship seven treatments to everyone (#2798)
This commit is contained in:
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user