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

feat: add variant etag (#8922)

## About the changes
This adds a variant that allows us to control client refreshes in case
of need.
This commit is contained in:
Gastón Fournier 2024-12-05 15:13:11 +01:00 committed by GitHub
parent ac1ba2f859
commit 04eaf8d5bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 210 additions and 144 deletions

View File

@ -285,9 +285,15 @@ export default class FeatureController extends Controller {
// TODO: We will need to standardize this to be able to implement this a cross languages (Edge in Rust?). // TODO: We will need to standardize this to be able to implement this a cross languages (Edge in Rust?).
const queryHash = hashSum(query); const queryHash = hashSum(query);
const etagVariant = this.flagResolver.getVariant('etagVariant');
if (etagVariant.feature_enabled && etagVariant.enabled) {
const etag = `"${queryHash}:${revisionId}:${etagVariant.name}"`;
return { revisionId, etag, queryHash };
} else {
const etag = `"${queryHash}:${revisionId}"`; const etag = `"${queryHash}:${revisionId}"`;
return { revisionId, etag, queryHash }; return { revisionId, etag, queryHash };
} }
}
async getFeatureToggle( async getFeatureToggle(
req: IAuthRequest<{ featureName: string }, ClientFeaturesQuerySchema>, req: IAuthRequest<{ featureName: string }, ClientFeaturesQuerySchema>,

View File

@ -54,6 +54,13 @@ beforeEach(async () => {
isEnabled: () => { isEnabled: () => {
return false; return false;
}, },
getVariant: () => {
return {
name: 'disabled',
feature_enabled: false,
enabled: false,
};
},
}; };
}); });

View File

