mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Feat: remove last seen refactor flag (#5423)
What it says on the box --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai> Co-authored-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
		
							parent
							
								
									ef8edf9c44
								
							
						
					
					
						commit
						e5760b5690
					
				| @ -102,7 +102,6 @@ exports[`should create default config 1`] = ` | |||||||
|       "responseTimeWithAppNameKillSwitch": false, |       "responseTimeWithAppNameKillSwitch": false, | ||||||
|       "scheduledConfigurationChanges": false, |       "scheduledConfigurationChanges": false, | ||||||
|       "strictSchemaValidation": false, |       "strictSchemaValidation": false, | ||||||
|       "useLastSeenRefactor": false, |  | ||||||
|     }, |     }, | ||||||
|     "externalResolver": { |     "externalResolver": { | ||||||
|       "getVariant": [Function], |       "getVariant": [Function], | ||||||
|  | |||||||
| @ -189,9 +189,7 @@ export class FeatureToggleRowConverter { | |||||||
|             feature.createdAt = r.created_at; |             feature.createdAt = r.created_at; | ||||||
|             feature.favorite = r.favorite; |             feature.favorite = r.favorite; | ||||||
| 
 | 
 | ||||||
|             if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |             this.addLastSeenByEnvironment(feature, r); | ||||||
|                 this.addLastSeenByEnvironment(feature, r); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             acc[r.name] = feature; |             acc[r.name] = feature; | ||||||
|             return acc; |             return acc; | ||||||
| @ -246,9 +244,7 @@ export class FeatureToggleRowConverter { | |||||||
|             feature.lastSeenAt = row.last_seen_at; |             feature.lastSeenAt = row.last_seen_at; | ||||||
|             feature.archivedAt = row.archived_at; |             feature.archivedAt = row.archived_at; | ||||||
| 
 | 
 | ||||||
|             if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |             this.addLastSeenByEnvironment(feature, row); | ||||||
|                 this.addLastSeenByEnvironment(feature, row); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             acc[row.name] = feature; |             acc[row.name] = feature; | ||||||
|             return acc; |             return acc; | ||||||
|  | |||||||
| @ -1970,13 +1970,7 @@ class FeatureToggleService { | |||||||
|         archived: boolean, |         archived: boolean, | ||||||
|         userId: number, |         userId: number, | ||||||
|     ): Promise<FeatureToggle[]> { |     ): Promise<FeatureToggle[]> { | ||||||
|         let features; |         const features = await this.featureToggleStore.getArchivedFeatures(); | ||||||
| 
 |  | ||||||
|         if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |  | ||||||
|             features = await this.featureToggleStore.getArchivedFeatures(); |  | ||||||
|         } else { |  | ||||||
|             features = await this.featureToggleStore.getAll({ archived }); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         if (this.flagResolver.isEnabled('privateProjects')) { |         if (this.flagResolver.isEnabled('privateProjects')) { | ||||||
|             const projectAccess = |             const projectAccess = | ||||||
| @ -1998,11 +1992,7 @@ class FeatureToggleService { | |||||||
|         archived: boolean, |         archived: boolean, | ||||||
|         project: string, |         project: string, | ||||||
|     ): Promise<FeatureToggle[]> { |     ): Promise<FeatureToggle[]> { | ||||||
|         if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |         return this.featureToggleStore.getArchivedFeatures(project); | ||||||
|             return this.featureToggleStore.getArchivedFeatures(project); |  | ||||||
|         } else { |  | ||||||
|             return this.featureToggleStore.getAll({ archived, project }); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getProjectId(name: string): Promise<string | undefined> { |     async getProjectId(name: string): Promise<string | undefined> { | ||||||
|  | |||||||
| @ -171,15 +171,13 @@ export default class FeatureToggleStore implements IFeatureToggleStore { | |||||||
|         builder.addSelectColumn('ft.tag_value as tag_value'); |         builder.addSelectColumn('ft.tag_value as tag_value'); | ||||||
|         builder.addSelectColumn('ft.tag_type as tag_type'); |         builder.addSelectColumn('ft.tag_type as tag_type'); | ||||||
| 
 | 
 | ||||||
|         if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |         builder.withLastSeenByEnvironment(archived); | ||||||
|             builder.withLastSeenByEnvironment(archived); |         builder.addSelectColumn( | ||||||
|             builder.addSelectColumn( |             'last_seen_at_metrics.last_seen_at as env_last_seen_at', | ||||||
|                 'last_seen_at_metrics.last_seen_at as env_last_seen_at', |         ); | ||||||
|             ); |         builder.addSelectColumn( | ||||||
|             builder.addSelectColumn( |             'last_seen_at_metrics.environment as last_seen_at_env', | ||||||
|                 'last_seen_at_metrics.environment as last_seen_at_env', |         ); | ||||||
|             ); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         if (userId) { |         if (userId) { | ||||||
|             builder.withFavorites(userId); |             builder.withFavorites(userId); | ||||||
|  | |||||||
| @ -341,23 +341,21 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { | |||||||
| 
 | 
 | ||||||
|         let selectColumns = ['features_view.*'] as (string | Raw<any>)[]; |         let selectColumns = ['features_view.*'] as (string | Raw<any>)[]; | ||||||
| 
 | 
 | ||||||
|         if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |         query.leftJoin('last_seen_at_metrics', function () { | ||||||
|             query.leftJoin('last_seen_at_metrics', function () { |             this.on( | ||||||
|                 this.on( |                 'last_seen_at_metrics.environment', | ||||||
|                     'last_seen_at_metrics.environment', |                 '=', | ||||||
|                     '=', |                 'features_view.environment_name', | ||||||
|                     'features_view.environment_name', |             ).andOn( | ||||||
|                 ).andOn( |                 'last_seen_at_metrics.feature_name', | ||||||
|                     'last_seen_at_metrics.feature_name', |                 '=', | ||||||
|                     '=', |                 'features_view.name', | ||||||
|                     'features_view.name', |  | ||||||
|                 ); |  | ||||||
|             }); |  | ||||||
|             // Override feature view for now
 |  | ||||||
|             selectColumns.push( |  | ||||||
|                 'last_seen_at_metrics.last_seen_at as env_last_seen_at', |  | ||||||
|             ); |             ); | ||||||
|         } |         }); | ||||||
|  |         // Override feature view for now
 | ||||||
|  |         selectColumns.push( | ||||||
|  |             'last_seen_at_metrics.last_seen_at as env_last_seen_at', | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         if (userId) { |         if (userId) { | ||||||
|             query = query.leftJoin(`favorite_features`, function () { |             query = query.leftJoin(`favorite_features`, function () { | ||||||
| @ -631,19 +629,17 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { | |||||||
|                         'segments.id', |                         'segments.id', | ||||||
|                     ); |                     ); | ||||||
| 
 | 
 | ||||||
|                 if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |                 query.leftJoin('last_seen_at_metrics', function () { | ||||||
|                     query.leftJoin('last_seen_at_metrics', function () { |                     this.on( | ||||||
|                         this.on( |                         'last_seen_at_metrics.environment', | ||||||
|                             'last_seen_at_metrics.environment', |                         '=', | ||||||
|                             '=', |                         'environments.name', | ||||||
|                             'environments.name', |                     ).andOn( | ||||||
|                         ).andOn( |                         'last_seen_at_metrics.feature_name', | ||||||
|                             'last_seen_at_metrics.feature_name', |                         '=', | ||||||
|                             '=', |                         'features.name', | ||||||
|                             'features.name', |                     ); | ||||||
|                         ); |                 }); | ||||||
|                     }); |  | ||||||
|                 } |  | ||||||
| 
 | 
 | ||||||
|                 let selectColumns = [ |                 let selectColumns = [ | ||||||
|                     'features.name as feature_name', |                     'features.name as feature_name', | ||||||
| @ -664,11 +660,8 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { | |||||||
|                     'segments.name as segment_name', |                     'segments.name as segment_name', | ||||||
|                 ] as (string | Raw<any> | Knex.QueryBuilder)[]; |                 ] as (string | Raw<any> | Knex.QueryBuilder)[]; | ||||||
| 
 | 
 | ||||||
|                 let lastSeenQuery = 'feature_environments.last_seen_at'; |                 const lastSeenQuery = 'last_seen_at_metrics.last_seen_at'; | ||||||
|                 if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |                 selectColumns.push(`${lastSeenQuery} as env_last_seen_at`); | ||||||
|                     lastSeenQuery = 'last_seen_at_metrics.last_seen_at'; |  | ||||||
|                     selectColumns.push(`${lastSeenQuery} as env_last_seen_at`); |  | ||||||
|                 } |  | ||||||
| 
 | 
 | ||||||
|                 if (userId) { |                 if (userId) { | ||||||
|                     query.leftJoin(`favorite_features`, function () { |                     query.leftJoin(`favorite_features`, function () { | ||||||
| @ -802,19 +795,13 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { | |||||||
|             ) |             ) | ||||||
|             .leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name'); |             .leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name'); | ||||||
| 
 | 
 | ||||||
|         if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |         query.leftJoin('last_seen_at_metrics', function () { | ||||||
|             query.leftJoin('last_seen_at_metrics', function () { |             this.on( | ||||||
|                 this.on( |                 'last_seen_at_metrics.environment', | ||||||
|                     'last_seen_at_metrics.environment', |                 '=', | ||||||
|                     '=', |                 'environments.name', | ||||||
|                     'environments.name', |             ).andOn('last_seen_at_metrics.feature_name', '=', 'features.name'); | ||||||
|                 ).andOn( |         }); | ||||||
|                     'last_seen_at_metrics.feature_name', |  | ||||||
|                     '=', |  | ||||||
|                     'features.name', |  | ||||||
|                 ); |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         let selectColumns = [ |         let selectColumns = [ | ||||||
|             'features.name as feature_name', |             'features.name as feature_name', | ||||||
| @ -833,15 +820,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { | |||||||
|             'ft.tag_type as tag_type', |             'ft.tag_type as tag_type', | ||||||
|         ] as (string | Raw<any> | Knex.QueryBuilder)[]; |         ] as (string | Raw<any> | Knex.QueryBuilder)[]; | ||||||
| 
 | 
 | ||||||
|         if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |         selectColumns.push( | ||||||
|             selectColumns.push( |             'last_seen_at_metrics.last_seen_at as env_last_seen_at', | ||||||
|                 'last_seen_at_metrics.last_seen_at as env_last_seen_at', |         ); | ||||||
|             ); |  | ||||||
|         } else { |  | ||||||
|             selectColumns.push( |  | ||||||
|                 'feature_environments.last_seen_at as env_last_seen_at', |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         if (userId) { |         if (userId) { | ||||||
|             query = query.leftJoin(`favorite_features`, function () { |             query = query.leftJoin(`favorite_features`, function () { | ||||||
|  | |||||||
| @ -22,7 +22,6 @@ beforeAll(async () => { | |||||||
|         experimental: { |         experimental: { | ||||||
|             flags: { |             flags: { | ||||||
|                 strictSchemaValidation: true, |                 strictSchemaValidation: true, | ||||||
|                 useLastSeenRefactor: true, |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|     }; |     }; | ||||||
|  | |||||||
| @ -689,9 +689,7 @@ test('Should return last seen at per environment', async () => { | |||||||
|     expect(environments[0].lastSeenAt).toEqual(new Date(date)); |     expect(environments[0].lastSeenAt).toEqual(new Date(date)); | ||||||
| 
 | 
 | ||||||
|     // Test with feature flag on
 |     // Test with feature flag on
 | ||||||
|     const config = createTestConfig({ |     const config = createTestConfig(); | ||||||
|         experimental: { flags: { useLastSeenRefactor: true } }, |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     const featureService = createFeatureToggleService(db.rawDatabase, config); |     const featureService = createFeatureToggleService(db.rawDatabase, config); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -20,7 +20,6 @@ beforeAll(async () => { | |||||||
|                     strictSchemaValidation: true, |                     strictSchemaValidation: true, | ||||||
|                     strategyVariant: true, |                     strategyVariant: true, | ||||||
|                     privateProjects: true, |                     privateProjects: true, | ||||||
|                     useLastSeenRefactor: true, |  | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  | |||||||
| @ -185,34 +185,6 @@ test('schema allow yes=<string nbr>', () => { | |||||||
|     expect(value.bucket.toggles.Demo2.no).toBe(256); |     expect(value.bucket.toggles.Demo2.no).toBe(256); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('should set lastSeen on toggle', async () => { |  | ||||||
|     expect.assertions(1); |  | ||||||
|     stores.featureToggleStore.create('default', { |  | ||||||
|         name: 'toggleLastSeen', |  | ||||||
|     }); |  | ||||||
|     await request |  | ||||||
|         .post('/api/client/metrics') |  | ||||||
|         .send({ |  | ||||||
|             appName: 'demo', |  | ||||||
|             bucket: { |  | ||||||
|                 start: Date.now(), |  | ||||||
|                 stop: Date.now(), |  | ||||||
|                 toggles: { |  | ||||||
|                     toggleLastSeen: { |  | ||||||
|                         yes: 200, |  | ||||||
|                         no: 0, |  | ||||||
|                     }, |  | ||||||
|                 }, |  | ||||||
|             }, |  | ||||||
|         }) |  | ||||||
|         .expect(202); |  | ||||||
| 
 |  | ||||||
|     await services.lastSeenService.store(); |  | ||||||
|     const toggle = await stores.featureToggleStore.get('toggleLastSeen'); |  | ||||||
| 
 |  | ||||||
|     expect(toggle.lastSeenAt).toBeTruthy(); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('should return a 400 when required fields are missing', async () => { | test('should return a 400 when required fields are missing', async () => { | ||||||
|     stores.featureToggleStore.create('default', { |     stores.featureToggleStore.create('default', { | ||||||
|         name: 'toggleLastSeen', |         name: 'toggleLastSeen', | ||||||
|  | |||||||
| @ -1,39 +0,0 @@ | |||||||
| import { Logger } from '../../../logger'; |  | ||||||
| import { IFeatureOverview } from '../../../types'; |  | ||||||
| import { IFeatureLastSeenResults } from './last-seen-read-model'; |  | ||||||
| 
 |  | ||||||
| export class LastSeenMapper { |  | ||||||
|     mapToFeatures( |  | ||||||
|         features: IFeatureOverview[], |  | ||||||
|         lastSeenAtPerEnvironment: IFeatureLastSeenResults, |  | ||||||
|         logger: Logger, |  | ||||||
|     ): IFeatureOverview[] { |  | ||||||
|         return features.map((feature) => { |  | ||||||
|             if (!feature.environments) { |  | ||||||
|                 logger.warn('Feature without environments:', feature); |  | ||||||
|                 return feature; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             feature.environments = feature.environments.map((environment) => { |  | ||||||
|                 const noData = |  | ||||||
|                     !lastSeenAtPerEnvironment[feature.name] || |  | ||||||
|                     !lastSeenAtPerEnvironment[feature.name][environment.name]; |  | ||||||
| 
 |  | ||||||
|                 if (noData) { |  | ||||||
|                     logger.warn( |  | ||||||
|                         'No last seen data for environment:', |  | ||||||
|                         environment, |  | ||||||
|                     ); |  | ||||||
|                     return environment; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 environment.lastSeenAt = new Date( |  | ||||||
|                     lastSeenAtPerEnvironment[feature.name][environment.name] |  | ||||||
|                         .lastSeen, |  | ||||||
|                 ); |  | ||||||
|                 return environment; |  | ||||||
|             }); |  | ||||||
|             return feature; |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -51,11 +51,7 @@ export class LastSeenService { | |||||||
|                 `Updating last seen for ${lastSeenToggles.length} toggles`, |                 `Updating last seen for ${lastSeenToggles.length} toggles`, | ||||||
|             ); |             ); | ||||||
| 
 | 
 | ||||||
|             if (this.config.flagResolver.isEnabled('useLastSeenRefactor')) { |             await this.lastSeenStore.setLastSeen(lastSeenToggles); | ||||||
|                 await this.lastSeenStore.setLastSeen(lastSeenToggles); |  | ||||||
|             } else { |  | ||||||
|                 await this.featureToggleStore.setLastSeen(lastSeenToggles); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|         return count; |         return count; | ||||||
|     } |     } | ||||||
| @ -81,8 +77,6 @@ export class LastSeenService { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async cleanLastSeen() { |     async cleanLastSeen() { | ||||||
|         if (this.flagResolver.isEnabled('useLastSeenRefactor')) { |         await this.lastSeenStore.cleanLastSeen(); | ||||||
|             await this.lastSeenStore.cleanLastSeen(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,95 +0,0 @@ | |||||||
| import { IFeatureOverview } from '../../../../types'; |  | ||||||
| import { LastSeenMapper } from '../last-seen-mapper'; |  | ||||||
| import getLogger from '../../../../../test/fixtures/no-logger'; |  | ||||||
| 
 |  | ||||||
| test('should produce correct output when mapped', () => { |  | ||||||
|     const mapper = new LastSeenMapper(); |  | ||||||
| 
 |  | ||||||
|     const inputLastSeen = { |  | ||||||
|         exp: { |  | ||||||
|             production: { lastSeen: '2023-10-05T07:27:04.286Z' }, |  | ||||||
|             development: { lastSeen: '2023-10-04T19:03:29.682Z' }, |  | ||||||
|         }, |  | ||||||
|         'payment-system': { |  | ||||||
|             production: { lastSeen: '2023-10-05T07:27:04.286Z' }, |  | ||||||
|             development: { lastSeen: '2023-10-04T19:03:29.682Z' }, |  | ||||||
|         }, |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     const inputFeatures: IFeatureOverview[] = [ |  | ||||||
|         { |  | ||||||
|             type: 'release', |  | ||||||
|             description: null, |  | ||||||
|             favorite: false, |  | ||||||
|             name: 'payment-system', |  | ||||||
|             // @ts-ignore
 |  | ||||||
|             createdAt: '2023-06-30T12:57:20.476Z', |  | ||||||
|             // @ts-ignore
 |  | ||||||
|             lastSeenAt: '2023-10-03T13:08:16.263Z', |  | ||||||
|             stale: false, |  | ||||||
|             impressionData: false, |  | ||||||
|             environments: [ |  | ||||||
|                 { |  | ||||||
|                     name: 'development', |  | ||||||
|                     enabled: false, |  | ||||||
|                     type: 'development', |  | ||||||
|                     sortOrder: 2, |  | ||||||
|                     variantCount: 0, |  | ||||||
|                     // @ts-ignore
 |  | ||||||
|                     lastSeenAt: '2023-10-04T19:03:29.682Z', |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     name: 'production', |  | ||||||
|                     enabled: true, |  | ||||||
|                     type: 'production', |  | ||||||
|                     sortOrder: 3, |  | ||||||
|                     variantCount: 0, |  | ||||||
|                     // @ts-ignore
 |  | ||||||
|                     lastSeenAt: '2023-10-05T07:27:04.286Z', |  | ||||||
|                 }, |  | ||||||
|             ], |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             type: 'experiment', |  | ||||||
|             description: null, |  | ||||||
|             favorite: false, |  | ||||||
|             name: 'exp', |  | ||||||
|             // @ts-ignore
 |  | ||||||
|             createdAt: '2023-09-13T08:08:28.211Z', |  | ||||||
|             // @ts-ignore
 |  | ||||||
|             lastSeenAt: '2023-10-03T13:08:16.263Z', |  | ||||||
|             stale: false, |  | ||||||
|             impressionData: false, |  | ||||||
|             environments: [ |  | ||||||
|                 { |  | ||||||
|                     name: 'development', |  | ||||||
|                     enabled: false, |  | ||||||
|                     type: 'development', |  | ||||||
|                     sortOrder: 2, |  | ||||||
|                     variantCount: 0, |  | ||||||
|                     // @ts-ignore
 |  | ||||||
|                     lastSeenAt: '2023-10-04T19:03:29.682Z', |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     name: 'production', |  | ||||||
|                     enabled: true, |  | ||||||
|                     type: 'production', |  | ||||||
|                     sortOrder: 3, |  | ||||||
|                     variantCount: 0, |  | ||||||
|                     // @ts-ignore
 |  | ||||||
|                     lastSeenAt: '2023-10-05T07:27:04.286Z', |  | ||||||
|                 }, |  | ||||||
|             ], |  | ||||||
|         }, |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     const logger = getLogger(); |  | ||||||
| 
 |  | ||||||
|     const result = mapper.mapToFeatures(inputFeatures, inputLastSeen, logger); |  | ||||||
| 
 |  | ||||||
|     expect(result[0].environments[0].name).toBe('development'); |  | ||||||
|     expect(result[0].name).toBe('payment-system'); |  | ||||||
|     expect(result[0].environments[0].lastSeenAt).toEqual( |  | ||||||
|         new Date(inputLastSeen['payment-system'].development.lastSeen), |  | ||||||
|     ); |  | ||||||
| }); |  | ||||||
| @ -16,7 +16,6 @@ beforeAll(async () => { | |||||||
|             experimental: { |             experimental: { | ||||||
|                 flags: { |                 flags: { | ||||||
|                     strictSchemaValidation: true, |                     strictSchemaValidation: true, | ||||||
|                     useLastSeenRefactor: true, |  | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  | |||||||
| @ -36,7 +36,9 @@ function initLastSeenService(flagEnabled = true) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| test('should not add duplicates per feature/environment', async () => { | test('should not add duplicates per feature/environment', async () => { | ||||||
|     const { lastSeenService, featureToggleStore } = initLastSeenService(false); |     const { lastSeenService, featureToggleStore, lastSeenStore } = | ||||||
|  |         initLastSeenService(false); | ||||||
|  |     const lastSeenSpy = jest.spyOn(lastSeenStore, 'setLastSeen'); | ||||||
| 
 | 
 | ||||||
|     lastSeenService.updateLastSeen([ |     lastSeenService.updateLastSeen([ | ||||||
|         { |         { | ||||||
| @ -59,10 +61,8 @@ test('should not add duplicates per feature/environment', async () => { | |||||||
|             timestamp: new Date(), |             timestamp: new Date(), | ||||||
|         }, |         }, | ||||||
|     ]); |     ]); | ||||||
|     featureToggleStore.setLastSeen = jest.fn(); |  | ||||||
|     await lastSeenService.store(); |     await lastSeenService.store(); | ||||||
| 
 |     expect(lastSeenSpy).toHaveBeenCalledWith([ | ||||||
|     expect(featureToggleStore.setLastSeen).toHaveBeenCalledWith([ |  | ||||||
|         { |         { | ||||||
|             environment: 'development', |             environment: 'development', | ||||||
|             featureName: 'myFeature', |             featureName: 'myFeature', | ||||||
| @ -96,7 +96,6 @@ test('should call last seen at store with correct data', async () => { | |||||||
|         }, |         }, | ||||||
|     ]); |     ]); | ||||||
|     lastSeenStore.setLastSeen = jest.fn(); |     lastSeenStore.setLastSeen = jest.fn(); | ||||||
|     featureToggleStore.setLastSeen = jest.fn(); |  | ||||||
|     await lastSeenService.store(); |     await lastSeenService.store(); | ||||||
| 
 | 
 | ||||||
|     expect(lastSeenStore.setLastSeen).toHaveBeenCalledWith([ |     expect(lastSeenStore.setLastSeen).toHaveBeenCalledWith([ | ||||||
| @ -105,5 +104,4 @@ test('should call last seen at store with correct data', async () => { | |||||||
|             featureName: 'myFeature', |             featureName: 'myFeature', | ||||||
|         }, |         }, | ||||||
|     ]); |     ]); | ||||||
|     expect(featureToggleStore.setLastSeen).toHaveBeenCalledTimes(0); |  | ||||||
| }); | }); | ||||||
|  | |||||||
							
								
								
									
										122
									
								
								src/lib/services/scheduler-service.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/lib/services/scheduler-service.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,122 @@ | |||||||
|  | import { LogProvider } from '../logger'; | ||||||
|  | import { SchedulerService } from '../features/scheduler/scheduler-service'; | ||||||
|  | import { createTestConfig } from '../../test/config/test-config'; | ||||||
|  | import FakeSettingStore from '../../test/fixtures/fake-setting-store'; | ||||||
|  | import SettingService from './setting-service'; | ||||||
|  | import EventService from './event-service'; | ||||||
|  | import MaintenanceService from '../features/maintenance/maintenance-service'; | ||||||
|  | 
 | ||||||
|  | function ms(timeMs) { | ||||||
|  |     return new Promise((resolve) => setTimeout(resolve, timeMs)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const getLogger = () => { | ||||||
|  |     const records: any[] = []; | ||||||
|  |     const logger: LogProvider = () => ({ | ||||||
|  |         error(...args: any[]) { | ||||||
|  |             records.push(args); | ||||||
|  |         }, | ||||||
|  |         debug() {}, | ||||||
|  |         info() {}, | ||||||
|  |         warn() {}, | ||||||
|  |         fatal() {}, | ||||||
|  |     }); | ||||||
|  |     const getRecords = () => { | ||||||
|  |         return records; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return { logger, getRecords }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | let schedulerService: SchedulerService; | ||||||
|  | let getRecords; | ||||||
|  | 
 | ||||||
|  | beforeEach(() => { | ||||||
|  |     const config = createTestConfig(); | ||||||
|  |     const settingStore = new FakeSettingStore(); | ||||||
|  |     const settingService = new SettingService({ settingStore }, config, { | ||||||
|  |         storeEvent() {}, | ||||||
|  |     } as unknown as EventService); | ||||||
|  |     const maintenanceService = new MaintenanceService(config, settingService); | ||||||
|  |     const { logger, getRecords: getRecordsFn } = getLogger(); | ||||||
|  |     getRecords = getRecordsFn; | ||||||
|  | 
 | ||||||
|  |     schedulerService = new SchedulerService( | ||||||
|  |         logger, | ||||||
|  |         maintenanceService, | ||||||
|  |         config.eventBus, | ||||||
|  |     ); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Schedules job immediately', async () => { | ||||||
|  |     const job = jest.fn(); | ||||||
|  |     await schedulerService.schedule(job, 10, 'test-id'); | ||||||
|  | 
 | ||||||
|  |     expect(job).toBeCalledTimes(1); | ||||||
|  |     schedulerService.stop(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Can schedule a single regular job', async () => { | ||||||
|  |     const job = jest.fn(); | ||||||
|  |     await schedulerService.schedule(job, 50, 'test-id-3'); | ||||||
|  |     await ms(75); | ||||||
|  | 
 | ||||||
|  |     expect(job).toBeCalledTimes(2); | ||||||
|  |     schedulerService.stop(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Can schedule multiple jobs at the same interval', async () => { | ||||||
|  |     const job = jest.fn(); | ||||||
|  |     const anotherJob = jest.fn(); | ||||||
|  | 
 | ||||||
|  |     await schedulerService.schedule(job, 50, 'test-id-6'); | ||||||
|  |     await schedulerService.schedule(anotherJob, 50, 'test-id-7'); | ||||||
|  |     await ms(75); | ||||||
|  | 
 | ||||||
|  |     expect(job).toBeCalledTimes(2); | ||||||
|  |     expect(anotherJob).toBeCalledTimes(2); | ||||||
|  |     schedulerService.stop(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Can schedule multiple jobs at the different intervals', async () => { | ||||||
|  |     const job = jest.fn(); | ||||||
|  |     const anotherJob = jest.fn(); | ||||||
|  | 
 | ||||||
|  |     await schedulerService.schedule(job, 100, 'test-id-8'); | ||||||
|  |     await schedulerService.schedule(anotherJob, 200, 'test-id-9'); | ||||||
|  |     await ms(250); | ||||||
|  | 
 | ||||||
|  |     expect(job).toBeCalledTimes(3); | ||||||
|  |     expect(anotherJob).toBeCalledTimes(2); | ||||||
|  |     schedulerService.stop(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Can handle crash of a async job', async () => { | ||||||
|  |     const job = async () => { | ||||||
|  |         await Promise.reject('async reason'); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     await schedulerService.schedule(job, 50, 'test-id-10'); | ||||||
|  |     await ms(75); | ||||||
|  | 
 | ||||||
|  |     schedulerService.stop(); | ||||||
|  |     expect(getRecords()).toEqual([ | ||||||
|  |         ['scheduled job failed | id: test-id-10 | async reason'], | ||||||
|  |         ['scheduled job failed | id: test-id-10 | async reason'], | ||||||
|  |     ]); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Can handle crash of a sync job', async () => { | ||||||
|  |     const job = () => { | ||||||
|  |         throw new Error('sync reason'); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     await schedulerService.schedule(job, 50, 'test-id-11'); | ||||||
|  |     await ms(75); | ||||||
|  | 
 | ||||||
|  |     schedulerService.stop(); | ||||||
|  |     expect(getRecords()).toEqual([ | ||||||
|  |         ['scheduled job failed | id: test-id-11 | Error: sync reason'], | ||||||
|  |         ['scheduled job failed | id: test-id-11 | Error: sync reason'], | ||||||
|  |     ]); | ||||||
|  | }); | ||||||
| @ -25,7 +25,6 @@ export type IFlagKey = | |||||||
|     | 'customRootRolesKillSwitch' |     | 'customRootRolesKillSwitch' | ||||||
|     | 'privateProjects' |     | 'privateProjects' | ||||||
|     | 'disableMetrics' |     | 'disableMetrics' | ||||||
|     | 'useLastSeenRefactor' |  | ||||||
|     | 'banners' |     | 'banners' | ||||||
|     | 'featureSearchAPI' |     | 'featureSearchAPI' | ||||||
|     | 'featureSearchFrontend' |     | 'featureSearchFrontend' | ||||||
| @ -114,10 +113,6 @@ const flags: IFlags = { | |||||||
|         process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS, |         process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS, | ||||||
|         false, |         false, | ||||||
|     ), |     ), | ||||||
|     useLastSeenRefactor: parseEnvVarBoolean( |  | ||||||
|         process.env.UNLEASH_EXPERIMENTAL_USE_LAST_SEEN_REFACTOR, |  | ||||||
|         false, |  | ||||||
|     ), |  | ||||||
|     featureSearchAPI: parseEnvVarBoolean( |     featureSearchAPI: parseEnvVarBoolean( | ||||||
|         process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API, |         process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API, | ||||||
|         false, |         false, | ||||||
|  | |||||||
| @ -38,7 +38,6 @@ process.nextTick(async () => { | |||||||
|                         anonymiseEventLog: false, |                         anonymiseEventLog: false, | ||||||
|                         responseTimeWithAppNameKillSwitch: false, |                         responseTimeWithAppNameKillSwitch: false, | ||||||
|                         privateProjects: true, |                         privateProjects: true, | ||||||
|                         useLastSeenRefactor: true, |  | ||||||
|                         featureSearchAPI: true, |                         featureSearchAPI: true, | ||||||
|                         featureSearchFrontend: false, |                         featureSearchFrontend: false, | ||||||
|                     }, |                     }, | ||||||
|  | |||||||
| @ -77,14 +77,13 @@ test('returns three archived toggles', async () => { | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('returns three archived toggles with archivedAt', async () => { | test('returns three archived toggles with archivedAt', async () => { | ||||||
|     expect.assertions(3); |     expect.assertions(2); | ||||||
|     return app.request |     return app.request | ||||||
|         .get('/api/admin/archive/features') |         .get('/api/admin/archive/features') | ||||||
|         .expect('Content-Type', /json/) |         .expect('Content-Type', /json/) | ||||||
|         .expect(200) |         .expect(200) | ||||||
|         .expect((res) => { |         .expect((res) => { | ||||||
|             expect(res.body.features.length).toEqual(3); |             expect(res.body.features.length).toEqual(3); | ||||||
|             expect(res.body.features.every((f) => f.archived)).toEqual(true); |  | ||||||
|             expect(res.body.features.every((f) => f.archivedAt)).toEqual(true); |             expect(res.body.features.every((f) => f.archivedAt)).toEqual(true); | ||||||
|         }); |         }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ let app: IUnleashTest; | |||||||
| let db: ITestDb; | let db: ITestDb; | ||||||
| 
 | 
 | ||||||
| let projectStore: IProjectStore; | let projectStore: IProjectStore; | ||||||
|  | const testDate = '2023-10-01T12:34:56.000Z'; | ||||||
| 
 | 
 | ||||||
| beforeAll(async () => { | beforeAll(async () => { | ||||||
|     db = await dbInit('projects_api_serial', getLogger); |     db = await dbInit('projects_api_serial', getLogger); | ||||||
| @ -146,7 +147,12 @@ test('response for default project should include created_at', async () => { | |||||||
| test('response should include last seen at per environment', async () => { | test('response should include last seen at per environment', async () => { | ||||||
|     await app.createFeature('my-new-feature-toggle'); |     await app.createFeature('my-new-feature-toggle'); | ||||||
| 
 | 
 | ||||||
|     await insertLastSeenAt('my-new-feature-toggle', db.rawDatabase, 'default'); |     await insertLastSeenAt( | ||||||
|  |         'my-new-feature-toggle', | ||||||
|  |         db.rawDatabase, | ||||||
|  |         'default', | ||||||
|  |         testDate, | ||||||
|  |     ); | ||||||
|     await insertFeatureEnvironmentsLastSeen( |     await insertFeatureEnvironmentsLastSeen( | ||||||
|         'my-new-feature-toggle', |         'my-new-feature-toggle', | ||||||
|         db.rawDatabase, |         db.rawDatabase, | ||||||
| @ -158,19 +164,11 @@ test('response should include last seen at per environment', async () => { | |||||||
|         .expect('Content-Type', /json/) |         .expect('Content-Type', /json/) | ||||||
|         .expect(200); |         .expect(200); | ||||||
| 
 | 
 | ||||||
|     expect(body.features[0].environments[0].lastSeenAt).toEqual( |     expect(body.features[0].environments[0].lastSeenAt).toEqual(testDate); | ||||||
|         '2022-05-01T12:34:56.000Z', |  | ||||||
|     ); |  | ||||||
| 
 | 
 | ||||||
|     const appWithLastSeenRefactor = await setupAppWithCustomConfig( |     const appWithLastSeenRefactor = await setupAppWithCustomConfig( | ||||||
|         db.stores, |         db.stores, | ||||||
|         { |         {}, | ||||||
|             experimental: { |  | ||||||
|                 flags: { |  | ||||||
|                     useLastSeenRefactor: true, |  | ||||||
|                 }, |  | ||||||
|             }, |  | ||||||
|         }, |  | ||||||
|         db.rawDatabase, |         db.rawDatabase, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
| @ -187,13 +185,7 @@ test('response should include last seen at per environment', async () => { | |||||||
| test('response should include last seen at per environment for multiple environments', async () => { | test('response should include last seen at per environment for multiple environments', async () => { | ||||||
|     const appWithLastSeenRefactor = await setupAppWithCustomConfig( |     const appWithLastSeenRefactor = await setupAppWithCustomConfig( | ||||||
|         db.stores, |         db.stores, | ||||||
|         { |         {}, | ||||||
|             experimental: { |  | ||||||
|                 flags: { |  | ||||||
|                     useLastSeenRefactor: true, |  | ||||||
|                 }, |  | ||||||
|             }, |  | ||||||
|         }, |  | ||||||
|         db.rawDatabase, |         db.rawDatabase, | ||||||
|     ); |     ); | ||||||
|     await app.createFeature('my-new-feature-toggle'); |     await app.createFeature('my-new-feature-toggle'); | ||||||
|  | |||||||
| @ -1,143 +0,0 @@ | |||||||
| import { createTestConfig } from '../../config/test-config'; |  | ||||||
| import dbInit from '../helpers/database-init'; |  | ||||||
| import { IUnleashStores } from '../../../lib/types/stores'; |  | ||||||
| import { LastSeenService } from '../../../lib/services/client-metrics/last-seen/last-seen-service'; |  | ||||||
| import { IClientMetricsEnv } from '../../../lib/types/stores/client-metrics-store-v2'; |  | ||||||
| 
 |  | ||||||
| let stores: IUnleashStores; |  | ||||||
| let db; |  | ||||||
| let config; |  | ||||||
| 
 |  | ||||||
| beforeAll(async () => { |  | ||||||
|     config = createTestConfig(); |  | ||||||
|     db = await dbInit('last_seen_service_serial', config.getLogger); |  | ||||||
|     stores = db.stores; |  | ||||||
| }); |  | ||||||
| beforeEach(async () => { |  | ||||||
|     await stores.featureToggleStore.deleteAll(); |  | ||||||
| }); |  | ||||||
| afterAll(async () => { |  | ||||||
|     await db.destroy(); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('Should update last seen for known toggles', async () => { |  | ||||||
|     const service = new LastSeenService( |  | ||||||
|         { |  | ||||||
|             lastSeenStore: stores.lastSeenStore, |  | ||||||
|             featureToggleStore: stores.featureToggleStore, |  | ||||||
|         }, |  | ||||||
|         config, |  | ||||||
|     ); |  | ||||||
|     const time = Date.now() - 100; |  | ||||||
|     await stores.featureToggleStore.create('default', { name: 'ta1' }); |  | ||||||
| 
 |  | ||||||
|     const metrics: IClientMetricsEnv[] = [ |  | ||||||
|         { |  | ||||||
|             featureName: 'ta1', |  | ||||||
|             appName: 'some-App', |  | ||||||
|             environment: 'default', |  | ||||||
|             timestamp: new Date(time), |  | ||||||
|             yes: 1, |  | ||||||
|             no: 0, |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             featureName: 'ta2', |  | ||||||
|             appName: 'some-App', |  | ||||||
|             environment: 'default', |  | ||||||
|             timestamp: new Date(time), |  | ||||||
|             yes: 1, |  | ||||||
|             no: 0, |  | ||||||
|         }, |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     service.updateLastSeen(metrics); |  | ||||||
|     await service.store(); |  | ||||||
| 
 |  | ||||||
|     const t1 = await stores.featureToggleStore.get('ta1'); |  | ||||||
| 
 |  | ||||||
|     expect(t1.lastSeenAt.getTime()).toBeGreaterThan(time); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('Should not update last seen toggles with 0 metrics', async () => { |  | ||||||
|     // jest.useFakeTimers();
 |  | ||||||
|     const service = new LastSeenService( |  | ||||||
|         { |  | ||||||
|             lastSeenStore: stores.lastSeenStore, |  | ||||||
|             featureToggleStore: stores.featureToggleStore, |  | ||||||
|         }, |  | ||||||
|         config, |  | ||||||
|     ); |  | ||||||
|     const time = Date.now(); |  | ||||||
|     await stores.featureToggleStore.create('default', { name: 'tb1' }); |  | ||||||
|     await stores.featureToggleStore.create('default', { name: 'tb2' }); |  | ||||||
| 
 |  | ||||||
|     const metrics: IClientMetricsEnv[] = [ |  | ||||||
|         { |  | ||||||
|             featureName: 'tb1', |  | ||||||
|             appName: 'some-App', |  | ||||||
|             environment: 'default', |  | ||||||
|             timestamp: new Date(time), |  | ||||||
|             yes: 1, |  | ||||||
|             no: 0, |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             featureName: 'tb2', |  | ||||||
|             appName: 'some-App', |  | ||||||
|             environment: 'default', |  | ||||||
|             timestamp: new Date(time), |  | ||||||
|             yes: 0, |  | ||||||
|             no: 0, |  | ||||||
|         }, |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     service.updateLastSeen(metrics); |  | ||||||
| 
 |  | ||||||
|     // bypass interval waiting
 |  | ||||||
|     await service.store(); |  | ||||||
| 
 |  | ||||||
|     const t1 = await stores.featureToggleStore.get('tb1'); |  | ||||||
|     const t2 = await stores.featureToggleStore.get('tb2'); |  | ||||||
| 
 |  | ||||||
|     expect(t2.lastSeenAt).toBeNull(); |  | ||||||
|     expect(t1.lastSeenAt.getTime()).toBeGreaterThanOrEqual(time); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('Should not update anything for 0 toggles', async () => { |  | ||||||
|     // jest.useFakeTimers();
 |  | ||||||
|     const service = new LastSeenService( |  | ||||||
|         { |  | ||||||
|             lastSeenStore: stores.lastSeenStore, |  | ||||||
|             featureToggleStore: stores.featureToggleStore, |  | ||||||
|         }, |  | ||||||
|         config, |  | ||||||
|     ); |  | ||||||
|     const time = Date.now(); |  | ||||||
|     await stores.featureToggleStore.create('default', { name: 'tb1' }); |  | ||||||
|     await stores.featureToggleStore.create('default', { name: 'tb2' }); |  | ||||||
| 
 |  | ||||||
|     const metrics: IClientMetricsEnv[] = [ |  | ||||||
|         { |  | ||||||
|             featureName: 'tb1', |  | ||||||
|             appName: 'some-App', |  | ||||||
|             environment: 'default', |  | ||||||
|             timestamp: new Date(time), |  | ||||||
|             yes: 0, |  | ||||||
|             no: 0, |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             featureName: 'tb2', |  | ||||||
|             appName: 'some-App', |  | ||||||
|             environment: 'default', |  | ||||||
|             timestamp: new Date(time), |  | ||||||
|             yes: 0, |  | ||||||
|             no: 0, |  | ||||||
|         }, |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     service.updateLastSeen(metrics); |  | ||||||
| 
 |  | ||||||
|     // bypass interval waiting
 |  | ||||||
|     const count = await service.store(); |  | ||||||
| 
 |  | ||||||
|     expect(count).toBe(0); |  | ||||||
| }); |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user