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 { Box, Button } from '@mui/material';
|
||||
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 { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useRecordUIErrorApi } from 'hooks/api/actions/useRecordUIErrorApi/useRecordUiErrorApi';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
window.global ||= window;
|
||||
|
||||
|
@ -133,6 +133,7 @@ exports[`should create default config 1`] = `
|
||||
"proPlanAutoCharge": false,
|
||||
"responseTimeWithAppNameKillSwitch": false,
|
||||
"scheduledConfigurationChanges": false,
|
||||
"sdkReporting": false,
|
||||
"showInactiveUsers": false,
|
||||
"strictSchemaValidation": 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 {
|
||||
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 =
|
||||
|
@ -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 (!sdk) {
|
||||
entry.sdks.push(getSdk(sdkParts));
|
||||
} else {
|
||||
sdk.versions.push(sdkParts[1]);
|
||||
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) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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([]);
|
||||
});
|
||||
|
@ -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(
|
||||
|
@ -49,6 +49,7 @@ process.nextTick(async () => {
|
||||
featureSearchFeedbackPosting: true,
|
||||
extendedUsageMetricsUI: true,
|
||||
executiveDashboard: true,
|
||||
sdkReporting: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user