mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(app): gate reworked typing indicators behind an experiment (#2782)
This commit is contained in:
@@ -47,6 +47,10 @@ 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,
|
||||
@@ -660,6 +664,7 @@ export const InstanceConfigResponse = z.object({
|
||||
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,
|
||||
@@ -702,6 +707,7 @@ export const InstanceConfigUpdateRequest = z.object({
|
||||
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(),
|
||||
|
||||
@@ -28,6 +28,10 @@ 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;
|
||||
@@ -72,6 +76,7 @@ const ExperimentAssignmentsSchema = z.object({
|
||||
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({
|
||||
@@ -129,3 +134,9 @@ export function readGuildHeaderCollapseAssignment(
|
||||
): 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createClientBehaviorExperiment} from '@fluxer/schema/src/domains/experiment/ClientBehaviorExperiment';
|
||||
import type {z} from 'zod';
|
||||
|
||||
const 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;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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