@ -60,7 +60,8 @@ export type IFlagKey =
| 'deleteStaleUserSessions' | 'deleteStaleUserSessions'
| 'memorizeStats' | 'memorizeStats'
| 'licensedUsers' | 'licensedUsers'
| 'streaming'; | 'streaming'
| 'etagVariant';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -284,6 +285,11 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_STREAMING, process.env.UNLEASH_EXPERIMENTAL_STREAMING,
false, false,
), ),
etagVariant: {
name: 'disabled',
feature_enabled: false,
enabled: false,
},
}; };
export const defaultExperimentalOptions: IExperimentalOptions = { export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -8,28 +8,46 @@ import type User from '../../../../lib/types/user';
import { TEST_AUDIT_USER } from '../../../../lib/types'; import { TEST_AUDIT_USER } from '../../../../lib/types';
// import { DEFAULT_ENV } from '../../../../lib/util/constants'; // import { DEFAULT_ENV } from '../../../../lib/util/constants';
let app: IUnleashTest;
let db: ITestDb;
const testUser = { name: 'test', id: -9999 } as User; const testUser = { name: 'test', id: -9999 } as User;
beforeAll(async () => { const shutdownHooks: (() => Promise<void>)[] = [];
db = await dbInit('feature_304_api_client', getLogger);
describe.each([
{
name: 'disabled',
enabled: false,
feature_enabled: false,
},
{
name: 'v2',
enabled: true,
feature_enabled: true,
},
])('feature 304 api client (etag variant = %s)', (etagVariant) => {
let app: IUnleashTest;
let db: ITestDb;
const apendix = etagVariant.feature_enabled
? `${etagVariant.name}`
: 'etag_variant_off';
beforeAll(async () => {
db = await dbInit(`feature_304_api_client_${apendix}`, getLogger);
app = await setupAppWithCustomConfig( app = await setupAppWithCustomConfig(
db.stores, db.stores,
{ {
experimental: { experimental: {
flags: { flags: {
strictSchemaValidation: true, strictSchemaValidation: true,
optimal304: true, etagVariant: etagVariant,
}, },
}, },
}, },
db.rawDatabase, db.rawDatabase,
); );
await app.services.featureToggleService.createFeatureToggle( await app.services.featureToggleService.createFeatureToggle(
'default', 'default',
{ {
name: 'featureX', name: `featureX${apendix}`,
description: 'the #1 feature', description: 'the #1 feature',
impressionData: true, impressionData: true,
}, },
@ -38,7 +56,7 @@ beforeAll(async () => {
await app.services.featureToggleService.createFeatureToggle( await app.services.featureToggleService.createFeatureToggle(
'default', 'default',
{ {
name: 'featureY', name: `featureY${apendix}`,
description: 'soon to be the #1 feature', description: 'soon to be the #1 feature',
}, },
TEST_AUDIT_USER, TEST_AUDIT_USER,
@ -46,7 +64,7 @@ beforeAll(async () => {
await app.services.featureToggleService.createFeatureToggle( await app.services.featureToggleService.createFeatureToggle(
'default', 'default',
{ {
name: 'featureZ', name: `featureZ${apendix}`,
description: 'terrible feature', description: 'terrible feature',
}, },
TEST_AUDIT_USER, TEST_AUDIT_USER,
@ -54,14 +72,14 @@ beforeAll(async () => {
await app.services.featureToggleService.createFeatureToggle( await app.services.featureToggleService.createFeatureToggle(
'default', 'default',
{ {
name: 'featureArchivedX', name: `featureArchivedX${apendix}`,
description: 'the #1 feature', description: 'the #1 feature',
}, },
TEST_AUDIT_USER, TEST_AUDIT_USER,
); );
await app.services.featureToggleService.archiveToggle( await app.services.featureToggleService.archiveToggle(
'featureArchivedX', `featureArchivedX${apendix}`,
testUser, testUser,
TEST_AUDIT_USER, TEST_AUDIT_USER,
); );
@ -69,40 +87,40 @@ beforeAll(async () => {
await app.services.featureToggleService.createFeatureToggle( await app.services.featureToggleService.createFeatureToggle(
'default', 'default',
{ {
name: 'featureArchivedY', name: `featureArchivedY${apendix}`,
description: 'soon to be the #1 feature', description: 'soon to be the #1 feature',
}, },
TEST_AUDIT_USER, TEST_AUDIT_USER,
); );
await app.services.featureToggleService.archiveToggle( await app.services.featureToggleService.archiveToggle(
'featureArchivedY', `featureArchivedY${apendix}`,
testUser, testUser,
TEST_AUDIT_USER, TEST_AUDIT_USER,
); );
await app.services.featureToggleService.createFeatureToggle( await app.services.featureToggleService.createFeatureToggle(
'default', 'default',
{ {
name: 'featureArchivedZ', name: `featureArchivedZ${apendix}`,
description: 'terrible feature', description: 'terrible feature',
}, },
TEST_AUDIT_USER, TEST_AUDIT_USER,
); );
await app.services.featureToggleService.archiveToggle( await app.services.featureToggleService.archiveToggle(
'featureArchivedZ', `featureArchivedZ${apendix}`,
testUser, testUser,
TEST_AUDIT_USER, TEST_AUDIT_USER,
); );
await app.services.featureToggleService.createFeatureToggle( await app.services.featureToggleService.createFeatureToggle(
'default', 'default',
{ {
name: 'feature.with.variants', name: `feature.with.variants${apendix}`,
description: 'A feature flag with variants', description: 'A feature flag with variants',
}, },
TEST_AUDIT_USER, TEST_AUDIT_USER,
); );
await app.services.featureToggleService.saveVariants( await app.services.featureToggleService.saveVariants(
'feature.with.variants', `feature.with.variants${apendix}`,
'default', 'default',
[ [
{ {
@ -120,34 +138,49 @@ beforeAll(async () => {
], ],
TEST_AUDIT_USER, TEST_AUDIT_USER,
); );
});
afterAll(async () => { shutdownHooks.push(async () => {
await app.destroy(); await app.destroy();
await db.destroy(); await db.destroy();
}); });
});
test('returns calculated hash', async () => { test('returns calculated hash', async () => {
const res = await app.request const res = await app.request
.get('/api/client/features') .get('/api/client/features')
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200); .expect(200);
if (etagVariant.feature_enabled) {
expect(res.headers.etag).toBe(`"61824cd0:17:${etagVariant.name}"`);
expect(res.body.meta.etag).toBe(
`"61824cd0:17:${etagVariant.name}"`,
);
} else {
expect(res.headers.etag).toBe('"61824cd0:16"'); expect(res.headers.etag).toBe('"61824cd0:16"');
expect(res.body.meta.etag).toBe('"61824cd0:16"'); expect(res.body.meta.etag).toBe('"61824cd0:16"');
}); }
});
test('returns 304 for pre-calculated hash', async () => { test(`returns ${etagVariant.feature_enabled ? 200 : 304} for pre-calculated hash${etagVariant.feature_enabled ? ' because hash changed' : ''}`, async () => {
return app.request const res = await app.request
.get('/api/client/features') .get('/api/client/features')
.set('if-none-match', '"61824cd0:16"') .set('if-none-match', '"61824cd0:16"')
.expect(304); .expect(etagVariant.feature_enabled ? 200 : 304);
});
test('returns 200 when content updates and hash does not match anymore', async () => { if (etagVariant.feature_enabled) {
expect(res.headers.etag).toBe(`"61824cd0:17:${etagVariant.name}"`);
expect(res.body.meta.etag).toBe(
`"61824cd0:17:${etagVariant.name}"`,
);
}
});
test('returns 200 when content updates and hash does not match anymore', async () => {
await app.services.featureToggleService.createFeatureToggle( await app.services.featureToggleService.createFeatureToggle(
'default', 'default',
{ {
name: 'featureNew304', name: `featureNew304${apendix}`,
description: 'the #1 feature', description: 'the #1 feature',
}, },
TEST_AUDIT_USER, TEST_AUDIT_USER,
@ -159,6 +192,20 @@ test('returns 200 when content updates and hash does not match anymore', async (
.set('if-none-match', 'ae443048:16') .set('if-none-match', 'ae443048:16')
.expect(200); .expect(200);
if (etagVariant.feature_enabled) {
expect(res.headers.etag).toBe(`"61824cd0:17:${etagVariant.name}"`);
expect(res.body.meta.etag).toBe(
`"61824cd0:17:${etagVariant.name}"`,
);
} else {
expect(res.headers.etag).toBe('"61824cd0:17"'); expect(res.headers.etag).toBe('"61824cd0:17"');
expect(res.body.meta.etag).toBe('"61824cd0:17"'); expect(res.body.meta.etag).toBe('"61824cd0:17"');
}
});
});
// running after all inside describe block, causes some of the queries to fail to acquire a connection
// this workaround is to run the afterAll outside the describe block
afterAll(async () => {
await Promise.all(shutdownHooks.map((hook) => hook()));
}); });