mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +02:00
chore: improve the performance of our instance stats (#8766)
## About the changes Our stats are used for many places and many times to publish prometheus metrics and some other things. Some of these queries are heavy, traversing all tables to calculate aggregates. This adds a feature flag to be able to memoize 1 minute (by default) how long to keep the calculated values in memory. We can use the key of the function to individually control which ones are memoized or not and for how long using a numeric variant. Initially, this will be disabled and we'll test in our instances first
This commit is contained in:
parent
0ce976a0d5
commit
39d227c33b
@ -6,11 +6,18 @@ import { createFakeGetActiveUsers } from './getActiveUsers';
|
|||||||
import { createFakeGetProductionChanges } from './getProductionChanges';
|
import { createFakeGetProductionChanges } from './getProductionChanges';
|
||||||
import { registerPrometheusMetrics } from '../../metrics';
|
import { registerPrometheusMetrics } from '../../metrics';
|
||||||
import { register } from 'prom-client';
|
import { register } from 'prom-client';
|
||||||
import type { IClientInstanceStore } from '../../types';
|
import type {
|
||||||
|
IClientInstanceStore,
|
||||||
|
IFlagResolver,
|
||||||
|
IUnleashStores,
|
||||||
|
} from '../../types';
|
||||||
import { createFakeGetLicensedUsers } from './getLicensedUsers';
|
import { createFakeGetLicensedUsers } from './getLicensedUsers';
|
||||||
let instanceStatsService: InstanceStatsService;
|
let instanceStatsService: InstanceStatsService;
|
||||||
let versionService: VersionService;
|
let versionService: VersionService;
|
||||||
let clientInstanceStore: IClientInstanceStore;
|
let clientInstanceStore: IClientInstanceStore;
|
||||||
|
let stores: IUnleashStores;
|
||||||
|
let flagResolver: IFlagResolver;
|
||||||
|
|
||||||
let updateMetrics: () => Promise<void>;
|
let updateMetrics: () => Promise<void>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@ -18,7 +25,8 @@ beforeEach(() => {
|
|||||||
register.clear();
|
register.clear();
|
||||||
|
|
||||||
const config = createTestConfig();
|
const config = createTestConfig();
|
||||||
const stores = createStores();
|
flagResolver = config.flagResolver;
|
||||||
|
stores = createStores();
|
||||||
versionService = new VersionService(
|
versionService = new VersionService(
|
||||||
stores,
|
stores,
|
||||||
config,
|
config,
|
||||||
@ -74,3 +82,96 @@ test('get snapshot should not call getStats', async () => {
|
|||||||
test('before the snapshot is refreshed we can still get the appCount', async () => {
|
test('before the snapshot is refreshed we can still get the appCount', async () => {
|
||||||
expect(instanceStatsService.getAppCountSnapshot('7d')).toBeUndefined();
|
expect(instanceStatsService.getAppCountSnapshot('7d')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe.each([true, false])(
|
||||||
|
'When feature enabled is %s',
|
||||||
|
(featureEnabled: boolean) => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(flagResolver, 'getVariant').mockReturnValue({
|
||||||
|
name: 'memorizeStats',
|
||||||
|
enabled: featureEnabled,
|
||||||
|
feature_enabled: featureEnabled,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`should${featureEnabled ? ' ' : ' not '}memoize query results`, async () => {
|
||||||
|
const segmentStore = stores.segmentStore;
|
||||||
|
jest.spyOn(segmentStore, 'count').mockReturnValue(
|
||||||
|
Promise.resolve(5),
|
||||||
|
);
|
||||||
|
expect(segmentStore.count).toHaveBeenCalledTimes(0);
|
||||||
|
expect(await instanceStatsService.segmentCount()).toBe(5);
|
||||||
|
expect(segmentStore.count).toHaveBeenCalledTimes(1);
|
||||||
|
expect(await instanceStatsService.segmentCount()).toBe(5);
|
||||||
|
expect(segmentStore.count).toHaveBeenCalledTimes(
|
||||||
|
featureEnabled ? 1 : 2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`should${featureEnabled ? ' ' : ' not '}memoize async query results`, async () => {
|
||||||
|
const trafficDataUsageStore = stores.trafficDataUsageStore;
|
||||||
|
jest.spyOn(
|
||||||
|
trafficDataUsageStore,
|
||||||
|
'getTrafficDataUsageForPeriod',
|
||||||
|
).mockReturnValue(
|
||||||
|
Promise.resolve([
|
||||||
|
{
|
||||||
|
day: new Date(),
|
||||||
|
trafficGroup: 'default',
|
||||||
|
statusCodeSeries: 200,
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: new Date(),
|
||||||
|
trafficGroup: 'default',
|
||||||
|
statusCodeSeries: 400,
|
||||||
|
count: 2,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
trafficDataUsageStore.getTrafficDataUsageForPeriod,
|
||||||
|
).toHaveBeenCalledTimes(0);
|
||||||
|
expect(await instanceStatsService.getCurrentTrafficData()).toBe(7);
|
||||||
|
expect(
|
||||||
|
trafficDataUsageStore.getTrafficDataUsageForPeriod,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
expect(await instanceStatsService.getCurrentTrafficData()).toBe(7);
|
||||||
|
expect(
|
||||||
|
trafficDataUsageStore.getTrafficDataUsageForPeriod,
|
||||||
|
).toHaveBeenCalledTimes(featureEnabled ? 1 : 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`getStats should${featureEnabled ? ' ' : ' not '}be memorized`, async () => {
|
||||||
|
const featureStrategiesReadModel =
|
||||||
|
stores.featureStrategiesReadModel;
|
||||||
|
jest.spyOn(
|
||||||
|
featureStrategiesReadModel,
|
||||||
|
'getMaxFeatureEnvironmentStrategies',
|
||||||
|
).mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
feature: 'x',
|
||||||
|
environment: 'default',
|
||||||
|
count: 3,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies,
|
||||||
|
).toHaveBeenCalledTimes(0);
|
||||||
|
expect(
|
||||||
|
(await instanceStatsService.getStats())
|
||||||
|
.maxEnvironmentStrategies,
|
||||||
|
).toBe(3);
|
||||||
|
expect(
|
||||||
|
featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
expect(
|
||||||
|
(await instanceStatsService.getStats())
|
||||||
|
.maxEnvironmentStrategies,
|
||||||
|
).toBe(3);
|
||||||
|
expect(
|
||||||
|
featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies,
|
||||||
|
).toHaveBeenCalledTimes(featureEnabled ? 1 : 2);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
@ -30,7 +30,8 @@ import { CUSTOM_ROOT_ROLE_TYPE } from '../../util';
|
|||||||
import type { GetActiveUsers } from './getActiveUsers';
|
import type { GetActiveUsers } from './getActiveUsers';
|
||||||
import type { ProjectModeCount } from '../project/project-store';
|
import type { ProjectModeCount } from '../project/project-store';
|
||||||
import type { GetProductionChanges } from './getProductionChanges';
|
import type { GetProductionChanges } from './getProductionChanges';
|
||||||
import { format } from 'date-fns';
|
import { format, minutesToMilliseconds } from 'date-fns';
|
||||||
|
import memoizee from 'memoizee';
|
||||||
import type { GetLicensedUsers } from './getLicensedUsers';
|
import type { GetLicensedUsers } from './getLicensedUsers';
|
||||||
|
|
||||||
export type TimeRange = 'allTime' | '30d' | '7d';
|
export type TimeRange = 'allTime' | '30d' | '7d';
|
||||||
@ -58,6 +59,8 @@ export interface InstanceStats {
|
|||||||
strategies: number;
|
strategies: number;
|
||||||
SAMLenabled: boolean;
|
SAMLenabled: boolean;
|
||||||
OIDCenabled: boolean;
|
OIDCenabled: boolean;
|
||||||
|
passwordAuthEnabled: boolean;
|
||||||
|
SCIMenabled: boolean;
|
||||||
clientApps: { range: TimeRange; count: number }[];
|
clientApps: { range: TimeRange; count: number }[];
|
||||||
activeUsers: Awaited<ReturnType<GetActiveUsers>>;
|
activeUsers: Awaited<ReturnType<GetActiveUsers>>;
|
||||||
licensedUsers: Awaited<ReturnType<GetLicensedUsers>>;
|
licensedUsers: Awaited<ReturnType<GetLicensedUsers>>;
|
||||||
@ -193,39 +196,75 @@ export class InstanceStatsService {
|
|||||||
this.trafficDataUsageStore = trafficDataUsageStore;
|
this.trafficDataUsageStore = trafficDataUsageStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memory = new Map<string, () => Promise<any>>();
|
||||||
|
memorize<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
||||||
|
const variant = this.flagResolver.getVariant('memorizeStats', {
|
||||||
|
memoryKey: key,
|
||||||
|
});
|
||||||
|
if (variant.feature_enabled) {
|
||||||
|
const minutes =
|
||||||
|
variant.payload?.type === 'number'
|
||||||
|
? Number(variant.payload.value)
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
let memoizedFunction = this.memory.get(key);
|
||||||
|
if (!memoizedFunction) {
|
||||||
|
memoizedFunction = memoizee(() => fn(), {
|
||||||
|
promise: true,
|
||||||
|
maxAge: minutesToMilliseconds(minutes),
|
||||||
|
});
|
||||||
|
this.memory.set(key, memoizedFunction);
|
||||||
|
}
|
||||||
|
return memoizedFunction();
|
||||||
|
} else {
|
||||||
|
return fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getProjectModeCount(): Promise<ProjectModeCount[]> {
|
getProjectModeCount(): Promise<ProjectModeCount[]> {
|
||||||
return this.projectStore.getProjectModeCounts();
|
return this.memorize('getProjectModeCount', () =>
|
||||||
|
this.projectStore.getProjectModeCounts(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getToggleCount(): Promise<number> {
|
getToggleCount(): Promise<number> {
|
||||||
return this.featureToggleStore.count({
|
return this.memorize('getToggleCount', () =>
|
||||||
|
this.featureToggleStore.count({
|
||||||
archived: false,
|
archived: false,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getArchivedToggleCount(): Promise<number> {
|
getArchivedToggleCount(): Promise<number> {
|
||||||
return this.featureToggleStore.count({
|
return this.memorize('hasOIDC', () =>
|
||||||
|
this.featureToggleStore.count({
|
||||||
archived: true,
|
archived: true,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasOIDC(): Promise<boolean> {
|
async hasOIDC(): Promise<boolean> {
|
||||||
|
return this.memorize('hasOIDC', async () => {
|
||||||
const settings = await this.settingStore.get<{ enabled: boolean }>(
|
const settings = await this.settingStore.get<{ enabled: boolean }>(
|
||||||
'unleash.enterprise.auth.oidc',
|
'unleash.enterprise.auth.oidc',
|
||||||
);
|
);
|
||||||
|
|
||||||
return settings?.enabled || false;
|
return settings?.enabled || false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasSAML(): Promise<boolean> {
|
async hasSAML(): Promise<boolean> {
|
||||||
|
return this.memorize('hasSAML', async () => {
|
||||||
const settings = await this.settingStore.get<{ enabled: boolean }>(
|
const settings = await this.settingStore.get<{ enabled: boolean }>(
|
||||||
'unleash.enterprise.auth.saml',
|
'unleash.enterprise.auth.saml',
|
||||||
);
|
);
|
||||||
|
|
||||||
return settings?.enabled || false;
|
return settings?.enabled || false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasPasswordAuth(): Promise<boolean> {
|
async hasPasswordAuth(): Promise<boolean> {
|
||||||
|
return this.memorize('hasPasswordAuth', async () => {
|
||||||
const settings = await this.settingStore.get<{ disabled: boolean }>(
|
const settings = await this.settingStore.get<{ disabled: boolean }>(
|
||||||
'unleash.auth.simple',
|
'unleash.auth.simple',
|
||||||
);
|
);
|
||||||
@ -234,14 +273,17 @@ export class InstanceStatsService {
|
|||||||
typeof settings?.disabled === 'undefined' ||
|
typeof settings?.disabled === 'undefined' ||
|
||||||
settings.disabled === false
|
settings.disabled === false
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasSCIM(): Promise<boolean> {
|
async hasSCIM(): Promise<boolean> {
|
||||||
|
return this.memorize('hasSCIM', async () => {
|
||||||
const settings = await this.settingStore.get<{ enabled: boolean }>(
|
const settings = await this.settingStore.get<{ enabled: boolean }>(
|
||||||
'scim',
|
'scim',
|
||||||
);
|
);
|
||||||
|
|
||||||
return settings?.enabled || false;
|
return settings?.enabled || false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStats(): Promise<InstanceStats> {
|
async getStats(): Promise<InstanceStats> {
|
||||||
@ -281,8 +323,8 @@ export class InstanceStatsService {
|
|||||||
this.getRegisteredUsers(),
|
this.getRegisteredUsers(),
|
||||||
this.countServiceAccounts(),
|
this.countServiceAccounts(),
|
||||||
this.countApiTokensByType(),
|
this.countApiTokensByType(),
|
||||||
this.getActiveUsers(),
|
this.memorize('getActiveUsers', this.getActiveUsers.bind(this)),
|
||||||
this.getLicencedUsers(),
|
this.memorize('getLicencedUsers', this.getLicencedUsers.bind(this)),
|
||||||
this.getProjectModeCount(),
|
this.getProjectModeCount(),
|
||||||
this.contextFieldCount(),
|
this.contextFieldCount(),
|
||||||
this.groupCount(),
|
this.groupCount(),
|
||||||
@ -297,17 +339,39 @@ export class InstanceStatsService {
|
|||||||
this.hasPasswordAuth(),
|
this.hasPasswordAuth(),
|
||||||
this.hasSCIM(),
|
this.hasSCIM(),
|
||||||
this.appCount ? this.appCount : this.getLabeledAppCounts(),
|
this.appCount ? this.appCount : this.getLabeledAppCounts(),
|
||||||
|
this.memorize('deprecatedFilteredCountFeaturesExported', () =>
|
||||||
this.eventStore.deprecatedFilteredCount({
|
this.eventStore.deprecatedFilteredCount({
|
||||||
type: FEATURES_EXPORTED,
|
type: FEATURES_EXPORTED,
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
|
this.memorize('deprecatedFilteredCountFeaturesImported', () =>
|
||||||
this.eventStore.deprecatedFilteredCount({
|
this.eventStore.deprecatedFilteredCount({
|
||||||
type: FEATURES_IMPORTED,
|
type: FEATURES_IMPORTED,
|
||||||
}),
|
}),
|
||||||
this.getProductionChanges(),
|
),
|
||||||
|
this.memorize(
|
||||||
|
'getProductionChanges',
|
||||||
|
this.getProductionChanges.bind(this),
|
||||||
|
),
|
||||||
this.countPreviousDayHourlyMetricsBuckets(),
|
this.countPreviousDayHourlyMetricsBuckets(),
|
||||||
this.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
|
this.memorize(
|
||||||
this.featureStrategiesReadModel.getMaxConstraintValues(),
|
'maxFeatureEnvironmentStrategies',
|
||||||
this.featureStrategiesReadModel.getMaxConstraintsPerStrategy(),
|
this.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies.bind(
|
||||||
|
this.featureStrategiesReadModel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
this.memorize(
|
||||||
|
'maxConstraintValues',
|
||||||
|
this.featureStrategiesReadModel.getMaxConstraintValues.bind(
|
||||||
|
this.featureStrategiesReadModel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
this.memorize(
|
||||||
|
'maxConstraintsPerStrategy',
|
||||||
|
this.featureStrategiesReadModel.getMaxConstraintsPerStrategy.bind(
|
||||||
|
this.featureStrategiesReadModel,
|
||||||
|
),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -333,6 +397,8 @@ export class InstanceStatsService {
|
|||||||
strategies,
|
strategies,
|
||||||
SAMLenabled,
|
SAMLenabled,
|
||||||
OIDCenabled,
|
OIDCenabled,
|
||||||
|
passwordAuthEnabled,
|
||||||
|
SCIMenabled,
|
||||||
clientApps: Object.entries(clientApps).map(([range, count]) => ({
|
clientApps: Object.entries(clientApps).map(([range, count]) => ({
|
||||||
range: range as TimeRange,
|
range: range as TimeRange,
|
||||||
count,
|
count,
|
||||||
@ -348,59 +414,78 @@ export class InstanceStatsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
groupCount(): Promise<number> {
|
groupCount(): Promise<number> {
|
||||||
return this.groupStore.count();
|
return this.memorize('groupCount', () => this.groupStore.count());
|
||||||
}
|
}
|
||||||
|
|
||||||
roleCount(): Promise<number> {
|
roleCount(): Promise<number> {
|
||||||
return this.roleStore.count();
|
return this.memorize('roleCount', () => this.roleStore.count());
|
||||||
}
|
}
|
||||||
|
|
||||||
customRolesCount(): Promise<number> {
|
customRolesCount(): Promise<number> {
|
||||||
return this.roleStore.filteredCount({ type: CUSTOM_ROOT_ROLE_TYPE });
|
return this.memorize('customRolesCount', () =>
|
||||||
|
this.roleStore.filteredCount({ type: CUSTOM_ROOT_ROLE_TYPE }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
customRolesCountInUse(): Promise<number> {
|
customRolesCountInUse(): Promise<number> {
|
||||||
return this.roleStore.filteredCountInUse({
|
return this.memorize('customRolesCountInUse', () =>
|
||||||
|
this.roleStore.filteredCountInUse({
|
||||||
type: CUSTOM_ROOT_ROLE_TYPE,
|
type: CUSTOM_ROOT_ROLE_TYPE,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
segmentCount(): Promise<number> {
|
segmentCount(): Promise<number> {
|
||||||
return this.segmentStore.count();
|
return this.memorize('segmentCount', () => this.segmentStore.count());
|
||||||
}
|
}
|
||||||
|
|
||||||
contextFieldCount(): Promise<number> {
|
contextFieldCount(): Promise<number> {
|
||||||
return this.contextFieldStore.count();
|
return this.memorize('contextFieldCount', () =>
|
||||||
|
this.contextFieldStore.count(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
strategiesCount(): Promise<number> {
|
strategiesCount(): Promise<number> {
|
||||||
return this.strategyStore.count();
|
return this.memorize('strategiesCount', () =>
|
||||||
|
this.strategyStore.count(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
environmentCount(): Promise<number> {
|
environmentCount(): Promise<number> {
|
||||||
return this.environmentStore.count();
|
return this.memorize('environmentCount', () =>
|
||||||
|
this.environmentStore.count(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
countPreviousDayHourlyMetricsBuckets(): Promise<{
|
countPreviousDayHourlyMetricsBuckets(): Promise<{
|
||||||
enabledCount: number;
|
enabledCount: number;
|
||||||
variantCount: number;
|
variantCount: number;
|
||||||
}> {
|
}> {
|
||||||
return this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets();
|
return this.memorize('countPreviousDayHourlyMetricsBuckets', () =>
|
||||||
|
this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
countApiTokensByType(): Promise<Map<string, number>> {
|
countApiTokensByType(): Promise<Map<string, number>> {
|
||||||
return this.apiTokenStore.countByType();
|
return this.memorize('countApiTokensByType', () =>
|
||||||
|
this.apiTokenStore.countByType(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRegisteredUsers(): Promise<number> {
|
getRegisteredUsers(): Promise<number> {
|
||||||
return this.userStore.count();
|
return this.memorize('getRegisteredUsers', () =>
|
||||||
|
this.userStore.count(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
countServiceAccounts(): Promise<number> {
|
countServiceAccounts(): Promise<number> {
|
||||||
return this.userStore.countServiceAccounts();
|
return this.memorize('countServiceAccounts', () =>
|
||||||
|
this.userStore.countServiceAccounts(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentTrafficData(): Promise<number> {
|
async getCurrentTrafficData(): Promise<number> {
|
||||||
|
return this.memorize('getCurrentTrafficData', async () => {
|
||||||
const traffic =
|
const traffic =
|
||||||
await this.trafficDataUsageStore.getTrafficDataUsageForPeriod(
|
await this.trafficDataUsageStore.getTrafficDataUsageForPeriod(
|
||||||
format(new Date(), 'yyyy-MM'),
|
format(new Date(), 'yyyy-MM'),
|
||||||
@ -408,11 +493,13 @@ export class InstanceStatsService {
|
|||||||
|
|
||||||
const counts = traffic.map((item) => item.count);
|
const counts = traffic.map((item) => item.count);
|
||||||
return counts.reduce((total, current) => total + current, 0);
|
return counts.reduce((total, current) => total + current, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLabeledAppCounts(): Promise<
|
async getLabeledAppCounts(): Promise<
|
||||||
Partial<{ [key in TimeRange]: number }>
|
Partial<{ [key in TimeRange]: number }>
|
||||||
> {
|
> {
|
||||||
|
return this.memorize('getLabeledAppCounts', async () => {
|
||||||
const [t7d, t30d, allTime] = await Promise.all([
|
const [t7d, t30d, allTime] = await Promise.all([
|
||||||
this.clientInstanceStore.getDistinctApplicationsCount(7),
|
this.clientInstanceStore.getDistinctApplicationsCount(7),
|
||||||
this.clientInstanceStore.getDistinctApplicationsCount(30),
|
this.clientInstanceStore.getDistinctApplicationsCount(30),
|
||||||
@ -424,6 +511,7 @@ export class InstanceStatsService {
|
|||||||
allTime,
|
allTime,
|
||||||
};
|
};
|
||||||
return this.appCount;
|
return this.appCount;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAppCountSnapshot(range: TimeRange): number | undefined {
|
getAppCountSnapshot(range: TimeRange): number | undefined {
|
||||||
|
@ -85,6 +85,8 @@ class InstanceAdminController extends Controller {
|
|||||||
return {
|
return {
|
||||||
OIDCenabled: true,
|
OIDCenabled: true,
|
||||||
SAMLenabled: false,
|
SAMLenabled: false,
|
||||||
|
passwordAuthEnabled: true,
|
||||||
|
SCIMenabled: false,
|
||||||
clientApps: [
|
clientApps: [
|
||||||
{ range: 'allTime', count: 15 },
|
{ range: 'allTime', count: 15 },
|
||||||
{ range: '30d', count: 9 },
|
{ range: '30d', count: 9 },
|
||||||
|
@ -61,7 +61,8 @@ export type IFlagKey =
|
|||||||
| 'simplifyProjectOverview'
|
| 'simplifyProjectOverview'
|
||||||
| 'flagOverviewRedesign'
|
| 'flagOverviewRedesign'
|
||||||
| 'showUserDeviceCount'
|
| 'showUserDeviceCount'
|
||||||
| 'deleteStaleUserSessions';
|
| 'deleteStaleUserSessions'
|
||||||
|
| 'memorizeStats';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user