Skip to content

Commit

Permalink
feat: sdk reporting flag and e2e test (#6216)
Browse files Browse the repository at this point in the history
1. Add flag
2. Add e2e test with more complete example
3. Some bug fixes
  • Loading branch information
sjaanus committed Feb 13, 2024
1 parent 746dfe7 commit eb5d7a3
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 36 deletions.
2 changes: 1 addition & 1 deletion frontend/src/component/layout/Error/Error.tsx
@@ -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';
Expand Down
1 change: 0 additions & 1 deletion frontend/src/index.tsx
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions src/lib/__snapshots__/create-config.test.ts.snap
Expand Up @@ -133,6 +133,7 @@ exports[`should create default config 1`] = `
"proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false,
"scheduledConfigurationChanges": false,
"sdkReporting": false,
"showInactiveUsers": false,
"strictSchemaValidation": false,
"stripClientHeadersOn304": false,
Expand Down
179 changes: 179 additions & 0 deletions src/lib/features/project/project-applications.e2e.test.ts
@@ -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: [],
},
],
},
]);
});
9 changes: 9 additions & 0 deletions src/lib/features/project/project-controller.ts
Expand Up @@ -2,6 +2,7 @@ import { Response } from 'express';
import Controller from '../../routes/controller';
import {
IArchivedQuery,
IFlagResolver,
IProjectParam,
IUnleashConfig,
IUnleashServices,
Expand Down Expand Up @@ -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;
Expand All @@ -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: '',
Expand Down Expand Up @@ -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 =
Expand Down
55 changes: 31 additions & 24 deletions src/lib/features/project/project-store.ts
Expand Up @@ -7,7 +7,6 @@ import {
IFlagResolver,
IProject,
IProjectApplication,
IProjectApplicationSdk,
IProjectUpdate,
IProjectWithCount,
ProjectMode,
Expand Down Expand Up @@ -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());
}
}

Expand Down
10 changes: 1 addition & 9 deletions src/lib/features/project/projects.e2e.test.ts
Expand Up @@ -24,6 +24,7 @@ beforeAll(async () => {
experimental: {
flags: {
strictSchemaValidation: true,
sdkReporting: true,
},
},
},
Expand Down Expand Up @@ -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([]);
});
7 changes: 6 additions & 1 deletion src/lib/types/experimental.ts
Expand Up @@ -48,7 +48,8 @@ export type IFlagKey =
| 'showInactiveUsers'
| 'inMemoryScheduledChangeRequests'
| 'collectTrafficDataUsage'
| 'useMemoizedActiveTokens';
| 'useMemoizedActiveTokens'
| 'sdkReporting';

export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;

Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/server-dev.ts
Expand Up @@ -49,6 +49,7 @@ process.nextTick(async () => {
featureSearchFeedbackPosting: true,
extendedUsageMetricsUI: true,
executiveDashboard: true,
sdkReporting: true,
},
},
authentication: {
Expand Down

0 comments on commit eb5d7a3

Please sign in to comment.