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

feat: sdk reporting flag and e2e test (#6216)

1. Add flag
2. Add e2e test with more complete example
3. Some bug fixes
This commit is contained in:
Jaanus Sellin 2024-02-13 14:13:21 +02:00 committed by GitHub
parent 746dfe714a
commit eb5d7a3788
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 229 additions and 36 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect, VFC } from 'react';
import { useEffect, VFC } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Button } from '@mui/material';
import { Dialogue } from 'component/common/Dialogue/Dialogue';

View File

@ -21,7 +21,6 @@ import { PlausibleProvider } from 'component/providers/PlausibleProvider/Plausib
import { Error as LayoutError } from './component/layout/Error/Error';
import { ErrorBoundary } from 'react-error-boundary';
import { useRecordUIErrorApi } from 'hooks/api/actions/useRecordUIErrorApi/useRecordUiErrorApi';
import { useEffect } from 'react';
window.global ||= window;

View File

@ -133,6 +133,7 @@ exports[`should create default config 1`] = `
"proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false,
"scheduledConfigurationChanges": false,
"sdkReporting": false,
"showInactiveUsers": false,
"strictSchemaValidation": false,
"stripClientHeadersOn304": false,

View File

@ -0,0 +1,179 @@
import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init';
import {
IUnleashTest,
setupAppWithCustomConfig,
} from '../../../test/e2e/helpers/test-helper';
import getLogger from '../../../test/fixtures/no-logger';
import { ApiTokenType, IApiToken } from '../../types/models/api-token';
let app: IUnleashTest;
let db: ITestDb;
let defaultToken: IApiToken;
const metrics = {
appName: 'appName',
instanceId: 'instanceId',
bucket: {
start: '2016-11-03T07:16:43.572Z',
stop: '2016-11-03T07:16:53.572Z',
toggles: {
'toggle-name-1': {
yes: 123,
no: 321,
variants: {
'variant-1': 123,
'variant-2': 321,
},
},
},
},
};
beforeAll(async () => {
db = await dbInit('projects_applications_serial', getLogger);
app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
strictSchemaValidation: true,
sdkReporting: true,
},
},
},
db.rawDatabase,
);
defaultToken =
await app.services.apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
projects: ['default'],
environment: 'default',
tokenName: 'tester',
});
});
afterEach(async () => {
await db.stores.clientMetricsStoreV2.deleteAll();
await db.stores.clientInstanceStore.deleteAll();
await db.stores.featureToggleStore.deleteAll();
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
test('should return applications', async () => {
await app.createFeature('toggle-name-1');
await app.request.post('/api/client/register').send({
appName: metrics.appName,
instanceId: metrics.instanceId,
strategies: ['default'],
sdkVersion: 'unleash-client-test:1.2',
started: Date.now(),
interval: 10,
});
await app.services.clientInstanceService.bulkAdd();
await app.request
.post('/api/client/metrics')
.set('Authorization', defaultToken.secret)
.send(metrics)
.expect(202);
await app.services.clientMetricsServiceV2.bulkAdd();
const { body } = await app.request
.get('/api/admin/projects/default/applications')
.expect('Content-Type', /json/)
.expect(200);
expect(body).toMatchObject([
{
environments: ['default'],
instances: ['instanceId'],
name: 'appName',
sdks: [
{
name: 'unleash-client-test',
versions: ['1.2'],
},
],
},
]);
});
test('should return applications if sdk was not in database', async () => {
await app.createFeature('toggle-name-1');
await app.request.post('/api/client/register').send({
appName: metrics.appName,
instanceId: metrics.instanceId,
strategies: ['default'],
started: Date.now(),
interval: 10,
});
await app.services.clientInstanceService.bulkAdd();
await app.request
.post('/api/client/metrics')
.set('Authorization', defaultToken.secret)
.send(metrics)
.expect(202);
await app.services.clientMetricsServiceV2.bulkAdd();
const { body } = await app.request
.get('/api/admin/projects/default/applications')
.expect('Content-Type', /json/)
.expect(200);
expect(body).toMatchObject([
{
environments: ['default'],
instances: ['instanceId'],
name: 'appName',
sdks: [],
},
]);
});
test('should return application without version if sdk has just name', async () => {
await app.createFeature('toggle-name-1');
await app.request.post('/api/client/register').send({
appName: metrics.appName,
instanceId: metrics.instanceId,
strategies: ['default'],
sdkVersion: 'unleash-client-test',
started: Date.now(),
interval: 10,
});
await app.services.clientInstanceService.bulkAdd();
await app.request
.post('/api/client/metrics')
.set('Authorization', defaultToken.secret)
.send(metrics)
.expect(202);
await app.services.clientMetricsServiceV2.bulkAdd();
const { body } = await app.request
.get('/api/admin/projects/default/applications')
.expect('Content-Type', /json/)
.expect(200);
expect(body).toMatchObject([
{
environments: ['default'],
instances: ['instanceId'],
name: 'appName',
sdks: [
{
name: 'unleash-client-test',
versions: [],
},
],
},
]);
});

View File

@ -2,6 +2,7 @@ import { Response } from 'express';
import Controller from '../../routes/controller';
import {
IArchivedQuery,
IFlagResolver,
IProjectParam,
IUnleashConfig,
IUnleashServices,
@ -36,6 +37,7 @@ import {
projectApplicationsSchema,
ProjectApplicationsSchema,
} from '../../openapi/spec/project-applications-schema';
import { NotFoundError } from '../../error';
export default class ProjectController extends Controller {
private projectService: ProjectService;
@ -44,11 +46,14 @@ export default class ProjectController extends Controller {
private openApiService: OpenApiService;
private flagResolver: IFlagResolver;
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
super(config);
this.projectService = services.projectService;
this.openApiService = services.openApiService;
this.settingService = services.settingService;
this.flagResolver = config.flagResolver;
this.route({
path: '',
@ -258,6 +263,10 @@ export default class ProjectController extends Controller {
req: IAuthRequest,
res: Response<ProjectApplicationsSchema>,
): Promise<void> {
if (!this.flagResolver.isEnabled('sdkReporting')) {
throw new NotFoundError();
}
const { projectId } = req.params;
const applications =

View File

@ -7,7 +7,6 @@ import {
IFlagResolver,
IProject,
IProjectApplication,
IProjectApplicationSdk,
IProjectUpdate,
IProjectWithCount,
ProjectMode,
@ -721,40 +720,48 @@ class ProjectStore implements IProjectStore {
}
getAggregatedApplicationsData(rows): IProjectApplication[] {
const entriesMap: Map<string, IProjectApplication> = new Map();
const orderedEntries: IProjectApplication[] = [];
const getSdk = (sdkParts: string[]): IProjectApplicationSdk => {
return {
name: sdkParts[0],
versions: [sdkParts[1]],
};
};
const entriesMap = new Map<string, IProjectApplication>();
rows.forEach((row) => {
let entry = entriesMap.get(row.app_name);
const sdkParts = row.sdk_version.split(':');
const { app_name, environment, instance_id, sdk_version } = row;
let entry = entriesMap.get(app_name);
if (!entry) {
entry = {
name: row.app_name,
environments: [row.environment],
instances: [row.instance_id],
sdks: [getSdk(sdkParts)],
name: app_name,
environments: [],
instances: [],
sdks: [],
};
entriesMap.set(row.feature_name, entry);
orderedEntries.push(entry);
entriesMap.set(app_name, entry);
}
const sdk = entry.sdks.find((sdk) => sdk.name === sdkParts[0]);
if (!entry.environments.includes(environment)) {
entry.environments.push(environment);
}
if (!entry.instances.includes(instance_id)) {
entry.instances.push(instance_id);
}
if (sdk_version) {
const sdkParts = sdk_version.split(':');
const sdkName = sdkParts[0];
const sdkVersion = sdkParts[1] || '';
let sdk = entry.sdks.find((sdk) => sdk.name === sdkName);
if (!sdk) {
entry.sdks.push(getSdk(sdkParts));
} else {
sdk.versions.push(sdkParts[1]);
sdk = { name: sdkName, versions: [] };
entry.sdks.push(sdk);
}
if (sdkVersion && !sdk.versions.includes(sdkVersion)) {
sdk.versions.push(sdkVersion);
}
}
});
return orderedEntries;
return Array.from(entriesMap.values());
}
}

View File

@ -24,6 +24,7 @@ beforeAll(async () => {
experimental: {
flags: {
strictSchemaValidation: true,
sdkReporting: true,
},
},
},
@ -286,12 +287,3 @@ test('response should include last seen at per environment for multiple environm
expect(body.features[1].lastSeenAt).toBe('2023-10-01T12:34:56.000Z');
});
test('should return empty list of applications', async () => {
const { body } = await app.request
.get('/api/admin/projects/default/applications')
.expect('Content-Type', /json/)
.expect(200);
expect(body).toMatchObject([]);
});

View File

@ -48,7 +48,8 @@ export type IFlagKey =
| 'showInactiveUsers'
| 'inMemoryScheduledChangeRequests'
| 'collectTrafficDataUsage'
| 'useMemoizedActiveTokens';
| 'useMemoizedActiveTokens'
| 'sdkReporting';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -204,6 +205,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_EXECUTIVE_DASHBOARD,
false,
),
sdkReporting: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_SDK_REPORTING,
false,
),
feedbackComments: {
name: 'feedbackComments',
enabled: parseEnvVarBoolean(

View File

@ -49,6 +49,7 @@ process.nextTick(async () => {
featureSearchFeedbackPosting: true,
extendedUsageMetricsUI: true,
executiveDashboard: true,
sdkReporting: true,
},
},
authentication: {