diff --git a/frontend/src/component/admin/auth/AuthSettings.tsx b/frontend/src/component/admin/auth/AuthSettings.tsx index 3f9629c1ede2..af024707dca4 100644 --- a/frontend/src/component/admin/auth/AuthSettings.tsx +++ b/frontend/src/component/admin/auth/AuthSettings.tsx @@ -12,6 +12,7 @@ import { ADMIN } from '@server/types/permissions'; import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature'; import { useState } from 'react'; import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel'; +import { usePageTitle } from 'hooks/usePageTitle'; export const AuthSettings = () => { const { authenticationType } = useUiConfig().uiConfig; @@ -46,6 +47,7 @@ export const AuthSettings = () => { } const [activeTab, setActiveTab] = useState(0); + usePageTitle(`Single sign-on: ${tabs[activeTab].label}`); return (
diff --git a/frontend/src/component/admin/roles/RoleForm/useRoleForm.test.ts b/frontend/src/component/admin/roles/RoleForm/useRoleForm.test.ts index c9e8b431c0b1..d25da27c6b1d 100644 --- a/frontend/src/component/admin/roles/RoleForm/useRoleForm.test.ts +++ b/frontend/src/component/admin/roles/RoleForm/useRoleForm.test.ts @@ -14,14 +14,14 @@ describe('trim names and description', () => { expect(result.current.name).toBe('my role'); }); - test('description is trimmed before being set', () => { + test('description is not trimmed before being set', () => { const { result } = renderHook(() => useRoleForm()); act(() => { result.current.setDescription(' my description '); }); - expect(result.current.description).toBe('my description'); + expect(result.current.description).toBe(' my description '); }); test('name that is just whitespace triggers an error', () => { diff --git a/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts b/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts index f16f3e11dce9..fc077fe76e8b 100644 --- a/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts +++ b/frontend/src/component/admin/roles/RoleForm/useRoleForm.ts @@ -29,8 +29,6 @@ export const useRoleForm = ( const [name, setName] = useState(initialName); const setTrimmedName = (newName: string) => setName(newName.trim()); const [description, setDescription] = useState(initialDescription); - const setTrimmedDescription = (newDescription: string) => - setDescription(newDescription.trim()); const [checkedPermissions, setCheckedPermissions] = useState({}); const [errors, setErrors] = useState(DEFAULT_ERRORS); @@ -147,7 +145,7 @@ export const useRoleForm = ( setName: setTrimmedName, validateName, description, - setDescription: setTrimmedDescription, + setDescription, validateDescription, checkedPermissions, setCheckedPermissions, diff --git a/frontend/src/component/admin/roles/RolesPage.tsx b/frontend/src/component/admin/roles/RolesPage.tsx index 4299b0a48ae8..84813c8c94b1 100644 --- a/frontend/src/component/admin/roles/RolesPage.tsx +++ b/frontend/src/component/admin/roles/RolesPage.tsx @@ -14,6 +14,7 @@ import Add from '@mui/icons-material/Add'; import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton'; import type { IRole } from 'interfaces/role'; import { TabLink } from 'component/common/TabNav/TabLink'; +import { usePageTitle } from 'hooks/usePageTitle'; const StyledHeader = styled('div')(() => ({ display: 'flex', @@ -31,6 +32,7 @@ const StyledActions = styled('div')({ }); export const RolesPage = () => { + usePageTitle('Roles'); const { pathname } = useLocation(); const { roles, projectRoles, loading } = useRoles(); diff --git a/frontend/src/component/featureTypes/FeatureTypesList.tsx b/frontend/src/component/featureTypes/FeatureTypesList.tsx index 6190f78740e7..2d9f3953bcde 100644 --- a/frontend/src/component/featureTypes/FeatureTypesList.tsx +++ b/frontend/src/component/featureTypes/FeatureTypesList.tsx @@ -5,7 +5,7 @@ import { sortTypes } from 'utils/sortTypes'; import { PageContent } from 'component/common/PageContent/PageContent'; import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; -import { Box, Typography } from '@mui/material'; +import { Box } from '@mui/material'; import { Table, TableBody, @@ -150,18 +150,7 @@ export const FeatureTypesList = () => { return ( - ({ - fontSize: theme.fontSizes.mainHeader, - })} - > - Feature flag types - - - } + header={} > diff --git a/frontend/src/component/insights/components/InsightsHeader/InsightsHeader.tsx b/frontend/src/component/insights/components/InsightsHeader/InsightsHeader.tsx index 7a2dee00e29e..b9e5e07ac123 100644 --- a/frontend/src/component/insights/components/InsightsHeader/InsightsHeader.tsx +++ b/frontend/src/component/insights/components/InsightsHeader/InsightsHeader.tsx @@ -69,6 +69,7 @@ export const InsightsHeader: VFC = ({ actions }) => { return ( <> = { '/admin/cors': CorsIcon, '/admin/billing': BillingIcon, '/history': EventLogIcon, + '/releases-management': LaunchIcon, + '/personal': PersonalDashboardIcon, GitHub: GitHubIcon, Documentation: LibraryBooksIcon, }; diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap index c536b1a46d46..3652622dc286 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap @@ -230,6 +230,20 @@ exports[`returns all baseRoutes 1`] = ` "title": "Strategy types", "type": "protected", }, + { + "component": [Function], + "enterprise": true, + "flag": "releasePlans", + "menu": { + "advanced": true, + "mode": [ + "enterprise", + ], + }, + "path": "/releases-management", + "title": "Release management", + "type": "protected", + }, { "component": [Function], "menu": {}, diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index 9428131149a0..07090d77c119 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -48,6 +48,7 @@ import { Application } from 'component/application/Application'; import { Signals } from 'component/signals/Signals'; import { LazyCreateProject } from '../project/Project/CreateProject/LazyCreateProject'; import { PersonalDashboard } from '../personalDashboard/PersonalDashboard'; +import { ReleaseManagement } from 'component/releases/ReleaseManagement'; export const routes: IRoute[] = [ // Splash @@ -246,6 +247,15 @@ export const routes: IRoute[] = [ type: 'protected', menu: { mobile: true, advanced: true }, }, + { + path: '/releases-management', + title: 'Release management', + component: ReleaseManagement, + type: 'protected', + menu: { advanced: true, mode: ['enterprise'] }, + flag: 'releasePlans', + enterprise: true, + }, { path: '/environments/create', title: 'Environments', diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index 21a1c998f3d2..bba512e01923 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -19,6 +19,7 @@ import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi'; import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash'; import { useDashboardState } from './useDashboardState'; import { MyFlags } from './MyFlags'; +import { usePageTitle } from 'hooks/usePageTitle'; const WelcomeSection = styled('div')(({ theme }) => ({ display: 'flex', @@ -103,9 +104,10 @@ export const PersonalDashboard = () => { const { trackEvent } = usePlausibleTracker(); const { setSplashSeen } = useSplashApi(); const { splash } = useAuthSplash(); - const name = user?.name; + usePageTitle(`Dashboard: ${name}`); + const { personalDashboard, refetch: refetchDashboard } = usePersonalDashboard(); diff --git a/frontend/src/component/releases/ReleaseManagement.tsx b/frontend/src/component/releases/ReleaseManagement.tsx new file mode 100644 index 000000000000..929eedca3711 --- /dev/null +++ b/frontend/src/component/releases/ReleaseManagement.tsx @@ -0,0 +1,3 @@ +export const ReleaseManagement = () => { + return null; +}; diff --git a/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx b/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx index 0ee82dba8ea0..06593e98e03f 100644 --- a/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx +++ b/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx @@ -397,9 +397,10 @@ export const StrategiesList = () => { - - + } + title='Strategy types' + /> } > diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index a627e8895e8c..ca6c80965b00 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -92,6 +92,7 @@ export type UiFlags = { personalDashboardUI?: boolean; purchaseAdditionalEnvironments?: boolean; unleashAI?: boolean; + releasePlans?: boolean; }; export interface IVersionInfo { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1d3c1d2271c5..a879ec856a14 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2743,20 +2743,20 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 22.5.5 - resolution: "@types/node@npm:22.5.5" + version: 22.7.6 + resolution: "@types/node@npm:22.7.6" dependencies: undici-types: "npm:~6.19.2" - checksum: 10c0/ead9495cfc6b1da5e7025856dcce2591e9bae635357410c0d2dd619fce797d2a1d402887580ca4b336cb78168b195224869967de370a23f61663cf1e4836121c + checksum: 10c0/d4406a63afce981c363fb1d1954aaf1759ad2d487c0833ebf667565ea4e45ff217d6fab4b5343badbdeccdf9d2e4a0841d633e0c929ceabcb33c288663dd0c73 languageName: node linkType: hard "@types/node@npm:^20.12.12, @types/node@npm:^20.12.13": - version: 20.16.5 - resolution: "@types/node@npm:20.16.5" + version: 20.16.12 + resolution: "@types/node@npm:20.16.12" dependencies: undici-types: "npm:~6.19.2" - checksum: 10c0/6af7994129815010bcbc4cf8221865559c8116ff43e74a6549525c2108267596fc2d18aff5d5ecfe089fb60a119f975631343e2c65c52bfa0955ed9dc56733d6 + checksum: 10c0/f6a3c90c6745881d47f8dae7eb39d0dd6df9a4060c8f1ab7004690f60b9b183d862c9c6b380b9e8d5a17dd670ac7b19d6318f24c170897c85a9729f9804f47cf languageName: node linkType: hard diff --git a/package.json b/package.json index c86836658ee8..6d86bc66ad6e 100644 --- a/package.json +++ b/package.json @@ -173,10 +173,10 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "10.1.0", - "@babel/core": "7.25.2", + "@babel/core": "7.25.8", "@biomejs/biome": "^1.8.3", "@cyclonedx/yarn-plugin-cyclonedx": "^1.0.0-rc.7", - "@swc/core": "1.7.26", + "@swc/core": "1.7.35", "@swc/jest": "0.2.36", "@types/bcryptjs": "2.4.6", "@types/cors": "2.8.17", diff --git a/src/lib/features/instance-stats/instance-stats-service.test.ts b/src/lib/features/instance-stats/instance-stats-service.test.ts index d3f423bec1a6..26a5f7009502 100644 --- a/src/lib/features/instance-stats/instance-stats-service.test.ts +++ b/src/lib/features/instance-stats/instance-stats-service.test.ts @@ -4,11 +4,18 @@ import createStores from '../../../test/fixtures/store'; import VersionService from '../../services/version-service'; import { createFakeGetActiveUsers } from './getActiveUsers'; import { createFakeGetProductionChanges } from './getProductionChanges'; - +import { registerPrometheusMetrics } from '../../metrics'; +import { register } from 'prom-client'; +import type { IClientInstanceStore } from '../../types'; let instanceStatsService: InstanceStatsService; let versionService: VersionService; - +let clientInstanceStore: IClientInstanceStore; +let updateMetrics: () => Promise; beforeEach(() => { + jest.clearAllMocks(); + + register.clear(); + const config = createTestConfig(); const stores = createStores(); versionService = new VersionService( @@ -17,6 +24,7 @@ beforeEach(() => { createFakeGetActiveUsers(), createFakeGetProductionChanges(), ); + clientInstanceStore = stores.clientInstanceStore; instanceStatsService = new InstanceStatsService( stores, config, @@ -25,23 +33,28 @@ beforeEach(() => { createFakeGetProductionChanges(), ); - jest.spyOn(instanceStatsService, 'refreshAppCountSnapshot'); - jest.spyOn(instanceStatsService, 'getLabeledAppCounts'); + const { collectDbMetrics } = registerPrometheusMetrics( + config, + stores, + undefined as unknown as string, + config.eventBus, + instanceStatsService, + ); + updateMetrics = collectDbMetrics; + + jest.spyOn(clientInstanceStore, 'getDistinctApplicationsCount'); jest.spyOn(instanceStatsService, 'getStats'); - // validate initial state without calls to these methods - expect(instanceStatsService.refreshAppCountSnapshot).toHaveBeenCalledTimes( - 0, - ); expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0); }); test('get snapshot should not call getStats', async () => { - await instanceStatsService.refreshAppCountSnapshot(); - expect(instanceStatsService.getLabeledAppCounts).toHaveBeenCalledTimes(1); + await updateMetrics(); + expect( + clientInstanceStore.getDistinctApplicationsCount, + ).toHaveBeenCalledTimes(3); expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0); - // subsequent calls to getStatsSnapshot don't call getStats for (let i = 0; i < 3; i++) { const { clientApps } = await instanceStatsService.getStats(); expect(clientApps).toStrictEqual([ @@ -51,12 +64,11 @@ test('get snapshot should not call getStats', async () => { ]); } // after querying the stats snapshot no call to getStats should be issued - expect(instanceStatsService.getLabeledAppCounts).toHaveBeenCalledTimes(1); + expect( + clientInstanceStore.getDistinctApplicationsCount, + ).toHaveBeenCalledTimes(3); }); test('before the snapshot is refreshed we can still get the appCount', async () => { - expect(instanceStatsService.refreshAppCountSnapshot).toHaveBeenCalledTimes( - 0, - ); expect(instanceStatsService.getAppCountSnapshot('7d')).toBeUndefined(); }); diff --git a/src/lib/features/instance-stats/instance-stats-service.ts b/src/lib/features/instance-stats/instance-stats-service.ts index 3fb505fd6480..24e04ef3dfaa 100644 --- a/src/lib/features/instance-stats/instance-stats-service.ts +++ b/src/lib/features/instance-stats/instance-stats-service.ts @@ -109,9 +109,9 @@ export class InstanceStatsService { private appCount?: Partial<{ [key in TimeRange]: number }>; - private getActiveUsers: GetActiveUsers; + getActiveUsers: GetActiveUsers; - private getProductionChanges: GetProductionChanges; + getProductionChanges: GetProductionChanges; private featureStrategiesReadModel: IFeatureStrategiesReadModel; @@ -180,25 +180,6 @@ export class InstanceStatsService { this.featureStrategiesReadModel = featureStrategiesReadModel; } - async refreshAppCountSnapshot(): Promise< - Partial<{ [key in TimeRange]: number }> - > { - try { - this.appCount = await this.getLabeledAppCounts(); - return this.appCount; - } catch (error) { - this.logger.warn( - 'Unable to retrieve statistics. This will be retried', - error, - ); - return { - '7d': 0, - '30d': 0, - allTime: 0, - }; - } - } - getProjectModeCount(): Promise { return this.projectStore.getProjectModeCounts(); } @@ -231,9 +212,6 @@ export class InstanceStatsService { return settings?.enabled || false; } - /** - * use getStatsSnapshot for low latency, sacrificing data-freshness - */ async getStats(): Promise { const versionInfo = await this.versionService.getVersionInfo(); const [ @@ -265,22 +243,22 @@ export class InstanceStatsService { ] = await Promise.all([ this.getToggleCount(), this.getArchivedToggleCount(), - this.userStore.count(), - this.userStore.countServiceAccounts(), - this.apiTokenStore.countByType(), + this.getRegisteredUsers(), + this.countServiceAccounts(), + this.countApiTokensByType(), this.getActiveUsers(), this.getProjectModeCount(), - this.contextFieldStore.count(), - this.groupStore.count(), - this.roleStore.count(), - this.roleStore.filteredCount({ type: CUSTOM_ROOT_ROLE_TYPE }), - this.roleStore.filteredCountInUse({ type: CUSTOM_ROOT_ROLE_TYPE }), - this.environmentStore.count(), - this.segmentStore.count(), - this.strategyStore.count(), + this.contextFieldCount(), + this.groupCount(), + this.roleCount(), + this.customRolesCount(), + this.customRolesCountInUse(), + this.environmentCount(), + this.segmentCount(), + this.strategiesCount(), this.hasSAML(), this.hasOIDC(), - this.appCount ? this.appCount : this.refreshAppCountSnapshot(), + this.appCount ? this.appCount : this.getLabeledAppCounts(), this.eventStore.deprecatedFilteredCount({ type: FEATURES_EXPORTED, }), @@ -288,7 +266,7 @@ export class InstanceStatsService { type: FEATURES_IMPORTED, }), this.getProductionChanges(), - this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(), + this.countPreviousDayHourlyMetricsBuckets(), this.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(), this.featureStrategiesReadModel.getMaxConstraintValues(), this.featureStrategiesReadModel.getMaxConstraintsPerStrategy(), @@ -330,6 +308,59 @@ export class InstanceStatsService { }; } + groupCount(): Promise { + return this.groupStore.count(); + } + + roleCount(): Promise { + return this.roleStore.count(); + } + + customRolesCount(): Promise { + return this.roleStore.filteredCount({ type: CUSTOM_ROOT_ROLE_TYPE }); + } + + customRolesCountInUse(): Promise { + return this.roleStore.filteredCountInUse({ + type: CUSTOM_ROOT_ROLE_TYPE, + }); + } + + segmentCount(): Promise { + return this.segmentStore.count(); + } + + contextFieldCount(): Promise { + return this.contextFieldStore.count(); + } + + strategiesCount(): Promise { + return this.strategyStore.count(); + } + + environmentCount(): Promise { + return this.environmentStore.count(); + } + + countPreviousDayHourlyMetricsBuckets(): Promise<{ + enabledCount: number; + variantCount: number; + }> { + return this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(); + } + + countApiTokensByType(): Promise> { + return this.apiTokenStore.countByType(); + } + + getRegisteredUsers(): Promise { + return this.userStore.count(); + } + + countServiceAccounts(): Promise { + return this.userStore.countServiceAccounts(); + } + async getLabeledAppCounts(): Promise< Partial<{ [key in TimeRange]: number }> > { @@ -338,11 +369,12 @@ export class InstanceStatsService { this.clientInstanceStore.getDistinctApplicationsCount(30), this.clientInstanceStore.getDistinctApplicationsCount(), ]); - return { + this.appCount = { '7d': t7d, '30d': t30d, allTime, }; + return this.appCount; } getAppCountSnapshot(range: TimeRange): number | undefined { diff --git a/src/lib/features/scheduler/schedule-services.ts b/src/lib/features/scheduler/schedule-services.ts index 95e03c114b19..69d3a218fc1e 100644 --- a/src/lib/features/scheduler/schedule-services.ts +++ b/src/lib/features/scheduler/schedule-services.ts @@ -59,8 +59,12 @@ export const scheduleServices = async ( 'updateLastSeen', ); + // TODO this works fine for keeping labeledAppCounts up to date, but + // it would be nice if we can keep client_apps_total prometheus metric + // up to date. We'd need to have access to DbMetricsMonitor, which is + // where the metric is registered and call an update only for that metric schedulerService.schedule( - instanceStatsService.refreshAppCountSnapshot.bind(instanceStatsService), + instanceStatsService.getLabeledAppCounts.bind(instanceStatsService), minutesToMilliseconds(5), 'refreshAppCountSnapshot', ); diff --git a/src/lib/metrics-gauge.test.ts b/src/lib/metrics-gauge.test.ts new file mode 100644 index 000000000000..e024563f9628 --- /dev/null +++ b/src/lib/metrics-gauge.test.ts @@ -0,0 +1,114 @@ +import { register } from 'prom-client'; +import { createTestConfig } from '../test/config/test-config'; +import type { IUnleashConfig } from './types'; +import { DbMetricsMonitor } from './metrics-gauge'; + +const prometheusRegister = register; +let config: IUnleashConfig; +let dbMetrics: DbMetricsMonitor; + +beforeAll(async () => { + config = createTestConfig({ + server: { + serverMetrics: true, + }, + }); +}); + +beforeEach(async () => { + dbMetrics = new DbMetricsMonitor(config); +}); + +test('should collect registered metrics', async () => { + dbMetrics.registerGaugeDbMetric({ + name: 'my_metric', + help: 'This is the answer to life, the univers, and everything', + labelNames: [], + query: () => Promise.resolve(42), + map: (result) => ({ value: result }), + }); + + await dbMetrics.refreshDbMetrics(); + + const metrics = await prometheusRegister.metrics(); + expect(metrics).toMatch(/my_metric 42/); +}); + +test('should collect registered metrics with labels', async () => { + dbMetrics.registerGaugeDbMetric({ + name: 'life_the_universe_and_everything', + help: 'This is the answer to life, the univers, and everything', + labelNames: ['test'], + query: () => Promise.resolve(42), + map: (result) => ({ value: result, labels: { test: 'case' } }), + }); + + await dbMetrics.refreshDbMetrics(); + + const metrics = await prometheusRegister.metrics(); + expect(metrics).toMatch( + /life_the_universe_and_everything\{test="case"\} 42/, + ); +}); + +test('should collect multiple registered metrics with and without labels', async () => { + dbMetrics.registerGaugeDbMetric({ + name: 'my_first_metric', + help: 'This is the answer to life, the univers, and everything', + labelNames: [], + query: () => Promise.resolve(42), + map: (result) => ({ value: result }), + }); + + dbMetrics.registerGaugeDbMetric({ + name: 'my_other_metric', + help: 'This is Eulers number', + labelNames: ['euler'], + query: () => Promise.resolve(Math.E), + map: (result) => ({ value: result, labels: { euler: 'number' } }), + }); + + await dbMetrics.refreshDbMetrics(); + + const metrics = await prometheusRegister.metrics(); + expect(metrics).toMatch(/my_first_metric 42/); + expect(metrics).toMatch(/my_other_metric\{euler="number"\} 2.71828/); +}); + +test('should support different label and value pairs', async () => { + dbMetrics.registerGaugeDbMetric({ + name: 'multi_dimensional', + help: 'This metric has different values for different labels', + labelNames: ['version', 'range'], + query: () => Promise.resolve(2), + map: (result) => [ + { value: result, labels: { version: '1', range: 'linear' } }, + { + value: result * result, + labels: { version: '2', range: 'square' }, + }, + { value: result / 2, labels: { version: '3', range: 'half' } }, + ], + }); + + await dbMetrics.refreshDbMetrics(); + + const metrics = await prometheusRegister.metrics(); + expect(metrics).toMatch( + /multi_dimensional\{version="1",range="linear"\} 2\nmulti_dimensional\{version="2",range="square"\} 4\nmulti_dimensional\{version="3",range="half"\} 1/, + ); + expect( + await dbMetrics.findValue('multi_dimensional', { range: 'linear' }), + ).toBe(2); + expect( + await dbMetrics.findValue('multi_dimensional', { range: 'half' }), + ).toBe(1); + expect( + await dbMetrics.findValue('multi_dimensional', { range: 'square' }), + ).toBe(4); + expect( + await dbMetrics.findValue('multi_dimensional', { range: 'x' }), + ).toBeUndefined(); + expect(await dbMetrics.findValue('multi_dimensional')).toBe(2); // first match + expect(await dbMetrics.findValue('other')).toBeUndefined(); +}); diff --git a/src/lib/metrics-gauge.ts b/src/lib/metrics-gauge.ts new file mode 100644 index 000000000000..7f1e5cc06265 --- /dev/null +++ b/src/lib/metrics-gauge.ts @@ -0,0 +1,94 @@ +import type { Logger } from './logger'; +import type { IUnleashConfig } from './types'; +import { createGauge, type Gauge } from './util/metrics'; + +type Query = () => Promise; +type MetricValue = { + value: number; + labels?: Record; +}; +type MapResult = ( + result: R, +) => MetricValue | MetricValue[]; + +type GaugeDefinition = { + name: string; + help: string; + labelNames: L[]; + query: Query; + map: MapResult; +}; + +type Task = () => Promise; + +interface GaugeUpdater { + target: Gauge; + task: Task; +} +export class DbMetricsMonitor { + private updaters: Map = new Map(); + private log: Logger; + + constructor({ getLogger }: Pick) { + this.log = getLogger('gauge-metrics'); + } + + private asArray(value: T | T[]): T[] { + return Array.isArray(value) ? value : [value]; + } + + registerGaugeDbMetric( + definition: GaugeDefinition, + ): Task { + const gauge = createGauge(definition); + const task = async () => { + try { + const result = await definition.query(); + if (result !== null && result !== undefined) { + const results = this.asArray(definition.map(result)); + gauge.reset(); + for (const r of results) { + if (r.labels) { + gauge.labels(r.labels).set(r.value); + } else { + gauge.set(r.value); + } + } + } + } catch (e) { + this.log.warn(`Failed to refresh ${definition.name}`, e); + } + }; + this.updaters.set(definition.name, { target: gauge, task }); + return task; + } + + refreshDbMetrics = async () => { + const tasks = Array.from(this.updaters.entries()).map( + ([name, updater]) => ({ name, task: updater.task }), + ); + for (const { name, task } of tasks) { + this.log.debug(`Refreshing metric ${name}`); + await task(); + } + }; + + async findValue( + name: string, + labels?: Record, + ): Promise { + const gauge = await this.updaters.get(name)?.target.gauge?.get(); + if (gauge && gauge.values.length > 0) { + const values = labels + ? gauge.values.filter(({ labels: l }) => { + return Object.entries(labels).every( + ([key, value]) => l[key] === value, + ); + }) + : gauge.values; + // return first value + return values.map(({ value }) => value).shift(); + } + return undefined; + } +} diff --git a/src/lib/metrics.test.ts b/src/lib/metrics.test.ts index bf92983caf55..8cfe78aa5d0a 100644 --- a/src/lib/metrics.test.ts +++ b/src/lib/metrics.test.ts @@ -15,7 +15,11 @@ import { FEATURE_UPDATED, PROJECT_ENVIRONMENT_REMOVED, } from './types/events'; -import { createMetricsMonitor } from './metrics'; +import { + createMetricsMonitor, + registerPrometheusMetrics, + registerPrometheusPostgresMetrics, +} from './metrics'; import createStores from '../test/fixtures/store'; import { InstanceStatsService } from './features/instance-stats/instance-stats-service'; import VersionService from './services/version-service'; @@ -46,6 +50,7 @@ let schedulerService: SchedulerService; let featureLifeCycleStore: IFeatureLifecycleStore; let featureLifeCycleReadModel: IFeatureLifecycleReadModel; let db: ITestDb; +let refreshDbMetrics: () => Promise; beforeAll(async () => { const config = createTestConfig({ @@ -102,16 +107,16 @@ beforeAll(async () => { }, }; - await monitor.startMonitoring( - config, - stores, - '4.0.0', - eventBus, - statsService, - schedulerService, - // @ts-ignore - We don't want a full knex implementation for our tests, it's enough that it actually yields the numbers we want. - metricsDbConf, - ); + const { collectDbMetrics, collectStaticCounters } = + registerPrometheusMetrics( + config, + stores, + '4.0.0', + eventBus, + statsService, + ); + refreshDbMetrics = collectDbMetrics; + await collectStaticCounters(); }); afterAll(async () => { @@ -212,6 +217,7 @@ test('should collect metrics for function timings', async () => { }); test('should collect metrics for feature flag size', async () => { + await refreshDbMetrics(); const metrics = await prometheusRegister.metrics(); expect(metrics).toMatch(/feature_toggles_total\{version="(.*)"\} 0/); }); @@ -222,12 +228,13 @@ test('should collect metrics for archived feature flag size', async () => { }); test('should collect metrics for total client apps', async () => { - await statsService.refreshAppCountSnapshot(); + await refreshDbMetrics(); const metrics = await prometheusRegister.metrics(); expect(metrics).toMatch(/client_apps_total\{range="(.*)"\} 0/); }); test('Should collect metrics for database', async () => { + registerPrometheusPostgresMetrics(db.rawDatabase, eventBus, '15.0.0'); const metrics = await prometheusRegister.metrics(); expect(metrics).toMatch(/db_pool_max/); expect(metrics).toMatch(/db_pool_min/); diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index be7311952cf8..e35245add754 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -25,7 +25,7 @@ import { PROJECT_DELETED, } from './types/events'; import type { IUnleashConfig } from './types/option'; -import type { ISettingStore, IUnleashStores } from './types/stores'; +import type { IUnleashStores } from './types/stores'; import { hoursToMilliseconds, minutesToMilliseconds } from 'date-fns'; import type { InstanceStatsService } from './features/instance-stats/instance-stats-service'; import type { IEnvironment, ISdkHeartbeat } from './types'; @@ -37,367 +37,792 @@ import { } from './util/metrics'; import type { SchedulerService } from './services'; import type { IClientMetricsEnv } from './features/metrics/client-metrics/client-metrics-store-v2-type'; - -export default class MetricsMonitor { - constructor() {} - - async startMonitoring( - config: IUnleashConfig, - stores: IUnleashStores, - version: string, - eventBus: EventEmitter, - instanceStatsService: InstanceStatsService, - schedulerService: SchedulerService, - db: Knex, - ): Promise { - if (!config.server.serverMetrics) { - return Promise.resolve(); - } - - const { eventStore, environmentStore } = stores; - const { flagResolver } = config; - - const cachedEnvironments: () => Promise = memoizee( - async () => environmentStore.getAll(), - { - promise: true, - maxAge: hoursToMilliseconds(1), - }, - ); - - collectDefaultMetrics(); - - const requestDuration = createSummary({ - name: 'http_request_duration_milliseconds', - help: 'App response time', - labelNames: ['path', 'method', 'status', 'appName'], - percentiles: [0.1, 0.5, 0.9, 0.95, 0.99], - maxAgeSeconds: 600, - ageBuckets: 5, - }); - const schedulerDuration = createSummary({ - name: 'scheduler_duration_seconds', - help: 'Scheduler duration time', - labelNames: ['jobId'], - percentiles: [0.1, 0.5, 0.9, 0.95, 0.99], - maxAgeSeconds: 600, - ageBuckets: 5, - }); - const dbDuration = createSummary({ - name: 'db_query_duration_seconds', - help: 'DB query duration time', - labelNames: ['store', 'action'], - percentiles: [0.1, 0.5, 0.9, 0.95, 0.99], - maxAgeSeconds: 600, - ageBuckets: 5, - }); - const functionDuration = createSummary({ - name: 'function_duration_seconds', - help: 'Function duration time', - labelNames: ['functionName', 'className'], - percentiles: [0.1, 0.5, 0.9, 0.95, 0.99], - maxAgeSeconds: 600, - ageBuckets: 5, - }); - const featureFlagUpdateTotal = createCounter({ - name: 'feature_toggle_update_total', - help: 'Number of times a toggle has been updated. Environment label would be "n/a" when it is not available, e.g. when a feature flag is created.', - labelNames: [ - 'toggle', - 'project', - 'environment', - 'environmentType', - 'action', - ], - }); - const featureFlagUsageTotal = createCounter({ - name: 'feature_toggle_usage_total', - help: 'Number of times a feature flag has been used', - labelNames: ['toggle', 'active', 'appName'], - }); - const featureFlagsTotal = createGauge({ - name: 'feature_toggles_total', - help: 'Number of feature flags', +import { DbMetricsMonitor } from './metrics-gauge'; + +export function registerPrometheusPostgresMetrics( + db: Knex, + eventBus: EventEmitter, + postgresVersion: string, +) { + if (db?.client) { + const dbPoolMin = createGauge({ + name: 'db_pool_min', + help: 'Minimum DB pool size', + }); + dbPoolMin.set(db.client.pool.min); + const dbPoolMax = createGauge({ + name: 'db_pool_max', + help: 'Maximum DB pool size', + }); + dbPoolMax.set(db.client.pool.max); + const dbPoolFree = createGauge({ + name: 'db_pool_free', + help: 'Current free connections in DB pool', + }); + const dbPoolUsed = createGauge({ + name: 'db_pool_used', + help: 'Current connections in use in DB pool', + }); + const dbPoolPendingCreates = createGauge({ + name: 'db_pool_pending_creates', + help: 'how many asynchronous create calls are running in DB pool', + }); + const dbPoolPendingAcquires = createGauge({ + name: 'db_pool_pending_acquires', + help: 'how many acquires are waiting for a resource to be released in DB pool', + }); + + eventBus.on(DB_POOL_UPDATE, (data) => { + dbPoolFree.set(data.free); + dbPoolUsed.set(data.used); + dbPoolPendingCreates.set(data.pendingCreates); + dbPoolPendingAcquires.set(data.pendingAcquires); + }); + + const database_version = createGauge({ + name: 'postgres_version', + help: 'Which version of postgres is running (SHOW server_version)', labelNames: ['version'], }); - const maxFeatureEnvironmentStrategies = createGauge({ - name: 'max_feature_environment_strategies', - help: 'Maximum number of environment strategies in one feature', - labelNames: ['feature', 'environment'], - }); - const maxFeatureStrategies = createGauge({ - name: 'max_feature_strategies', - help: 'Maximum number of strategies in one feature', - labelNames: ['feature'], - }); - const maxConstraintValues = createGauge({ - name: 'max_constraint_values', - help: 'Maximum number of constraint values used in a single constraint', - labelNames: ['feature', 'environment'], - }); - const maxConstraintsPerStrategy = createGauge({ - name: 'max_strategy_constraints', - help: 'Maximum number of constraints used on a single strategy', - labelNames: ['feature', 'environment'], - }); - const largestProjectEnvironment = createGauge({ - name: 'largest_project_environment_size', - help: 'The largest project environment size (bytes) based on strategies, constraints, variants and parameters', - labelNames: ['project', 'environment'], - }); - const largestFeatureEnvironment = createGauge({ - name: 'largest_feature_environment_size', - help: 'The largest feature environment size (bytes) base on strategies, constraints, variants and parameters', - labelNames: ['feature', 'environment'], - }); - - const featureTogglesArchivedTotal = createGauge({ - name: 'feature_toggles_archived_total', - help: 'Number of archived feature flags', - }); - const usersTotal = createGauge({ - name: 'users_total', - help: 'Number of users', - }); - const serviceAccounts = createGauge({ - name: 'service_accounts_total', - help: 'Number of service accounts', - }); - const apiTokens = createGauge({ - name: 'api_tokens_total', - help: 'Number of API tokens', - labelNames: ['type'], - }); - const enabledMetricsBucketsPreviousDay = createGauge({ - name: 'enabled_metrics_buckets_previous_day', - help: 'Number of hourly enabled/disabled metric buckets in the previous day', - }); - const variantMetricsBucketsPreviousDay = createGauge({ - name: 'variant_metrics_buckets_previous_day', - help: 'Number of hourly variant metric buckets in the previous day', - }); - const usersActive7days = createGauge({ - name: 'users_active_7', - help: 'Number of users active in the last 7 days', - }); - const usersActive30days = createGauge({ - name: 'users_active_30', - help: 'Number of users active in the last 30 days', - }); - const usersActive60days = createGauge({ - name: 'users_active_60', - help: 'Number of users active in the last 60 days', - }); - const usersActive90days = createGauge({ - name: 'users_active_90', - help: 'Number of users active in the last 90 days', - }); - const projectsTotal = createGauge({ - name: 'projects_total', - help: 'Number of projects', - labelNames: ['mode'], - }); - const environmentsTotal = createGauge({ - name: 'environments_total', - help: 'Number of environments', - }); - const groupsTotal = createGauge({ - name: 'groups_total', - help: 'Number of groups', - }); - - const rolesTotal = createGauge({ - name: 'roles_total', - help: 'Number of roles', - }); - - const customRootRolesTotal = createGauge({ - name: 'custom_root_roles_total', - help: 'Number of custom root roles', - }); - - const customRootRolesInUseTotal = createGauge({ - name: 'custom_root_roles_in_use_total', - help: 'Number of custom root roles in use', - }); - - const segmentsTotal = createGauge({ - name: 'segments_total', - help: 'Number of segments', - }); - - const contextTotal = createGauge({ - name: 'context_total', - help: 'Number of context', - }); - - const strategiesTotal = createGauge({ - name: 'strategies_total', - help: 'Number of strategies', - }); - - const clientAppsTotal = createGauge({ - name: 'client_apps_total', - help: 'Number of registered client apps aggregated by range by last seen', - labelNames: ['range'], - }); - - const samlEnabled = createGauge({ - name: 'saml_enabled', - help: 'Whether SAML is enabled', - }); - - const oidcEnabled = createGauge({ - name: 'oidc_enabled', - help: 'Whether OIDC is enabled', - }); - - const clientSdkVersionUsage = createCounter({ - name: 'client_sdk_versions', - help: 'Which sdk versions are being used', - labelNames: [ - 'sdk_name', - 'sdk_version', - 'platform_name', - 'platform_version', - 'yggdrasil_version', - 'spec_version', - ], - }); - - const productionChanges30 = createGauge({ - name: 'production_changes_30', - help: 'Changes made to production environment last 30 days', - labelNames: ['environment'], - }); - const productionChanges60 = createGauge({ - name: 'production_changes_60', - help: 'Changes made to production environment last 60 days', - labelNames: ['environment'], - }); - const productionChanges90 = createGauge({ - name: 'production_changes_90', - help: 'Changes made to production environment last 90 days', - labelNames: ['environment'], - }); - - const rateLimits = createGauge({ - name: 'rate_limits', - help: 'Rate limits (per minute) for METHOD/ENDPOINT pairs', - labelNames: ['endpoint', 'method'], - }); - const featureCreatedByMigration = createCounter({ - name: 'feature_created_by_migration_count', - help: 'Feature createdBy migration count', - }); - const eventCreatedByMigration = createCounter({ - name: 'event_created_by_migration_count', - help: 'Event createdBy migration count', - }); - const proxyRepositoriesCreated = createCounter({ - name: 'proxy_repositories_created', - help: 'Proxy repositories created', - }); - const frontendApiRepositoriesCreated = createCounter({ - name: 'frontend_api_repositories_created', - help: 'Frontend API repositories created', - }); - const mapFeaturesForClientDuration = createHistogram({ - name: 'map_features_for_client_duration', - help: 'Duration of mapFeaturesForClient function', - }); - - const featureLifecycleStageDuration = createGauge({ - name: 'feature_lifecycle_stage_duration', - labelNames: ['stage', 'project_id'], - help: 'Duration of feature lifecycle stages', - }); - - const onboardingDuration = createGauge({ - name: 'onboarding_duration', - labelNames: ['event'], - help: 'firstLogin, secondLogin, firstFeatureFlag, firstPreLive, firstLive from first user creation', - }); - const projectOnboardingDuration = createGauge({ - name: 'project_onboarding_duration', - labelNames: ['event', 'project'], - help: 'firstFeatureFlag, firstPreLive, firstLive from project creation', - }); - - const featureLifecycleStageCountByProject = createGauge({ - name: 'feature_lifecycle_stage_count_by_project', - help: 'Count features in a given stage by project id', - labelNames: ['stage', 'project_id'], - }); - - const featureLifecycleStageEnteredCounter = createCounter({ - name: 'feature_lifecycle_stage_entered', - help: 'Count how many features entered a given stage', - labelNames: ['stage'], - }); - - const projectActionsCounter = createCounter({ - name: 'project_actions_count', - help: 'Count project actions', - labelNames: ['action'], - }); - - const projectEnvironmentsDisabled = createCounter({ - name: 'project_environments_disabled', - help: 'How many "environment disabled" events we have received for each project', - labelNames: ['project_id'], - }); - - const orphanedTokensTotal = createGauge({ - name: 'orphaned_api_tokens_total', - help: 'Number of API tokens without a project', - }); + database_version.labels({ version: postgresVersion }).set(1); + } +} - const orphanedTokensActive = createGauge({ - name: 'orphaned_api_tokens_active', - help: 'Number of API tokens without a project, last seen within 3 months', - }); +export function registerPrometheusMetrics( + config: IUnleashConfig, + stores: IUnleashStores, + version: string, + eventBus: EventEmitter, + instanceStatsService: InstanceStatsService, +) { + const resolveEnvironmentType = async ( + environment: string, + cachedEnvironments: () => Promise, + ): Promise => { + const environments = await cachedEnvironments(); + const env = environments.find((e) => e.name === environment); - const legacyTokensTotal = createGauge({ - name: 'legacy_api_tokens_total', - help: 'Number of API tokens with v1 format', - }); + if (env) { + return env.type; + } else { + return 'unknown'; + } + }; + + const { eventStore, environmentStore } = stores; + const { flagResolver, db } = config; + const dbMetrics = new DbMetricsMonitor(config); + + const cachedEnvironments: () => Promise = memoizee( + async () => environmentStore.getAll(), + { + promise: true, + maxAge: hoursToMilliseconds(1), + }, + ); + + const requestDuration = createSummary({ + name: 'http_request_duration_milliseconds', + help: 'App response time', + labelNames: ['path', 'method', 'status', 'appName'], + percentiles: [0.1, 0.5, 0.9, 0.95, 0.99], + maxAgeSeconds: 600, + ageBuckets: 5, + }); + const schedulerDuration = createSummary({ + name: 'scheduler_duration_seconds', + help: 'Scheduler duration time', + labelNames: ['jobId'], + percentiles: [0.1, 0.5, 0.9, 0.95, 0.99], + maxAgeSeconds: 600, + ageBuckets: 5, + }); + const dbDuration = createSummary({ + name: 'db_query_duration_seconds', + help: 'DB query duration time', + labelNames: ['store', 'action'], + percentiles: [0.1, 0.5, 0.9, 0.95, 0.99], + maxAgeSeconds: 600, + ageBuckets: 5, + }); + const functionDuration = createSummary({ + name: 'function_duration_seconds', + help: 'Function duration time', + labelNames: ['functionName', 'className'], + percentiles: [0.1, 0.5, 0.9, 0.95, 0.99], + maxAgeSeconds: 600, + ageBuckets: 5, + }); + const featureFlagUpdateTotal = createCounter({ + name: 'feature_toggle_update_total', + help: 'Number of times a toggle has been updated. Environment label would be "n/a" when it is not available, e.g. when a feature flag is created.', + labelNames: [ + 'toggle', + 'project', + 'environment', + 'environmentType', + 'action', + ], + }); + const featureFlagUsageTotal = createCounter({ + name: 'feature_toggle_usage_total', + help: 'Number of times a feature flag has been used', + labelNames: ['toggle', 'active', 'appName'], + }); + + dbMetrics.registerGaugeDbMetric({ + name: 'feature_toggles_total', + help: 'Number of feature flags', + labelNames: ['version'], + query: () => instanceStatsService.getToggleCount(), + map: (value) => ({ value, labels: { version } }), + }); + + dbMetrics.registerGaugeDbMetric({ + name: 'max_feature_environment_strategies', + help: 'Maximum number of environment strategies in one feature', + labelNames: ['feature', 'environment'], + query: () => + stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(), + map: (result) => ({ + value: result.count, + labels: { + environment: result.environment, + feature: result.feature, + }, + }), + }); + + dbMetrics.registerGaugeDbMetric({ + name: 'max_feature_strategies', + help: 'Maximum number of strategies in one feature', + labelNames: ['feature'], + query: () => + stores.featureStrategiesReadModel.getMaxFeatureStrategies(), + map: (result) => ({ + value: result.count, + labels: { feature: result.feature }, + }), + }); + + const maxConstraintValues = createGauge({ + name: 'max_constraint_values', + help: 'Maximum number of constraint values used in a single constraint', + labelNames: ['feature', 'environment'], + }); + const maxConstraintsPerStrategy = createGauge({ + name: 'max_strategy_constraints', + help: 'Maximum number of constraints used on a single strategy', + labelNames: ['feature', 'environment'], + }); + const largestProjectEnvironment = createGauge({ + name: 'largest_project_environment_size', + help: 'The largest project environment size (bytes) based on strategies, constraints, variants and parameters', + labelNames: ['project', 'environment'], + }); + const largestFeatureEnvironment = createGauge({ + name: 'largest_feature_environment_size', + help: 'The largest feature environment size (bytes) base on strategies, constraints, variants and parameters', + labelNames: ['feature', 'environment'], + }); + + const featureTogglesArchivedTotal = createGauge({ + name: 'feature_toggles_archived_total', + help: 'Number of archived feature flags', + }); + const usersTotal = createGauge({ + name: 'users_total', + help: 'Number of users', + }); + const serviceAccounts = createGauge({ + name: 'service_accounts_total', + help: 'Number of service accounts', + }); + const apiTokens = createGauge({ + name: 'api_tokens_total', + help: 'Number of API tokens', + labelNames: ['type'], + }); + const enabledMetricsBucketsPreviousDay = createGauge({ + name: 'enabled_metrics_buckets_previous_day', + help: 'Number of hourly enabled/disabled metric buckets in the previous day', + }); + const variantMetricsBucketsPreviousDay = createGauge({ + name: 'variant_metrics_buckets_previous_day', + help: 'Number of hourly variant metric buckets in the previous day', + }); + const usersActive7days = createGauge({ + name: 'users_active_7', + help: 'Number of users active in the last 7 days', + }); + const usersActive30days = createGauge({ + name: 'users_active_30', + help: 'Number of users active in the last 30 days', + }); + const usersActive60days = createGauge({ + name: 'users_active_60', + help: 'Number of users active in the last 60 days', + }); + const usersActive90days = createGauge({ + name: 'users_active_90', + help: 'Number of users active in the last 90 days', + }); + const projectsTotal = createGauge({ + name: 'projects_total', + help: 'Number of projects', + labelNames: ['mode'], + }); + const environmentsTotal = createGauge({ + name: 'environments_total', + help: 'Number of environments', + }); + const groupsTotal = createGauge({ + name: 'groups_total', + help: 'Number of groups', + }); + + const rolesTotal = createGauge({ + name: 'roles_total', + help: 'Number of roles', + }); + + const customRootRolesTotal = createGauge({ + name: 'custom_root_roles_total', + help: 'Number of custom root roles', + }); + + const customRootRolesInUseTotal = createGauge({ + name: 'custom_root_roles_in_use_total', + help: 'Number of custom root roles in use', + }); + + const segmentsTotal = createGauge({ + name: 'segments_total', + help: 'Number of segments', + }); + + const contextTotal = createGauge({ + name: 'context_total', + help: 'Number of context', + }); + + const strategiesTotal = createGauge({ + name: 'strategies_total', + help: 'Number of strategies', + }); + + dbMetrics.registerGaugeDbMetric({ + name: 'client_apps_total', + help: 'Number of registered client apps aggregated by range by last seen', + labelNames: ['range'], + query: () => instanceStatsService.getLabeledAppCounts(), + map: (result) => + Object.entries(result).map(([range, count]) => ({ + value: count, + labels: { range }, + })), + }); + + const samlEnabled = createGauge({ + name: 'saml_enabled', + help: 'Whether SAML is enabled', + }); + + const oidcEnabled = createGauge({ + name: 'oidc_enabled', + help: 'Whether OIDC is enabled', + }); + + const clientSdkVersionUsage = createCounter({ + name: 'client_sdk_versions', + help: 'Which sdk versions are being used', + labelNames: [ + 'sdk_name', + 'sdk_version', + 'platform_name', + 'platform_version', + 'yggdrasil_version', + 'spec_version', + ], + }); + + const productionChanges30 = createGauge({ + name: 'production_changes_30', + help: 'Changes made to production environment last 30 days', + labelNames: ['environment'], + }); + const productionChanges60 = createGauge({ + name: 'production_changes_60', + help: 'Changes made to production environment last 60 days', + labelNames: ['environment'], + }); + const productionChanges90 = createGauge({ + name: 'production_changes_90', + help: 'Changes made to production environment last 90 days', + labelNames: ['environment'], + }); + + const rateLimits = createGauge({ + name: 'rate_limits', + help: 'Rate limits (per minute) for METHOD/ENDPOINT pairs', + labelNames: ['endpoint', 'method'], + }); + rateLimits + .labels({ + endpoint: '/api/client/metrics', + method: 'POST', + }) + .set(config.metricsRateLimiting.clientMetricsMaxPerMinute); + rateLimits + .labels({ + endpoint: '/api/client/register', + method: 'POST', + }) + .set(config.metricsRateLimiting.clientRegisterMaxPerMinute); + rateLimits + .labels({ + endpoint: '/api/frontend/metrics', + method: 'POST', + }) + .set(config.metricsRateLimiting.frontendMetricsMaxPerMinute); + rateLimits + .labels({ + endpoint: '/api/frontend/register', + method: 'POST', + }) + .set(config.metricsRateLimiting.frontendRegisterMaxPerMinute); + rateLimits + .labels({ + endpoint: '/api/admin/user-admin', + method: 'POST', + }) + .set(config.rateLimiting.createUserMaxPerMinute); + rateLimits + .labels({ + endpoint: '/auth/simple', + method: 'POST', + }) + .set(config.rateLimiting.simpleLoginMaxPerMinute); + rateLimits + .labels({ + endpoint: '/auth/reset/password-email', + method: 'POST', + }) + .set(config.rateLimiting.passwordResetMaxPerMinute); + rateLimits + .labels({ + endpoint: '/api/signal-endpoint/:name', + method: 'POST', + }) + .set(config.rateLimiting.callSignalEndpointMaxPerSecond * 60); + + const featureCreatedByMigration = createCounter({ + name: 'feature_created_by_migration_count', + help: 'Feature createdBy migration count', + }); + const eventCreatedByMigration = createCounter({ + name: 'event_created_by_migration_count', + help: 'Event createdBy migration count', + }); + const proxyRepositoriesCreated = createCounter({ + name: 'proxy_repositories_created', + help: 'Proxy repositories created', + }); + const frontendApiRepositoriesCreated = createCounter({ + name: 'frontend_api_repositories_created', + help: 'Frontend API repositories created', + }); + const mapFeaturesForClientDuration = createHistogram({ + name: 'map_features_for_client_duration', + help: 'Duration of mapFeaturesForClient function', + }); + + const featureLifecycleStageDuration = createGauge({ + name: 'feature_lifecycle_stage_duration', + labelNames: ['stage', 'project_id'], + help: 'Duration of feature lifecycle stages', + }); + + const onboardingDuration = createGauge({ + name: 'onboarding_duration', + labelNames: ['event'], + help: 'firstLogin, secondLogin, firstFeatureFlag, firstPreLive, firstLive from first user creation', + }); + const projectOnboardingDuration = createGauge({ + name: 'project_onboarding_duration', + labelNames: ['event', 'project'], + help: 'firstFeatureFlag, firstPreLive, firstLive from project creation', + }); + + const featureLifecycleStageCountByProject = createGauge({ + name: 'feature_lifecycle_stage_count_by_project', + help: 'Count features in a given stage by project id', + labelNames: ['stage', 'project_id'], + }); + + const featureLifecycleStageEnteredCounter = createCounter({ + name: 'feature_lifecycle_stage_entered', + help: 'Count how many features entered a given stage', + labelNames: ['stage'], + }); + + const projectActionsCounter = createCounter({ + name: 'project_actions_count', + help: 'Count project actions', + labelNames: ['action'], + }); + + const projectEnvironmentsDisabled = createCounter({ + name: 'project_environments_disabled', + help: 'How many "environment disabled" events we have received for each project', + labelNames: ['project_id'], + }); + + const orphanedTokensTotal = createGauge({ + name: 'orphaned_api_tokens_total', + help: 'Number of API tokens without a project', + }); + + const orphanedTokensActive = createGauge({ + name: 'orphaned_api_tokens_active', + help: 'Number of API tokens without a project, last seen within 3 months', + }); + + const legacyTokensTotal = createGauge({ + name: 'legacy_api_tokens_total', + help: 'Number of API tokens with v1 format', + }); + + const legacyTokensActive = createGauge({ + name: 'legacy_api_tokens_active', + help: 'Number of API tokens with v1 format, last seen within 3 months', + }); + + const exceedsLimitErrorCounter = createCounter({ + name: 'exceeds_limit_error', + help: 'The number of exceeds limit errors registered by this instance.', + labelNames: ['resource', 'limit'], + }); + + const requestOriginCounter = createCounter({ + name: 'request_origin_counter', + help: 'Number of authenticated requests, including origin information.', + labelNames: ['type', 'method', 'source'], + }); + + const resourceLimit = createGauge({ + name: 'resource_limit', + help: 'The maximum number of resources allowed.', + labelNames: ['resource'], + }); + + const addonEventsHandledCounter = createCounter({ + name: 'addon_events_handled', + help: 'Events handled by addons and the result.', + labelNames: ['result', 'destination'], + }); + + // register event listeners + eventBus.on( + events.EXCEEDS_LIMIT, + ({ resource, limit }: { resource: string; limit: number }) => { + exceedsLimitErrorCounter.increment({ resource, limit }); + }, + ); + + eventBus.on( + events.STAGE_ENTERED, + (entered: { stage: string; feature: string }) => { + if (flagResolver.isEnabled('trackLifecycleMetrics')) { + logger.info( + `STAGE_ENTERED listened ${JSON.stringify(entered)}`, + ); + } + featureLifecycleStageEnteredCounter.increment({ + stage: entered.stage, + }); + }, + ); - const legacyTokensActive = createGauge({ - name: 'legacy_api_tokens_active', - help: 'Number of API tokens with v1 format, last seen within 3 months', - }); + eventBus.on( + events.REQUEST_TIME, + ({ path, method, time, statusCode, appName }) => { + requestDuration + .labels({ + path, + method, + status: statusCode, + appName, + }) + .observe(time); + }, + ); + + eventBus.on(events.SCHEDULER_JOB_TIME, ({ jobId, time }) => { + schedulerDuration.labels(jobId).observe(time); + }); + + eventBus.on(events.FUNCTION_TIME, ({ functionName, className, time }) => { + functionDuration + .labels({ + functionName, + className, + }) + .observe(time); + }); + + eventBus.on(events.EVENTS_CREATED_BY_PROCESSED, ({ updated }) => { + eventCreatedByMigration.inc(updated); + }); + + eventBus.on(events.FEATURES_CREATED_BY_PROCESSED, ({ updated }) => { + featureCreatedByMigration.inc(updated); + }); + + eventBus.on(events.DB_TIME, ({ store, action, time }) => { + dbDuration + .labels({ + store, + action, + }) + .observe(time); + }); + + eventBus.on(events.PROXY_REPOSITORY_CREATED, () => { + proxyRepositoriesCreated.inc(); + }); + + eventBus.on(events.FRONTEND_API_REPOSITORY_CREATED, () => { + frontendApiRepositoriesCreated.inc(); + }); + + eventBus.on(events.PROXY_FEATURES_FOR_TOKEN_TIME, ({ duration }) => { + mapFeaturesForClientDuration.observe(duration); + }); + + events.onMetricEvent( + eventBus, + events.REQUEST_ORIGIN, + ({ type, method, source }) => { + requestOriginCounter.increment({ type, method, source }); + }, + ); + + eventStore.on(FEATURE_CREATED, ({ featureName, project }) => { + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment: 'n/a', + environmentType: 'n/a', + action: 'created', + }); + }); + eventStore.on(FEATURE_VARIANTS_UPDATED, ({ featureName, project }) => { + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment: 'n/a', + environmentType: 'n/a', + action: 'updated', + }); + }); + eventStore.on(FEATURE_METADATA_UPDATED, ({ featureName, project }) => { + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment: 'n/a', + environmentType: 'n/a', + action: 'updated', + }); + }); + eventStore.on(FEATURE_UPDATED, ({ featureName, project }) => { + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment: 'default', + environmentType: 'production', + action: 'updated', + }); + }); + eventStore.on( + FEATURE_STRATEGY_ADD, + async ({ featureName, project, environment }) => { + const environmentType = await resolveEnvironmentType( + environment, + cachedEnvironments, + ); + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment, + environmentType, + action: 'updated', + }); + }, + ); + eventStore.on( + FEATURE_STRATEGY_REMOVE, + async ({ featureName, project, environment }) => { + const environmentType = await resolveEnvironmentType( + environment, + cachedEnvironments, + ); + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment, + environmentType, + action: 'updated', + }); + }, + ); + eventStore.on( + FEATURE_STRATEGY_UPDATE, + async ({ featureName, project, environment }) => { + const environmentType = await resolveEnvironmentType( + environment, + cachedEnvironments, + ); + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment, + environmentType, + action: 'updated', + }); + }, + ); + eventStore.on( + FEATURE_ENVIRONMENT_DISABLED, + async ({ featureName, project, environment }) => { + const environmentType = await resolveEnvironmentType( + environment, + cachedEnvironments, + ); + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment, + environmentType, + action: 'updated', + }); + }, + ); + eventStore.on( + FEATURE_ENVIRONMENT_ENABLED, + async ({ featureName, project, environment }) => { + const environmentType = await resolveEnvironmentType( + environment, + cachedEnvironments, + ); + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment, + environmentType, + action: 'updated', + }); + }, + ); + eventStore.on(FEATURE_ARCHIVED, ({ featureName, project }) => { + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment: 'n/a', + environmentType: 'n/a', + action: 'archived', + }); + }); + eventStore.on(FEATURE_REVIVED, ({ featureName, project }) => { + featureFlagUpdateTotal.increment({ + toggle: featureName, + project, + environment: 'n/a', + environmentType: 'n/a', + action: 'revived', + }); + }); + eventStore.on(PROJECT_CREATED, () => { + projectActionsCounter.increment({ action: PROJECT_CREATED }); + }); + eventStore.on(PROJECT_ARCHIVED, () => { + projectActionsCounter.increment({ action: PROJECT_ARCHIVED }); + }); + eventStore.on(PROJECT_REVIVED, () => { + projectActionsCounter.increment({ action: PROJECT_REVIVED }); + }); + eventStore.on(PROJECT_DELETED, () => { + projectActionsCounter.increment({ action: PROJECT_DELETED }); + }); + + const logger = config.getLogger('metrics.ts'); + eventBus.on(CLIENT_METRICS, (metrics: IClientMetricsEnv[]) => { + try { + for (const metric of metrics) { + featureFlagUsageTotal.increment( + { + toggle: metric.featureName, + active: 'true', + appName: metric.appName, + }, + metric.yes, + ); + featureFlagUsageTotal.increment( + { + toggle: metric.featureName, + active: 'false', + appName: metric.appName, + }, + metric.no, + ); + } + } catch (e) { + logger.warn('Metrics registration failed', e); + } + }); - const exceedsLimitErrorCounter = createCounter({ - name: 'exceeds_limit_error', - help: 'The number of exceeds limit errors registered by this instance.', - labelNames: ['resource', 'limit'], - }); + eventStore.on(CLIENT_REGISTER, (heartbeatEvent: ISdkHeartbeat) => { + if (!heartbeatEvent.sdkName || !heartbeatEvent.sdkVersion) { + return; + } - const requestOriginCounter = createCounter({ - name: 'request_origin_counter', - help: 'Number of authenticated requests, including origin information.', - labelNames: ['type', 'method', 'source'], - }); + if (flagResolver.isEnabled('extendedMetrics')) { + clientSdkVersionUsage.increment({ + sdk_name: heartbeatEvent.sdkName, + sdk_version: heartbeatEvent.sdkVersion, + platform_name: + heartbeatEvent.metadata?.platformName ?? 'not-set', + platform_version: + heartbeatEvent.metadata?.platformVersion ?? 'not-set', + yggdrasil_version: + heartbeatEvent.metadata?.yggdrasilVersion ?? 'not-set', + spec_version: heartbeatEvent.metadata?.specVersion ?? 'not-set', + }); + } else { + clientSdkVersionUsage.increment({ + sdk_name: heartbeatEvent.sdkName, + sdk_version: heartbeatEvent.sdkVersion, + platform_name: 'not-set', + platform_version: 'not-set', + yggdrasil_version: 'not-set', + spec_version: 'not-set', + }); + } + }); - const resourceLimit = createGauge({ - name: 'resource_limit', - help: 'The maximum number of resources allowed.', - labelNames: ['resource'], - }); + eventStore.on(PROJECT_ENVIRONMENT_REMOVED, ({ project }) => { + projectEnvironmentsDisabled.increment({ project_id: project }); + }); - const addonEventsHandledCounter = createCounter({ - name: 'addon_events_handled', - help: 'Events handled by addons and the result.', - labelNames: ['result', 'destination'], - }); + eventBus.on(events.ADDON_EVENTS_HANDLED, ({ result, destination }) => { + addonEventsHandledCounter.increment({ result, destination }); + }); - async function collectStaticCounters() { + return { + collectDbMetrics: dbMetrics.refreshDbMetrics, + collectStaticCounters: async () => { try { - const stats = await instanceStatsService.getStats(); const [ - maxStrategies, - maxEnvironmentStrategies, maxConstraintValuesResult, maxConstraintsPerStrategyResult, stageCountByProjectResult, @@ -408,8 +833,6 @@ export default class MetricsMonitor { instanceOnboardingMetrics, projectsOnboardingMetrics, ] = await Promise.all([ - stores.featureStrategiesReadModel.getMaxFeatureStrategies(), - stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(), stores.featureStrategiesReadModel.getMaxConstraintValues(), stores.featureStrategiesReadModel.getMaxConstraintsPerStrategy(), stores.featureLifecycleReadModel.getStageCountByProject(), @@ -429,17 +852,18 @@ export default class MetricsMonitor { : Promise.resolve([]), ]); - featureFlagsTotal.reset(); - featureFlagsTotal.labels({ version }).set(stats.featureToggles); - featureTogglesArchivedTotal.reset(); - featureTogglesArchivedTotal.set(stats.archivedFeatureToggles); + featureTogglesArchivedTotal.set( + await instanceStatsService.getArchivedToggleCount(), + ); usersTotal.reset(); - usersTotal.set(stats.users); + usersTotal.set(await instanceStatsService.getRegisteredUsers()); serviceAccounts.reset(); - serviceAccounts.set(stats.serviceAccounts); + serviceAccounts.set( + await instanceStatsService.countServiceAccounts(), + ); stageDurationByProject.forEach((stage) => { featureLifecycleStageDuration @@ -450,30 +874,6 @@ export default class MetricsMonitor { .set(stage.duration); }); - eventBus.on( - events.STAGE_ENTERED, - (entered: { stage: string; feature: string }) => { - if (flagResolver.isEnabled('trackLifecycleMetrics')) { - logger.info( - `STAGE_ENTERED listened ${JSON.stringify(entered)}`, - ); - } - featureLifecycleStageEnteredCounter.increment({ - stage: entered.stage, - }); - }, - ); - - eventBus.on( - events.EXCEEDS_LIMIT, - ({ - resource, - limit, - }: { resource: string; limit: number }) => { - exceedsLimitErrorCounter.increment({ resource, limit }); - }, - ); - featureLifecycleStageCountByProject.reset(); stageCountByProjectResult.forEach((stageResult) => featureLifecycleStageCountByProject @@ -486,7 +886,10 @@ export default class MetricsMonitor { apiTokens.reset(); - for (const [type, value] of stats.apiTokens) { + for (const [ + type, + value, + ] of await instanceStatsService.countApiTokensByType()) { apiTokens.labels({ type }).set(value); } @@ -502,21 +905,6 @@ export default class MetricsMonitor { legacyTokensActive.reset(); legacyTokensActive.set(deprecatedTokens.activeLegacyTokens); - if (maxEnvironmentStrategies) { - maxFeatureEnvironmentStrategies.reset(); - maxFeatureEnvironmentStrategies - .labels({ - environment: maxEnvironmentStrategies.environment, - feature: maxEnvironmentStrategies.feature, - }) - .set(maxEnvironmentStrategies.count); - } - if (maxStrategies) { - maxFeatureStrategies.reset(); - maxFeatureStrategies - .labels({ feature: maxStrategies.feature }) - .set(maxStrategies.count); - } if (maxConstraintValuesResult) { maxConstraintValues.reset(); maxConstraintValues @@ -586,488 +974,135 @@ export default class MetricsMonitor { resourceLimit.labels({ resource }).set(limit); } + const previousDayMetricsBucketsCount = + await instanceStatsService.countPreviousDayHourlyMetricsBuckets(); enabledMetricsBucketsPreviousDay.reset(); enabledMetricsBucketsPreviousDay.set( - stats.previousDayMetricsBucketsCount.enabledCount, + previousDayMetricsBucketsCount.enabledCount, ); variantMetricsBucketsPreviousDay.reset(); variantMetricsBucketsPreviousDay.set( - stats.previousDayMetricsBucketsCount.variantCount, + previousDayMetricsBucketsCount.variantCount, ); + const activeUsers = await instanceStatsService.getActiveUsers(); usersActive7days.reset(); - usersActive7days.set(stats.activeUsers.last7); + usersActive7days.set(activeUsers.last7); usersActive30days.reset(); - usersActive30days.set(stats.activeUsers.last30); + usersActive30days.set(activeUsers.last30); usersActive60days.reset(); - usersActive60days.set(stats.activeUsers.last60); + usersActive60days.set(activeUsers.last60); usersActive90days.reset(); - usersActive90days.set(stats.activeUsers.last90); + usersActive90days.set(activeUsers.last90); + const productionChanges = + await instanceStatsService.getProductionChanges(); productionChanges30.reset(); - productionChanges30.set(stats.productionChanges.last30); + productionChanges30.set(productionChanges.last30); productionChanges60.reset(); - productionChanges60.set(stats.productionChanges.last60); + productionChanges60.set(productionChanges.last60); productionChanges90.reset(); - productionChanges90.set(stats.productionChanges.last90); + productionChanges90.set(productionChanges.last90); + const projects = + await instanceStatsService.getProjectModeCount(); projectsTotal.reset(); - stats.projects.forEach((projectStat) => { + projects.forEach((projectStat) => { projectsTotal .labels({ mode: projectStat.mode }) .set(projectStat.count); }); environmentsTotal.reset(); - environmentsTotal.set(stats.environments); + environmentsTotal.set( + await instanceStatsService.environmentCount(), + ); groupsTotal.reset(); - groupsTotal.set(stats.groups); + groupsTotal.set(await instanceStatsService.groupCount()); rolesTotal.reset(); - rolesTotal.set(stats.roles); + rolesTotal.set(await instanceStatsService.roleCount()); customRootRolesTotal.reset(); - customRootRolesTotal.set(stats.customRootRoles); + customRootRolesTotal.set( + await instanceStatsService.customRolesCount(), + ); customRootRolesInUseTotal.reset(); - customRootRolesInUseTotal.set(stats.customRootRolesInUse); + customRootRolesInUseTotal.set( + await instanceStatsService.customRolesCountInUse(), + ); segmentsTotal.reset(); - segmentsTotal.set(stats.segments); + segmentsTotal.set(await instanceStatsService.segmentCount()); contextTotal.reset(); - contextTotal.set(stats.contextFields); + contextTotal.set( + await instanceStatsService.contextFieldCount(), + ); strategiesTotal.reset(); - strategiesTotal.set(stats.strategies); + strategiesTotal.set( + await instanceStatsService.strategiesCount(), + ); samlEnabled.reset(); - samlEnabled.set(stats.SAMLenabled ? 1 : 0); + samlEnabled.set((await instanceStatsService.hasSAML()) ? 1 : 0); oidcEnabled.reset(); - oidcEnabled.set(stats.OIDCenabled ? 1 : 0); - - clientAppsTotal.reset(); - stats.clientApps.forEach(({ range, count }) => - clientAppsTotal.labels({ range }).set(count), - ); - - rateLimits.reset(); - rateLimits - .labels({ - endpoint: '/api/client/metrics', - method: 'POST', - }) - .set(config.metricsRateLimiting.clientMetricsMaxPerMinute); - rateLimits - .labels({ - endpoint: '/api/client/register', - method: 'POST', - }) - .set(config.metricsRateLimiting.clientRegisterMaxPerMinute); - rateLimits - .labels({ - endpoint: '/api/frontend/metrics', - method: 'POST', - }) - .set( - config.metricsRateLimiting.frontendMetricsMaxPerMinute, - ); - rateLimits - .labels({ - endpoint: '/api/frontend/register', - method: 'POST', - }) - .set( - config.metricsRateLimiting.frontendRegisterMaxPerMinute, - ); - rateLimits - .labels({ - endpoint: '/api/admin/user-admin', - method: 'POST', - }) - .set(config.rateLimiting.createUserMaxPerMinute); - rateLimits - .labels({ - endpoint: '/auth/simple', - method: 'POST', - }) - .set(config.rateLimiting.simpleLoginMaxPerMinute); - rateLimits - .labels({ - endpoint: '/auth/reset/password-email', - method: 'POST', - }) - .set(config.rateLimiting.passwordResetMaxPerMinute); - rateLimits - .labels({ - endpoint: '/api/signal-endpoint/:name', - method: 'POST', - }) - .set( - config.rateLimiting.callSignalEndpointMaxPerSecond * 60, - ); + oidcEnabled.set((await instanceStatsService.hasOIDC()) ? 1 : 0); } catch (e) {} - } - - await schedulerService.schedule( - collectStaticCounters.bind(this), - hoursToMilliseconds(2), - 'collectStaticCounters', - 0, // no jitter - ); - - eventBus.on( - events.REQUEST_TIME, - ({ path, method, time, statusCode, appName }) => { - requestDuration - .labels({ - path, - method, - status: statusCode, - appName, - }) - .observe(time); - }, - ); - - eventBus.on(events.SCHEDULER_JOB_TIME, ({ jobId, time }) => { - schedulerDuration.labels(jobId).observe(time); - }); - - eventBus.on( - events.FUNCTION_TIME, - ({ functionName, className, time }) => { - functionDuration - .labels({ - functionName, - className, - }) - .observe(time); - }, - ); - - eventBus.on(events.EVENTS_CREATED_BY_PROCESSED, ({ updated }) => { - eventCreatedByMigration.inc(updated); - }); - - eventBus.on(events.FEATURES_CREATED_BY_PROCESSED, ({ updated }) => { - featureCreatedByMigration.inc(updated); - }); - - eventBus.on(events.DB_TIME, ({ store, action, time }) => { - dbDuration - .labels({ - store, - action, - }) - .observe(time); - }); + }, + }; +} +export default class MetricsMonitor { + constructor() {} - eventBus.on(events.PROXY_REPOSITORY_CREATED, () => { - proxyRepositoriesCreated.inc(); - }); + async startMonitoring( + config: IUnleashConfig, + stores: IUnleashStores, + version: string, + eventBus: EventEmitter, + instanceStatsService: InstanceStatsService, + schedulerService: SchedulerService, + db: Knex, + ): Promise { + if (!config.server.serverMetrics) { + return Promise.resolve(); + } - eventBus.on(events.FRONTEND_API_REPOSITORY_CREATED, () => { - frontendApiRepositoriesCreated.inc(); - }); + collectDefaultMetrics(); - eventBus.on(events.PROXY_FEATURES_FOR_TOKEN_TIME, ({ duration }) => { - mapFeaturesForClientDuration.observe(duration); - }); + const { collectStaticCounters, collectDbMetrics } = + registerPrometheusMetrics( + config, + stores, + version, + eventBus, + instanceStatsService, + ); - events.onMetricEvent( - eventBus, - events.REQUEST_ORIGIN, - ({ type, method, source }) => { - requestOriginCounter.increment({ type, method, source }); - }, - ); + const postgresVersion = await stores.settingStore.postgresVersion(); + registerPrometheusPostgresMetrics(db, eventBus, postgresVersion); - eventStore.on(FEATURE_CREATED, ({ featureName, project }) => { - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment: 'n/a', - environmentType: 'n/a', - action: 'created', - }); - }); - eventStore.on(FEATURE_VARIANTS_UPDATED, ({ featureName, project }) => { - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment: 'n/a', - environmentType: 'n/a', - action: 'updated', - }); - }); - eventStore.on(FEATURE_METADATA_UPDATED, ({ featureName, project }) => { - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment: 'n/a', - environmentType: 'n/a', - action: 'updated', - }); - }); - eventStore.on(FEATURE_UPDATED, ({ featureName, project }) => { - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment: 'default', - environmentType: 'production', - action: 'updated', - }); - }); - eventStore.on( - FEATURE_STRATEGY_ADD, - async ({ featureName, project, environment }) => { - const environmentType = await this.resolveEnvironmentType( - environment, - cachedEnvironments, - ); - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment, - environmentType, - action: 'updated', - }); - }, - ); - eventStore.on( - FEATURE_STRATEGY_REMOVE, - async ({ featureName, project, environment }) => { - const environmentType = await this.resolveEnvironmentType( - environment, - cachedEnvironments, - ); - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment, - environmentType, - action: 'updated', - }); - }, - ); - eventStore.on( - FEATURE_STRATEGY_UPDATE, - async ({ featureName, project, environment }) => { - const environmentType = await this.resolveEnvironmentType( - environment, - cachedEnvironments, - ); - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment, - environmentType, - action: 'updated', - }); - }, - ); - eventStore.on( - FEATURE_ENVIRONMENT_DISABLED, - async ({ featureName, project, environment }) => { - const environmentType = await this.resolveEnvironmentType( - environment, - cachedEnvironments, - ); - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment, - environmentType, - action: 'updated', - }); - }, - ); - eventStore.on( - FEATURE_ENVIRONMENT_ENABLED, - async ({ featureName, project, environment }) => { - const environmentType = await this.resolveEnvironmentType( - environment, - cachedEnvironments, - ); - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment, - environmentType, - action: 'updated', - }); - }, + await schedulerService.schedule( + async () => + Promise.all([collectStaticCounters(), collectDbMetrics()]), + hoursToMilliseconds(2), + 'collectStaticCounters', ); - eventStore.on(FEATURE_ARCHIVED, ({ featureName, project }) => { - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment: 'n/a', - environmentType: 'n/a', - action: 'archived', - }); - }); - eventStore.on(FEATURE_REVIVED, ({ featureName, project }) => { - featureFlagUpdateTotal.increment({ - toggle: featureName, - project, - environment: 'n/a', - environmentType: 'n/a', - action: 'revived', - }); - }); - eventStore.on(PROJECT_CREATED, () => { - projectActionsCounter.increment({ action: PROJECT_CREATED }); - }); - eventStore.on(PROJECT_ARCHIVED, () => { - projectActionsCounter.increment({ action: PROJECT_ARCHIVED }); - }); - eventStore.on(PROJECT_REVIVED, () => { - projectActionsCounter.increment({ action: PROJECT_REVIVED }); - }); - eventStore.on(PROJECT_DELETED, () => { - projectActionsCounter.increment({ action: PROJECT_DELETED }); - }); - - const logger = config.getLogger('metrics.ts'); - eventBus.on(CLIENT_METRICS, (metrics: IClientMetricsEnv[]) => { - try { - for (const metric of metrics) { - featureFlagUsageTotal.increment( - { - toggle: metric.featureName, - active: 'true', - appName: metric.appName, - }, - metric.yes, - ); - featureFlagUsageTotal.increment( - { - toggle: metric.featureName, - active: 'false', - appName: metric.appName, - }, - metric.no, - ); - } - } catch (e) { - logger.warn('Metrics registration failed', e); - } - }); - - eventStore.on(CLIENT_REGISTER, (heartbeatEvent: ISdkHeartbeat) => { - if (!heartbeatEvent.sdkName || !heartbeatEvent.sdkVersion) { - return; - } - - if (flagResolver.isEnabled('extendedMetrics')) { - clientSdkVersionUsage.increment({ - sdk_name: heartbeatEvent.sdkName, - sdk_version: heartbeatEvent.sdkVersion, - platform_name: - heartbeatEvent.metadata?.platformName ?? 'not-set', - platform_version: - heartbeatEvent.metadata?.platformVersion ?? 'not-set', - yggdrasil_version: - heartbeatEvent.metadata?.yggdrasilVersion ?? 'not-set', - spec_version: - heartbeatEvent.metadata?.specVersion ?? 'not-set', - }); - } else { - clientSdkVersionUsage.increment({ - sdk_name: heartbeatEvent.sdkName, - sdk_version: heartbeatEvent.sdkVersion, - platform_name: 'not-set', - platform_version: 'not-set', - yggdrasil_version: 'not-set', - spec_version: 'not-set', - }); - } - }); - - eventStore.on(PROJECT_ENVIRONMENT_REMOVED, ({ project }) => { - projectEnvironmentsDisabled.increment({ project_id: project }); - }); - - eventBus.on(events.ADDON_EVENTS_HANDLED, ({ result, destination }) => { - addonEventsHandledCounter.increment({ result, destination }); - }); - - await this.configureDbMetrics( - db, - eventBus, - schedulerService, - stores.settingStore, + await schedulerService.schedule( + async () => + this.registerPoolMetrics.bind(this, db.client.pool, eventBus), + minutesToMilliseconds(1), + 'registerPoolMetrics', + 0, // no jitter ); return Promise.resolve(); } - async configureDbMetrics( - db: Knex, - eventBus: EventEmitter, - schedulerService: SchedulerService, - settingStore: ISettingStore, - ): Promise { - if (db?.client) { - const dbPoolMin = createGauge({ - name: 'db_pool_min', - help: 'Minimum DB pool size', - }); - dbPoolMin.set(db.client.pool.min); - const dbPoolMax = createGauge({ - name: 'db_pool_max', - help: 'Maximum DB pool size', - }); - dbPoolMax.set(db.client.pool.max); - const dbPoolFree = createGauge({ - name: 'db_pool_free', - help: 'Current free connections in DB pool', - }); - const dbPoolUsed = createGauge({ - name: 'db_pool_used', - help: 'Current connections in use in DB pool', - }); - const dbPoolPendingCreates = createGauge({ - name: 'db_pool_pending_creates', - help: 'how many asynchronous create calls are running in DB pool', - }); - const dbPoolPendingAcquires = createGauge({ - name: 'db_pool_pending_acquires', - help: 'how many acquires are waiting for a resource to be released in DB pool', - }); - - eventBus.on(DB_POOL_UPDATE, (data) => { - dbPoolFree.set(data.free); - dbPoolUsed.set(data.used); - dbPoolPendingCreates.set(data.pendingCreates); - dbPoolPendingAcquires.set(data.pendingAcquires); - }); - - await schedulerService.schedule( - async () => - this.registerPoolMetrics.bind( - this, - db.client.pool, - eventBus, - ), - minutesToMilliseconds(1), - 'registerPoolMetrics', - 0, // no jitter - ); - const postgresVersion = await settingStore.postgresVersion(); - const database_version = createGauge({ - name: 'postgres_version', - help: 'Which version of postgres is running (SHOW server_version)', - labelNames: ['version'], - }); - database_version.labels({ version: postgresVersion }).set(1); - } - } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types registerPoolMetrics(pool: any, eventBus: EventEmitter) { try { @@ -1080,26 +1115,8 @@ export default class MetricsMonitor { // eslint-disable-next-line no-empty } catch (e) {} } - - async resolveEnvironmentType( - environment: string, - cachedEnvironments: () => Promise, - ): Promise { - const environments = await cachedEnvironments(); - const env = environments.find((e) => e.name === environment); - - if (env) { - return env.type; - } else { - return 'unknown'; - } - } } export function createMetricsMonitor(): MetricsMonitor { return new MetricsMonitor(); } - -module.exports = { - createMetricsMonitor, -}; diff --git a/src/server-dev.ts b/src/server-dev.ts index bd52ea379cad..29a0e83fda1c 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -57,7 +57,7 @@ process.nextTick(async () => { unleashAI: true, webhookDomainLogging: true, addonUsageMetrics: true, - releasePlans: true, + releasePlans: false, }, }, authentication: { diff --git a/src/test/e2e/api/admin/instance-admin.e2e.test.ts b/src/test/e2e/api/admin/instance-admin.e2e.test.ts index 9b1cdb34ed1a..a3e5c08c7386 100644 --- a/src/test/e2e/api/admin/instance-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/instance-admin.e2e.test.ts @@ -6,10 +6,12 @@ import { import getLogger from '../../../fixtures/no-logger'; import type { IUnleashStores } from '../../../../lib/types'; import { ApiTokenType } from '../../../../lib/types/models/api-token'; +import { registerPrometheusMetrics } from '../../../../lib/metrics'; let app: IUnleashTest; let db: ITestDb; let stores: IUnleashStores; +let refreshDbMetrics: () => Promise; beforeAll(async () => { db = await dbInit('instance_admin_api_serial', getLogger); @@ -26,6 +28,15 @@ beforeAll(async () => { }, db.rawDatabase, ); + + const { collectDbMetrics } = registerPrometheusMetrics( + app.config, + stores, + undefined as unknown as string, + app.config.eventBus, + app.services.instanceStatsService, + ); + refreshDbMetrics = collectDbMetrics; }); afterAll(async () => { @@ -39,6 +50,8 @@ test('should return instance statistics', async () => { createdByUserId: 9999, }); + await refreshDbMetrics(); + return app.request .get('/api/admin/instance-admin/statistics') .expect('Content-Type', /json/) diff --git a/website/docs/feature-flag-tutorials/use-cases/ai.md b/website/docs/feature-flag-tutorials/use-cases/ai.md new file mode 100644 index 000000000000..10596d0a47f1 --- /dev/null +++ b/website/docs/feature-flag-tutorials/use-cases/ai.md @@ -0,0 +1,235 @@ +--- +title: How to use feature flags with AI +slug: /feature-flag-tutorials/use-cases/ai +--- + +Hello, + +Like many people in lots of tech companies today, you might be playing with generative AI and large language models (LLMs). You might even be integrating these AI technologies into your company's products, probably in the form of chatbots or content generators. + +The main way to interact with LLMs today is via a set of APIs, usually either OpenAI, Anthropic or aggregators like Groq. Most of these APIs have similar parameters, like: + +- `model`: The model and the specific version of the model to use. +- `prompt`: The prompt to give to the LLM. + +Given how much these models vary in their capabilities, chances are you'll be testing multiple models, multiple versions of the same model, and multiple prompts. + +This is where feature flags are super useful. They allow you to easily switch between different configurations. In this tutorial, we’ll explore how to use feature flags with AI models. + +We'll start with a basic chatbot. I know building a chatbot is a total cliché at this point, but it's a small, self-contained example that everyone knows how to interact with. You can take this example and apply it to any other use case where you need to interact with an LLM via an API. + +First, clone the repo: + +```sh +git clone https://github.com/alvinometric/feature-flags-ai +cd feature-flags-ai +``` + +Install the dependencies: + +```sh +npm install +``` + +Copy the `.env.example` file to a new `.env` file, and add your OpenAI API key to it: + +```sh +OPENAI_API_KEY=sk-... +``` + +Run the app: + +```sh +npm run dev +``` + +You should now see a chatbot UI, like this: + +![chatbot UI](./sveltekit-chatbot.png) + +This is a simple SvelteKit app with a chatbot interface. If you’re not familiar with SvelteKit, it works similarly to frameworks like Next.js, Nuxt, or SolidStart, where your file structure defines the routing. + +The most important file for this tutorial is the `src/routes/api/chat/+server.js` file. + +It creates an API endpoint at `/api/chat`. When your frontend sends a POST request to `/api/chat`, this is the code that handles the request. + +```javascript +import { createOpenAI } from "@ai-sdk/openai"; +import { streamText } from "ai"; +import { env } from "$env/dynamic/private"; + +const openai = createOpenAI({ + apiKey: env.OPENAI_API_KEY ?? "", +}); + +const variants = { + variant1: { + model: "gpt-3.5-turbo", + max_tokens: 4096, + temperature: 1, + }, + variant2: { + model: "gpt-4-turbo", + max_tokens: 4096, + temperature: 1, + }, + variant3: { + model: "gpt-4-vision-preview", + max_tokens: 4096, + temperature: 1, + }, +}; + +export const POST = async ({ request }) => { + const { messages } = await request.json(); + const variant = variants["variant1"]; + const result = await streamText({ + model: openai(variant.model), + messages, + max_tokens: variant.max_tokens, + temperature: variant.temperature, + }); + return result.toDataStreamResponse(); +}; +``` + +This file is doing a few key things: + +1. Sets up our OpenAI client with an API key. +2. Defines different AI model configurations in the `variants` object. +3. Handles incoming chat requests with the `POST` function. + +When a request comes in, it: + +- Extracts the messages from the request body. +- Selects a variant (currently hardcoded to 'variant1'). +- Uses the OpenAI API to generate a response. +- Streams the response back to the client. + +The `streamText` function is one of the utilities provided by [Vercel's AI SDK](https://sdk.vercel.ai/). It helps deal with the real-time streaming of the AI's responses. + +## Creating a feature flag with AI variants + +Instead of hardcoding `variant1`, we want to use feature flags to dynamically choose which AI model to use. This will let us easily switch between models, test different configurations, or even do some A/B testing to see which model performs better for which task. And we can do this without having to redeploy our app. + +We can also disable it altogether if the upstream API stops working, or we run out of credits. + +To implement this, we'll need to: + +1. Set up a feature flag provider (we'll use Unleash). +2. Replace our static objects with dynamic feature flag variants. +3. Use the feature flag in our code to determine which AI model and settings to use for each request. + +### Install a local feature flag provider + +In this section, we’ll install Unleash, run the instance locally, log in, and create a feature flag. If you prefer, you can use other tools instead of Unleash, but you’ll need to update the code accordingly. + +Use Git to clone the Unleash repository and Docker to build and run it. Open a terminal window and run the following commands: + +```sh +git clone https://github.com/unleash/unleash.git +cd unleash +docker compose up -d +``` + +You now have Unleash installed on your machine and running in the background. You can access this instance in your web browser at [http://localhost:4242](http://localhost:4242). + +Log in to the platform using these credentials: + +``` +Username: admin +Password: unleash4all +``` + +Click **New feature flag** to create a new feature flag, called `ai-model`. + +After that, and this is the most important part, we need to add a variant to our feature flag. + +Add a strategy to the feature flag in the `development` environment, in that strategy, create a variant for each of these model configurations: + +```javascript +const variants = { + variant1: { + model: "gpt-3.5-turbo", + max_tokens: 4096, + temperature: 1, + }, + variant2: { + model: "gpt-4-turbo", + max_tokens: 4096, + temperature: 1, + }, + variant3: { + model: "gpt-4-vision-preview", + max_tokens: 4096, + temperature: 1, + }, +}; +``` + +What we'll do is move all the model configurations from the code to the feature flag variants. + +![a variant with parameters for an OpenAI model](./model-variant.png) + +## Querying AI feature flags + +Now let's go back to the code and grab our AI config from the feature flag that we just created. + +First, install the Unleash Node.js client: + +```sh +npm install unleash-client +``` + +Now, let's modify our `+server.js` file to use Unleash: + +```javascript +import { initialize } from "unleash-client"; +import { createOpenAI } from "@ai-sdk/openai"; +import { streamText } from "ai"; +import { env } from "$env/dynamic/private"; + +const openai = createOpenAI({ + apiKey: env.OPENAI_API_KEY ?? "", +}); + +const unleash = initialize({ + url: "http://localhost:4242/api/", + appName: "my-ai-app", + customHeaders: { Authorization: env.UNLEASH_API_KEY ?? "" }, +}); + +export const POST = async ({ request }) => { + const { messages } = await request.json(); + + // Get the feature flag variant + const variant = unleash.getVariant("ai-model"); + + const result = await streamText({ + model: openai(variant.model), + messages, + max_tokens: variant.max_tokens, + temperature: variant.temperature, + }); + + return result.toDataStreamResponse(); +}; +``` + +This setup uses the Unleash client to fetch the value of a feature flag called `ai-model`. + +Now, instead of hardcoding `variant1`, we're dynamically choosing the AI model based on the feature flag variant. + +This setup gives us a ton of flexibility. + +Do you want to roll out GPT-4 to 10% of your users? Easy. Need to quickly switch everyone back to GPT-3.5 because of a bug? No problem. + +You can do all of this from your Unleash dashboard without touching your code, and without needing to redeploy. + +## Conclusion + +Thanks for following along! + +In this guide, we covered how to use feature flags to help you manage AI models. + +That approach lets you switch between different model configurations, experiment with variations, and even roll out updates without needing to touch your code or redeploy. This gives you more control when experimenting with LLMs, and more power to respond to the unexpected things that will inevitably happen, like running out of credits or discovering a bug. diff --git a/website/docs/feature-flag-tutorials/use-cases/model-variant.png b/website/docs/feature-flag-tutorials/use-cases/model-variant.png new file mode 100644 index 000000000000..0a6bf9adf8c1 Binary files /dev/null and b/website/docs/feature-flag-tutorials/use-cases/model-variant.png differ diff --git a/website/docs/feature-flag-tutorials/use-cases/sveltekit-chatbot.png b/website/docs/feature-flag-tutorials/use-cases/sveltekit-chatbot.png new file mode 100644 index 000000000000..e2fe363cc14e Binary files /dev/null and b/website/docs/feature-flag-tutorials/use-cases/sveltekit-chatbot.png differ diff --git a/website/package.json b/website/package.json index 56555f5b9d8d..29cda1b2dd92 100644 --- a/website/package.json +++ b/website/package.json @@ -69,7 +69,7 @@ ] }, "devDependencies": { - "@babel/core": "7.25.2", + "@babel/core": "7.25.8", "@docusaurus/module-type-aliases": "2.3.1", "@tsconfig/docusaurus": "2.0.3", "babel-loader": "9.1.3", diff --git a/website/sidebars.js b/website/sidebars.js index dfa1a6e1c746..03672a47f2ad 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -116,6 +116,11 @@ module.exports = { label: 'A/B Testing', id: 'feature-flag-tutorials/use-cases/a-b-testing', }, + { + type: 'doc', + label: 'Feature Flags for AI', + id: 'feature-flag-tutorials/use-cases/ai', + }, ], }, { diff --git a/yarn.lock b/yarn.lock index b63ead50211d..49779d20bf07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -104,13 +104,13 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/code-frame@npm:7.24.7" +"@babel/code-frame@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/code-frame@npm:7.25.7" dependencies: - "@babel/highlight": "npm:^7.24.7" + "@babel/highlight": "npm:^7.25.7" picocolors: "npm:^1.0.0" - checksum: 10c0/ab0af539473a9f5aeaac7047e377cb4f4edd255a81d84a76058595f8540784cc3fbe8acf73f1e073981104562490aabfb23008cd66dc677a456a4ed5390fdde6 + checksum: 10c0/14825c298bdec914caf3d24d1383b6d4cd6b030714686004992f4fc251831ecf432236652896f99d5d341f17170ae9a07b58d8d7b15aa0df8cfa1c5a7d5474bc languageName: node linkType: hard @@ -121,33 +121,33 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.25.2": - version: 7.25.2 - resolution: "@babel/compat-data@npm:7.25.2" - checksum: 10c0/5bf1f14d6e5f0d37c19543e99209ff4a94bb97915e1ce01e5334a144aa08cd56b6e62ece8135dac77e126723d63d4d4b96fc603a12c43b88c28f4b5e070270c5 +"@babel/compat-data@npm:^7.25.7": + version: 7.25.8 + resolution: "@babel/compat-data@npm:7.25.8" + checksum: 10c0/8b81c17580e5fb4cbb6a3c52079f8c283fc59c0c6bd2fe14cfcf9c44b32d2eaab71b02c5633e2c679f5896f73f8ac4036ba2e67a4c806e8f428e4b11f526d7f4 languageName: node linkType: hard -"@babel/core@npm:7.25.2": - version: 7.25.2 - resolution: "@babel/core@npm:7.25.2" +"@babel/core@npm:7.25.8": + version: 7.25.8 + resolution: "@babel/core@npm:7.25.8" dependencies: "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.24.7" - "@babel/generator": "npm:^7.25.0" - "@babel/helper-compilation-targets": "npm:^7.25.2" - "@babel/helper-module-transforms": "npm:^7.25.2" - "@babel/helpers": "npm:^7.25.0" - "@babel/parser": "npm:^7.25.0" - "@babel/template": "npm:^7.25.0" - "@babel/traverse": "npm:^7.25.2" - "@babel/types": "npm:^7.25.2" + "@babel/code-frame": "npm:^7.25.7" + "@babel/generator": "npm:^7.25.7" + "@babel/helper-compilation-targets": "npm:^7.25.7" + "@babel/helper-module-transforms": "npm:^7.25.7" + "@babel/helpers": "npm:^7.25.7" + "@babel/parser": "npm:^7.25.8" + "@babel/template": "npm:^7.25.7" + "@babel/traverse": "npm:^7.25.7" + "@babel/types": "npm:^7.25.8" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10c0/a425fa40e73cb72b6464063a57c478bc2de9dbcc19c280f1b55a3d88b35d572e87e8594e7d7b4880331addb6faef641bbeb701b91b41b8806cd4deae5d74f401 + checksum: 10c0/8411ea506e6f7c8a39ab5c1524b00589fa3b087edb47389708f7fe07170929192171734666e3ea10b95a951643a531a6d09eedfe071572c9ea28516646265086 languageName: node linkType: hard @@ -197,27 +197,15 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/generator@npm:7.24.7" - dependencies: - "@babel/types": "npm:^7.24.7" - "@jridgewell/gen-mapping": "npm:^0.3.5" - "@jridgewell/trace-mapping": "npm:^0.3.25" - jsesc: "npm:^2.5.1" - checksum: 10c0/06b1f3350baf527a3309e50ffd7065f7aee04dd06e1e7db794ddfde7fe9d81f28df64edd587173f8f9295496a7ddb74b9a185d4bf4de7bb619e6d4ec45c8fd35 - languageName: node - linkType: hard - -"@babel/generator@npm:^7.25.0": - version: 7.25.0 - resolution: "@babel/generator@npm:7.25.0" +"@babel/generator@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/generator@npm:7.25.7" dependencies: - "@babel/types": "npm:^7.25.0" + "@babel/types": "npm:^7.25.7" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" - jsesc: "npm:^2.5.1" - checksum: 10c0/d0e2dfcdc8bdbb5dded34b705ceebf2e0bc1b06795a1530e64fb6a3ccf313c189db7f60c1616effae48114e1a25adc75855bc4496f3779a396b3377bae718ce7 + jsesc: "npm:^3.0.2" + checksum: 10c0/c03a26c79864d60d04ce36b649c3fa0d6fd7b2bf6a22e22854a0457aa09206508392dd73ee40e7bc8d50b3602f9ff068afa47770cda091d332e7db1ca382ee96 languageName: node linkType: hard @@ -236,16 +224,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.25.2": - version: 7.25.2 - resolution: "@babel/helper-compilation-targets@npm:7.25.2" +"@babel/helper-compilation-targets@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-compilation-targets@npm:7.25.7" dependencies: - "@babel/compat-data": "npm:^7.25.2" - "@babel/helper-validator-option": "npm:^7.24.8" - browserslist: "npm:^4.23.1" + "@babel/compat-data": "npm:^7.25.7" + "@babel/helper-validator-option": "npm:^7.25.7" + browserslist: "npm:^4.24.0" lru-cache: "npm:^5.1.1" semver: "npm:^6.3.1" - checksum: 10c0/de10e986b5322c9f807350467dc845ec59df9e596a5926a3b5edbb4710d8e3b8009d4396690e70b88c3844fe8ec4042d61436dd4b92d1f5f75655cf43ab07e99 + checksum: 10c0/705be7e5274a3fdade68e3e2cf42e2b600316ab52794e13b91299a16f16c926f15886b6e9d6df20eb943ccc1cdba5a363d4766f8d01e47b8e6f4e01175f5e66c languageName: node linkType: hard @@ -263,15 +251,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-environment-visitor@npm:7.24.7" - dependencies: - "@babel/types": "npm:^7.24.7" - checksum: 10c0/36ece78882b5960e2d26abf13cf15ff5689bf7c325b10a2895a74a499e712de0d305f8d78bb382dd3c05cfba7e47ec98fe28aab5674243e0625cd38438dd0b2d - languageName: node - linkType: hard - "@babel/helper-function-name@npm:^7.23.0": version: 7.23.0 resolution: "@babel/helper-function-name@npm:7.23.0" @@ -282,16 +261,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-function-name@npm:7.24.7" - dependencies: - "@babel/template": "npm:^7.24.7" - "@babel/types": "npm:^7.24.7" - checksum: 10c0/e5e41e6cf86bd0f8bf272cbb6e7c5ee0f3e9660414174435a46653efba4f2479ce03ce04abff2aa2ef9359cf057c79c06cb7b134a565ad9c0e8a50dcdc3b43c4 - languageName: node - linkType: hard - "@babel/helper-hoist-variables@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-hoist-variables@npm:7.22.5" @@ -301,15 +270,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-hoist-variables@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-hoist-variables@npm:7.24.7" - dependencies: - "@babel/types": "npm:^7.24.7" - checksum: 10c0/19ee37563bbd1219f9d98991ad0e9abef77803ee5945fd85aa7aa62a67c69efca9a801696a1b58dda27f211e878b3327789e6fd2a6f6c725ccefe36774b5ce95 - languageName: node - linkType: hard - "@babel/helper-module-imports@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-module-imports@npm:7.18.6" @@ -319,13 +279,13 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-module-imports@npm:7.24.7" +"@babel/helper-module-imports@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-module-imports@npm:7.25.7" dependencies: - "@babel/traverse": "npm:^7.24.7" - "@babel/types": "npm:^7.24.7" - checksum: 10c0/97c57db6c3eeaea31564286e328a9fb52b0313c5cfcc7eee4bc226aebcf0418ea5b6fe78673c0e4a774512ec6c86e309d0f326e99d2b37bfc16a25a032498af0 + "@babel/traverse": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" + checksum: 10c0/0fd0c3673835e5bf75558e184bcadc47c1f6dd2fe2016d53ebe1e5a6ae931a44e093015c2f9a6651c1a89f25c76d9246710c2b0b460b95ee069c464f2837fa2c languageName: node linkType: hard @@ -345,17 +305,17 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.25.2": - version: 7.25.2 - resolution: "@babel/helper-module-transforms@npm:7.25.2" +"@babel/helper-module-transforms@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-module-transforms@npm:7.25.7" dependencies: - "@babel/helper-module-imports": "npm:^7.24.7" - "@babel/helper-simple-access": "npm:^7.24.7" - "@babel/helper-validator-identifier": "npm:^7.24.7" - "@babel/traverse": "npm:^7.25.2" + "@babel/helper-module-imports": "npm:^7.25.7" + "@babel/helper-simple-access": "npm:^7.25.7" + "@babel/helper-validator-identifier": "npm:^7.25.7" + "@babel/traverse": "npm:^7.25.7" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/adaa15970ace0aee5934b5a633789b5795b6229c6a9cf3e09a7e80aa33e478675eee807006a862aa9aa517935d81f88a6db8a9f5936e3a2a40ec75f8062bc329 + checksum: 10c0/f37fa7d1d4df21690535b278468cbd5faf0133a3080f282000cfa4f3ffc9462a1458f866b04b6a2f2d1eec4691236cba9a867da61270dab3ab19846e62f05090 languageName: node linkType: hard @@ -375,13 +335,13 @@ __metadata: languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-simple-access@npm:7.24.7" +"@babel/helper-simple-access@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-simple-access@npm:7.25.7" dependencies: - "@babel/traverse": "npm:^7.24.7" - "@babel/types": "npm:^7.24.7" - checksum: 10c0/7230e419d59a85f93153415100a5faff23c133d7442c19e0cd070da1784d13cd29096ee6c5a5761065c44e8164f9f80e3a518c41a0256df39e38f7ad6744fed7 + "@babel/traverse": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" + checksum: 10c0/eed1b499bfb4f613c18debd61517e3de77b6da2727ca025aa05ac81599e0269f1dddb5237db04e8bb598115d015874752e0a7f11ff38672d74a4976097417059 languageName: node linkType: hard @@ -403,15 +363,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-split-export-declaration@npm:7.24.7" - dependencies: - "@babel/types": "npm:^7.24.7" - checksum: 10c0/0254577d7086bf09b01bbde98f731d4fcf4b7c3fa9634fdb87929801307c1f6202a1352e3faa5492450fa8da4420542d44de604daf540704ff349594a78184f6 - languageName: node - linkType: hard - "@babel/helper-string-parser@npm:^7.19.4": version: 7.19.4 resolution: "@babel/helper-string-parser@npm:7.19.4" @@ -433,10 +384,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.24.8": - version: 7.24.8 - resolution: "@babel/helper-string-parser@npm:7.24.8" - checksum: 10c0/6361f72076c17fabf305e252bf6d580106429014b3ab3c1f5c4eb3e6d465536ea6b670cc0e9a637a77a9ad40454d3e41361a2909e70e305116a23d68ce094c08 +"@babel/helper-string-parser@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-string-parser@npm:7.25.7" + checksum: 10c0/73ef2ceb81f8294678a0afe8ab0103729c0370cac2e830e0d5128b03be5f6a2635838af31d391d763e3c5a4460ed96f42fd7c9b552130670d525be665913bc4c languageName: node linkType: hard @@ -475,6 +426,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-validator-identifier@npm:7.25.7" + checksum: 10c0/07438e5bf01ab2882a15027fdf39ac3b0ba1b251774a5130917907014684e2f70fef8fd620137ca062c4c4eedc388508d2ea7a3a7d9936a32785f4fe116c68c0 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-validator-option@npm:7.18.6" @@ -482,10 +440,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.24.8": - version: 7.24.8 - resolution: "@babel/helper-validator-option@npm:7.24.8" - checksum: 10c0/73db93a34ae89201351288bee7623eed81a54000779462a986105b54ffe82069e764afd15171a428b82e7c7a9b5fec10b5d5603b216317a414062edf5c67a21f +"@babel/helper-validator-option@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-validator-option@npm:7.25.7" + checksum: 10c0/12ed418c8e3ed9ed44c8c80d823f4e42d399b5eb2e423adccb975e31a31a008cd3b5d8eab688b31f740caff4a1bb28fe06ea2fa7d635aee34cc0ad6995d50f0a languageName: node linkType: hard @@ -500,13 +458,13 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.25.0": - version: 7.25.0 - resolution: "@babel/helpers@npm:7.25.0" +"@babel/helpers@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helpers@npm:7.25.7" dependencies: - "@babel/template": "npm:^7.25.0" - "@babel/types": "npm:^7.25.0" - checksum: 10c0/b7fe007fc4194268abf70aa3810365085e290e6528dcb9fbbf7a765d43c74b6369ce0f99c5ccd2d44c413853099daa449c9a0123f0b212ac8d18643f2e8174b8 + "@babel/template": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" + checksum: 10c0/3b3ae9e373bd785414195ef8f59976a69d5a6ebe0ef2165fdcc5165e5c3ee09e0fcee94bb457df2ddb8c0532e4146d0a9b7a96b3497399a4bff4ffe196b30228 languageName: node linkType: hard @@ -532,15 +490,15 @@ __metadata: languageName: node linkType: hard -"@babel/highlight@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/highlight@npm:7.24.7" +"@babel/highlight@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/highlight@npm:7.25.7" dependencies: - "@babel/helper-validator-identifier": "npm:^7.24.7" + "@babel/helper-validator-identifier": "npm:^7.25.7" chalk: "npm:^2.4.2" js-tokens: "npm:^4.0.0" picocolors: "npm:^1.0.0" - checksum: 10c0/674334c571d2bb9d1c89bdd87566383f59231e16bcdcf5bb7835babdf03c9ae585ca0887a7b25bdf78f303984af028df52831c7989fecebb5101cc132da9393a + checksum: 10c0/1f5894fdb0a0af6101fb2822369b2eeeae32cbeae2ef73ff73fc6a0a4a20471565cd9cfa589f54ed69df66adeca7c57266031ca9134b7bd244d023a488d419aa languageName: node linkType: hard @@ -571,21 +529,14 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/parser@npm:7.24.7" - bin: - parser: ./bin/babel-parser.js - checksum: 10c0/8b244756872185a1c6f14b979b3535e682ff08cb5a2a5fd97cc36c017c7ef431ba76439e95e419d43000c5b07720495b00cf29a7f0d9a483643d08802b58819b - languageName: node - linkType: hard - -"@babel/parser@npm:^7.25.0": - version: 7.25.0 - resolution: "@babel/parser@npm:7.25.0" +"@babel/parser@npm:^7.25.7, @babel/parser@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/parser@npm:7.25.8" + dependencies: + "@babel/types": "npm:^7.25.8" bin: parser: ./bin/babel-parser.js - checksum: 10c0/4aecf13829fa6f4a66835429bd235458544d9cd14374b17c19bc7726f472727ca33f500e51e1298ddc72db93bdd77fcaa9ddc095200b0b792173069e6cf9742e + checksum: 10c0/a1a13845b7e8dda4c970791814a4bbf60004969882f18f470e260ad822d2e1f8941948f851e9335895563610f240fa6c98481ce8019865e469502bbf21daafa4 languageName: node linkType: hard @@ -783,25 +734,14 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/template@npm:7.24.7" - dependencies: - "@babel/code-frame": "npm:^7.24.7" - "@babel/parser": "npm:^7.24.7" - "@babel/types": "npm:^7.24.7" - checksum: 10c0/95b0b3ee80fcef685b7f4426f5713a855ea2cd5ac4da829b213f8fb5afe48a2a14683c2ea04d446dbc7f711c33c5cd4a965ef34dcbe5bc387c9e966b67877ae3 - languageName: node - linkType: hard - -"@babel/template@npm:^7.25.0": - version: 7.25.0 - resolution: "@babel/template@npm:7.25.0" +"@babel/template@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/template@npm:7.25.7" dependencies: - "@babel/code-frame": "npm:^7.24.7" - "@babel/parser": "npm:^7.25.0" - "@babel/types": "npm:^7.25.0" - checksum: 10c0/4e31afd873215744c016e02b04f43b9fa23205d6d0766fb2e93eb4091c60c1b88897936adb895fb04e3c23de98dfdcbe31bc98daaa1a4e0133f78bb948e1209b + "@babel/code-frame": "npm:^7.25.7" + "@babel/parser": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" + checksum: 10c0/8ae9e36e4330ee83d4832531d1d9bec7dc2ef6a2a8afa1ef1229506fd60667abcb17f306d1c3d7e582251270597022990c845d5d69e7add70a5aea66720decb9 languageName: node linkType: hard @@ -823,36 +763,18 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/traverse@npm:7.24.7" - dependencies: - "@babel/code-frame": "npm:^7.24.7" - "@babel/generator": "npm:^7.24.7" - "@babel/helper-environment-visitor": "npm:^7.24.7" - "@babel/helper-function-name": "npm:^7.24.7" - "@babel/helper-hoist-variables": "npm:^7.24.7" - "@babel/helper-split-export-declaration": "npm:^7.24.7" - "@babel/parser": "npm:^7.24.7" - "@babel/types": "npm:^7.24.7" - debug: "npm:^4.3.1" - globals: "npm:^11.1.0" - checksum: 10c0/a5135e589c3f1972b8877805f50a084a04865ccb1d68e5e1f3b94a8841b3485da4142e33413d8fd76bc0e6444531d3adf1f59f359c11ffac452b743d835068ab - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.25.2": - version: 7.25.2 - resolution: "@babel/traverse@npm:7.25.2" +"@babel/traverse@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/traverse@npm:7.25.7" dependencies: - "@babel/code-frame": "npm:^7.24.7" - "@babel/generator": "npm:^7.25.0" - "@babel/parser": "npm:^7.25.0" - "@babel/template": "npm:^7.25.0" - "@babel/types": "npm:^7.25.2" + "@babel/code-frame": "npm:^7.25.7" + "@babel/generator": "npm:^7.25.7" + "@babel/parser": "npm:^7.25.7" + "@babel/template": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10c0/1edcb602801d6ea577584e957a3f6ad48753c4ccb9373fce4c92ebfdee04833f5bd5f1b74758ab7d61fe66d6d83ffdd7c8d482f46199767feeaed6af7df2191e + checksum: 10c0/75d73e52c507a7a7a4c7971d6bf4f8f26fdd094e0d3a0193d77edf6a5efa36fc3db91ec5cc48e8b94e6eb5d5ad21af0a1040e71309172851209415fd105efb1a languageName: node linkType: hard @@ -900,25 +822,25 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.24.7, @babel/types@npm:^7.8.3": - version: 7.24.7 - resolution: "@babel/types@npm:7.24.7" +"@babel/types@npm:^7.25.7, @babel/types@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/types@npm:7.25.8" dependencies: - "@babel/helper-string-parser": "npm:^7.24.7" - "@babel/helper-validator-identifier": "npm:^7.24.7" + "@babel/helper-string-parser": "npm:^7.25.7" + "@babel/helper-validator-identifier": "npm:^7.25.7" to-fast-properties: "npm:^2.0.0" - checksum: 10c0/d9ecbfc3eb2b05fb1e6eeea546836ac30d990f395ef3fe3f75ced777a222c3cfc4489492f72e0ce3d9a5a28860a1ce5f81e66b88cf5088909068b3ff4fab72c1 + checksum: 10c0/55ca2d6df6426c98db2769ce884ce5e9de83a512ea2dd7bcf56c811984dc14351cacf42932a723630c5afcff2455809323decd645820762182f10b7b5252b59f languageName: node linkType: hard -"@babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2": - version: 7.25.2 - resolution: "@babel/types@npm:7.25.2" +"@babel/types@npm:^7.8.3": + version: 7.24.7 + resolution: "@babel/types@npm:7.24.7" dependencies: - "@babel/helper-string-parser": "npm:^7.24.8" + "@babel/helper-string-parser": "npm:^7.24.7" "@babel/helper-validator-identifier": "npm:^7.24.7" to-fast-properties: "npm:^2.0.0" - checksum: 10c0/e489435856be239f8cc1120c90a197e4c2865385121908e5edb7223cfdff3768cba18f489adfe0c26955d9e7bbb1fb10625bc2517505908ceb0af848989bd864 + checksum: 10c0/d9ecbfc3eb2b05fb1e6eeea546836ac30d990f395ef3fe3f75ced777a222c3cfc4489492f72e0ce3d9a5a28860a1ce5f81e66b88cf5088909068b3ff4fab72c1 languageName: node linkType: hard @@ -1652,92 +1574,92 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core-darwin-arm64@npm:1.7.26" +"@swc/core-darwin-arm64@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core-darwin-arm64@npm:1.7.35" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core-darwin-x64@npm:1.7.26" +"@swc/core-darwin-x64@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core-darwin-x64@npm:1.7.35" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.7.26" +"@swc/core-linux-arm-gnueabihf@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.7.35" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core-linux-arm64-gnu@npm:1.7.26" +"@swc/core-linux-arm64-gnu@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core-linux-arm64-gnu@npm:1.7.35" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core-linux-arm64-musl@npm:1.7.26" +"@swc/core-linux-arm64-musl@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core-linux-arm64-musl@npm:1.7.35" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core-linux-x64-gnu@npm:1.7.26" +"@swc/core-linux-x64-gnu@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core-linux-x64-gnu@npm:1.7.35" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core-linux-x64-musl@npm:1.7.26" +"@swc/core-linux-x64-musl@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core-linux-x64-musl@npm:1.7.35" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core-win32-arm64-msvc@npm:1.7.26" +"@swc/core-win32-arm64-msvc@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core-win32-arm64-msvc@npm:1.7.35" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core-win32-ia32-msvc@npm:1.7.26" +"@swc/core-win32-ia32-msvc@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core-win32-ia32-msvc@npm:1.7.35" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core-win32-x64-msvc@npm:1.7.26" +"@swc/core-win32-x64-msvc@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core-win32-x64-msvc@npm:1.7.35" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:1.7.26": - version: 1.7.26 - resolution: "@swc/core@npm:1.7.26" +"@swc/core@npm:1.7.35": + version: 1.7.35 + resolution: "@swc/core@npm:1.7.35" dependencies: - "@swc/core-darwin-arm64": "npm:1.7.26" - "@swc/core-darwin-x64": "npm:1.7.26" - "@swc/core-linux-arm-gnueabihf": "npm:1.7.26" - "@swc/core-linux-arm64-gnu": "npm:1.7.26" - "@swc/core-linux-arm64-musl": "npm:1.7.26" - "@swc/core-linux-x64-gnu": "npm:1.7.26" - "@swc/core-linux-x64-musl": "npm:1.7.26" - "@swc/core-win32-arm64-msvc": "npm:1.7.26" - "@swc/core-win32-ia32-msvc": "npm:1.7.26" - "@swc/core-win32-x64-msvc": "npm:1.7.26" + "@swc/core-darwin-arm64": "npm:1.7.35" + "@swc/core-darwin-x64": "npm:1.7.35" + "@swc/core-linux-arm-gnueabihf": "npm:1.7.35" + "@swc/core-linux-arm64-gnu": "npm:1.7.35" + "@swc/core-linux-arm64-musl": "npm:1.7.35" + "@swc/core-linux-x64-gnu": "npm:1.7.35" + "@swc/core-linux-x64-musl": "npm:1.7.35" + "@swc/core-win32-arm64-msvc": "npm:1.7.35" + "@swc/core-win32-ia32-msvc": "npm:1.7.35" + "@swc/core-win32-x64-msvc": "npm:1.7.35" "@swc/counter": "npm:^0.1.3" - "@swc/types": "npm:^0.1.12" + "@swc/types": "npm:^0.1.13" peerDependencies: "@swc/helpers": "*" dependenciesMeta: @@ -1764,7 +1686,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10c0/6c5ce3d048cb100cd545145b1701a857207b1aeecc8f6bc44ed10b0a1792052834d155a6fa651dad20f38d3fff595034649cc75618946be8da751fa86a9c75b7 + checksum: 10c0/aae11f2f311f16a21348e33768debe2295a7e0a04f4b37ffbbb15cf5303e0cd08cf0c72661b72f8e4e33cf530d82c15bb2cef090548c65c4bf3ab3854724465b languageName: node linkType: hard @@ -1788,12 +1710,12 @@ __metadata: languageName: node linkType: hard -"@swc/types@npm:^0.1.12": - version: 0.1.12 - resolution: "@swc/types@npm:0.1.12" +"@swc/types@npm:^0.1.13": + version: 0.1.13 + resolution: "@swc/types@npm:0.1.13" dependencies: "@swc/counter": "npm:^0.1.3" - checksum: 10c0/f95fea7dee8fc07f8389afbb9578f3d0cd84b429b1d0dbff7fd99b2ef06ed88e96bc33631f36c3bc0505d5a783bee1374acd84b8fc2593001219b6c2caba241b + checksum: 10c0/f85a850dead981ca9a26ae366529f2b383fa26324ffcbbee46d7b48399e6ed36d6a6a3d55398f17f87c65f550e28d642a35877d40f389c78765a31ecdfc88bd9 languageName: node linkType: hard @@ -2819,17 +2741,17 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.23.1": - version: 4.23.2 - resolution: "browserslist@npm:4.23.2" +"browserslist@npm:^4.24.0": + version: 4.24.0 + resolution: "browserslist@npm:4.24.0" dependencies: - caniuse-lite: "npm:^1.0.30001640" - electron-to-chromium: "npm:^1.4.820" - node-releases: "npm:^2.0.14" + caniuse-lite: "npm:^1.0.30001663" + electron-to-chromium: "npm:^1.5.28" + node-releases: "npm:^2.0.18" update-browserslist-db: "npm:^1.1.0" bin: browserslist: cli.js - checksum: 10c0/0217d23c69ed61cdd2530c7019bf7c822cd74c51f8baab18dd62457fed3129f52499f8d3a6f809ae1fb7bb3050aa70caa9a529cc36c7478427966dbf429723a5 + checksum: 10c0/95e76ad522753c4c470427f6e3c8a4bb5478ff448841e22b3d3e53f89ecaf17b6984666d6c7e715c370f1e7fa0cf684f42e34e554236a8b2fab38ea76b9e4c52 languageName: node linkType: hard @@ -2957,10 +2879,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001640": - version: 1.0.30001644 - resolution: "caniuse-lite@npm:1.0.30001644" - checksum: 10c0/96de82909f3ba9f44e5b261c42d3d8814ba99b7b8b48eb8f8eafb7015804ccb2bc2120c5fbc5e193e14e0c87bf1cd0d4de920d8f5a5b477e66e8f0c3972d0eb7 +"caniuse-lite@npm:^1.0.30001663": + version: 1.0.30001669 + resolution: "caniuse-lite@npm:1.0.30001669" + checksum: 10c0/f125f23440d3dbb6c25ffb8d55f4ce48af36a84d0932b152b3b74f143a4170cbe92e02b0a9676209c86609bf7bf34119ff10cc2bc7c1b7ea40e936cc16598408 languageName: node linkType: hard @@ -3849,10 +3771,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.820": - version: 1.5.3 - resolution: "electron-to-chromium@npm:1.5.3" - checksum: 10c0/acd4dad650dafa16c4bd19868fe79c58ae3208f666d868ef8d4c81012707b2855b1816241d1c243b50c75a6933817a9e33401a5a17bc4222c52a5ee8abf457e8 +"electron-to-chromium@npm:^1.5.28": + version: 1.5.40 + resolution: "electron-to-chromium@npm:1.5.40" + checksum: 10c0/3f97360627cf179b344a7d45b3d12fd3f18f1287529d9835a8e802c7a3b99f09e326b4ed3097be1b135e45a33e8497e758b0c101e38e5bb405eaa6aa887eca82 languageName: node linkType: hard @@ -6066,6 +5988,15 @@ __metadata: languageName: node linkType: hard +"jsesc@npm:^3.0.2": + version: 3.0.2 + resolution: "jsesc@npm:3.0.2" + bin: + jsesc: bin/jsesc + checksum: 10c0/ef22148f9e793180b14d8a145ee6f9f60f301abf443288117b4b6c53d0ecd58354898dc506ccbb553a5f7827965cd38bc5fb726575aae93c5e8915e2de8290e1 + languageName: node + linkType: hard + "json-diff@npm:^1.0.6": version: 1.0.6 resolution: "json-diff@npm:1.0.6" @@ -7035,10 +6966,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.14": - version: 2.0.14 - resolution: "node-releases@npm:2.0.14" - checksum: 10c0/199fc93773ae70ec9969bc6d5ac5b2bbd6eb986ed1907d751f411fef3ede0e4bfdb45ceb43711f8078bea237b6036db8b1bf208f6ff2b70c7d615afd157f3ab9 +"node-releases@npm:^2.0.18": + version: 2.0.18 + resolution: "node-releases@npm:2.0.18" + checksum: 10c0/786ac9db9d7226339e1dc84bbb42007cb054a346bd9257e6aa154d294f01bc6a6cddb1348fa099f079be6580acbb470e3c048effd5f719325abd0179e566fd27 languageName: node linkType: hard @@ -9407,11 +9338,11 @@ __metadata: resolution: "unleash-server@workspace:." dependencies: "@apidevtools/swagger-parser": "npm:10.1.0" - "@babel/core": "npm:7.25.2" + "@babel/core": "npm:7.25.8" "@biomejs/biome": "npm:^1.8.3" "@cyclonedx/yarn-plugin-cyclonedx": "npm:^1.0.0-rc.7" "@slack/web-api": "npm:^7.3.4" - "@swc/core": "npm:1.7.26" + "@swc/core": "npm:1.7.35" "@swc/jest": "npm:0.2.36" "@types/bcryptjs": "npm:2.4.6" "@types/cors": "npm:2.8.17"