1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-13 13:48:59 +02:00

Merge branch 'main' into docs/organize-feature-flags-projects

# Conflicts:
#	website/sidebars.ts
This commit is contained in:
melindafekete 2025-07-23 16:48:44 +02:00
commit 3cf9ccecf3
No known key found for this signature in database
131 changed files with 1778 additions and 488 deletions

View File

@ -350,31 +350,21 @@ export const ChangeRequestRequestedApprovers: FC<{
saveClicked={saveClicked}
/>
)}
{reviewers.map((reviewer) => (
<>
{reviewer.status === 'approved' && (
<ChangeRequestApprover
key={reviewer.name}
name={reviewer.name || 'Unknown user'}
imageUrl={reviewer.imageUrl}
/>
)}
{reviewer.status === 'rejected' && (
<ChangeRequestRejector
key={reviewer.name}
name={reviewer.name || 'Unknown user'}
imageUrl={reviewer.imageUrl}
/>
)}
{reviewer.status === 'pending' && (
<ChangeRequestPending
key={reviewer.name}
name={reviewer.name || 'Unknown user'}
imageUrl={reviewer.imageUrl}
/>
)}
</>
))}
{reviewers.map((reviewer) => {
const key = reviewer.id;
const props = {
name: reviewer.name || 'Unknown user',
imageUrl: reviewer.imageUrl,
};
switch (reviewer.status) {
case 'approved':
return <ChangeRequestApprover key={key} {...props} />;
case 'rejected':
return <ChangeRequestRejector key={key} {...props} />;
case 'pending':
return <ChangeRequestPending key={key} {...props} />;
}
})}
</Paper>
);
};

View File

@ -8,12 +8,16 @@ import {
TextField,
Box,
styled,
useTheme,
useMediaQuery,
Divider,
} from '@mui/material';
import { ImpactMetricsControls } from './ImpactMetricsControls/ImpactMetricsControls.tsx';
import { ImpactMetricsChartPreview } from './ImpactMetricsChartPreview.tsx';
import { useChartFormState } from './hooks/useChartFormState.ts';
import type { ChartConfig } from './types.ts';
import { useChartFormState } from '../hooks/useChartFormState.ts';
import type { ChartConfig } from '../types.ts';
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
import { LabelsFilter } from './LabelFilter/LabelsFilter.tsx';
import { ImpactMetricsChart } from '../ImpactMetricsChart.tsx';
export const StyledConfigPanel = styled(Box)(({ theme }) => ({
display: 'flex',
@ -62,6 +66,8 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({
open,
initialConfig,
});
const theme = useTheme();
const screenBreakpoint = useMediaQuery(theme.breakpoints.down('lg'));
const handleSave = () => {
if (!isValid) return;
@ -111,21 +117,33 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({
actions={actions}
metricSeries={metricSeries}
loading={loading}
availableLabels={currentAvailableLabels}
/>
</StyledConfigPanel>
<StyledPreviewPanel>
<ImpactMetricsChartPreview
selectedSeries={formData.selectedSeries}
selectedRange={formData.selectedRange}
selectedLabels={formData.selectedLabels}
beginAtZero={formData.beginAtZero}
showRate={formData.showRate}
/>
<Box sx={(theme) => ({ padding: theme.spacing(1) })}>
<ImpactMetricsChart
key={screenBreakpoint ? 'small' : 'large'}
selectedSeries={formData.selectedSeries}
selectedRange={formData.selectedRange}
selectedLabels={formData.selectedLabels}
beginAtZero={formData.beginAtZero}
aggregationMode={formData.aggregationMode}
isPreview
/>
</Box>
</StyledPreviewPanel>
</Box>
{currentAvailableLabels ? (
<LabelsFilter
selectedLabels={formData.selectedLabels}
onChange={actions.setSelectedLabels}
availableLabels={currentAvailableLabels}
/>
) : null}
</DialogContent>
<DialogActions>
<Divider />
<DialogActions sx={(theme) => ({ margin: theme.spacing(2, 3, 3) })}>
<Button onClick={onClose}>Cancel</Button>
<Button
onClick={handleSave}

View File

@ -0,0 +1,81 @@
import type { FC } from 'react';
import { Box, Typography, FormControlLabel, Checkbox } from '@mui/material';
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
import { SeriesSelector } from './SeriesSelector/SeriesSelector.tsx';
import { RangeSelector } from './RangeSelector/RangeSelector.tsx';
import { ModeSelector } from './ModeSelector/ModeSelector.tsx';
import type { ChartFormState } from '../../hooks/useChartFormState.ts';
import { getMetricType } from '../../utils.ts';
export type ImpactMetricsControlsProps = {
formData: ChartFormState['formData'];
actions: Pick<
ChartFormState['actions'],
| 'handleSeriesChange'
| 'setSelectedRange'
| 'setBeginAtZero'
| 'setSelectedLabels'
| 'setAggregationMode'
>;
metricSeries: (ImpactMetricsSeries & { name: string })[];
loading?: boolean;
};
export const ImpactMetricsControls: FC<ImpactMetricsControlsProps> = ({
formData,
actions,
metricSeries,
loading,
}) => (
<Box>
<Box
sx={(theme) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(3),
})}
>
<Typography variant='body2' color='text.secondary'>
Select a custom metric to see its value over time. This can help
you understand the impact of your feature rollout on key
outcomes, such as system performance, usage patterns or error
rates.
</Typography>
<SeriesSelector
value={formData.selectedSeries}
onChange={actions.handleSeriesChange}
options={metricSeries}
loading={loading}
/>
{formData.selectedSeries ? (
<>
<RangeSelector
value={formData.selectedRange}
onChange={actions.setSelectedRange}
/>
<ModeSelector
value={formData.aggregationMode}
onChange={actions.setAggregationMode}
seriesType={getMetricType(formData.selectedSeries)!}
/>
</>
) : null}
</Box>
{formData.selectedSeries ? (
<FormControlLabel
sx={(theme) => ({ margin: theme.spacing(1.5, 0) })}
control={
<Checkbox
checked={formData.beginAtZero}
onChange={(e) =>
actions.setBeginAtZero(e.target.checked)
}
/>
}
label='Begin at zero'
/>
) : null}
</Box>
);

View File

@ -0,0 +1,46 @@
import type { FC } from 'react';
import { FormControl, InputLabel, Select, MenuItem } from '@mui/material';
import type { AggregationMode } from '../../../types.ts';
export type ModeSelectorProps = {
value: AggregationMode;
onChange: (mode: AggregationMode) => void;
seriesType: 'counter' | 'gauge' | 'unknown';
};
export const ModeSelector: FC<ModeSelectorProps> = ({
value,
onChange,
seriesType,
}) => {
if (seriesType === 'unknown') return null;
return (
<FormControl variant='outlined' size='small' sx={{ minWidth: 200 }}>
<InputLabel id='mode-select-label'>Mode</InputLabel>
<Select
labelId='mode-select-label'
value={value}
onChange={(e) => onChange(e.target.value as AggregationMode)}
label='Mode'
>
{seriesType === 'counter'
? [
<MenuItem key='rps' value='rps'>
Rate per second
</MenuItem>,
<MenuItem key='count' value='count'>
Count
</MenuItem>,
]
: [
<MenuItem key='avg' value='avg'>
Average
</MenuItem>,
<MenuItem key='sum' value='sum'>
Sum
</MenuItem>,
]}
</Select>
</FormControl>
);
};

View File

@ -47,6 +47,7 @@ export const SeriesSelector: FC<SeriesSelectorProps> = ({
placeholder='Search for a metric…'
variant='outlined'
size='small'
required
/>
)}
noOptionsText='No metrics available'

View File

@ -0,0 +1,104 @@
import {
Autocomplete,
Box,
Checkbox,
Chip,
FormControlLabel,
TextField,
} from '@mui/material';
import type { FC } from 'react';
type LabelFilterItemProps = {
labelKey: string;
options: string[];
value: string[];
onChange: (values: string[]) => void;
handleAllToggle: (labelKey: string, checked: boolean) => void;
};
export const LabelFilterItem: FC<LabelFilterItemProps> = ({
labelKey,
options,
value,
onChange,
}) => {
const isAllSelected = value.includes('*');
const autocompleteId = `autocomplete-${labelKey}`;
const selectAllId = `select-all-${labelKey}`;
return (
<>
<FormControlLabel
sx={(theme) => ({
marginLeft: theme.spacing(0),
})}
control={
<Checkbox
id={selectAllId}
size='small'
checked={isAllSelected}
onChange={(e) =>
onChange(e.target.checked ? ['*'] : [])
}
inputProps={{
'aria-describedby': autocompleteId,
'aria-label': `Select all ${labelKey} options`,
}}
/>
}
label={
<Box
component='span'
sx={(theme) => ({
fontSize: theme.fontSizes.smallBody,
})}
>
Select all
</Box>
}
/>
<Autocomplete
multiple
id={autocompleteId}
options={options}
value={isAllSelected ? options : value}
onChange={(_, newValues) => {
onChange(newValues);
}}
disabled={isAllSelected}
renderTags={(value, getTagProps) =>
value.map((option, index) => {
const { key, ...chipProps } = getTagProps({
index,
});
return (
<Chip
{...chipProps}
key={key}
label={option}
size='small'
/>
);
})
}
renderInput={(params) => (
<TextField
{...params}
label={labelKey}
placeholder={
isAllSelected ? undefined : 'Select values…'
}
variant='outlined'
size='small'
inputProps={{
...params.inputProps,
'aria-describedby': isAllSelected
? `${selectAllId}-description`
: undefined,
}}
/>
)}
/>
</>
);
};

View File

@ -0,0 +1,102 @@
import type { FC } from 'react';
import { Box, Typography, Chip } from '@mui/material';
import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import { LabelFilterItem } from './LabelFilterItem/LabelFilterItem.tsx';
export type LabelsFilterProps = {
selectedLabels: Record<string, string[]>;
onChange: (labels: Record<string, string[]>) => void;
availableLabels: ImpactMetricsLabels;
};
export const LabelsFilter: FC<LabelsFilterProps> = ({
selectedLabels,
onChange,
availableLabels,
}) => {
const handleLabelChange = (labelKey: string, values: string[]) => {
const newLabels = { ...selectedLabels };
if (values.length === 0) {
delete newLabels[labelKey];
} else {
newLabels[labelKey] = values;
}
onChange(newLabels);
};
const handleAllToggle = (labelKey: string, checked: boolean) => {
const newLabels = { ...selectedLabels };
if (checked) {
newLabels[labelKey] = ['*'];
} else {
delete newLabels[labelKey];
}
onChange(newLabels);
};
const clearAllLabels = () => {
onChange({});
};
if (!availableLabels || Object.keys(availableLabels).length === 0) {
return null;
}
return (
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
width: '100%',
}}
>
<Typography variant='subtitle2'>Filter by labels</Typography>
{Object.keys(selectedLabels).length > 0 && (
<Chip
label='Clear all'
size='small'
variant='outlined'
onClick={clearAllLabels}
/>
)}
</Box>
<Box
sx={{
display: 'grid',
gridTemplateColumns:
'repeat(auto-fill, minmax(300px, 1fr))',
gap: 2,
flexGrow: 1,
}}
>
{Object.entries(availableLabels).map(([labelKey, values]) => {
const currentSelection = selectedLabels[labelKey] || [];
return (
<Box
key={labelKey}
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}}
>
<LabelFilterItem
labelKey={labelKey}
options={values}
value={currentSelection}
onChange={(newValues) =>
handleLabelChange(labelKey, newValues)
}
handleAllToggle={handleAllToggle}
/>
</Box>
);
})}
</Box>
</Box>
);
};

