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:
parent
746dfe714a
commit
eb5d7a3788
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, VFC } from 'react';
|
import { useEffect, VFC } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Box, Button } from '@mui/material';
|
import { Box, Button } from '@mui/material';
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
@ -21,7 +21,6 @@ import { PlausibleProvider } from 'component/providers/PlausibleProvider/Plausib
|
|||||||
import { Error as LayoutError } from './component/layout/Error/Error';
|
import { Error as LayoutError } from './component/layout/Error/Error';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useRecordUIErrorApi } from 'hooks/api/actions/useRecordUIErrorApi/useRecordUiErrorApi';
|
import { useRecordUIErrorApi } from 'hooks/api/actions/useRecordUIErrorApi/useRecordUiErrorApi';
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
window.global ||= window;
|
window.global ||= window;
|
||||||
|
|
||||||
|
@ -133,6 +133,7 @@ exports[`should create default config 1`] = `
|
|||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
"scheduledConfigurationChanges": false,
|
"scheduledConfigurationChanges": false,
|
||||||
|
"sdkReporting": false,
|
||||||
"showInactiveUsers": false,
|
"showInactiveUsers": false,
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
"stripClientHeadersOn304": false,
|
"stripClientHeadersOn304": false,
|
||||||
|
179
src/lib/features/project/project-applications.e2e.test.ts
Normal file
179
src/lib/features/project/project-applications.e2e.test.ts
Normal 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: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
@ -2,6 +2,7 @@ import { Response } from 'express';
|
|||||||
import Controller from '../../routes/controller';
|
import Controller from '../../routes/controller';
|
||||||
import {
|
import {
|
||||||
IArchivedQuery,
|
IArchivedQuery,
|
||||||
|
IFlagResolver,
|
||||||
IProjectParam,
|
IProjectParam,
|
||||||
IUnleashConfig,
|
IUnleashConfig,
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
@ -36,6 +37,7 @@ import {
|
|||||||
projectApplicationsSchema,
|
projectApplicationsSchema,
|
||||||
ProjectApplicationsSchema,
|
ProjectApplicationsSchema,
|
||||||
} from '../../openapi/spec/project-applications-schema';
|
} from '../../openapi/spec/project-applications-schema';
|
||||||
|
import { NotFoundError } from '../../error';
|
||||||
|
|
||||||
export default class ProjectController extends Controller {
|
export default class ProjectController extends Controller {
|
||||||
private projectService: ProjectService;
|
private projectService: ProjectService;
|
||||||
@ -44,11 +46,14 @@ export default class ProjectController extends Controller {
|
|||||||
|
|
||||||
private openApiService: OpenApiService;
|
private openApiService: OpenApiService;
|
||||||
|
|
||||||
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
|
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
|
||||||
super(config);
|
super(config);
|
||||||
this.projectService = services.projectService;
|
this.projectService = services.projectService;
|
||||||
this.openApiService = services.openApiService;
|
this.openApiService = services.openApiService;
|
||||||
this.settingService = services.settingService;
|
this.settingService = services.settingService;
|
||||||
|
this.flagResolver = config.flagResolver;
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
path: '',
|
path: '',
|
||||||
@ -258,6 +263,10 @@ export default class ProjectController extends Controller {
|
|||||||
req: IAuthRequest,
|
req: IAuthRequest,
|
||||||
res: Response<ProjectApplicationsSchema>,
|
res: Response<ProjectApplicationsSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (!this.flagResolver.isEnabled('sdkReporting')) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
|
|
||||||
const applications =
|
const applications =
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
IProject,
|
IProject,
|
||||||
IProjectApplication,
|
IProjectApplication,
|
||||||
IProjectApplicationSdk,
|
|
||||||
IProjectUpdate,
|
IProjectUpdate,
|
||||||
IProjectWithCount,
|
IProjectWithCount,
|
||||||
ProjectMode,
|
ProjectMode,
|
||||||
@ -721,40 +720,48 @@ class ProjectStore implements IProjectStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAggregatedApplicationsData(rows): IProjectApplication[] {
|
getAggregatedApplicationsData(rows): IProjectApplication[] {
|
||||||
const entriesMap: Map<string, IProjectApplication> = new Map();
|
const entriesMap = new Map<string, IProjectApplication>();
|
||||||
const orderedEntries: IProjectApplication[] = [];
|
|
||||||
|
|
||||||
const getSdk = (sdkParts: string[]): IProjectApplicationSdk => {
|
|
||||||
return {
|
|
||||||
name: sdkParts[0],
|
|
||||||
versions: [sdkParts[1]],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
let entry = entriesMap.get(row.app_name);
|
const { app_name, environment, instance_id, sdk_version } = row;
|
||||||
const sdkParts = row.sdk_version.split(':');
|
let entry = entriesMap.get(app_name);
|
||||||
|
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
entry = {
|
entry = {
|
||||||
name: row.app_name,
|
name: app_name,
|
||||||
environments: [row.environment],
|
environments: [],
|
||||||
instances: [row.instance_id],
|
instances: [],
|
||||||
sdks: [getSdk(sdkParts)],
|
sdks: [],
|
||||||
};
|
};
|
||||||
entriesMap.set(row.feature_name, entry);
|
entriesMap.set(app_name, entry);
|
||||||
orderedEntries.push(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) {
|
if (!sdk) {
|
||||||
entry.sdks.push(getSdk(sdkParts));
|
sdk = { name: sdkName, versions: [] };
|
||||||
} else {
|
entry.sdks.push(sdk);
|
||||||
sdk.versions.push(sdkParts[1]);
|
}
|
||||||
|
|
||||||
|
if (sdkVersion && !sdk.versions.includes(sdkVersion)) {
|
||||||
|
sdk.versions.push(sdkVersion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return orderedEntries;
|
return Array.from(entriesMap.values());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ beforeAll(async () => {
|
|||||||
experimental: {
|
experimental: {
|
||||||
flags: {
|
flags: {
|
||||||
strictSchemaValidation: true,
|
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');
|
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([]);
|
|
||||||
});
|
|
||||||
|
@ -48,7 +48,8 @@ export type IFlagKey =
|
|||||||
| 'showInactiveUsers'
|
| 'showInactiveUsers'
|
||||||
| 'inMemoryScheduledChangeRequests'
|
| 'inMemoryScheduledChangeRequests'
|
||||||
| 'collectTrafficDataUsage'
|
| 'collectTrafficDataUsage'
|
||||||
| 'useMemoizedActiveTokens';
|
| 'useMemoizedActiveTokens'
|
||||||
|
| 'sdkReporting';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -204,6 +205,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_EXECUTIVE_DASHBOARD,
|
process.env.UNLEASH_EXPERIMENTAL_EXECUTIVE_DASHBOARD,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
sdkReporting: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_SDK_REPORTING,
|
||||||
|
false,
|
||||||
|
),
|
||||||
feedbackComments: {
|
feedbackComments: {
|
||||||
name: 'feedbackComments',
|
name: 'feedbackComments',
|
||||||
enabled: parseEnvVarBoolean(
|
enabled: parseEnvVarBoolean(
|
||||||
|
@ -49,6 +49,7 @@ process.nextTick(async () => {
|
|||||||
featureSearchFeedbackPosting: true,
|
featureSearchFeedbackPosting: true,
|
||||||
extendedUsageMetricsUI: true,
|
extendedUsageMetricsUI: true,
|
||||||
executiveDashboard: true,
|
executiveDashboard: true,
|
||||||
|
sdkReporting: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
Loading…
Reference in New Issue
Block a user