Repository navigation
fix(flags): size flags by recently active persons, not all-time count #92668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c44b598
a387722
c1b3c03
21a9fb6
3a8b062
138263a
0960146
f36c7bd
43c7895
0497dd2
77cbb4e
1ef3da9
a38c04c
a0d7fc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -36,9 +36,10 @@ | |||||||||||
| MultivariateFlagVariant, | ||||||||||||
| PropertyFilterType, | ||||||||||||
| PropertyOperator, | ||||||||||||
| UserBlastRadiusType, | ||||||||||||
| } from '~/types' | ||||||||||||
|
|
||||||||||||
| import { featureFlagsUserBlastRadiusCreate } from 'products/feature_flags/frontend/generated/api' | ||||||||||||
|
|
||||||||||||
| import type { SimpleOption } from '../../lib/components/TaxonomicFilter/types' | ||||||||||||
| import type { Noun } from '../../models/groupsModel' | ||||||||||||
| import type { GroupType } from '../../types' | ||||||||||||
|
|
@@ -172,6 +173,7 @@ | |||||||||||
| aggregationLabel: (groupTypeIndex: number | null | undefined, deferToUserWording?: boolean) => Noun // groupsModel | ||||||||||||
| groupTypes: Map<GroupTypeIndex, GroupType> // groupsModel | ||||||||||||
| currentProjectId: number | null // projectLogic | ||||||||||||
| activityWindowDays: number | null | ||||||||||||
| affectedCounts: Record<string, number | undefined> | ||||||||||||
| aggregationTargetName: (conditionGroupTypeIndex?: number | null | undefined) => string | ||||||||||||
| blastRadiusErrors: Record<string, BlastRadiusError | undefined> | ||||||||||||
|
|
@@ -252,6 +254,9 @@ | |||||||||||
| activeId: string | ||||||||||||
| overId: string | ||||||||||||
| } | ||||||||||||
| setActivityWindowDays: (activityWindowDays: number | null) => { | ||||||||||||
| activityWindowDays: number | null | ||||||||||||
| } | ||||||||||||
| setAffectedCount: ( | ||||||||||||
| sortKey: string, | ||||||||||||
| count?: number | ||||||||||||
|
|
@@ -341,7 +346,7 @@ | |||||||||||
| filters: FeatureFlagFilters & { | ||||||||||||
| groups: FeatureFlagGroupTypeWithSortKey[] | ||||||||||||
| }, | ||||||||||||
| aggregationLabel: (groupTypeIndex: number | null | undefined, deferToUserWording?: boolean) => Noun | ||||||||||||
| aggregationLabel: (groupTypeIndex: number | null | undefined, deferToUserWording?: boolean) => Noun // groupsModel | ||||||||||||
| ) => (conditionGroupTypeIndex?: number | null | undefined) => string | ||||||||||||
| taxonomicGroupTypesForCondition: ( | ||||||||||||
| filters: FeatureFlagFilters & { | ||||||||||||
|
|
@@ -430,6 +435,7 @@ | |||||||||||
| count, | ||||||||||||
| }), | ||||||||||||
| setBlastRadiusError: (sortKey: string, error: BlastRadiusError) => ({ sortKey, error }), | ||||||||||||
| setActivityWindowDays: (activityWindowDays: number | null) => ({ activityWindowDays }), | ||||||||||||
| calculateBlastRadius: true, | ||||||||||||
| calculateBlastRadiusForCondition: ( | ||||||||||||
| sortKey: string, | ||||||||||||
|
|
@@ -659,6 +665,14 @@ | |||||||||||
| }), | ||||||||||||
| }, | ||||||||||||
| ], | ||||||||||||
| // The basis depends on the project, not the condition, so every response carries the same | ||||||||||||
| // value and one reducer covers all of them. | ||||||||||||
| activityWindowDays: [ | ||||||||||||
| null as number | null, | ||||||||||||
| { | ||||||||||||
| setActivityWindowDays: (_, { activityWindowDays }) => activityWindowDays, | ||||||||||||
| }, | ||||||||||||
| ], | ||||||||||||
| // Tracks conditions whose blast-radius estimate failed, keyed by sort_key. Holds the | ||||||||||||
| // caught error so the UI can explain why (and decide whether a retry is worth offering) | ||||||||||||
| // instead of just distinguishing failure from the still-loading (undefined) state. | ||||||||||||
|
|
@@ -855,15 +869,13 @@ | |||||||||||
| actions.setTotalCount(sortKey, undefined) | ||||||||||||
|
|
||||||||||||
| try { | ||||||||||||
| const response: UserBlastRadiusType = await api.create( | ||||||||||||
| `api/projects/${values.currentProjectId}/feature_flags/user_blast_radius`, | ||||||||||||
| { | ||||||||||||
| condition: { properties }, | ||||||||||||
| group_type_index: groupTypeIndex, | ||||||||||||
| } | ||||||||||||
| ) | ||||||||||||
| const response = await featureFlagsUserBlastRadiusCreate(String(values.currentProjectId), { | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Deduplicating in the logic removes the duplicate calls rather than the duplicate scans, and also covers two groups that happen to carry identical filters. Key an in-flight promise map on the request body and share the pending promise. |
||||||||||||
| condition: { properties }, | ||||||||||||
| group_type_index: groupTypeIndex, | ||||||||||||
| }) | ||||||||||||
| actions.setAffectedCount(sortKey, response.affected) | ||||||||||||
| actions.setTotalCount(sortKey, response.total) | ||||||||||||
| actions.setActivityWindowDays(response.activity_window_days ?? null) | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The backend reports the window per request, not per project.
Suggested change
Keying by |
||||||||||||
| } catch (error) { | ||||||||||||
| // Keep the caught error so the UI can show what went wrong and hide a retry that | ||||||||||||
| // can't help, rather than masking it as -1, which the render path can't tell | ||||||||||||
|
|
@@ -1130,7 +1142,7 @@ | |||||||||||
| computeBlastRadiusPercentage: [ | ||||||||||||
| (s) => [s.affectedCounts, s.totalCounts], | ||||||||||||
| (affectedCounts: Record<string, number | undefined>, totalCounts: Record<string, number | undefined>) => | ||||||||||||
| (rolloutPercentage, sortKey) => { | ||||||||||||
| let effectiveRolloutPercentage = rolloutPercentage | ||||||||||||
| if ( | ||||||||||||
| rolloutPercentage === undefined || | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -638,6 +638,23 @@ class SessionContextsSustainedRateThrottle(_TeamBucketRateThrottle): | |
| rate = "600/hour" | ||
|
|
||
|
|
||
| # Flag sizing scans a 60-day events window once per condition group, and the release-conditions | ||
| # editor fans one request out per group on every mount with no dedup. That editor is the only real | ||
| # caller, and the ClickHouse*RateThrottle pair does not cover it: those derive from | ||
| # PersonalApiKeyRateThrottle, which lets an authenticated request carrying no personal API key | ||
| # through before counting it. A team bucket caps the project's scan spend whatever the auth method. | ||
| # Repeat empty-properties requests are served from a 5-minute cache, so a legitimate session needs | ||
| # one cold scan per distinct condition group. | ||
| class FlagSizingBurstRateThrottle(_TeamBucketRateThrottle): | ||
| scope = "flag_sizing_burst" | ||
| rate = "60/minute" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On master The last two lines of the comment justify the rate by pointing at the 5-minute cache, but that cache sits inside Request volume per project is not readable from the repo, so the limit's actual impact isn't obvious without production data. Your call between raising the sustained rate toward what shipped, keeping a per-key bucket for API callers alongside the team bucket that covers session callers, or recording the 8x cut in the PR description so it reads as a decision rather than a side effect. |
||
|
|
||
|
|
||
| class FlagSizingSustainedRateThrottle(_TeamBucketRateThrottle): | ||
| scope = "flag_sizing_sustained" | ||
| rate = "600/hour" | ||
|
|
||
|
|
||
| # Feature flag request usage scans up to 31 days of billing events in ClickHouse. Its primary | ||
| # caller is the session-authenticated feature flags UI, which the generic ClickHouse throttle | ||
| # pair does not cover. Use a team-wide bucket so users and API keys share one query budget. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: 'Filters match: ~N' and the 'View matching users' link right below it now disagree. The count excludes matching persons with no event in the last 60 days, butmatchingActorsUrlbuilds the persons list from the condition's property filters alone, with no activity window, so the link lists every matching person including the ones the count just excluded. An author who clicks through to check the number finds more people than the panel said.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, and not fixed here.
matchingActorsUrlbuilds the persons list from the property filters alone, so windowing it means adding an activity filter to both the URL and the persons query. That is its own change, so it is called out as a known gap in the PR description rather than left for a reviewer to rediscover.