View File

@ -21,8 +21,14 @@ const getConfigDescription = (config: DisplayChartConfig): string => {
parts.push(`last ${config.selectedRange}`);
if (config.showRate) {
if (config.aggregationMode === 'rps') {
parts.push('rate per second');
} else if (config.aggregationMode === 'count') {
parts.push('count');
} else if (config.aggregationMode === 'avg') {
parts.push('average');
} else if (config.aggregationMode === 'sum') {
parts.push('sum');
}
const labelCount = Object.keys(config.selectedLabels).length;
@ -127,7 +133,7 @@ export const ChartItem: FC<ChartItemProps> = ({ config, onEdit, onDelete }) => (
selectedRange={config.selectedRange}
selectedLabels={config.selectedLabels}
beginAtZero={config.beginAtZero}
showRate={config.showRate}
aggregationMode={config.aggregationMode}
aspectRatio={1.5}
overrideOptions={{ maintainAspectRatio: false }}
emptyDataDescription='Send impact metrics using Unleash SDK for this series to view the chart.'

View File

@ -4,7 +4,7 @@ import { Typography, Button, Paper, styled } from '@mui/material';
import Add from '@mui/icons-material/Add';
import { PageHeader } from 'component/common/PageHeader/PageHeader.tsx';
import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
import { ChartConfigModal } from './ChartConfigModal.tsx';
import { ChartConfigModal } from './ChartConfigModal/ChartConfigModal.tsx';
import { ChartItem } from './ChartItem.tsx';
import { GridLayoutWrapper, type GridItem } from './GridLayoutWrapper.tsx';
import { useImpactMetricsState } from './hooks/useImpactMetricsState.ts';

View File

@ -10,13 +10,14 @@ import { usePlaceholderData } from '../insights/hooks/usePlaceholderData.js';
import { getDisplayFormat, getTimeUnit, formatLargeNumbers } from './utils.ts';
import { fromUnixTime } from 'date-fns';
import { useChartData } from './hooks/useChartData.ts';
import type { AggregationMode } from './types.ts';
type ImpactMetricsChartProps = {
selectedSeries: string;
selectedRange: 'hour' | 'day' | 'week' | 'month';
selectedLabels: Record<string, string[]>;
beginAtZero: boolean;
showRate?: boolean;
aggregationMode?: AggregationMode;
aspectRatio?: number;
overrideOptions?: Record<string, unknown>;
errorTitle?: string;
@ -30,7 +31,7 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
selectedRange,
selectedLabels,
beginAtZero,
showRate,
aggregationMode,
aspectRatio,
overrideOptions = {},
errorTitle = 'Failed to load impact metrics.',
@ -47,7 +48,7 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
? {
series: selectedSeries,
range: selectedRange,
showRate,
aggregationMode,
labels:
Object.keys(selectedLabels).length > 0
? selectedLabels
@ -61,7 +62,7 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
type: 'constant',
});
const data = useChartData(timeSeriesData);
const data = useChartData(timeSeriesData, debug?.query);
const hasError = !!dataError;
const isLoading = dataLoading;
@ -121,14 +122,17 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
y: {
beginAtZero,
title: {
display: !!showRate,
text: showRate ? 'Rate per second' : '',
display: aggregationMode === 'rps',
text:
aggregationMode === 'rps'
? 'Rate per second'
: '',
},
ticks: {
precision: 0,
callback: (value: unknown): string | number =>
typeof value === 'number'
? `${formatLargeNumbers(value)}${showRate ? '/s' : ''}`
? `${formatLargeNumbers(value)}${aggregationMode === 'rps' ? '/s' : ''}`
: (value as number),
},
},

View File

@ -1,43 +0,0 @@
import type { FC } from 'react';
import { Typography } from '@mui/material';
import { StyledChartContainer } from 'component/insights/InsightsCharts.styles';
import { ImpactMetricsChart } from './ImpactMetricsChart.tsx';
type ImpactMetricsChartPreviewProps = {
selectedSeries: string;
selectedRange: 'hour' | 'day' | 'week' | 'month';
selectedLabels: Record<string, string[]>;
beginAtZero: boolean;
showRate?: boolean;
};
export const ImpactMetricsChartPreview: FC<ImpactMetricsChartPreviewProps> = ({
selectedSeries,
selectedRange,
selectedLabels,
beginAtZero,
showRate,
}) => (
<>
<Typography variant='h6' color='text.secondary'>
Preview
</Typography>
{!selectedSeries ? (
<Typography variant='body2' color='text.secondary'>
Select a metric series to view the preview
</Typography>
) : null}
<StyledChartContainer>
<ImpactMetricsChart
selectedSeries={selectedSeries}
selectedRange={selectedRange}
selectedLabels={selectedLabels}
beginAtZero={beginAtZero}
showRate={showRate}
isPreview
/>
</StyledChartContainer>
</>
);

View File

@ -1,93 +0,0 @@
import type { FC } from 'react';
import { Box, Typography, FormControlLabel, Checkbox } from '@mui/material';
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import { SeriesSelector } from './components/SeriesSelector.tsx';
import { RangeSelector } from './components/RangeSelector.tsx';
import { LabelsFilter } from './components/LabelsFilter.tsx';
import type { ChartFormState } from '../hooks/useChartFormState.ts';
export type ImpactMetricsControlsProps = {
formData: ChartFormState['formData'];
actions: Pick<
ChartFormState['actions'],
| 'handleSeriesChange'
| 'setSelectedRange'
| 'setBeginAtZero'
| 'setSelectedLabels'
| 'setShowRate'
>;
metricSeries: (ImpactMetricsSeries & { name: string })[];
loading?: boolean;
availableLabels?: ImpactMetricsLabels;
};
export const ImpactMetricsControls: FC<ImpactMetricsControlsProps> = ({
formData,
actions,
metricSeries,
loading,
availableLabels,
}) => (
<Box
sx={(theme) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(3),
maxWidth: 400,
})}
>
<Typography variant='body2' color='text.secondary'>
Select a custom metric to see its value over time. This can help you
understand the impact of your feature rollout on key outcomes, such
as system performance, usage patterns or error rates.
</Typography>
<SeriesSelector
value={formData.selectedSeries}
onChange={actions.handleSeriesChange}
options={metricSeries}
loading={loading}
/>
<RangeSelector
value={formData.selectedRange}
onChange={actions.setSelectedRange}
/>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<FormControlLabel
control={
<Checkbox
checked={formData.beginAtZero}
onChange={(e) =>
actions.setBeginAtZero(e.target.checked)
}
/>
}
label='Begin at zero'
/>
{formData.selectedSeries.startsWith('unleash_counter_') ? (
<FormControlLabel
control={
<Checkbox
checked={formData.showRate}
onChange={(e) =>
actions.setShowRate(e.target.checked)
}
/>
}
label='Show rate per second'
/>
) : null}
</Box>
{availableLabels && (
<LabelsFilter
selectedLabels={formData.selectedLabels}
onChange={actions.setSelectedLabels}
availableLabels={availableLabels}
/>
)}
</Box>
);

View File

@ -1,86 +0,0 @@
import type { FC } from 'react';
import { Box, Autocomplete, TextField, Typography, Chip } from '@mui/material';
import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
export type LabelsFilterProps = {
selectedLabels: Record<string, string[]>;
onChange: (labels: Record<string, string[]>) => void;
availableLabels: ImpactMetricsLabels;
};
export const LabelsFilter: FC<LabelsFilterProps> = ({
selectedLabels,
onChange,
availableLabels,
}) => {
const handleLabelChange = (labelKey: string, values: string[]) => {
const newLabels = { ...selectedLabels };
if (values.length === 0) {
delete newLabels[labelKey];
} else {
newLabels[labelKey] = values;
}
onChange(newLabels);
};
const clearAllLabels = () => {
onChange({});
};
if (!availableLabels || Object.keys(availableLabels).length === 0) {
return null;
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant='subtitle2'>Filter by labels</Typography>
{Object.keys(selectedLabels).length > 0 && (
<Chip
label='Clear all'
size='small'
variant='outlined'
onClick={clearAllLabels}
/>
)}
</Box>
{Object.entries(availableLabels).map(([labelKey, values]) => (
<Autocomplete
key={labelKey}
multiple
options={values}
value={selectedLabels[labelKey] || []}
onChange={(_, newValues) =>
handleLabelChange(labelKey, newValues)
}
renderTags={(value, getTagProps) =>
value.map((option, index) => {
const { key, ...chipProps } = getTagProps({
index,
});
return (
<Chip
{...chipProps}
key={key}
label={option}
size='small'
/>
);
})
}
renderInput={(params) => (
<TextField
{...params}
label={labelKey}
placeholder='Select values...'
variant='outlined'
size='small'
/>
)}
sx={{ minWidth: 300 }}
/>
))}
</Box>
);
};

View File

@ -1,14 +1,31 @@
import { useMemo } from 'react';
import { useTheme } from '@mui/material';
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import { useSeriesColor } from './useSeriesColor.ts';
import { getSeriesLabel } from '../utils.ts';
const getColorStartingIndex = (modulo: number, series?: string): number => {
if (!series || series.length === 0 || modulo <= 0) {
return 0;
}
// https://stackoverflow.com/a/7616484/1729641
let hash = 0;
for (let i = 0; i < series.length; i++) {
const char = series.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash) % modulo;
};
export const useChartData = (
timeSeriesData: ImpactMetricsSeries[] | undefined,
colorIndexBy?: string,
) => {
const theme = useTheme();
const getSeriesColor = useSeriesColor();
const colors = theme.palette.charts.series;
const startColorIndex = getColorStartingIndex(colors.length, colorIndexBy);
return useMemo(() => {
if (!timeSeriesData || timeSeriesData.length === 0) {
@ -66,9 +83,9 @@ export const useChartData = (
(timestamp) => new Date(timestamp * 1000),
);
const datasets = timeSeriesData.map((series) => {
const datasets = timeSeriesData.map((series, index) => {
const seriesLabel = getSeriesLabel(series.metric);
const color = getSeriesColor(seriesLabel);
const color = colors[(index + startColorIndex) % colors.length];
const dataMap = new Map(series.data);
@ -90,5 +107,5 @@ export const useChartData = (
datasets,
};
}
}, [timeSeriesData, theme, getSeriesColor]);
}, [timeSeriesData, theme]);
};

View File

@ -1,7 +1,8 @@
import { useState, useEffect } from 'react';
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import type { ChartConfig } from '../types.ts';
import type { AggregationMode, ChartConfig } from '../types.ts';
import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import { getMetricType } from '../utils.ts';
type UseChartConfigParams = {
open: boolean;
@ -14,7 +15,7 @@ export type ChartFormState = {
selectedSeries: string;
selectedRange: 'hour' | 'day' | 'week' | 'month';
beginAtZero: boolean;
showRate: boolean;
aggregationMode: AggregationMode;
selectedLabels: Record<string, string[]>;
};
actions: {
@ -22,7 +23,7 @@ export type ChartFormState = {
setSelectedSeries: (series: string) => void;
setSelectedRange: (range: 'hour' | 'day' | 'week' | 'month') => void;
setBeginAtZero: (beginAtZero: boolean) => void;
setShowRate: (showRate: boolean) => void;
setAggregationMode: (mode: AggregationMode) => void;
setSelectedLabels: (labels: Record<string, string[]>) => void;
handleSeriesChange: (series: string) => void;
getConfigToSave: () => Omit<ChartConfig, 'id'>;
@ -48,7 +49,12 @@ export const useChartFormState = ({
const [selectedLabels, setSelectedLabels] = useState<
Record<string, string[]>
>(initialConfig?.selectedLabels || {});
const [showRate, setShowRate] = useState(initialConfig?.showRate || false);
const [aggregationMode, setAggregationMode] = useState<AggregationMode>(
(initialConfig?.aggregationMode || getMetricType(selectedSeries)) ===
'counter'
? 'count'
: 'avg',
);
const {
data: { labels: currentAvailableLabels },
@ -57,7 +63,7 @@ export const useChartFormState = ({
? {
series: selectedSeries,
range: selectedRange,
showRate,
aggregationMode,
}
: undefined,
);
@ -69,20 +75,31 @@ export const useChartFormState = ({
setSelectedRange(initialConfig.selectedRange);
setBeginAtZero(initialConfig.beginAtZero);
setSelectedLabels(initialConfig.selectedLabels);
setShowRate(initialConfig.showRate || false);
setAggregationMode(
initialConfig.aggregationMode ||
(getMetricType(initialConfig.selectedSeries) === 'counter'
? 'count'
: 'avg'),
);
} else if (open && !initialConfig) {
setTitle('');
setSelectedSeries('');
setSelectedRange('day');
setBeginAtZero(false);
setSelectedLabels({});
setShowRate(false);
setAggregationMode('count');
}
}, [open, initialConfig]);
const handleSeriesChange = (series: string) => {
setSelectedSeries(series);
setSelectedLabels({});
const metric = getMetricType(series);
if (metric === 'counter') {
setAggregationMode('count');
} else if (metric === 'gauge') {
setAggregationMode('avg');
}
};
const getConfigToSave = (): Omit<ChartConfig, 'id'> => ({
@ -91,7 +108,7 @@ export const useChartFormState = ({
selectedRange,
beginAtZero,
selectedLabels,
showRate,
aggregationMode,
});
const isValid = selectedSeries.length > 0;
@ -102,7 +119,7 @@ export const useChartFormState = ({
selectedSeries,
selectedRange,
beginAtZero,
showRate,
aggregationMode,
selectedLabels,
},
actions: {
@ -110,7 +127,7 @@ export const useChartFormState = ({
setSelectedSeries,
setSelectedRange,
setBeginAtZero,
setShowRate,
setAggregationMode,
setSelectedLabels,
handleSeriesChange,
getConfigToSave,

View File

@ -37,7 +37,7 @@ const TestComponent: FC<{
selectedSeries: 'test-series',
selectedRange: 'day',
beginAtZero: true,
showRate: false,
aggregationMode: 'count',
selectedLabels: {},
title: 'Test Chart',
})
@ -79,7 +79,7 @@ const mockSettings: ImpactMetricsState = {
selectedSeries: 'test-series',
selectedRange: 'day' as const,
beginAtZero: true,
showRate: false,
aggregationMode: 'count',
selectedLabels: {},
title: 'Test Chart',
},
@ -182,7 +182,7 @@ describe('useImpactMetricsState', () => {
selectedSeries: 'test-series',
selectedRange: 'day',
beginAtZero: true,
showRate: false,
mode: 'count',
selectedLabels: {},
title: 'Test Chart',
},
@ -215,7 +215,7 @@ describe('useImpactMetricsState', () => {
selectedSeries: 'test-series',
selectedRange: 'day',
beginAtZero: true,
showRate: false,
mode: 'count',
selectedLabels: {},
title: 'Test Chart',
},

View File

@ -1,17 +0,0 @@
import { useTheme } from '@mui/material';
export const useSeriesColor = () => {
const theme = useTheme();
const colors = theme.palette.charts.series;
return (seriesLabel: string): string => {
let hash = 0;
for (let i = 0; i < seriesLabel.length; i++) {
const char = seriesLabel.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
const index = Math.abs(hash) % colors.length;
return colors[index];
};
};

View File

@ -3,11 +3,13 @@ export type ChartConfig = {
selectedSeries: string; // e.g. unleash_counter_my_metric
selectedRange: 'hour' | 'day' | 'week' | 'month';
beginAtZero: boolean;
showRate: boolean;
aggregationMode: AggregationMode;
selectedLabels: Record<string, string[]>;
title?: string;
};
export type AggregationMode = 'rps' | 'count' | 'avg' | 'sum';
export type DisplayChartConfig = ChartConfig & {
type: 'counter' | 'gauge';
displayName: string; // e.g. my_metric with unleash_counter stripped

View File

@ -60,3 +60,9 @@ export const formatLargeNumbers = (value: number): string => {
}
return value.toString();
};
export const getMetricType = (seriesName: string) => {
if (seriesName.startsWith('unleash_counter_')) return 'counter';
if (seriesName.startsWith('unleash_gauge_')) return 'gauge';
return 'unknown';
};

View File

@ -25,7 +25,7 @@ export type ImpactMetricsQuery = {
series: string;
range: 'hour' | 'day' | 'week' | 'month';
labels?: Record<string, string[]>;
showRate?: boolean;
aggregationMode?: 'rps' | 'count' | 'avg' | 'sum';
};
export const useImpactMetricsData = (query?: ImpactMetricsQuery) => {
@ -38,8 +38,8 @@ export const useImpactMetricsData = (query?: ImpactMetricsQuery) => {
range: query.range,
});
if (query.showRate !== undefined) {
params.append('showRate', query.showRate.toString());
if (query.aggregationMode !== undefined) {
params.append('aggregationMode', query.aggregationMode);
}
if (query.labels && Object.keys(query.labels).length > 0) {

View File

@ -254,9 +254,12 @@ test('should not backfill for existing lifecycle', async () => {
await featureLifecycleStore.backfill();
const { body } = await getFeatureLifecycle('my_feature_e');
expect(body).toEqual([
{ stage: 'initial', enteredStageAt: expect.any(String) },
{ stage: 'pre-live', enteredStageAt: expect.any(String) },
{ stage: 'live', enteredStageAt: expect.any(String) },
]);
expect(body).toEqual(
expect.arrayContaining([
{ stage: 'initial', enteredStageAt: expect.any(String) },
{ stage: 'pre-live', enteredStageAt: expect.any(String) },
{ stage: 'live', enteredStageAt: expect.any(String) },
]),
);
expect(body).toHaveLength(3);
});

View File

@ -32,6 +32,7 @@ import {
MetricsTranslator,
} from '../impact/metrics-translator.js';
import { impactRegister } from '../impact/impact-register.js';
import type { UnknownFlag } from '../unknown-flags/unknown-flags-store.js';
export default class ClientMetricsServiceV2 {
private config: IUnleashConfig;
@ -178,12 +179,79 @@ export default class ClientMetricsServiceV2 {
return toggleNames;
}
private async siftMetrics(
metrics: IClientMetricsEnv[],
): Promise<IClientMetricsEnv[]> {
if (!metrics.length) return [];
const metricsByToggle = new Map<string, IClientMetricsEnv[]>();
for (const m of metrics) {
if (m.yes === 0 && m.no === 0) continue;
let arr = metricsByToggle.get(m.featureName);
if (!arr) {
arr = [];
metricsByToggle.set(m.featureName, arr);
}
arr.push(m);
}
if (metricsByToggle.size === 0) return [];
const toggleNames = Array.from(metricsByToggle.keys());
const { validatedToggleNames, unknownToggleNames } =
await this.filterExistingToggleNames(toggleNames);
const validatedSet = new Set(validatedToggleNames);
const unknownSet = new Set(unknownToggleNames);
const invalidCount = toggleNames.length - validatedSet.size;
this.logger.debug(
`Got ${toggleNames.length} metrics (${invalidCount > 0 ? `${invalidCount} invalid` : 'all valid'}).`,
);
const unknownFlags: UnknownFlag[] = [];
for (const [featureName, group] of metricsByToggle) {
if (unknownSet.has(featureName)) {
for (const m of group) {
unknownFlags.push({
name: featureName,
appName: m.appName,
seenAt: m.timestamp,
environment: m.environment,
});
}
}
}
if (unknownFlags.length) {
const sample = unknownFlags
.slice(0, 10)
.map((f) => `"${f.name}"`)
.join(', ');
this.logger.debug(
`Registering ${unknownFlags.length} unknown flags; sample: ${sample}`,
);
this.unknownFlagsService.register(unknownFlags);
}
const siftedMetrics: IClientMetricsEnv[] = [];
for (const [featureName, group] of metricsByToggle) {
if (validatedSet.has(featureName)) {
siftedMetrics.push(...group);
}
}
return siftedMetrics;
}
async registerBulkMetrics(metrics: IClientMetricsEnv[]): Promise<void> {
const siftedMetrics = await this.siftMetrics(metrics);
if (siftedMetrics.length === 0) return;
this.unsavedMetrics = collapseHourlyMetrics([
...this.unsavedMetrics,
...metrics,
...siftedMetrics,
]);
this.lastSeenService.updateLastSeen(metrics);
this.lastSeenService.updateLastSeen(siftedMetrics);
this.config.eventBus.emit(CLIENT_METRICS, siftedMetrics);
}
async registerImpactMetrics(impactMetrics: Metric[]) {
@ -202,22 +270,6 @@ export default class ClientMetricsServiceV2 {
clientIp: string,
): Promise<void> {
const value = await clientMetricsSchema.validateAsync(data);
const toggleNames = Object.keys(value.bucket.toggles).filter(
(name) =>
!(
value.bucket.toggles[name].yes === 0 &&
value.bucket.toggles[name].no === 0
),
);
const { validatedToggleNames, unknownToggleNames } =
await this.filterExistingToggleNames(toggleNames);
const invalidToggleNames =
toggleNames.length - validatedToggleNames.length;
this.logger.debug(
`Got ${toggleNames.length} (${invalidToggleNames > 0 ? `${invalidToggleNames} invalid ones` : 'all valid'}) metrics from ${value.appName}`,
);
if (data.sdkVersion) {
const [sdkName, sdkVersion] = data.sdkVersion.split(':');
@ -235,39 +287,20 @@ export default class ClientMetricsServiceV2 {
this.config.eventBus.emit(CLIENT_REGISTER, heartbeatEvent);
}
const environment = value.environment ?? 'default';
const clientMetrics: IClientMetricsEnv[] = Object.keys(
value.bucket.toggles,
).map((name) => ({
featureName: name,
appName: value.appName,
environment: value.environment ?? 'default',
timestamp: value.bucket.stop, //we might need to approximate between start/stop...
yes: value.bucket.toggles[name].yes ?? 0,
no: value.bucket.toggles[name].no ?? 0,
variants: value.bucket.toggles[name].variants,
}));
if (unknownToggleNames.length > 0) {
const unknownFlags = unknownToggleNames.map((name) => ({
name,
appName: value.appName,
seenAt: value.bucket.stop,
environment,
}));
this.logger.debug(
`Registering ${unknownFlags.length} unknown flags from ${value.appName} in the ${environment} environment. Some of the unknown flag names include: ${unknownFlags
.slice(0, 10)
.map(({ name }) => `"${name}"`)
.join(', ')}`,
);
this.unknownFlagsService.register(unknownFlags);
}
if (validatedToggleNames.length > 0) {
const clientMetrics: IClientMetricsEnv[] = validatedToggleNames.map(
(name) => ({
featureName: name,
appName: value.appName,
environment,
timestamp: value.bucket.stop, //we might need to approximate between start/stop...
yes: value.bucket.toggles[name].yes ?? 0,
no: value.bucket.toggles[name].no ?? 0,
variants: value.bucket.toggles[name].variants,
}),
);
if (clientMetrics.length) {
await this.registerBulkMetrics(clientMetrics);
this.config.eventBus.emit(CLIENT_METRICS, clientMetrics);
}
}

View File

@ -414,6 +414,11 @@ describe('bulk metrics', () => {
test('without access to production environment due to no auth setup, we can only access the default env', async () => {
const now = new Date();
// @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2
services.clientMetricsServiceV2.cachedFeatureNames = vi
.fn<() => Promise<string[]>>()
.mockResolvedValue(['test_feature_one', 'test_feature_two']);
await request
.post('/api/client/metrics/bulk')
.send({

View File

@ -23,7 +23,6 @@ import {
customMetricsSchema,
} from '../shared/schema.js';
import type { IClientMetricsEnv } from '../client-metrics/client-metrics-store-v2-type.js';
import { CLIENT_METRICS } from '../../../events/index.js';
import type { CustomMetricsSchema } from '../../../openapi/spec/custom-metrics-schema.js';
import type { StoredCustomMetric } from '../custom/custom-metrics-store.js';
import type { CustomMetricsService } from '../custom/custom-metrics-service.js';
@ -276,7 +275,6 @@ export default class ClientMetricsController extends Controller {
promises.push(
this.metricsV2.registerBulkMetrics(filteredData),
);
this.config.eventBus.emit(CLIENT_METRICS, data);
}
if (

View File

@ -0,0 +1,184 @@
import supertest, { type Test } from 'supertest';
import getApp from '../../../app.js';
import { createTestConfig } from '../../../../test/config/test-config.js';
import {
type IUnleashServices,
createServices,
} from '../../../services/index.js';
import type {
IUnleashConfig,
IUnleashOptions,
IUnleashStores,
} from '../../../types/index.js';
import dbInit, {
type ITestDb,
} from '../../../../test/e2e/helpers/database-init.js';
import { startOfHour } from 'date-fns';
import type TestAgent from 'supertest/lib/agent.d.ts';
import type { BulkRegistrationSchema } from '../../../openapi/index.js';
import type { EventEmitter } from 'stream';
import { CLIENT_METRICS } from '../../../events/index.js';
let db: ITestDb;
let config: IUnleashConfig;
let eventBus: EventEmitter;
async function getSetup(opts?: IUnleashOptions) {
config = createTestConfig(opts);
db = await dbInit('unknown_flags', config.getLogger);
const services = createServices(db.stores, config, db.rawDatabase);
const app = await getApp(config, db.stores, services);
config.eventBus.emit = vi.fn();
return {
request: supertest(app),
stores: db.stores,
services,
db: db.rawDatabase,
destroy: db.destroy,
eventBus: config.eventBus,
};
}
let request: TestAgent<Test>;
let stores: IUnleashStores;
let services: IUnleashServices;
let destroy: () => Promise<void>;
beforeAll(async () => {
const setup = await getSetup({
experimental: {
flags: {
reportUnknownFlags: true,
},
},
});
request = setup.request;
stores = setup.stores;
destroy = setup.destroy;
services = setup.services;
eventBus = setup.eventBus;
});
afterAll(async () => {
await destroy();
});
afterEach(async () => {
await stores.unknownFlagsStore.deleteAll();
});
describe('should register unknown flags', () => {
test('/metrics endpoint', async () => {
// @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2
services.clientMetricsServiceV2.cachedFeatureNames = vi
.fn<() => Promise<string[]>>()
.mockResolvedValue(['existing_flag']);
await request
.post('/api/client/metrics')
.send({
appName: 'demo',
instanceId: '1',
bucket: {
start: Date.now(),
stop: Date.now(),
toggles: {
existing_flag: {
yes: 200,
no: 0,
},
unknown_flag: {
yes: 100,
no: 50,
},
},
},
})
.expect(202);
await services.unknownFlagsService.flush();
const unknownFlags = await services.unknownFlagsService.getAll();
expect(unknownFlags).toHaveLength(1);
expect(unknownFlags[0]).toMatchObject({
name: 'unknown_flag',
environment: 'development',
appName: 'demo',
seenAt: expect.any(Date),
});
expect(eventBus.emit).toHaveBeenCalledWith(
CLIENT_METRICS,
expect.arrayContaining([
expect.objectContaining({
featureName: 'existing_flag',
yes: 200,
}),
]),
);
});
test('/metrics/bulk endpoint', async () => {
// @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2
services.clientMetricsServiceV2.cachedFeatureNames = vi
.fn<() => Promise<string[]>>()
.mockResolvedValue(['existing_flag_bulk']);
const unknownFlag: BulkRegistrationSchema = {
appName: 'demo',
instanceId: '1',
environment: 'development',
sdkVersion: 'unleash-client-js:1.0.0',
sdkType: 'frontend',
};
await request
.post('/api/client/metrics/bulk')
.send({
applications: [unknownFlag],
metrics: [
{
featureName: 'existing_flag_bulk',
environment: 'development',
appName: 'demo',
timestamp: startOfHour(new Date()),
yes: 1337,
no: 0,
variants: {},
},
{
featureName: 'unknown_flag_bulk',
environment: 'development',
appName: 'demo',
timestamp: startOfHour(new Date()),
yes: 200,
no: 100,
variants: {},
},
],
})
.expect(202);
await services.unknownFlagsService.flush();
const unknownFlags = await services.unknownFlagsService.getAll();
expect(unknownFlags).toHaveLength(1);
expect(unknownFlags[0]).toMatchObject({
name: 'unknown_flag_bulk',
environment: 'development',
appName: 'demo',
seenAt: expect.any(Date),
});
expect(eventBus.emit).toHaveBeenCalledWith(
CLIENT_METRICS,
expect.arrayContaining([
expect.objectContaining({
featureName: 'existing_flag_bulk',
yes: 1337,
}),
]),
);
});
});

View File

@ -9,7 +9,6 @@ export type IFlagKey =
| 'anonymiseEventLog'
| 'encryptEmails'
| 'enableLicense'
| 'enableLicenseChecker'
| 'responseTimeWithAppNameKillSwitch'
| 'maintenanceMode'
| 'messageBanner'
@ -17,7 +16,6 @@ export type IFlagKey =
| 'personalAccessTokensKillSwitch'
| 'migrationLock'
| 'demo'
| 'googleAuthEnabled'
| 'advancedPlayground'
| 'filterInvalidClientMetrics'
| 'disableMetrics'
@ -27,7 +25,6 @@ export type IFlagKey =
| 'feedbackPosting'
| 'extendedUsageMetrics'
| 'feedbackComments'
| 'showInactiveUsers'
| 'killScheduledChangeRequestCache'
| 'estimateTrafficDataCost'
| 'useMemoizedActiveTokens'
@ -65,14 +62,14 @@ export type IFlagKey =
| 'changeRequestApproverEmails'
| 'paygTrialEvents'
| 'eventGrouping'
| 'paygInstanceStatsEvents';
| 'paygInstanceStatsEvents'
| 'lifecycleGraphs';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
const flags: IFlags = {
anonymiseEventLog: false,
enableLicense: false,
enableLicenseChecker: false,
responseTimeWithAppNameKillSwitch: parseEnvVarBoolean(
process.env.UNLEASH_RESPONSE_TIME_WITH_APP_NAME_KILL_SWITCH,
false,
@ -103,10 +100,6 @@ const flags: IFlags = {
),
migrationLock: parseEnvVarBoolean(process.env.MIGRATION_LOCK, true),
demo: parseEnvVarBoolean(process.env.UNLEASH_DEMO, false),
googleAuthEnabled: parseEnvVarBoolean(
process.env.GOOGLE_AUTH_ENABLED,
false,
),
filterInvalidClientMetrics: parseEnvVarBoolean(
process.env.FILTER_INVALID_CLIENT_METRICS,
false,
@ -156,10 +149,6 @@ const flags: IFlags = {
'',
},
},
showInactiveUsers: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_SHOW_INACTIVE_USERS,
false,
),
useMemoizedActiveTokens: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_MEMOIZED_ACTIVE_TOKENS,
false,
@ -302,6 +291,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_PAYG_INSTANCE_STATS_EVENTS,
false,
),
lifecycleGraphs: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_LIFECYCLE_GRAPHS,
false,
),
};
export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -59,6 +59,7 @@ process.nextTick(async () => {
crDiffView: true,
eventGrouping: true,
paygTrialEvents: true,
lifecycleGraphs: true,
},
},
authentication: {

View File

@ -114,6 +114,7 @@ test('Can delete session by sid', async () => {
test('Can delete stale sessions', async () => {
await sessionService.insertSession(newSession);
await new Promise((resolve) => setTimeout(resolve, 10)); // Ensure a different createdAt
await sessionService.insertSession({ ...newSession, sid: 'new' });
const sessionsToKeep = 1;

View File

@ -49,6 +49,26 @@ rm -rf yarn.lock
touch yarn.lock
```
## Search
This website uses Algolia DocSearch v3 with a dedicated Algolia application and a hosted crawler. All configuration is managed directly through the Algolia dashboard.
### Search prioritization
Conceptual and reference documentation pages should rank over specific API endpoints, while still allowing Algolia's relevance algorithm to find the most accurate results.
#### The configuration
**Inside the Algolia crawler**:
The crawler configuration has been updated to look for a <meta name="search_priority" content="..." /> tag in the HTML of each page.
It extracts the numerical value and saves it as a priority attribute on the search record. Pages without this tag are automatically assigned a default priority of 0.
Algolia is configured to use the `priority` attribute for custom ranking in descending order.
**Within the docs**:
We have a reusable React component, `<SearchPriority />` -> `src/components/SearchPriority.jsx`. This component provides a simple shortcut to add the correct <meta> tag to any .mdx page.
For high priority pages, use `<SearchPriority level="high" />`. For pages referencing deprecated features use `<SearchPriority level="noindex" />`.
## Troubleshooting
### `TypeError: source_default(...).bold is not a function`

View File

@ -4,6 +4,10 @@ description: "An overview of the three main Unleash APIs: Client API, Frontend A
displayed_sidebar: documentation
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
## Unleash APIs
Unleash provides a set of APIs to give you full programmatic control over your feature flags and to connect your applications and services to Unleash. There are three main APIs, each designed for a specific purpose.
@ -12,7 +16,7 @@ Unleash provides a set of APIs to give you full programmatic control over your f
|---------------|---------|---|
| **Client API** | Server-side SDKs | Fetch feature flag configurations. |
| **Frontend API** | Client-side SDKs | Fetch enabled feature flags for a specific [Unleash Context](/reference/unleash-context). |
| **Admin API** | [Admin UI](#the-unleash-admin-ui), internal tooling, and third-party [integrations](/reference/integrations) | Access and manage all resources within Unleash, such as context, environments, events, metrics, and users. |
| **Admin API** | [Admin UI(/understanding-unleash/unleash-overview#the-unleash-admin-ui), internal tooling, and third-party [integrations](/reference/integrations) | Access and manage all resources within Unleash, such as context, environments, events, metrics, and users. |
## API authentication and tokens

View File

@ -4,6 +4,10 @@ slug: /feature-flag-tutorials/use-cases/a-b-testing
pagination_next: feature-flag-tutorials/use-cases/ai
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
Feature flags are a great way to run A/B or multivariate tests with minimal code modifications, and Unleash offers built-in features that make it easy to get started. In this tutorial, we will walk through how to do an A/B test using Unleash with your application.
## How to perform A/B testing with feature flags

View File

@ -4,6 +4,10 @@ slug: /feature-flag-tutorials/use-cases/gradual-rollout
pagination_next: feature-flag-tutorials/use-cases/a-b-testing
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
## What is a gradual rollout?
A **gradual rollout** is a controlled release strategy where a new feature is first released to a small subset of users. This allows for monitoring user behavior, identifying potential issues, and gathering feedback before a full-scale launch. It also allows us to experiment quickly and safely.

View File

@ -44,7 +44,7 @@ To create a feature flag with impression data enabled, set the `impressionData`
### Enable impression data for existing feature flags
To enable impression data for an existing flag, go to the "Settings" tab of that feature flag and use the "edit" button near the Feature information title in the admin UI. It will take you to a form that looks like the flag creation form. Use the "Enable impression data" flag to enable it, the same way you would when [enabling impression data for a new feature flag](#step-1-new-toggles).
To enable impression data for an existing flag, go to the "Settings" tab of that feature flag and use the "edit" button near the Feature information title in the admin UI. It will take you to a form that looks like the flag creation form. Use the "Enable impression data" flag to enable it, the same way you would when [enabling impression data for a new feature flag](#enable-impression-data-for-new-feature-flags).
![The create feature flag form. There's a flag at the end of the form that enables or disables impression data. It's labeled "impression data".](/img/enable-impression-data-existing-toggle.png)

View File

@ -0,0 +1,598 @@
---
title: Managing feature flags in your codebase
toc_max_heading_level: 2
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
How you manage feature flags in code directly impacts the performance, testability, and long-term maintainability of your applications.
Without the right processes and structure, flags quickly become [tech debt](/reference/technical-debt), making your code harder to understand and risky to change.
In this guide, we explore hands-on strategies for managing feature flags in your code effectively. We'll give you practical recommendations and code examples to help you build a system that's reliable, scalable, and easy to maintain.
We'll cover how to:
- [Define and store flag names](#defining-and-storing-flag-names) in code.
- [Architect flag evaluations](#architecting-flag-evaluation) with an abstraction layer to keep your code clean.
- [Structure conditional logic](#structuring-conditional-logic) to simplify flag cleanup.
- [Manage flags in microservices](#managing-flags-in-microservices).
- [Minimize tech debt and manage the flag lifecycle](#minimizing-tech-debt-and-managing-the-flag-lifecycle) to prevent technical debt.
## Building on a foundation of clean code
Before we dive into specifics, remember that good software design practices make everything easier. Principles like modularity and a clear separation of concerns are your best friends when integrating feature flags.
Here are the goals we're aiming for:
- **Clarity**: Your feature flag logic should be easy to find and understand. Any developer on your team should be able to quickly grasp what a flag does and how it affects the system.
- **Maintainability**: Adding, changing, and removing flags should be a simple and low-risk process.
- **Testability**: Your code under a flag should be easily and reliably testable.
- **Scalability**: Your approach needs to handle a growing number of flags and developers without increasing code complexity.
## Defining and storing flag names
Your first step is deciding how to represent and store flag names in code. These identifiers are the critical link between your application and your feature flag configurations in the Unleash Admin UI. A disorganized approach here can quickly lead to typos, inconsistencies, and difficulty in tracking down where a flag is used.
We recommend centralizing your flag name definitions using constants or enums. This approach establishes a single source of truth for all flag names in your application.
**Why centralize definitions?**
- **Avoids inconsistencies or errors**: Using constants or enums prevents typos and inconsistencies that arise from scattering string literals (`"my-new-feature"`) throughout the application. Your compiler or linter can catch errors for you.
- **Improves discoverability**: A central file acts as a manifest of all flags used in the application, making it easy for developers to see what's available and how flags are named.
- **Simplifies refactoring and cleanup**: If you need to change a flag's name in your code (for example, to fix a typo), you only need to update it in one place.
Here is a simple and highly effective pattern using TypeScript's `as const` feature. It's robust, type-safe, and easy to understand.
```typescript
// src/feature-flags.ts
// A simple, effective way to centralize flags
export const FeatureFlags = {
NEW_USER_PROFILE_PAGE: 'newUserProfilePage',
DARK_MODE_THEME: 'darkModeTheme',
ADVANCED_REPORTING: 'advancedReportingEngine',
} as const; // 'as const' makes values read-only and types specific
// This automatically creates a type for all possible flag keys.
export type AppFlag = typeof FeatureFlags[keyof typeof FeatureFlags];
```
For applications that need even stricter type safety or rely heavily on flag variants, you can use a more advanced pattern. This approach, used within the [Unleash codebase itself](https://github.com/Unleash/unleash/blob/main/src/lib/types/experimental.ts), combines union and mapped types for maximum compile-time checking.
```typescript
// An alternative approach in: src/feature-flags.ts
import { type Variant, PayloadType } from 'unleash-client';
// 1. Define all possible flag names as a type-safe union
export type AppFlagKey =
| 'newUserProfilePage'
| 'darkModeTheme'
| 'advancedReportingEngine';
// 2. Define a type for the flags object using the official `Variant` type
export type AppFlags = Partial<{
[key in AppFlagKey]: boolean | Variant;
}>;
// 3. Provide explicit default values for each flag
export const defaultFlags: AppFlags = {
// Simple boolean defaults
newUserProfilePage: false,
darkModeTheme: true,
// A complex variant with a payload, defaulted to off
advancedReportingEngine: {
name: 'disabled',
enabled: false,
payload: {
type: PayloadType.JSON,
value: '{}',
},
},
};
```
Finally, no matter which pattern you choose, you should avoid dynamic flag names. Constructing flag names at runtime (such as, `{domain} + "_feature"`) prevents static analysis, making it nearly impossible to find all references to a flag automatically. It makes [clean-up with automated tools](https://www.getunleash.io/blog/ai-flag-cleanup) more difficult.
## Architecting flag evaluation
How and where you check a flag's state is one of the most important architectural decisions you'll make. A well-designed evaluation strategy keeps your code clean and your system's behavior predictable.
### Use an abstraction layer
Directly calling the Unleash SDK's `unleash.isEnabled()` throughout your codebase tightly couples your application to the specific SDK implementation.
Instead, we recommend implementing an abstraction layer, often called a "wrapper", to encapsulate all interactions with the Unleash SDK. This service becomes the single entry point for all feature flag checks in your application.
```typescript
// src/services/feature-service.ts
import { Unleash, Context as UnleashContext } from 'unleash-client';
import { AppFlag, FeatureFlags } from '../feature-flags'; // Import both the type and the constants
// Define your application's context structure
export interface AppUserContext {
userId?: string;
sessionId?: string;
properties?: {
[key: string]: string;
};
}
class FeatureService {
private unleash: Unleash;
constructor(unleashInstance: Unleash) {
this.unleash = unleashInstance;
}
private buildUnleashContext(appContext?: AppUserContext): UnleashContext {
if (!appContext) return {};
return { ...appContext };
}
public isEnabled(flagName: AppFlag, appContext?: AppUserContext): boolean {
// Always provide a safe, default value (usually `false`)
const defaultValue = false;
try {
const unleashContext = this.buildUnleashContext(appContext);
return this.unleash.isEnabled(flagName, unleashContext, defaultValue);
} catch (error) {
// Log the error for observability
console.error(`Error evaluating flag "${flagName}":`, error);
// Fallback to the safe default
return defaultValue;
}
}
// You can also create more semantic, business-language methods
public canUserSeeNewProfilePage(userContext?: AppUserContext): boolean {
return this.isEnabled(FeatureFlags.NEW_USER_PROFILE_PAGE, userContext);
}
}
// Initialize and export a singleton instance for your app to use
const unleash = initializeUnleashClient(); // ← replace with your real init
export const featureService = new FeatureService(unleash);
```
**Why build an abstraction layer?**
- **Vendor abstraction**: If you ever switch feature flagging providers, you only need to update your wrapper instead of hunting for SDK calls across the entire codebase.
- **Centralized control**: It gives you a single place to manage logging, performance monitoring, and robust error handling for all flag checks.
- **Improved readability**: Methods with business-friendly names (`canUserSeeNewProfilePage()`) make the code's intent clearer than a generic `isEnabled("newUserProfilePage")`.
### Handling variant payloads inside your wrapper
This wrapper is also a good place to validate any feature flag payload you receive from Unleash.
While using [variant payloads](/reference/strategy-variants#variant-payload) for dynamic configuration enables flexibility and rapid iteration, it also introduces risk. Since the variant payload is managed in a UI, a change can have unintended consequences on the application's behavior or appearance, even if the JSON itself is syntactically valid.
If you decide to use variant payloads, we recommend enforcing a [four-eyes approval](/reference/change-requests) process, so any change must be reviewed and approved by a second team member before it can be saved. In addition, you should test payloads with internal users first before exposing them to real users.
Then, implement additional guardrails in your wrapper to validate the payload structure and return a safe default value if the data is invalid.
### Evaluate flags at the right level and time
For a given user request, evaluate a feature flag once at the highest practical level of your application stack. Propagate the result of that evaluation (the true/false value or the feature flag variant) downstream to other components or functions.
This prevents "flag-aware" logic from spreading deep into your application's components, making them simpler and easier to test.
In a backend application, the highest level is often the controller or the entry point of a service request. The controller evaluates the flag and then directs the application to use either the new or old logic path.
In a frontend framework like React, evaluate the flag in a top-level container component. This component then renders different child components based on the flag's state, passing down data as props. The child components themselves remain unaware of the feature flag.
<Tabs groupId="evaluate-once">
<TabItem value="backend" label="Backend">
```javascript
// src/controllers/checkoutController.js
import { featureService } from '../services/featureService';
import { FeatureFlags } from '../feature-flags'; // Import the centralized flag names
export function handleCheckoutRequest(req, res) {
const userContext = { userId: req.user.id };
// Evaluate once at the highest level
const useNewCheckout = featureService.isEnabled(FeatureFlags.NEW_CHECKOUT_PROCESS, userContext);
// Propagate the result, not the flag check
if (useNewCheckout) {
renderNewCheckoutPage(req, res); // This component tree uses the new logic
} else {
renderOldCheckoutPage(req, res); // This component tree uses the old logic
}
}
```
</TabItem>
<TabItem value="frontend" label="Frontend">
```javascript
// This top-level component checks the flag and decides what to render.
export function Dashboard() {
const user = useUser();
const userContext = { userId: user.id };
// Evaluate the flag once in the parent component
const showNewAnalytics = featureService.isEnabled(
FeatureFlags.DASHBOARD_ANALYTICS,
userContext
);
return (
<div>
<h1>Your Dashboard</h1>
{/* Other dashboard components */}
{/* Render a child component based on the result */}
{showNewAnalytics ? (
<NewAnalyticsSection data={analyticsData} />
) : (
<OldAnalyticsSection data={analyticsData} />
)}
</div>
);
}
```
```javascript
// These child components don't know about feature flags—they just render props
// The new component just focuses on rendering its UI
export function NewAnalyticsSection({ data }) {
return (
<div className="new-analytics">
<h2>✨ New & Improved Analytics</h2>
{/* Renders charts and stats using the new design */}
</div>
);
}
// The old component is similarly unaware of the flag
export function OldAnalyticsSection({ data }) {
return (
<div className="old-analytics">
<h2>Analytics</h2>
{/* Renders charts and stats using the old design */}
</div>
);
}
```
</TabItem>
</Tabs>
**Why evaluate once?**
- **Consistency**: It ensures a user sees the same feature state throughout their interaction. Evaluating the same flag multiple times during a single request could yield different results if the flag's configuration is changed mid-request, leading to a broken or confusing user experience.
- **Simplicity**: It prevents "flag-aware" logic from spreading deep into your application's components, making them simpler and easier to test.
## Structuring conditional logic
The way you structure your conditional logic for your flags has a major impact on readability and, most importantly, on how easy it is to clean up later.
For the vast majority of cases, a simple if/else statement is the best approach. It's direct, easy to understand, and straightforward to remove.
```java
// A simple, clean conditional statement
public void processPayment(PaymentDetails details, UserContext user) {
if (featureService.isNewPaymentGatewayEnabled(user)) {
newPaymentService.charge(details);
} else {
legacyPaymentService.charge(details);
}
}
```
The primary goal is to keep the conditional logic localized and simple. When it's time for cleanup, the task is trivial: delete the if and the else block, and the new code path remains.
### Using design patterns
Design patterns like the [Strategy pattern](https://www.digitalocean.com/community/tutorials/strategy-design-pattern-in-java-example-tutorial) or the [Factory pattern](https://hackernoon.com/understanding-the-factory-pattern-in-c-with-examples) are sometimes used in place of direct conditional logic. For example, the strategy pattern uses a flag to select a concrete implementation of a shared interface at runtime, encapsulating different behaviors into distinct classes.
![Strategy-design-pattern](/img/strategy-pattern.jpg)
The strategy pattern is well-suited for certain [Permission](/what-is-a-feature-flag#permission-flags) flags that grant premium users access to an advanced feature, or for long-term [Kill switches](/what-is-a-feature-flag#kill-switches) that toggle a core system component. For these complex, multi-faceted features with distinct and interchangeable behaviors, the pattern can be a powerful tool for maintaining a clean, scalable, and testable codebase.
<Tabs groupId="strategy-pattern">
<TabItem value="strategy-ts" label="TypeScript">
```typescript
// Define a contract that all payment strategies must follow
export interface PaymentStrategy {
charge(details: PaymentDetails): Promise<void>;
}
// The implementation for the legacy payment system
export class LegacyPaymentService implements PaymentStrategy {
async charge(details: PaymentDetails): Promise<void> {
console.log('Processing payment with legacy system...');
// Legacy logic...
}
}
// The implementation for the new payment system
export class NewPaymentService implements PaymentStrategy {
async charge(details: PaymentDetails): Promise<void> {
console.log('Processing payment with shiny new system!');
// New logic...
}
}
// This factory isolates the flag check, returning the correct strategy
export function getPaymentStrategy(user: UserContext): PaymentStrategy {
// The flag check is isolated here
if (featureService.isEnabled(FeatureFlags.NEW_PAYMENT_GATEWAY, user)) {
return new NewPaymentService();
} else {
return new LegacyPaymentService();
}
}
// The application code uses the factory to get a strategy and execute it
export async function processPayment(details: PaymentDetails, user: UserContext) {
// Get the appropriate strategy based on the flag
const paymentService = getPaymentStrategy(user);
// Execute the payment
await paymentService.charge(details);
}
```
</TabItem>
<TabItem value="strategy-java" label="Java">
```java
// Define a contract that all payment strategies must follow
public interface PaymentStrategy {
void charge(PaymentDetails details);
}
// The implementation for the legacy payment system
@Service("legacyPayment")
public class LegacyPaymentService implements PaymentStrategy {
@Override
public void charge(PaymentDetails details) {
System.out.println("Processing payment with legacy system");
// Legacy logic
}
}
// The implementation for the new payment system
@Service("newPayment")
public class NewPaymentService implements PaymentStrategy {
@Override
public void charge(PaymentDetails details) {
System.out.println("Processing payment with shiny new system!");
// New logic
}
}
// This factory isolates the flag check, returning the correct service bean
@Service
public class PaymentStrategyFactory {
private final FeatureService featureService;
private final PaymentStrategy legacyPaymentService;
private final PaymentStrategy newPaymentService;
@Autowired
public PaymentStrategyFactory(
FeatureService featureService,
@Qualifier("legacyPayment") PaymentStrategy legacyPaymentService,
@Qualifier("newPayment") PaymentStrategy newPaymentService
) {
this.featureService = featureService;
this.legacyPaymentService = legacyPaymentService;
this.newPaymentService = newPaymentService;
}
public PaymentStrategy getPaymentStrategy(UserContext user) {
// The flag check is isolated here
if (featureService.isEnabled(FeatureFlags.NEW_PAYMENT_GATEWAY, user)) {
return newPaymentService;
} else {
return legacyPaymentService;
}
}
}
// The controller uses the factory to get a strategy and execute it
@RestController
public class PaymentController {
private final PaymentStrategyFactory paymentStrategyFactory;
@Autowired
public PaymentController(PaymentStrategyFactory factory) {
this.paymentStrategyFactory = factory;
}
@PostMapping("/pay")
public void processPayment(@RequestBody PaymentDetails details, @CurrentUser UserContext user) {
// Get the appropriate strategy based on the flag
PaymentStrategy paymentService = paymentStrategyFactory.getPaymentStrategy(user);
// Execute the payment
paymentService.charge(details);
}
}
```
</TabItem>
</Tabs>
However, the majority of feature flags control small, temporary changes. For most [Release](/what-is-a-feature-flag#release-flags), [Experiment](/what-is-a-feature-flag#experiment-flags), and [Operational](/what-is-a-feature-flag#operational-flags) flags, the strategy pattern introduces unnecessary overhead. It makes the eventual cleanup process far more complex than removing a simple if/else block. Furthermore, because the pattern scales poorly when multiple flags interact, a direct conditional statement is almost always the cleaner and more maintainable choice for these temporary flags.
## Managing flags in microservices
Managing feature flags in a microservices architecture requires guaranteeing consistency. When a single user request triggers a chain of calls across multiple services, each service needs to operate on the same feature state.
You might assume that if each service evaluates a flag with the same user context, the result will be consistent. In a perfectly static system, this is true. However, in a live production environment, flag configurations can change. This introduces a critical race condition: a flag can be toggled mid-request, causing different services in the same call chain to get different results.
Imagine a `NEW_PRICING_MODEL` flag is active:
1. The `gateway-service` receives the request, sees the flag is *on*, and calls the `product-service`.
2. The `product-service` also sees the flag is *on* and prepares to show a promotional banner. It calls the `pricing-service`.
3. In the milliseconds between these calls, an engineer turns off the flag in the Unleash UI due to an issue.
4. The `pricing-service` now evaluates the flag, sees it as *off*, and returns the standard price.
The result? A confused user who sees a promotional banner but gets charged the old price.
![Evaluate flags multiple times](/img/flags-in-code-microservices-multiple.jpg)
The solution is to evaluate a feature flag's state exactly one time at the "edge" of your system—typically in an API Gateway or the first service that receives the external request.
Then, you must propagate the result of that evaluation—the true/false or a specific variant—downstream to all other services.
![Evaluate flags once](/img/flags-in-code-microservices-once.jpg)
To make this work, downstream services need the initial flag evaluation result and the user context (ID, location, etc.) used to make them. The standard, most robust way to achieve this is with [OpenTelemetry Baggage](https://opentelemetry.io/docs/concepts/signals/baggage/).
While OpenTelemetry is known for distributed tracing, its Baggage specification is purpose-built to carry application-defined key-value pairs across process boundaries. It's the ideal mechanism for this use case.
Here's how it works:
1. The `gateway-service` receives a request, authenticates the user, and evaluates all necessary flags.
2. It uses the OpenTelemetry SDK to add the user context and the flag evaluation result to the current baggage.
3. When the `gateway-service` makes an HTTP call to a downstream service, the OpenTelemetry instrumentation automatically serializes the baggage into the `baggage` HTTP header and sends it.
4. The downstream service's instrumentation automatically receives this header, deserializes it, and makes the baggage available to your application code.
<Tabs groupId="microservices">
<TabItem value="microservices-java" label="Java">
```java
// Example in Java (`gateway-service`) using OpenTelemetry SDK
import io.opentelemetry.api.baggage.Baggage;
Baggage.current()
.toBuilder()
.put("user.id", "user-123")
.put("user.tier", "premium")
// Propagate the evaluation result, not the flag name
.put("flag.new-checkout.enabled", "true")
.build()
.makeCurrent(); // This context is now active and will be propagated.
```
</TabItem>
<TabItem value="microservices-python" label="Python">
```python
# Example in Python (Downstream Service)
from opentelemetry import baggage
def handle_request():
# Retrieve the propagated context
all_baggage = baggage.get_all()
user_id = all_baggage.get('user.id')
new_checkout_is_enabled = all_baggage.get('flag.new-checkout.enabled') == 'true'
# Use the consistent, propagated result
if new_checkout_is_enabled:
# ...
else:
# ...
```
</TabItem>
</Tabs>
## Minimizing tech debt and managing the flag lifecycle
Let's face it: stale flags are [tech debt](/reference/technical-debt). Without a plan, your codebase will fill up with forgotten and risky flags. The only way to win is with a clear process for managing their entire [lifecycle](/reference/feature-toggles#feature-flag-lifecycle).
### Naming conventions and flag metadata
The first line of defense against flag debt is clarity. A flag named `temp_fix_v2` is a mystery waiting to happen. A good name provides immediate context about the flag's purpose and owner, both in the Unleash UI and in your code.
A highly effective pattern is: `[team]_[feature-name]`. For example: `checkout_multistep-payment-flow`.
The `[team]` prefix is invaluable when multiple teams share a single Unleash project, as it clarifies ownership. However, if you organize your Unleash instance with per-team projects, this prefix may be unnecessary, though it can still be helpful for searching through code.
`[feature-name]` should be a short, descriptive slug for the feature.
While you can add more information directly to the name, such as an issue number or a full issue name, this is a trade-off. It can make the flag name long and complicated to work with in code.
A better practice is to keep the name clean and use Unleash's built-in metadata features for richer context:
- **Use external links**: Instead of putting an issue number like JIRA-376 in the name, use the [external links](/reference/feature-toggles#external-links) feature in Unleash to connect the flag directly to the corresponding Jira ticket, GitHub issue, or design document.
- **Write a clear description**: Use the description field to explain what the flag does, what the rollout plan is, and any additional context that may be relevant.
This approach keeps flag names readable in your code while ensuring all the necessary context for lifecycle management is available in the Unleash UI. Once you've identified a naming convention that works, you can [enforce them at the project level](/reference/feature-toggles#set-a-naming-pattern).
### Flag cleanup best practices
You can use a flag's [lifecycle data](/reference/feature-toggles#feature-flag-lifecycle) and automated reminders to ensure flags are removed from your code once they have served their purpose.
Here's our recommended workflow for flag cleanup:
- **Update the flag's lifecycle status**: Once a feature is stable and fully rolled out, mark it as *Completed* in Unleash. If you forget, Unleash will prompt you to update the status for stale flags that are no longer sending metrics. This moves the flag to the Cleanup lifecycle stage, creating a clear backlog for removal.
- **Clean up the code**: Remove the conditional logic, any old code paths, the flag's definition from your central file, and any helper methods from your wrapper. This ensures you remove dead code and reduce complexity.
- **Test and deploy**: Run your tests to ensure everything still works as expected, then deploy your changes.
- **Archive the flag in Unleash**: Finally, archive the flag in the Unleash UI. Don't delete it—archiving preserves its history for auditing and analysis.
Just hoping that people remember to clean up is not a sustainable strategy. You need to automate your governance process.
Here are some practical tips:
- **Automated ticketing**: Use webhooks or integrations to automatically create "Remove Flag" tickets.
- **Scheduled reviews**: Make flag reviews a part of your process, for example your planning. Teams should justify a flag's existence or schedule them for removal.
- **Update "Definition of Done"**: A feature isn't "done" until its associated feature flag has been removed from the code and archived in Unleash.
- **Use AI to speed up the cleanup**: Rely on [AI coding assistants](https://www.getunleash.io/blog/ai-flag-cleanup) to automate and fix flag removal issues.
## Testing with feature flags
There is no single "right" way to test with feature flags, as every organization has a different testing strategy. The key is to build confidence that your application remains stable regardless of which flags are active. An effective approach typically involves a combination of strategies across different levels of the testing pyramid.
A common fear is that flags will cause a "combinatorial explosion" of test cases. You don't need to test every possible combination. Instead, focus on a few high-value scenarios.
### Unit tests
At the unit level, the focus is narrow: does this specific piece of code work correctly when its controlling flag is on and when it's off?
For any unit of code affected by a flag, you should have tests that cover both states:
- **Flag on**: Verifies the new code path works as expected.
- **Flag off**: Verifies the old code path still works and the new code is not executed.
### Component and E2E tests
For broader tests that cover a single service, component, or the entire application, you need a strategy to handle the growing number of flags. Here are some common states to cover:
- **Baseline state** = all flags off: These tests run with all feature flags turned off, simulating your stable production environment. It verifies that adding new, dormant flag-protected code hasn't caused regressions in existing functionality.
- **New features state** = all flags on: This suite runs with all (or most) feature flags enabled. Its purpose is to catch unexpected, negative interactions between multiple new features that might be developed in parallel. It helps ensure that the application is functioning as expected in its most feature-rich state.
- **Common combinations**: For mature applications, it may be useful to test the most common combination of flags that your users experience. This pragmatic approach focuses testing effort on the configurations that have the biggest real-world impact.
- **SDK fallback state**: What happens if Unleash is unavailable? Does your wrapper handle it gracefully and fall back to safe defaults?
### Testing in production
The real superpower that flags give you is testing in production—safely.
This doesn't mean showing bugs to your customers. It means using targeting rules to enable a feature only for your internal teams in the live production environment.
For example, you can set a flag to be "on" only for users with an `@your-company.com` email address.
This allows your team to interact with the new feature on real production infrastructure, with real data—a context that is impossible to perfectly replicate in a staging environment.
If you find a bug, it has zero impact on real users. You can fix it and then release it with confidence.
## Key takeaways
To wrap things up, managing feature flags effectively boils down to a few core, hands-on practices.
First, centralize flag definitions in a single file to prevent errors and make them easy to find and remove. Second, always build an abstraction layer or wrapper around the SDK; this gives you a single point for error handling and simplifies future migrations.
For structuring your conditional logic, a simple if/else is usually the best choice for temporary flags, as it's the easiest to clean up.
Finally, evaluate flags once at the highest reasonable level in your application. This is especially crucial in a microservices architecture, where propagating the result of the evaluation downstream ensures a consistent user experience.
-----
## Frequently asked questions (FAQs)
This FAQ section addresses common questions about using feature flags in code, focusing on flag evaluation in different architectures, conditional logic implementation, and testing.
**Where should I define my feature flag names in the code?**
Centralize them in a dedicated file using constants or enums. This creates a single source of truth, prevents typos, and makes cleanup easier.
**Should I call the Unleash SDK directly everywhere or build a helper service?**
Build a wrapper (an abstraction layer). It decouples your app from the SDK, gives you a central place for error handling and logging, and makes future migrations painless.
**How do I handle code for complex features controlled by flags?**
Start with a simple if/else statement. This is the cleanest and easiest-to-maintain solution for most cases. The Strategy pattern should be reserved for complex, long-lived flags like kill switches or permissions, as it can introduce unnecessary complexity for short-lived release flags.
**How do we avoid flag debt?**
Have a process! Use strict [naming conventions](/reference/feature-toggles#set-a-naming-pattern), link flags to tickets in Unleash, make flag removal part of your "Definition of Done," and automate cleanup reminders.
**When and how should I remove a feature flag from the code?**
Once the flag is stable at 100% rollout (or permanently off). The process is: remove the conditional logic and old code, delete the flag definition, and then archive the flag in the Unleash UI.
**Can you use feature flags in microservices?**
Absolutely! Evaluate the flag once in the first service that gets the request (for example, your API gateway). Then, propagate the result of the evaluation (the true/false result or assigned variant) to all downstream services using OpenTelemetry Baggage or custom HTTP headers. This guarantees consistency.
**What's the best way to evaluate a feature flag in code?**
Evaluate it once per request at the highest logical point in your application. Then, pass the boolean result down to the components that need it. This ensures a consistent user experience for that entire interaction.

View File

@ -4,6 +4,9 @@ title: Scaling Unleash for enterprise workloads
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
When evaluating Unleash for [enterprise-wide adoption](https://www.getunleash.io/blog/feature-ops-is-the-next-frontier), your primary concerns likely revolve around scalability, performance, and availability. You need assurance that your chosen [feature management system](https://www.getunleash.io/) can handle potentially tens of millions of users and millions of flags across a number of regions, without compromising user experience or introducing system fragility.

View File

@ -3,6 +3,10 @@ title: Feature flag security and compliance for enterprises
slug: /feature-flag-tutorials/use-cases/security-and-compliance
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
Security and compliance are important aspects of building and managing complex software in large enterprises. For software architects, engineering leaders, and technical decision-makers, every tool in your tech stack needs to pass security reviews. The weakest link in your software bill of materials, known as the SBOM, can be the one that compromises your security, so every dependency and integration must meet strict standards. Security isn't just about individual components—its about the entire system working together without introducing risk.
In the modern security landscape, compliance frameworks like FedRAMP, SOC 2, and ISO 27001 set strict standards for proving good security posture in your software tool implementations. Feature flag management systems are no exception.

View File

@ -3,6 +3,10 @@ title: Implement trunk-based development using feature flags
slug: /feature-flag-tutorials/use-cases/trunk-based-development
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
Developers are increasingly adopting trunk-based development to accelerate software delivery and improve efficiency and reliability. A key principle of trunk-based development is merging code into the main branch (aka "trunk") as quickly as possible. This practice reduces the complexity of long-lived feature branches, minimizes merge conflicts, and ensures that teams can continuously integrate and test their code. However, it also means unfinished or experimental features may exist in production. This is where feature flags become essential.
Unleash provides a powerful mechanism for safely managing and controlling these features in production, enabling enterprises to deliver software faster and with greater reliability. Effective feature flag management ensures that trunk-based development supports continuous delivery without compromising stability. In this tutorial, well use Unleash to manage trunk-based development in your codebase.

View File

@ -4,6 +4,12 @@ slug: /feature-flag-tutorials/use-cases/user-management-access-controls-auditing
pagination_next: feature-flag-tutorials/use-cases/security-compliance
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
Feature flags are a game-changer for how software teams build, test, and release products. They enable you to roll out new features with confidence, manage risk, and keep your software development agile and secure.
Imagine a large banking platform company with hundreds of engineering teams across multiple continents. Their software development lifecycle is complex and dynamic. Feature flags simplify their processes, but managing all those users is an additional layer of complexity. Unleash solves this with user management capabilities, role-based access controls, and auditing features to help organizations release code with confidence while maintaining security and compliance.

View File

@ -3,6 +3,12 @@ title: 'Set up SSO with Google'
description: Set up SSO for Unleash with Google.
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::caution Deprecation notice
Single Sign-on via the Google Authenticator provider has been removed in Unleash v5 (deprecated in v4). We recommend using [OpenID Connect](./how-to-add-sso-open-id-connect.md) instead. If you're running a self hosted version of Unleash and you need to temporarily re-enable Google SSO, you can do so by setting the `GOOGLE_AUTH_ENABLED` environment variable to `true`. If you're running a hosted version of Unleash, you'll need to reach out to us and ask us to re-enable the flag. Note that this code will be removed in a future release and this is not safe to depend on.

View File

@ -2,6 +2,10 @@
title: How to create API Tokens
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
All Unleash APIs require authentication using an [API token](/reference/api-tokens-and-client-keys). The type of token you use depends on the API you are accessing and your specific use case.
### Token types

View File

@ -3,6 +3,11 @@ title: Environment import and export
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
import VideoContent from '@site/src/components/VideoContent.jsx'
:::note Availability

View File

@ -2,6 +2,12 @@
title: How to run the Unleash Proxy
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
import ApiRequest from '@site/src/components/ApiRequest'
:::warning

View File

@ -1,84 +0,0 @@
---
title: Feature Updates To Slack
---
:::caution
This guide is deprecated. If you're looking for ways to integrate with Slack, you should refer to [the Slack integration guide](../reference/integrations/slack.md) instead.
Event hook option is removed in Unleash v5
:::
## Create a custom Slack WebHook URL: {#create-a-custom-slack-webhook-url}
1. Go to [https://slack.com/apps/manage/custom-integrations](https://slack.com/apps/manage/custom-integrations)
1. Click Incoming WebHooks
1. Click “Add Configuration”
1. This is Slack's help page on how to do this: https://api.slack.com/messaging/webhooks
- Choose a channel, follow the wizard, get the custom URL.
## Send data to Slack using an event hook function {#send-data-to-slack-using-an-event-hook-function}
Using the `eventHook` option, create a function that will send the data you'd like into Slack when mutation events happen.
```javascript
const unleash = require('unleash-server');
const axios = require('axios');
function onEventHook(event, eventData) {
const { createdBy: user, data } = eventData;
let text = '';
const unleashUrl = 'http://your.unleash.host.com';
const feature = `<${unleashUrl}/#/features/strategies/${data.name}|${data.name}>`;
switch (event) {
case 'feature-created':
case 'feature-updated': {
const verb =
event === 'feature-created' ? 'created a new' : 'updated the';
text = `${user} ${verb} feature ${feature}\ndescription: ${
data.description
}\nenabled: ${data.enabled}\nstrategies: \`${JSON.stringify(
data.strategies,
)}\``;
break;
}
case 'feature-archived':
case 'feature-revived': {
const verb = event === 'feature-archived' ? 'archived' : 'revived';
text = `${user} ${verb} the feature ${feature}`;
break;
}
default: {
console.error(`Unknown event ${event}`);
return;
}
}
axios
.post(
'https://hooks.slack.com/services/THIS_IS_WHERE_THE_CUSTOM_URL_GOES',
{
username: 'Unleash',
icon_emoji: ':unleash:', // if you added a custom emoji, otherwise you can remove this field.
text: text,
},
)
.then((res) => {
console.log(`Slack post statusCode: ${res.status}. Text: ${text}`);
})
.catch((error) => {
console.error(error);
});
}
const options = {
eventHook: onEventHook,
};
unleash.start(options).then((server) => {
console.log(`Unleash started on http://localhost:${server.app.get('port')}`);
});
```

View File

@ -4,14 +4,10 @@ pagination_next: topics/what-is-a-feature-flag
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import SearchPriority from '@site/src/components/SearchPriority';
import Head from '@docusaurus/Head';
<Head>
<meta name="search_priority" content="3" />
</Head>
<SearchPriority level="high" />
The easiest way to get started with Unleash is through a [cloud-hosted free trial](https://www.getunleash.io/plans/enterprise-payg). This gives you a ready-to-use instance, so you can explore all Unleash features without any local setup.

View File

@ -2,6 +2,10 @@
title: Actions
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability

View File

@ -3,6 +3,9 @@ title: Activation strategies
---
import VideoContent from '@site/src/components/VideoContent.jsx'
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
## Overview

View File

@ -3,6 +3,10 @@ title: API tokens and client keys
pagination_next: reference/front-end-api
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
## Overview
Unleash uses API keys to facilitate communication between consuming clients such as [SDKs](../reference/sdks), [Unleash Edge](../reference/unleash-edge), or other tools and automation.

View File

@ -2,6 +2,10 @@
title: /api/admin/addons
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> In order to access the admin API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create an ADMIN token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.
### List integrations and providers {#list-integrations-and-providers}

View File

@ -2,6 +2,10 @@
title: /api/admin/archive
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> In order to access the admin API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create an ADMIN token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.

View File

@ -2,6 +2,11 @@
title: /api/admin/context
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> The context feature is only available as part of Unleash Enterprise. In order to access the API programmatically you need to make sure you [obtain a API token](/how-to/how-to-create-api-tokens) with admin permissions.
### List context fields defined in Unleash {#list-context-fields-defined-in-unleash}

View File

@ -3,6 +3,10 @@ title: /api/admin/events
---
import ApiRequest from '@site/src/components/ApiRequest'
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::note

View File

@ -3,6 +3,10 @@ id: feature-types
title: /api/admin/feature-types
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> In order to access the admin API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create an ADMIN token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.
# Feature Types API

View File

@ -2,6 +2,10 @@
title: /api/admin/projects/:projectId
---
import ApiRequest from '@site/src/components/ApiRequest'
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::info
In order to access the admin API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create an **admin** token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.

View File

@ -2,6 +2,10 @@
title: /api/admin/features
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::caution Deprecation notice
Most of this API was removed in Unleash v5 (after being deprecated since Unleash v4.3). You should use [the project-based API (/api/admin/projects/:projectId)](/reference/api/legacy/unleash/admin/features-v2.mdx) instead.

View File

@ -3,6 +3,10 @@ id: metrics
title: /api/admin/metrics
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> In order to access the admin API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create an ADMIN token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.
# This document describes the metrics endpoint for admin ui

View File

@ -2,6 +2,10 @@
title: /api/admin/projects
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> The context feature is only available as part of Unleash Enterprise. In order to access the API programmatically you need to make sure you [obtain an API token](/how-to/how-to-create-api-tokens) with admin permissions.
### List projects in Unleash {#list-projects-in-unleash}

View File

@ -2,6 +2,10 @@
title: /api/admin/segments
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
import ApiRequest from '@site/src/components/ApiRequest'; export const basePath = "api/admin/segments"; export const path = (p) => `${basePath}/${p}`;
:::note Availability

View File

@ -3,6 +3,10 @@ id: state
title: /api/admin/state
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::caution Removal notice
Api admin state is deprecated from version 5 and removed in version 6. We recommend using the new [Environment Import & Export](https://docs.getunleash.io/reference/deploy/environment-import-export).

View File

@ -2,6 +2,10 @@
title: /api/admin/strategies
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> In order to access the admin API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create an ADMIN token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.
### Fetch Strategies {#fetch-strategies}

View File

@ -3,6 +3,10 @@ id: tags
title: /api/admin/tags
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> In order to access the admin API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create an ADMIN token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.
### Create a new tag {#create-a-new-tag}

View File

@ -2,6 +2,10 @@
title: /api/admin/user-admin
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> In order to access the admin API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create an ADMIN token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.
### List all users {#list-all-users}

View File

@ -2,6 +2,10 @@
title: Basic Auth
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
# Basic auth
When using the `insecure` authentication method, identifying using basic auth against the API is enough. Since the `insecure` method doesn't require a password, it is enough to define the username when making HTTP requests.

View File

@ -2,6 +2,10 @@
title: /api/client/features
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> In order to access the client API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create a CLIENT token](/how-to/how-to-create-api-tokens.mdx) and add an Authorization header using the token.
### Fetching Feature Flags {#fetching-feature-toggles}

View File

@ -3,6 +3,10 @@ id: metrics
title: /api/client/metrics
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> In order to access the client API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create a CLIENT token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.
### Send metrics {#send-metrics}

View File

@ -3,6 +3,10 @@ id: register
title: /api/client/register
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
> In order to access the client API endpoints you need to identify yourself. Unless you're using the `none` authentication method, you'll need to [create a CLIENT token](/how-to/how-to-create-api-tokens) and add an Authorization header using the token.
### Client registration {#client-registration}

View File

@ -3,6 +3,10 @@ id: index
title: Legacy API Documentation
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::caution
These APIs have been deprecared. Wse the [Unleash OpenAPI docs](/api-overview) reference instead.

View File

@ -2,6 +2,10 @@
title: /health
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
# Health API
`GET http://unleash.host.com/health`

View File

@ -2,6 +2,11 @@
title: /internal-backstage/prometheus
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
# Internal Backstage API
`GET http://unleash.host.com/internal-backstage/prometheus`

View File

@ -3,6 +3,10 @@ title: Applications
pagination_next: reference/service-accounts
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `5.11+`

View File

@ -2,6 +2,10 @@
title: Banners
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Plan**: [Enterprise](https://www.getunleash.io/pricing) | **Version**: `5.7+`

View File

@ -3,6 +3,9 @@ title: Change requests
---
import VideoContent from '@site/src/components/VideoContent.jsx';
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability

View File

@ -2,6 +2,10 @@
title: Command menu
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `6.2+`

View File

@ -2,6 +2,10 @@
title: Custom activation strategies
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="medium" />
**Custom activation strategies** let you define your own activation strategies to use with Unleash. When the [built-in activation strategies](../reference/activation-strategies.md) aren't enough, custom activation strategies are there to provide you with the flexibility you need.
Custom activation strategies work exactly like the built-in activation strategies when working in the admin UI.

View File

@ -2,6 +2,9 @@
title: Import and export
---
import ApiRequest from '@site/src/components/ApiRequest'
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::note Availability

View File

@ -3,11 +3,16 @@ id: environments
title: Environments
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `4.3+`
:::
## Overview
Environments represent different stages in your development lifecycle. They allow you to manage your product releases from local development to production. [Projects](/reference/projects) and [feature flags](/reference/feature-toggles) are accessible in all environments, but each environment has different feature flag configurations. This allows you to enable a flag in development or test without enabling it in production.

View File

@ -1,6 +1,10 @@
---
title: Events
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
## Overview

View File

@ -1,6 +1,11 @@
---
title: Feature flag variants (deprecated)
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::warning
Feature Flag Variants at the environment level are deprecated in favor of the [strategy variants](./strategy-variants).

View File

@ -3,6 +3,10 @@ title: Feature flags
pagination_next: reference/activation-strategies
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
## Overview
Feature flags are a core concept of Unleash. They allow you to release, test, and manage features and functionality across your application without changing the source code.

View File

@ -2,6 +2,10 @@
title: Frontend API
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `4.18+`

View File

@ -3,6 +3,10 @@ title: Impression data
pagination_next: reference/events
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `4.7+`. Requires [SDK compatibility](../reference/sdks#feature-compatibility-in-server-side-sdks).

View File

@ -2,6 +2,10 @@
title: Analytics
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Plan**: [Pro](/availability#plans) and [Enterprise](https://www.getunleash.io/pricing) | **Version**: `6.0+`

View File

@ -3,6 +3,10 @@ id: datadog
title: Datadog
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `4.0+`

View File

@ -3,6 +3,9 @@ id: index
title: Integrations
---
import DocCardList from '@theme/DocCardList';
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability

View File

@ -2,6 +2,11 @@
title: Jira Cloud Integration - Installation
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Plan**: [Pro](/availability#plans) and [Enterprise](https://www.getunleash.io/pricing) | **Version**: `4.0+`

View File

@ -2,6 +2,10 @@
title: Jira Cloud Integration - Usage
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
With the Unleash Jira Cloud Plugin you can create, view and manage Unleash feature flags directly from a Jira issue.
The plugin also shows you current status of connected flags.

View File

@ -2,6 +2,10 @@
title: Jira Server Integration - Installation
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::info Deprecated
The Jira Server plugin is **deprecated**, please use the [Unleash Jira Cloud plugin](https://docs.getunleash.io/reference/integrations/jira-cloud-plugin-installation) instead

View File

@ -2,6 +2,10 @@
title: Jira Server Integration - Usage
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::info Deprecated
The Jira Server plugin is **deprecated**, please use the [Unleash Jira Cloud plugin](https://docs.getunleash.io/reference/integrations/jira-cloud-plugin-installation) instead

View File

@ -2,6 +2,10 @@
title: App for Slack
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `5.5+`

View File

@ -3,6 +3,10 @@ id: slack
title: Slack (deprecated)
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="noindex" />
:::caution Deprecation notice
This Slack integration is deprecated and will be removed in a future release. We recommend using the new [App for Slack](./slack-app) integration instead.

View File

@ -3,6 +3,10 @@ id: teams
title: Microsoft Teams
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `4.0+`

View File

@ -3,6 +3,10 @@ id: webhook
title: Webhook
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `3.11+`

View File

@ -2,6 +2,10 @@
title: Login history
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Plan**: [Enterprise](https://www.getunleash.io/pricing) | **Version**: `4.22+`

View File

@ -2,6 +2,10 @@
title: Maintenance mode
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `4.22+`

View File

@ -2,6 +2,10 @@
title: Network
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Plan**: [Pro](/availability#plans) and [Enterprise](https://www.getunleash.io/pricing) | **Version**: `4.21+`

View File

@ -2,6 +2,9 @@
title: Playground
---
import VideoContent from '@site/src/components/VideoContent.jsx'
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability

View File

@ -2,6 +2,10 @@
title: Project collaboration mode
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Plan**: [Enterprise](https://www.getunleash.io/pricing) | **Version**: `4.22+`

View File

@ -4,6 +4,10 @@ title: Projects
pagination_next: reference/project-collaboration-mode
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
## Overview
Projects help you organize feature flags within Unleash. For example, you can use projects to group feature flags by development teams or different functional modules within your application.

View File

@ -2,6 +2,10 @@
title: Public invite links
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
Public invite links allow you to invite new team members to your Unleash instance. Any user who receives an invite link can use it to sign up for the Unleash instance that generated the link. When users sign up using an invite link, they are automatically assigned the [Viewer](../reference/rbac.md#predefined-roles) role.
A token becomes active as soon as you create it, and remains valid until it expires or is deleted. Once a token is invalid, users can no longer sign up using an invite link containing that token.

View File

@ -3,6 +3,10 @@ id: rbac
title: Role-based access control
---
import SearchPriority from '@site/src/components/SearchPriority';
<SearchPriority level="high" />
:::note Availability
**Version**: `4.0+`

Some files were not shown because too many files have changed in this diff Show More