mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(experiments): target rollouts by guild and premium status (#3012)
This commit is contained in:
@@ -8,9 +8,11 @@ import {
|
||||
DEFAULT_ALTCHA_CAPTCHA_CONFIG,
|
||||
resolveAltchaCaptchaAssignment,
|
||||
} from '@fluxer/schema/src/domains/admin/AltchaCaptchaSchemas';
|
||||
import {experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {type ExperimentTargeting, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const NO_TARGETING: ExperimentTargeting = {memberGuildIds: new Set(), premium: false};
|
||||
|
||||
const TARGETED_USER_ID = '1000000000000000001';
|
||||
|
||||
function createConfig(overrides: Partial<AltchaCaptchaConfig> = {}): AltchaCaptchaConfig {
|
||||
@@ -30,6 +32,8 @@ describe('altcha captcha configuration', () => {
|
||||
rollout_salt: 'altcha-captcha-v1',
|
||||
included_user_ids: [],
|
||||
excluded_user_ids: [],
|
||||
included_guild_ids: [],
|
||||
include_premium_users: false,
|
||||
anonymous_enabled: false,
|
||||
cost: 5000,
|
||||
max_counter: 10000,
|
||||
@@ -48,7 +52,7 @@ describe('altcha captcha configuration', () => {
|
||||
describe('resolveAltchaCaptchaAssignment', () => {
|
||||
test('serves nobody while disabled, even included users', () => {
|
||||
const config = createConfig({rollout_basis_points: 10000, included_user_ids: [TARGETED_USER_ID]});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID)).toEqual({enabled: false});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('applies exclusions before inclusions', () => {
|
||||
@@ -57,18 +61,18 @@ describe('resolveAltchaCaptchaAssignment', () => {
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID)).toEqual({enabled: false});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('serves included users at zero rollout', () => {
|
||||
const config = createConfig({enabled: true, included_user_ids: [TARGETED_USER_ID]});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID)).toEqual({enabled: true});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: true});
|
||||
});
|
||||
|
||||
test('buckets the rollout by salt and user id', () => {
|
||||
const config = createConfig({enabled: true, rollout_basis_points: 2500});
|
||||
for (const userId of syntheticUserIds(200)) {
|
||||
expect(resolveAltchaCaptchaAssignment(config, userId).enabled).toBe(
|
||||
expect(resolveAltchaCaptchaAssignment(config, userId, NO_TARGETING).enabled).toBe(
|
||||
experimentBucket(userId, config.rollout_salt) < 2500,
|
||||
);
|
||||
}
|
||||
@@ -77,13 +81,65 @@ describe('resolveAltchaCaptchaAssignment', () => {
|
||||
|
||||
describe('altchaCaptchaAppliesTo', () => {
|
||||
test('serves anonymous requests only when anonymous_enabled is set', () => {
|
||||
expect(altchaCaptchaAppliesTo(createConfig({enabled: true}), null)).toBe(false);
|
||||
expect(altchaCaptchaAppliesTo(createConfig({enabled: true, anonymous_enabled: true}), null)).toBe(true);
|
||||
expect(altchaCaptchaAppliesTo(createConfig({anonymous_enabled: true}), null)).toBe(false);
|
||||
expect(altchaCaptchaAppliesTo(createConfig({enabled: true}), null, NO_TARGETING)).toBe(false);
|
||||
expect(altchaCaptchaAppliesTo(createConfig({enabled: true, anonymous_enabled: true}), null, NO_TARGETING)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(altchaCaptchaAppliesTo(createConfig({anonymous_enabled: true}), null, NO_TARGETING)).toBe(false);
|
||||
});
|
||||
|
||||
test('keeps signed-in users on their own bucket regardless of the anonymous switch', () => {
|
||||
const config = createConfig({enabled: true, anonymous_enabled: true, excluded_user_ids: [TARGETED_USER_ID]});
|
||||
expect(altchaCaptchaAppliesTo(config, TARGETED_USER_ID)).toBe(false);
|
||||
expect(altchaCaptchaAppliesTo(config, TARGETED_USER_ID, NO_TARGETING)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveAltchaCaptchaAssignment guild targeting', () => {
|
||||
const INCLUDED_GUILD_ID = '3000000000000000001';
|
||||
const MEMBER_GUILDS: ExperimentTargeting = {
|
||||
memberGuildIds: new Set(['3000000000000000009', INCLUDED_GUILD_ID]),
|
||||
premium: false,
|
||||
};
|
||||
|
||||
test('serves members of an included guild at zero rollout', () => {
|
||||
const config = createConfig({enabled: true, included_guild_ids: [INCLUDED_GUILD_ID]});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: true});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('keeps user exclusions ahead of guild membership', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
included_guild_ids: [INCLUDED_GUILD_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('serves no guild members while disabled', () => {
|
||||
const config = createConfig({included_guild_ids: [INCLUDED_GUILD_ID]});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: false});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveAltchaCaptchaAssignment premium targeting', () => {
|
||||
const PREMIUM: ExperimentTargeting = {memberGuildIds: new Set(), premium: true};
|
||||
|
||||
test('serves premium users only when the switch is on', () => {
|
||||
const on = createConfig({enabled: true, include_premium_users: true});
|
||||
expect(resolveAltchaCaptchaAssignment(on, TARGETED_USER_ID, PREMIUM)).toEqual({enabled: true});
|
||||
expect(resolveAltchaCaptchaAssignment(on, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
expect(resolveAltchaCaptchaAssignment(createConfig({enabled: true}), TARGETED_USER_ID, PREMIUM)).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps user exclusions ahead of the premium switch', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
include_premium_users: true,
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveAltchaCaptchaAssignment(config, TARGETED_USER_ID, PREMIUM)).toEqual({enabled: false});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {
|
||||
EXPERIMENT_BUCKET_RESOLUTION,
|
||||
type ExperimentTargeting,
|
||||
experimentAudienceIncludes,
|
||||
experimentBucket,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {z} from 'zod';
|
||||
|
||||
const ALTCHA_CAPTCHA_ROLLOUT_BASIS_POINTS_MAX = EXPERIMENT_BUCKET_RESOLUTION;
|
||||
@@ -23,6 +28,8 @@ const altchaCaptchaConfigFields = {
|
||||
rollout_basis_points: z.number().int().min(0).max(ALTCHA_CAPTCHA_ROLLOUT_BASIS_POINTS_MAX),
|
||||
rollout_salt: z.string().trim().min(1).max(64).regex(ALTCHA_CAPTCHA_SALT_PATTERN),
|
||||
included_user_ids: AltchaCaptchaTargetedUserIdsSchema,
|
||||
included_guild_ids: AltchaCaptchaTargetedUserIdsSchema,
|
||||
include_premium_users: z.boolean(),
|
||||
excluded_user_ids: AltchaCaptchaTargetedUserIdsSchema,
|
||||
anonymous_enabled: z.boolean(),
|
||||
cost: z.number().int().min(ALTCHA_CAPTCHA_MIN_COST).max(ALTCHA_CAPTCHA_MAX_COST),
|
||||
@@ -35,6 +42,8 @@ export const AltchaCaptchaConfigSchema = z.object({
|
||||
rollout_basis_points: altchaCaptchaConfigFields.rollout_basis_points.default(0),
|
||||
rollout_salt: altchaCaptchaConfigFields.rollout_salt.default(DEFAULT_ALTCHA_CAPTCHA_SALT),
|
||||
included_user_ids: altchaCaptchaConfigFields.included_user_ids.default([]),
|
||||
included_guild_ids: altchaCaptchaConfigFields.included_guild_ids.default([]),
|
||||
include_premium_users: altchaCaptchaConfigFields.include_premium_users.default(false),
|
||||
excluded_user_ids: altchaCaptchaConfigFields.excluded_user_ids.default([]),
|
||||
anonymous_enabled: altchaCaptchaConfigFields.anonymous_enabled.default(false),
|
||||
cost: altchaCaptchaConfigFields.cost.default(5000),
|
||||
@@ -69,14 +78,20 @@ export const INERT_ALTCHA_CAPTCHA_ASSIGNMENT: AltchaCaptchaAssignmentResponse =
|
||||
export function resolveAltchaCaptchaAssignment(
|
||||
config: AltchaCaptchaConfig,
|
||||
userId: string,
|
||||
targeting: ExperimentTargeting,
|
||||
): AltchaCaptchaAssignmentResponse {
|
||||
if (!config.enabled) return {...INERT_ALTCHA_CAPTCHA_ASSIGNMENT};
|
||||
if (config.excluded_user_ids.includes(userId)) return {...INERT_ALTCHA_CAPTCHA_ASSIGNMENT};
|
||||
if (config.included_user_ids.includes(userId)) return {enabled: true};
|
||||
if (experimentAudienceIncludes(config, targeting)) return {enabled: true};
|
||||
return {enabled: experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points};
|
||||
}
|
||||
|
||||
export function altchaCaptchaAppliesTo(config: AltchaCaptchaConfig, userId: string | null): boolean {
|
||||
export function altchaCaptchaAppliesTo(
|
||||
config: AltchaCaptchaConfig,
|
||||
userId: string | null,
|
||||
targeting: ExperimentTargeting,
|
||||
): boolean {
|
||||
if (userId === null) return config.enabled && config.anonymous_enabled;
|
||||
return resolveAltchaCaptchaAssignment(config, userId).enabled;
|
||||
return resolveAltchaCaptchaAssignment(config, userId, targeting).enabled;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ import {
|
||||
resolveDomainMigrationAssignment,
|
||||
toDomainMigrationDiscovery,
|
||||
} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas';
|
||||
import {experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {type ExperimentTargeting, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const NO_TARGETING: ExperimentTargeting = {memberGuildIds: new Set(), premium: false};
|
||||
|
||||
const TARGETED_USER_ID = '1000000000000000001';
|
||||
const OTHER_USER_ID = '1000000000000000002';
|
||||
|
||||
@@ -35,7 +37,7 @@ function syntheticUserIds(count: number): Array<string> {
|
||||
function targetedUserIds(config: DomainMigrationConfig, userIds: ReadonlyArray<string>): Set<string> {
|
||||
const targeted = new Set<string>();
|
||||
for (const userId of userIds) {
|
||||
if (resolveDomainMigrationAssignment(config, userId).enabled) {
|
||||
if (resolveDomainMigrationAssignment(config, userId, NO_TARGETING).enabled) {
|
||||
targeted.add(userId);
|
||||
}
|
||||
}
|
||||
@@ -110,17 +112,19 @@ describe('resolveDomainMigrationAssignment', () => {
|
||||
rollout_basis_points: 10000,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID)).toEqual(INERT_DOMAIN_MIGRATION_ASSIGNMENT);
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual(
|
||||
INERT_DOMAIN_MIGRATION_ASSIGNMENT,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns the inert assignment for the default config', () => {
|
||||
expect(resolveDomainMigrationAssignment(createConfig(), TARGETED_USER_ID)).toEqual(
|
||||
expect(resolveDomainMigrationAssignment(createConfig(), TARGETED_USER_ID, NO_TARGETING)).toEqual(
|
||||
INERT_DOMAIN_MIGRATION_ASSIGNMENT,
|
||||
);
|
||||
});
|
||||
|
||||
test('never hands back the shared inert object', () => {
|
||||
const assignment = resolveDomainMigrationAssignment(createConfig(), TARGETED_USER_ID);
|
||||
const assignment = resolveDomainMigrationAssignment(createConfig(), TARGETED_USER_ID, NO_TARGETING);
|
||||
expect(assignment).not.toBe(INERT_DOMAIN_MIGRATION_ASSIGNMENT);
|
||||
});
|
||||
|
||||
@@ -130,7 +134,7 @@ describe('resolveDomainMigrationAssignment', () => {
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID).enabled).toBe(false);
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, NO_TARGETING).enabled).toBe(false);
|
||||
});
|
||||
|
||||
test('denylist beats the bucket', () => {
|
||||
@@ -139,8 +143,8 @@ describe('resolveDomainMigrationAssignment', () => {
|
||||
rollout_basis_points: 10000,
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID).enabled).toBe(false);
|
||||
expect(resolveDomainMigrationAssignment(config, OTHER_USER_ID).enabled).toBe(true);
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, NO_TARGETING).enabled).toBe(false);
|
||||
expect(resolveDomainMigrationAssignment(config, OTHER_USER_ID, NO_TARGETING).enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('allowlist beats the bucket', () => {
|
||||
@@ -149,8 +153,8 @@ describe('resolveDomainMigrationAssignment', () => {
|
||||
rollout_basis_points: 0,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID).enabled).toBe(true);
|
||||
expect(resolveDomainMigrationAssignment(config, OTHER_USER_ID).enabled).toBe(false);
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, NO_TARGETING).enabled).toBe(true);
|
||||
expect(resolveDomainMigrationAssignment(config, OTHER_USER_ID, NO_TARGETING).enabled).toBe(false);
|
||||
});
|
||||
|
||||
test.each([
|
||||
@@ -159,7 +163,7 @@ describe('resolveDomainMigrationAssignment', () => {
|
||||
])('a rollout of $basisPoints basis points targets $enabled', ({basisPoints, enabled}) => {
|
||||
const config = createConfig({enabled: true, rollout_basis_points: basisPoints});
|
||||
for (const userId of syntheticUserIds(200)) {
|
||||
expect(resolveDomainMigrationAssignment(config, userId).enabled).toBe(enabled);
|
||||
expect(resolveDomainMigrationAssignment(config, userId, NO_TARGETING).enabled).toBe(enabled);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -167,13 +171,17 @@ describe('resolveDomainMigrationAssignment', () => {
|
||||
const salt = DEFAULT_DOMAIN_MIGRATION_CONFIG.rollout_salt;
|
||||
const bucket = experimentBucket(TARGETED_USER_ID, salt);
|
||||
expect(
|
||||
resolveDomainMigrationAssignment(createConfig({enabled: true, rollout_basis_points: bucket}), TARGETED_USER_ID)
|
||||
.enabled,
|
||||
resolveDomainMigrationAssignment(
|
||||
createConfig({enabled: true, rollout_basis_points: bucket}),
|
||||
TARGETED_USER_ID,
|
||||
NO_TARGETING,
|
||||
).enabled,
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolveDomainMigrationAssignment(
|
||||
createConfig({enabled: true, rollout_basis_points: bucket + 1}),
|
||||
TARGETED_USER_ID,
|
||||
NO_TARGETING,
|
||||
).enabled,
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -232,3 +240,53 @@ describe('toDomainMigrationDiscovery', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDomainMigrationAssignment guild targeting', () => {
|
||||
const INCLUDED_GUILD_ID = '3000000000000000001';
|
||||
const MEMBER_GUILDS: ExperimentTargeting = {
|
||||
memberGuildIds: new Set(['3000000000000000009', INCLUDED_GUILD_ID]),
|
||||
premium: false,
|
||||
};
|
||||
|
||||
test('serves members of an included guild at zero rollout', () => {
|
||||
const config = createConfig({enabled: true, included_guild_ids: [INCLUDED_GUILD_ID]});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: true});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('keeps user exclusions ahead of guild membership', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
included_guild_ids: [INCLUDED_GUILD_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('serves no guild members while disabled', () => {
|
||||
const config = createConfig({included_guild_ids: [INCLUDED_GUILD_ID]});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: false});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDomainMigrationAssignment premium targeting', () => {
|
||||
const PREMIUM: ExperimentTargeting = {memberGuildIds: new Set(), premium: true};
|
||||
|
||||
test('serves premium users only when the switch is on', () => {
|
||||
const on = createConfig({enabled: true, include_premium_users: true});
|
||||
expect(resolveDomainMigrationAssignment(on, TARGETED_USER_ID, PREMIUM)).toEqual({enabled: true});
|
||||
expect(resolveDomainMigrationAssignment(on, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
expect(resolveDomainMigrationAssignment(createConfig({enabled: true}), TARGETED_USER_ID, PREMIUM)).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps user exclusions ahead of the premium switch', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
include_premium_users: true,
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID, PREMIUM)).toEqual({enabled: false});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {
|
||||
EXPERIMENT_BUCKET_RESOLUTION,
|
||||
type ExperimentTargeting,
|
||||
experimentAudienceIncludes,
|
||||
experimentBucket,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {z} from 'zod';
|
||||
|
||||
const DOMAIN_MIGRATION_ROLLOUT_BASIS_POINTS_MAX = EXPERIMENT_BUCKET_RESOLUTION;
|
||||
@@ -20,6 +25,8 @@ const domainMigrationConfigFields = {
|
||||
rollout_basis_points: z.number().int().min(0).max(DOMAIN_MIGRATION_ROLLOUT_BASIS_POINTS_MAX),
|
||||
rollout_salt: z.string().trim().min(1).max(64).regex(DOMAIN_MIGRATION_SALT_PATTERN),
|
||||
included_user_ids: DomainMigrationTargetedUserIdsSchema,
|
||||
included_guild_ids: DomainMigrationTargetedUserIdsSchema,
|
||||
include_premium_users: z.boolean(),
|
||||
excluded_user_ids: DomainMigrationTargetedUserIdsSchema,
|
||||
anonymous_rollout_basis_points: z.number().int().min(0).max(DOMAIN_MIGRATION_ROLLOUT_BASIS_POINTS_MAX),
|
||||
standalone_forwarding: z.boolean(),
|
||||
@@ -31,6 +38,8 @@ export const DomainMigrationConfigSchema = z.object({
|
||||
rollout_basis_points: domainMigrationConfigFields.rollout_basis_points.default(0),
|
||||
rollout_salt: domainMigrationConfigFields.rollout_salt.default(DEFAULT_DOMAIN_MIGRATION_SALT),
|
||||
included_user_ids: domainMigrationConfigFields.included_user_ids.default([]),
|
||||
included_guild_ids: domainMigrationConfigFields.included_guild_ids.default([]),
|
||||
include_premium_users: domainMigrationConfigFields.include_premium_users.default(false),
|
||||
excluded_user_ids: domainMigrationConfigFields.excluded_user_ids.default([]),
|
||||
anonymous_rollout_basis_points: domainMigrationConfigFields.anonymous_rollout_basis_points.default(0),
|
||||
standalone_forwarding: domainMigrationConfigFields.standalone_forwarding.default(false),
|
||||
@@ -64,10 +73,12 @@ export const INERT_DOMAIN_MIGRATION_ASSIGNMENT: DomainMigrationAssignmentRespons
|
||||
export function resolveDomainMigrationAssignment(
|
||||
config: DomainMigrationConfig,
|
||||
userId: string,
|
||||
targeting: ExperimentTargeting,
|
||||
): DomainMigrationAssignmentResponse {
|
||||
if (!config.enabled) return {...INERT_DOMAIN_MIGRATION_ASSIGNMENT};
|
||||
if (config.excluded_user_ids.includes(userId)) return {...INERT_DOMAIN_MIGRATION_ASSIGNMENT};
|
||||
if (config.included_user_ids.includes(userId)) return {enabled: true};
|
||||
if (experimentAudienceIncludes(config, targeting)) return {enabled: true};
|
||||
return {enabled: experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
ProfileTimezoneConfigUpdateRequest,
|
||||
resolveProfileTimezoneAssignment,
|
||||
} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas';
|
||||
import {experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {type ExperimentTargeting, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const NO_TARGETING: ExperimentTargeting = {memberGuildIds: new Set(), premium: false};
|
||||
|
||||
const TARGETED_USER_ID = '1000000000000000001';
|
||||
|
||||
function createConfig(overrides: Partial<ProfileTimezoneConfig> = {}): ProfileTimezoneConfig {
|
||||
@@ -29,6 +31,8 @@ describe('profile timezone configuration', () => {
|
||||
rollout_salt: 'profile-timezone-v1',
|
||||
included_user_ids: [],
|
||||
excluded_user_ids: [],
|
||||
included_guild_ids: [],
|
||||
include_premium_users: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +45,7 @@ describe('profile timezone configuration', () => {
|
||||
describe('resolveProfileTimezoneAssignment', () => {
|
||||
test('serves nobody while disabled, even included users', () => {
|
||||
const config = createConfig({rollout_basis_points: 10000, included_user_ids: [TARGETED_USER_ID]});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID)).toEqual({enabled: false});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('applies exclusions before inclusions', () => {
|
||||
@@ -50,20 +54,70 @@ describe('resolveProfileTimezoneAssignment', () => {
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID)).toEqual({enabled: false});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('serves included users at zero rollout', () => {
|
||||
const config = createConfig({enabled: true, included_user_ids: [TARGETED_USER_ID]});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID)).toEqual({enabled: true});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: true});
|
||||
});
|
||||
|
||||
test('buckets the rollout by salt and user id', () => {
|
||||
const config = createConfig({enabled: true, rollout_basis_points: 2500});
|
||||
for (const userId of syntheticUserIds(200)) {
|
||||
expect(resolveProfileTimezoneAssignment(config, userId).enabled).toBe(
|
||||
expect(resolveProfileTimezoneAssignment(config, userId, NO_TARGETING).enabled).toBe(
|
||||
experimentBucket(userId, config.rollout_salt) < 2500,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveProfileTimezoneAssignment guild targeting', () => {
|
||||
const INCLUDED_GUILD_ID = '3000000000000000001';
|
||||
const MEMBER_GUILDS: ExperimentTargeting = {
|
||||
memberGuildIds: new Set(['3000000000000000009', INCLUDED_GUILD_ID]),
|
||||
premium: false,
|
||||
};
|
||||
|
||||
test('serves members of an included guild at zero rollout', () => {
|
||||
const config = createConfig({enabled: true, included_guild_ids: [INCLUDED_GUILD_ID]});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: true});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('keeps user exclusions ahead of guild membership', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
included_guild_ids: [INCLUDED_GUILD_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
test('serves no guild members while disabled', () => {
|
||||
const config = createConfig({included_guild_ids: [INCLUDED_GUILD_ID]});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS)).toEqual({enabled: false});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveProfileTimezoneAssignment premium targeting', () => {
|
||||
const PREMIUM: ExperimentTargeting = {memberGuildIds: new Set(), premium: true};
|
||||
|
||||
test('serves premium users only when the switch is on', () => {
|
||||
const on = createConfig({enabled: true, include_premium_users: true});
|
||||
expect(resolveProfileTimezoneAssignment(on, TARGETED_USER_ID, PREMIUM)).toEqual({enabled: true});
|
||||
expect(resolveProfileTimezoneAssignment(on, TARGETED_USER_ID, NO_TARGETING)).toEqual({enabled: false});
|
||||
expect(resolveProfileTimezoneAssignment(createConfig({enabled: true}), TARGETED_USER_ID, PREMIUM)).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps user exclusions ahead of the premium switch', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
include_premium_users: true,
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveProfileTimezoneAssignment(config, TARGETED_USER_ID, PREMIUM)).toEqual({enabled: false});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {
|
||||
EXPERIMENT_BUCKET_RESOLUTION,
|
||||
type ExperimentTargeting,
|
||||
experimentAudienceIncludes,
|
||||
experimentBucket,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {z} from 'zod';
|
||||
|
||||
const PROFILE_TIMEZONE_ROLLOUT_BASIS_POINTS_MAX = EXPERIMENT_BUCKET_RESOLUTION;
|
||||
@@ -20,6 +25,8 @@ const profileTimezoneConfigFields = {
|
||||
rollout_basis_points: z.number().int().min(0).max(PROFILE_TIMEZONE_ROLLOUT_BASIS_POINTS_MAX),
|
||||
rollout_salt: z.string().trim().min(1).max(64).regex(PROFILE_TIMEZONE_SALT_PATTERN),
|
||||
included_user_ids: ProfileTimezoneTargetedUserIdsSchema,
|
||||
included_guild_ids: ProfileTimezoneTargetedUserIdsSchema,
|
||||
include_premium_users: z.boolean(),
|
||||
excluded_user_ids: ProfileTimezoneTargetedUserIdsSchema,
|
||||
};
|
||||
|
||||
@@ -29,6 +36,8 @@ export const ProfileTimezoneConfigSchema = z.object({
|
||||
rollout_basis_points: profileTimezoneConfigFields.rollout_basis_points.default(0),
|
||||
rollout_salt: profileTimezoneConfigFields.rollout_salt.default(DEFAULT_PROFILE_TIMEZONE_SALT),
|
||||
included_user_ids: profileTimezoneConfigFields.included_user_ids.default([]),
|
||||
included_guild_ids: profileTimezoneConfigFields.included_guild_ids.default([]),
|
||||
include_premium_users: profileTimezoneConfigFields.include_premium_users.default(false),
|
||||
excluded_user_ids: profileTimezoneConfigFields.excluded_user_ids.default([]),
|
||||
});
|
||||
|
||||
@@ -60,9 +69,11 @@ export const INERT_PROFILE_TIMEZONE_ASSIGNMENT: ProfileTimezoneAssignmentRespons
|
||||
export function resolveProfileTimezoneAssignment(
|
||||
config: ProfileTimezoneConfig,
|
||||
userId: string,
|
||||
targeting: ExperimentTargeting,
|
||||
): ProfileTimezoneAssignmentResponse {
|
||||
if (!config.enabled) return {...INERT_PROFILE_TIMEZONE_ASSIGNMENT};
|
||||
if (config.excluded_user_ids.includes(userId)) return {...INERT_PROFILE_TIMEZONE_ASSIGNMENT};
|
||||
if (config.included_user_ids.includes(userId)) return {enabled: true};
|
||||
if (experimentAudienceIncludes(config, targeting)) return {enabled: true};
|
||||
return {enabled: experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points};
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
VoiceNoiseSuppressionConfigSchema,
|
||||
VoiceNoiseSuppressionConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
|
||||
import {experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {type ExperimentTargeting, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const NO_TARGETING: ExperimentTargeting = {memberGuildIds: new Set(), premium: false};
|
||||
|
||||
const TARGETED_USER_ID = '1000000000000000001';
|
||||
const OTHER_USER_ID = '1000000000000000002';
|
||||
const GUILD_ID = '2000000000000000001';
|
||||
@@ -95,7 +97,7 @@ function syntheticUserIds(count: number): Array<string> {
|
||||
function targetedUserIds(config: VoiceNoiseSuppressionConfig, userIds: ReadonlyArray<string>): Set<string> {
|
||||
const targeted = new Set<string>();
|
||||
for (const userId of userIds) {
|
||||
if (resolveVoiceNoiseSuppressionAssignment(config, userId).user_targeted) {
|
||||
if (resolveVoiceNoiseSuppressionAssignment(config, userId, NO_TARGETING).user_targeted) {
|
||||
targeted.add(userId);
|
||||
}
|
||||
}
|
||||
@@ -139,14 +141,14 @@ describe('resolveVoiceNoiseSuppressionAssignment', () => {
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
guild_overrides: [{guild_id: GUILD_ID, backend: 'rnnoise'}],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID)).toEqual({
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING)).toEqual({
|
||||
...INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
config_version: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns the inert assignment for the default config', () => {
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(createConfig(), TARGETED_USER_ID)).toEqual(
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(createConfig(), TARGETED_USER_ID, NO_TARGETING)).toEqual(
|
||||
INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
);
|
||||
});
|
||||
@@ -157,7 +159,7 @@ describe('resolveVoiceNoiseSuppressionAssignment', () => {
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID);
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING);
|
||||
expect(assignment.user_targeted).toBe(false);
|
||||
expect(assignment.backend).toBeNull();
|
||||
expect(assignment.source).toBeNull();
|
||||
@@ -169,10 +171,10 @@ describe('resolveVoiceNoiseSuppressionAssignment', () => {
|
||||
rollout_basis_points: 10000,
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID);
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING);
|
||||
expect(assignment.user_targeted).toBe(false);
|
||||
expect(assignment.source).toBeNull();
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, OTHER_USER_ID).user_targeted).toBe(true);
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, OTHER_USER_ID, NO_TARGETING).user_targeted).toBe(true);
|
||||
});
|
||||
|
||||
test('denylist strips guild overrides and the client-side knobs', () => {
|
||||
@@ -183,7 +185,7 @@ describe('resolveVoiceNoiseSuppressionAssignment', () => {
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
guild_overrides: [{guild_id: GUILD_ID, backend: 'rnnoise'}],
|
||||
});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID);
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING);
|
||||
expect(assignment.guild_overrides).toEqual([]);
|
||||
expect(assignment.enabled_backends).toEqual([]);
|
||||
expect(assignment.allow_user_override).toBe(false);
|
||||
@@ -196,7 +198,7 @@ describe('resolveVoiceNoiseSuppressionAssignment', () => {
|
||||
rollout_basis_points: 0,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID);
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING);
|
||||
expect(assignment.user_targeted).toBe(true);
|
||||
expect(assignment.backend).toBe('gtcrn');
|
||||
expect(assignment.source).toBe('user_rule');
|
||||
@@ -208,7 +210,7 @@ describe('resolveVoiceNoiseSuppressionAssignment', () => {
|
||||
])('canary at $basisPoints basis points targets $targeted', ({basisPoints, targeted}) => {
|
||||
const config = createConfig({enabled: true, rollout_basis_points: basisPoints});
|
||||
for (const userId of syntheticUserIds(200)) {
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, userId);
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, userId, NO_TARGETING);
|
||||
expect(assignment.user_targeted).toBe(targeted);
|
||||
expect(assignment.source).toBe(targeted ? 'canary' : null);
|
||||
}
|
||||
@@ -247,7 +249,7 @@ describe('resolveVoiceNoiseSuppressionAssignment', () => {
|
||||
included_user_ids: [OTHER_USER_ID],
|
||||
});
|
||||
for (const userId of [TARGETED_USER_ID, OTHER_USER_ID]) {
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, userId);
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, userId, NO_TARGETING);
|
||||
expect(assignment.user_targeted).toBe(false);
|
||||
expect(assignment.backend).toBeNull();
|
||||
expect(assignment.source).toBeNull();
|
||||
@@ -265,7 +267,7 @@ describe('resolveVoiceNoiseSuppressionAssignment', () => {
|
||||
{guild_id: OTHER_GUILD_ID, backend: 'deep_filter'},
|
||||
],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID).guild_overrides).toEqual([
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING).guild_overrides).toEqual([
|
||||
{guild_id: GUILD_ID, backend: 'rnnoise'},
|
||||
]);
|
||||
});
|
||||
@@ -280,14 +282,14 @@ describe('resolveVoiceNoiseSuppressionAssignment', () => {
|
||||
{guild_id: OTHER_GUILD_ID, backend: 'gtcrn'},
|
||||
],
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID).guild_overrides).toEqual([
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING).guild_overrides).toEqual([
|
||||
{guild_id: GUILD_ID, backend: 'rnnoise'},
|
||||
]);
|
||||
});
|
||||
|
||||
test('echoes the config version on every path', () => {
|
||||
const config = createConfig({enabled: true, config_version: 11, rollout_basis_points: 10000});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID).config_version).toBe(11);
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING).config_version).toBe(11);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -412,3 +414,46 @@ describe('resolveVoiceNoiseSuppressionForCall', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveVoiceNoiseSuppressionAssignment guild targeting', () => {
|
||||
const INCLUDED_GUILD_ID = '3000000000000000001';
|
||||
const MEMBER_GUILDS: ExperimentTargeting = {
|
||||
memberGuildIds: new Set(['3000000000000000009', INCLUDED_GUILD_ID]),
|
||||
premium: false,
|
||||
};
|
||||
|
||||
test('targets members of an included guild the way it targets included users', () => {
|
||||
const config = createConfig({enabled: true, included_guild_ids: [INCLUDED_GUILD_ID]});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS);
|
||||
expect(assignment.user_targeted).toBe(true);
|
||||
expect(assignment.source).toBe('user_rule');
|
||||
expect(assignment.backend).toBe(config.default_backend);
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, NO_TARGETING).user_targeted).toBe(false);
|
||||
});
|
||||
|
||||
test('keeps user exclusions ahead of guild membership', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
included_guild_ids: [INCLUDED_GUILD_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
const assignment = resolveVoiceNoiseSuppressionAssignment(config, TARGETED_USER_ID, MEMBER_GUILDS);
|
||||
expect(assignment.user_targeted).toBe(false);
|
||||
expect(assignment.source).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveVoiceNoiseSuppressionAssignment premium targeting', () => {
|
||||
const PREMIUM: ExperimentTargeting = {memberGuildIds: new Set(), premium: true};
|
||||
|
||||
test('targets premium users only when the switch is on', () => {
|
||||
const on = createConfig({enabled: true, include_premium_users: true});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(on, TARGETED_USER_ID, PREMIUM)).toMatchObject({
|
||||
user_targeted: true,
|
||||
source: 'user_rule',
|
||||
});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(on, TARGETED_USER_ID, NO_TARGETING).user_targeted).toBe(false);
|
||||
const off = createConfig({enabled: true});
|
||||
expect(resolveVoiceNoiseSuppressionAssignment(off, TARGETED_USER_ID, PREMIUM).user_targeted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {
|
||||
EXPERIMENT_BUCKET_RESOLUTION,
|
||||
type ExperimentTargeting,
|
||||
experimentAudienceIncludes,
|
||||
experimentBucket,
|
||||
} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const VOICE_NOISE_SUPPRESSION_BACKENDS = [
|
||||
@@ -39,6 +44,8 @@ const voiceConfigFields = {
|
||||
rollout_basis_points: z.number().int().min(0).max(VOICE_NOISE_SUPPRESSION_ROLLOUT_BASIS_POINTS_MAX),
|
||||
rollout_salt: z.string().trim().min(1).max(64),
|
||||
included_user_ids: TargetedUserIdsSchema,
|
||||
included_guild_ids: TargetedUserIdsSchema,
|
||||
include_premium_users: z.boolean(),
|
||||
excluded_user_ids: TargetedUserIdsSchema,
|
||||
guild_overrides: z.array(VoiceNoiseSuppressionGuildOverrideSchema).max(VOICE_NOISE_SUPPRESSION_MAX_GUILD_OVERRIDES),
|
||||
suppression_strength: z.number().int().min(0).max(100),
|
||||
@@ -53,6 +60,8 @@ export const VoiceNoiseSuppressionConfigSchema = z.object({
|
||||
rollout_basis_points: voiceConfigFields.rollout_basis_points.default(0),
|
||||
rollout_salt: voiceConfigFields.rollout_salt.default(DEFAULT_VOICE_NOISE_SUPPRESSION_SALT),
|
||||
included_user_ids: voiceConfigFields.included_user_ids.default([]),
|
||||
included_guild_ids: voiceConfigFields.included_guild_ids.default([]),
|
||||
include_premium_users: voiceConfigFields.include_premium_users.default(false),
|
||||
excluded_user_ids: voiceConfigFields.excluded_user_ids.default([]),
|
||||
guild_overrides: voiceConfigFields.guild_overrides.default([]),
|
||||
suppression_strength: voiceConfigFields.suppression_strength.default(80),
|
||||
@@ -107,6 +116,7 @@ export const INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT: VoiceNoiseSuppressionAssi
|
||||
export function resolveVoiceNoiseSuppressionAssignment(
|
||||
config: VoiceNoiseSuppressionConfig,
|
||||
userId: string,
|
||||
targeting: ExperimentTargeting,
|
||||
): VoiceNoiseSuppressionAssignmentResponse {
|
||||
if (!config.enabled) {
|
||||
return {
|
||||
@@ -136,7 +146,7 @@ export function resolveVoiceNoiseSuppressionAssignment(
|
||||
const guildOverrides = config.guild_overrides.filter((override) =>
|
||||
config.enabled_backends.includes(override.backend),
|
||||
);
|
||||
if (config.included_user_ids.includes(userId)) {
|
||||
if (config.included_user_ids.includes(userId) || experimentAudienceIncludes(config, targeting)) {
|
||||
return {
|
||||
...shared,
|
||||
user_targeted: backendIsUsable,
|
||||
|
||||
@@ -14,3 +14,18 @@ export function experimentBucket(userId: string, salt: string): number {
|
||||
}
|
||||
return hash % EXPERIMENT_BUCKET_RESOLUTION;
|
||||
}
|
||||
|
||||
export interface ExperimentTargeting {
|
||||
readonly memberGuildIds: ReadonlySet<string>;
|
||||
readonly premium: boolean;
|
||||
}
|
||||
|
||||
interface ExperimentAudienceRules {
|
||||
readonly included_guild_ids: ReadonlyArray<string>;
|
||||
readonly include_premium_users: boolean;
|
||||
}
|
||||
|
||||
export function experimentAudienceIncludes(rules: ExperimentAudienceRules, targeting: ExperimentTargeting): boolean {
|
||||
if (rules.include_premium_users && targeting.premium) return true;
|
||||
return rules.included_guild_ids.some((guildId) => targeting.memberGuildIds.has(guildId));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user