1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-27 01:19:00 +02:00

feat: bulk apps should respect multi projects and multi envs (#9879)

This commit is contained in:
Mateusz Kwasniewski 2025-05-02 10:12:41 +02:00 committed by GitHub
parent b0223e38ef
commit 1b9c0e5000
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 142 additions and 7 deletions

View File

@ -91,7 +91,7 @@ const reduceRows = (rows: any[]): IClientApplication[] => {
return Object.values(appsObj);
};
const remapRow = (input) => {
const remapRow = (input: Partial<IClientApplication>) => {
const temp = {
app_name: input.appName,
updated_at: input.updatedAt || new Date(),
@ -152,10 +152,28 @@ export default class ClientApplicationsStore
async bulkUpsert(apps: Partial<IClientApplication>[]): Promise<void> {
const rows = apps.map(remapRow);
const uniqueRows = Object.values(
rows.reduce((acc, row) => {
if (row.app_name) {
acc[row.app_name] = row;
}
return acc;
}, {}),
);
const usageRows = apps.flatMap(this.remapUsageRow);
await this.db(TABLE).insert(rows).onConflict('app_name').merge();
const uniqueUsageRows = Object.values(
usageRows.reduce((acc, row) => {
if (row.app_name) {
acc[`${row.app_name} ${row.project} ${row.environment}`] =
row;
}
return acc;
}, {}),
);
await this.db(TABLE).insert(uniqueRows).onConflict('app_name').merge();
await this.db(TABLE_USAGE)
.insert(usageRows)
.insert(uniqueUsageRows)
.onConflict(['app_name', 'project', 'environment'])
.merge();
}
@ -444,7 +462,7 @@ export default class ClientApplicationsStore
};
}
private remapUsageRow = (input) => {
private remapUsageRow = (input: Partial<IClientApplication>) => {
if (!input.projects || input.projects.length === 0) {
return [
{

View File

@ -173,8 +173,19 @@ export default class ClientInstanceService {
const uniqueRegistrations = Object.values(this.seenClients);
const uniqueApps: Partial<IClientApplication>[] = Object.values(
uniqueRegistrations.reduce((soFar, reg) => {
// eslint-disable-next-line no-param-reassign
soFar[reg.appName] = reg;
let existingProjects = [];
if (soFar[`${reg.appName} ${reg.environment}`]) {
existingProjects =
soFar[`${reg.appName} ${reg.environment}`]
.projects || [];
}
soFar[`${reg.appName} ${reg.environment}`] = {
...reg,
projects: [
...existingProjects,
...(reg.projects || []),
],
};
return soFar;
}, {}),
);

View File

@ -61,6 +61,7 @@ afterAll(async () => {
afterEach(async () => {
await stores.featureToggleStore.deleteAll();
await stores.clientApplicationsStore.deleteAll();
});
test('should validate client metrics', () => {
@ -125,7 +126,7 @@ test('should accept client metrics with yes/no with metricsV2', async () => {
})
.expect(202);
testRunner.destroy();
await testRunner.destroy();
});
test('should accept client metrics with variants', () => {
@ -343,6 +344,99 @@ describe('bulk metrics', () => {
});
});
test('should respect project from token', async () => {
const frontendApp: BulkRegistrationSchema = {
appName: 'application-name-token',
instanceId: 'browser',
environment: 'production',
sdkVersion: 'unleash-client-js:1.0.0',
sdkType: 'frontend',
projects: ['project-a', 'project-b'],
};
const backendApp: BulkRegistrationSchema = {
appName: 'application-name-token',
instanceId: 'instance1234',
environment: 'development',
sdkVersion: 'unleash-client-node',
sdkType: 'backend',
started: '1952-03-11T12:00:00.000Z',
interval: 15000,
projects: ['project-b', 'project-c'],
};
const defaultApp: BulkRegistrationSchema = {
appName: 'application-name-token',
instanceId: 'instance5678',
environment: 'development',
sdkVersion: 'unleash-client-java',
sdkType: null,
started: '1952-03-11T12:00:00.000Z',
interval: 15000,
projects: ['project-c', 'project-d'],
};
await request
.post('/api/client/metrics/bulk')
.send({
applications: [frontendApp, backendApp, defaultApp],
metrics: [],
})
.expect(202);
await services.clientInstanceService.bulkAdd();
const app = await services.clientInstanceService.getApplication(
'application-name-token',
);
expect(app).toMatchObject({
appName: 'application-name-token',
instances: [
{
instanceId: 'instance1234',
sdkVersion: 'unleash-client-node',
environment: 'development',
},
{
instanceId: 'instance5678',
sdkVersion: 'unleash-client-java',
environment: 'development',
},
{
instanceId: 'browser',
sdkVersion: 'unleash-client-js:1.0.0',
environment: 'production',
},
],
});
const applications =
await stores.clientApplicationsStore.getApplications({
limit: 10,
offset: 0,
sortBy: 'name',
sortOrder: 'asc',
});
expect(applications).toMatchObject({
applications: [
{
usage: [
{
project: 'project-a',
environments: ['production'],
},
{
project: 'project-b',
environments: ['production', 'development'],
},
{
project: 'project-c',
environments: ['development'],
},
{ project: 'project-d', environments: ['development'] },
],
},
],
});
});
test('filters out metrics for environments we do not have access for. No auth setup so we can only access default env', async () => {
const now = new Date();

View File

@ -158,6 +158,7 @@ export default class ClientMetricsController extends Controller {
environment: app.environment,
sdkType: app.sdkType,
sdkVersion: app.sdkVersion,
projects: app.projects,
});
}
} else {

View File

@ -64,6 +64,14 @@ export const bulkRegistrationSchema = {
type: 'string',
},
},
projects: {
description: 'The list of projects used in the application',
type: 'array',
example: ['projectA', 'projectB'],
items: {
type: 'string',
},
},
sdkVersion: {
description:
'The version the sdk is running. Typically <client>:<version>',

View File

@ -478,6 +478,7 @@ export interface IClientApp {
seenToggles?: string[];
metricsCount?: number;
strategies?: string[] | Record<string, string>[];
projects?: string[];
count?: number;
started?: string | number | Date;
interval?: number;

View File

@ -19,6 +19,8 @@ export interface IClientApplication {
icon: string;
strategies: string[];
usage?: IClientApplicationUsage[];
projects?: string[];
environment?: string;
}
export interface IClientApplications {