mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-27 01:19:00 +02:00
feat: track last seen clients using bulk update (#9981)
Let's not update `lastSeen` in the db on each client call
This commit is contained in:
parent
480689e828
commit
4d92d54f9a
@ -71,12 +71,18 @@ export default class ClientInstanceStore implements IClientInstanceStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* `bulkUpsert` is beeing used instead. remove with `lastSeenBulkQuery` flag
|
||||||
|
*/
|
||||||
async setLastSeen({
|
async setLastSeen({
|
||||||
appName,
|
appName,
|
||||||
instanceId,
|
instanceId,
|
||||||
environment,
|
environment,
|
||||||
clientIp,
|
clientIp,
|
||||||
}: INewClientInstance): Promise<void> {
|
}: INewClientInstance): Promise<void> {
|
||||||
|
const stopTimer = this.metricTimer('setLastSeen');
|
||||||
|
|
||||||
await this.db(TABLE)
|
await this.db(TABLE)
|
||||||
.insert({
|
.insert({
|
||||||
app_name: appName,
|
app_name: appName,
|
||||||
@ -90,14 +96,20 @@ export default class ClientInstanceStore implements IClientInstanceStore {
|
|||||||
last_seen: new Date(),
|
last_seen: new Date(),
|
||||||
client_ip: clientIp,
|
client_ip: clientIp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stopTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkUpsert(instances: INewClientInstance[]): Promise<void> {
|
async bulkUpsert(instances: INewClientInstance[]): Promise<void> {
|
||||||
|
const stopTimer = this.metricTimer('bulkUpsert');
|
||||||
|
|
||||||
const rows = instances.map(mapToDb);
|
const rows = instances.map(mapToDb);
|
||||||
await this.db(TABLE)
|
await this.db(TABLE)
|
||||||
.insert(rows)
|
.insert(rows)
|
||||||
.onConflict(['app_name', 'instance_id', 'environment'])
|
.onConflict(['app_name', 'instance_id', 'environment'])
|
||||||
.merge();
|
.merge();
|
||||||
|
|
||||||
|
stopTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete({
|
async delete({
|
||||||
|
@ -22,6 +22,7 @@ import type { FeatureLifecycleCompletedSchema } from '../../openapi/index.js';
|
|||||||
import { FeatureLifecycleReadModel } from './feature-lifecycle-read-model.js';
|
import { FeatureLifecycleReadModel } from './feature-lifecycle-read-model.js';
|
||||||
import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type.js';
|
import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type.js';
|
||||||
import { STAGE_ENTERED } from '../../metric-events.js';
|
import { STAGE_ENTERED } from '../../metric-events.js';
|
||||||
|
import type ClientInstanceService from '../metrics/instance/instance-service.js';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
@ -29,6 +30,7 @@ let featureLifecycleStore: IFeatureLifecycleStore;
|
|||||||
let eventStore: IEventStore;
|
let eventStore: IEventStore;
|
||||||
let eventBus: EventEmitter;
|
let eventBus: EventEmitter;
|
||||||
let featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
let featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
||||||
|
let clientInstanceService: ClientInstanceService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('feature_lifecycle', getLogger, {
|
db = await dbInit('feature_lifecycle', getLogger, {
|
||||||
@ -47,6 +49,7 @@ beforeAll(async () => {
|
|||||||
eventBus = app.config.eventBus;
|
eventBus = app.config.eventBus;
|
||||||
featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
|
featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
|
||||||
featureLifecycleStore = db.stores.featureLifecycleStore;
|
featureLifecycleStore = db.stores.featureLifecycleStore;
|
||||||
|
clientInstanceService = app.services.clientInstanceService;
|
||||||
|
|
||||||
await app.request
|
await app.request
|
||||||
.post(`/auth/demo/login`)
|
.post(`/auth/demo/login`)
|
||||||
@ -62,6 +65,7 @@ afterAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
await clientInstanceService.bulkAdd(); // flush
|
||||||
await featureLifecycleStore.deleteAll();
|
await featureLifecycleStore.deleteAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -100,12 +100,22 @@ export default class ClientInstanceService {
|
|||||||
clientIp: string,
|
clientIp: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const value = await clientMetricsSchema.validateAsync(data);
|
const value = await clientMetricsSchema.validateAsync(data);
|
||||||
await this.clientInstanceStore.setLastSeen({
|
|
||||||
appName: value.appName,
|
if (this.flagResolver.isEnabled('lastSeenBulkQuery')) {
|
||||||
instanceId: value.instanceId,
|
this.seenClients[this.clientKey(value)] = {
|
||||||
environment: value.environment,
|
appName: value.appName,
|
||||||
clientIp: clientIp,
|
instanceId: value.instanceId,
|
||||||
});
|
environment: value.environment,
|
||||||
|
clientIp: clientIp,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
await this.clientInstanceStore.setLastSeen({
|
||||||
|
appName: value.appName,
|
||||||
|
instanceId: value.instanceId,
|
||||||
|
environment: value.environment,
|
||||||
|
clientIp: clientIp,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerFrontendClient(data: IFrontendClientApp): void {
|
public registerFrontendClient(data: IFrontendClientApp): void {
|
||||||
|
@ -108,29 +108,6 @@ test('should accept client metrics with yes/no', () => {
|
|||||||
.expect(202);
|
.expect(202);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should accept client metrics with yes/no with metricsV2', async () => {
|
|
||||||
const testRunner = await getSetup();
|
|
||||||
await testRunner.request
|
|
||||||
.post('/api/client/metrics')
|
|
||||||
.send({
|
|
||||||
appName: 'demo',
|
|
||||||
instanceId: '1',
|
|
||||||
bucket: {
|
|
||||||
start: Date.now(),
|
|
||||||
stop: Date.now(),
|
|
||||||
toggles: {
|
|
||||||
toggleA: {
|
|
||||||
yes: 200,
|
|
||||||
no: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.expect(202);
|
|
||||||
|
|
||||||
await testRunner.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should accept client metrics with variants', () => {
|
test('should accept client metrics with variants', () => {
|
||||||
return request
|
return request
|
||||||
.post('/api/client/metrics')
|
.post('/api/client/metrics')
|
||||||
|
@ -67,6 +67,7 @@ export type IFlagKey =
|
|||||||
| 'featureLinks'
|
| 'featureLinks'
|
||||||
| 'projectLinkTemplates'
|
| 'projectLinkTemplates'
|
||||||
| 'reportUnknownFlags'
|
| 'reportUnknownFlags'
|
||||||
|
| 'lastSeenBulkQuery'
|
||||||
| 'newGettingStartedEmail';
|
| 'newGettingStartedEmail';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
@ -316,6 +317,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_REPORT_UNKNOWN_FLAGS,
|
process.env.UNLEASH_EXPERIMENTAL_REPORT_UNKNOWN_FLAGS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
lastSeenBulkQuery: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_LAST_SEEN_BULK_QUERY,
|
||||||
|
false,
|
||||||
|
),
|
||||||
newGettingStartedEmail: parseEnvVarBoolean(
|
newGettingStartedEmail: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_NEW_GETTING_STARTED_EMAIL,
|
process.env.UNLEASH_EXPERIMENTAL_NEW_GETTING_STARTED_EMAIL,
|
||||||
false,
|
false,
|
||||||
|
@ -18,6 +18,10 @@ export interface IClientInstanceStore
|
|||||||
Pick<INewClientInstance, 'appName' | 'instanceId'>
|
Pick<INewClientInstance, 'appName' | 'instanceId'>
|
||||||
> {
|
> {
|
||||||
bulkUpsert(instances: INewClientInstance[]): Promise<void>;
|
bulkUpsert(instances: INewClientInstance[]): Promise<void>;
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* `bulkUpsert` is beeing used instead. remove with `lastSeenBulkQuery` flag
|
||||||
|
*/
|
||||||
setLastSeen(INewClientInstance): Promise<void>;
|
setLastSeen(INewClientInstance): Promise<void>;
|
||||||
insert(details: INewClientInstance): Promise<void>;
|
insert(details: INewClientInstance): Promise<void>;
|
||||||
getByAppName(appName: string): Promise<IClientInstance[]>;
|
getByAppName(appName: string): Promise<IClientInstance[]>;
|
||||||
|
@ -268,6 +268,7 @@ test('should not return instances older than 24h', async () => {
|
|||||||
.expect(202);
|
.expect(202);
|
||||||
|
|
||||||
await app.services.clientMetricsServiceV2.bulkAdd();
|
await app.services.clientMetricsServiceV2.bulkAdd();
|
||||||
|
await app.services.clientInstanceService.bulkAdd();
|
||||||
|
|
||||||
await db.stores.clientApplicationsStore.upsert({
|
await db.stores.clientApplicationsStore.upsert({
|
||||||
appName: metrics.appName,
|
appName: metrics.appName,
|
||||||
|
@ -24,6 +24,7 @@ beforeAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
await app.services.clientInstanceService.bulkAdd(); // flush
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.stores.clientMetricsStoreV2.deleteAll(),
|
db.stores.clientMetricsStoreV2.deleteAll(),
|
||||||
db.stores.clientInstanceStore.deleteAll(),
|
db.stores.clientInstanceStore.deleteAll(),
|
||||||
@ -73,6 +74,7 @@ test('should create instance if does not exist', async () => {
|
|||||||
.post('/api/client/metrics')
|
.post('/api/client/metrics')
|
||||||
.send(metricsExample)
|
.send(metricsExample)
|
||||||
.expect(202);
|
.expect(202);
|
||||||
|
await app.services.clientInstanceService.bulkAdd();
|
||||||
const finalInstances = await db.stores.clientInstanceStore.getAll();
|
const finalInstances = await db.stores.clientInstanceStore.getAll();
|
||||||
expect(finalInstances.length).toBe(1);
|
expect(finalInstances.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user