1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-10 17:53:36 +02:00

fix: delta do not return archived as changed (#9062)

Our delta API was returning archived feature as updated. Now making sure
we do not put `archived-feature `event into `updated` event array.
Also stop returning removed as complex object.
This commit is contained in:
Jaanus Sellin 2025-01-06 14:48:30 +02:00 committed by GitHub
parent 13fb7c4a63
commit 8bf1b783e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 79 additions and 11 deletions

View File

@ -9,10 +9,8 @@ import type {
import type { Logger } from '../../logger'; import type { Logger } from '../../logger';
import type { FeatureConfigurationClient } from '../feature-toggle/types/feature-toggle-strategies-store-type'; import type { FeatureConfigurationClient } from '../feature-toggle/types/feature-toggle-strategies-store-type';
import type { import type { ClientFeatureToggleDelta } from './delta/client-feature-toggle-delta';
RevisionDeltaEntry, import type { ClientFeaturesDeltaSchema } from '../../openapi';
ClientFeatureToggleDelta,
} from './delta/client-feature-toggle-delta';
export class ClientFeatureToggleService { export class ClientFeatureToggleService {
private logger: Logger; private logger: Logger;
@ -44,7 +42,7 @@ export class ClientFeatureToggleService {
async getClientDelta( async getClientDelta(
revisionId: number | undefined, revisionId: number | undefined,
query: IFeatureToggleQuery, query: IFeatureToggleQuery,
): Promise<RevisionDeltaEntry | undefined> { ): Promise<ClientFeaturesDeltaSchema | undefined> {
if (this.clientFeatureToggleDelta !== null) { if (this.clientFeatureToggleDelta !== null) {
return this.clientFeatureToggleDelta.getDelta(revisionId, query); return this.clientFeatureToggleDelta.getDelta(revisionId, query);
} else { } else {

View File

@ -140,3 +140,37 @@ const syncRevisions = async () => {
// @ts-ignore // @ts-ignore
await app.services.clientFeatureToggleService.clientFeatureToggleDelta.onUpdateRevisionEvent(); await app.services.clientFeatureToggleService.clientFeatureToggleDelta.onUpdateRevisionEvent();
}; };
test('archived features should not be returned as updated', async () => {
await app.createFeature('base_feature');
await syncRevisions();
const { body } = await app.request.get('/api/client/delta').expect(200);
const currentRevisionId = body.revisionId;
expect(body).toMatchObject({
updated: [
{
name: 'base_feature',
},
],
});
await app.archiveFeature('base_feature');
await app.createFeature('new_feature');
await syncRevisions();
const { body: deltaBody } = await app.request
.get('/api/client/delta')
.set('If-None-Match', currentRevisionId)
.expect(200);
expect(deltaBody).toMatchObject({
updated: [
{
name: 'new_feature',
},
],
removed: ['base_feature'],
});
});

View File

@ -17,8 +17,10 @@ import type { OpenApiService } from '../../../services/openapi-service';
import { NONE } from '../../../types/permissions'; import { NONE } from '../../../types/permissions';
import { createResponseSchema } from '../../../openapi/util/create-response-schema'; import { createResponseSchema } from '../../../openapi/util/create-response-schema';
import type { ClientFeatureToggleService } from '../client-feature-toggle-service'; import type { ClientFeatureToggleService } from '../client-feature-toggle-service';
import type { RevisionDeltaEntry } from './client-feature-toggle-delta'; import {
import { clientFeaturesDeltaSchema } from '../../../openapi'; type ClientFeaturesDeltaSchema,
clientFeaturesDeltaSchema,
} from '../../../openapi';
import type { QueryOverride } from '../client-feature-toggle.controller'; import type { QueryOverride } from '../client-feature-toggle.controller';
export default class ClientFeatureToggleDeltaController extends Controller { export default class ClientFeatureToggleDeltaController extends Controller {
@ -75,7 +77,7 @@ export default class ClientFeatureToggleDeltaController extends Controller {
async getDelta( async getDelta(
req: IAuthRequest, req: IAuthRequest,
res: Response<RevisionDeltaEntry>, res: Response<ClientFeaturesDeltaSchema>,
): Promise<void> { ): Promise<void> {
if (!this.flagResolver.isEnabled('deltaApi')) { if (!this.flagResolver.isEnabled('deltaApi')) {
throw new NotFoundError(); throw new NotFoundError();

View File

@ -4,7 +4,7 @@ import type { FeatureConfigurationClient } from '../../feature-toggle/types/feat
export interface FeatureConfigurationDeltaClient export interface FeatureConfigurationDeltaClient
extends FeatureConfigurationClient { extends FeatureConfigurationClient {
description: string; description: string;
impressionData: false; impressionData: boolean;
} }
export interface IClientFeatureToggleDeltaReadModel { export interface IClientFeatureToggleDeltaReadModel {

View File

@ -17,6 +17,7 @@ import type {
import { CLIENT_DELTA_MEMORY } from '../../../metric-events'; import { CLIENT_DELTA_MEMORY } from '../../../metric-events';
import type EventEmitter from 'events'; import type EventEmitter from 'events';
import type { Logger } from '../../../logger'; import type { Logger } from '../../../logger';
import type { ClientFeaturesDeltaSchema } from '../../../openapi';
type DeletedFeature = { type DeletedFeature = {
name: string; name: string;
@ -79,6 +80,7 @@ const filterRevisionByProject = (
(feature) => (feature) =>
projects.includes('*') || projects.includes(feature.project), projects.includes('*') || projects.includes(feature.project),
); );
return { ...revision, updated, removed }; return { ...revision, updated, removed };
}; };
@ -153,7 +155,7 @@ export class ClientFeatureToggleDelta {
async getDelta( async getDelta(
sdkRevisionId: number | undefined, sdkRevisionId: number | undefined,
query: IFeatureToggleQuery, query: IFeatureToggleQuery,
): Promise<RevisionDeltaEntry | undefined> { ): Promise<ClientFeaturesDeltaSchema | undefined> {
const projects = query.project ? query.project : ['*']; const projects = query.project ? query.project : ['*'];
const environment = query.environment ? query.environment : 'default'; const environment = query.environment ? query.environment : 'default';
// TODO: filter by tags, what is namePrefix? anything else? // TODO: filter by tags, what is namePrefix? anything else?
@ -181,9 +183,10 @@ export class ClientFeatureToggleDelta {
projects, projects,
); );
const revisionResponse = { const revisionResponse: ClientFeaturesDeltaSchema = {
...compressedRevision, ...compressedRevision,
segments: this.segments, segments: this.segments,
removed: compressedRevision.removed.map((feature) => feature.name),
}; };
return Promise.resolve(revisionResponse); return Promise.resolve(revisionResponse);
@ -197,6 +200,9 @@ export class ClientFeatureToggleDelta {
} }
} }
/**
* This is used in client-feature-delta-api.e2e.test.ts, do not remove
*/
public resetDelta() { public resetDelta() {
this.delta = {}; this.delta = {};
} }
@ -217,6 +223,7 @@ export class ClientFeatureToggleDelta {
...new Set( ...new Set(
changeEvents changeEvents
.filter((event) => event.featureName) .filter((event) => event.featureName)
.filter((event) => event.type !== 'feature-archived')
.map((event) => event.featureName!), .map((event) => event.featureName!),
), ),
]; ];

View File

@ -0,0 +1,27 @@
import { validateSchema } from '../validate';
import type { ClientFeaturesDeltaSchema } from './client-features-delta-schema';
test('clientFeaturesDeltaSchema all fields', () => {
const data: ClientFeaturesDeltaSchema = {
revisionId: 6,
updated: [
{
impressionData: false,
enabled: false,
name: 'base_feature',
description: null,
project: 'default',
stale: false,
type: 'release',
variants: [],
strategies: [],
},
],
removed: [],
segments: [],
};
expect(
validateSchema('#/components/schemas/clientFeaturesDeltaSchema', data),
).toBeUndefined();
});