1
0
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:
Tymoteusz Czech 2025-05-15 13:06:54 +02:00 committed by GitHub
parent 480689e828
commit 4d92d54f9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 44 additions and 29 deletions

View File

@ -71,12 +71,18 @@ export default class ClientInstanceStore implements IClientInstanceStore {
}
}
/**
* @deprecated
* `bulkUpsert` is beeing used instead. remove with `lastSeenBulkQuery` flag
*/
async setLastSeen({
appName,
instanceId,
environment,
clientIp,
}: INewClientInstance): Promise<void> {
const stopTimer = this.metricTimer('setLastSeen');
await this.db(TABLE)
.insert({
app_name: appName,
@ -90,14 +96,20 @@ export default class ClientInstanceStore implements IClientInstanceStore {
last_seen: new Date(),
client_ip: clientIp,
});
stopTimer();
}
async bulkUpsert(instances: INewClientInstance[]): Promise<void> {
const stopTimer = this.metricTimer('bulkUpsert');
const rows = instances.map(mapToDb);
await this.db(TABLE)
.insert(rows)
.onConflict(['app_name', 'instance_id', 'environment'])
.merge();
stopTimer();
}
async delete({

View File

@ -22,6 +22,7 @@ import type { FeatureLifecycleCompletedSchema } from '../../openapi/index.js';
import { FeatureLifecycleReadModel } from './feature-lifecycle-read-model.js';
import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type.js';
import { STAGE_ENTERED } from '../../metric-events.js';
import type ClientInstanceService from '../metrics/instance/instance-service.js';
let app: IUnleashTest;
let db: ITestDb;
@ -29,6 +30,7 @@ let featureLifecycleStore: IFeatureLifecycleStore;
let eventStore: IEventStore;
let eventBus: EventEmitter;
let featureLifecycleReadModel: IFeatureLifecycleReadModel;
let clientInstanceService: ClientInstanceService;
beforeAll(async () => {
db = await dbInit('feature_lifecycle', getLogger, {
@ -47,6 +49,7 @@ beforeAll(async () => {
eventBus = app.config.eventBus;
featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
featureLifecycleStore = db.stores.featureLifecycleStore;
clientInstanceService = app.services.clientInstanceService;
await app.request
.post(`/auth/demo/login`)
@ -62,6 +65,7 @@ afterAll(async () => {
});
beforeEach(async () => {
await clientInstanceService.bulkAdd(); // flush
await featureLifecycleStore.deleteAll();
});

View File

@ -100,12 +100,22 @@ export default class ClientInstanceService {
clientIp: string,
): Promise<void> {
const value = await clientMetricsSchema.validateAsync(data);
await this.clientInstanceStore.setLastSeen({
appName: value.appName,
instanceId: value.instanceId,
environment: value.environment,
clientIp: clientIp,
});
if (this.flagResolver.isEnabled('lastSeenBulkQuery')) {
this.seenClients[this.clientKey(value)] = {
appName: value.appName,
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 {

View File

@ -108,29 +108,6 @@ test('should accept client metrics with yes/no', () => {
.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', () => {
return request
.post('/api/client/metrics')

View File

@ -67,6 +67,7 @@ export type IFlagKey =
| 'featureLinks'
| 'projectLinkTemplates'
| 'reportUnknownFlags'
| 'lastSeenBulkQuery'
| 'newGettingStartedEmail';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -316,6 +317,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_REPORT_UNKNOWN_FLAGS,
false,
),
lastSeenBulkQuery: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_LAST_SEEN_BULK_QUERY,
false,
),
newGettingStartedEmail: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_NEW_GETTING_STARTED_EMAIL,
false,

View File

@ -18,6 +18,10 @@ export interface IClientInstanceStore
Pick<INewClientInstance, 'appName' | 'instanceId'>
> {
bulkUpsert(instances: INewClientInstance[]): Promise<void>;
/**
* @deprecated
* `bulkUpsert` is beeing used instead. remove with `lastSeenBulkQuery` flag
*/
setLastSeen(INewClientInstance): Promise<void>;
insert(details: INewClientInstance): Promise<void>;
getByAppName(appName: string): Promise<IClientInstance[]>;

View File

@ -268,6 +268,7 @@ test('should not return instances older than 24h', async () => {
.expect(202);
await app.services.clientMetricsServiceV2.bulkAdd();
await app.services.clientInstanceService.bulkAdd();
await db.stores.clientApplicationsStore.upsert({
appName: metrics.appName,

View File

@ -24,6 +24,7 @@ beforeAll(async () => {
});
afterEach(async () => {
await app.services.clientInstanceService.bulkAdd(); // flush
await Promise.all([
db.stores.clientMetricsStoreV2.deleteAll(),
db.stores.clientInstanceStore.deleteAll(),
@ -73,6 +74,7 @@ test('should create instance if does not exist', async () => {
.post('/api/client/metrics')
.send(metricsExample)
.expect(202);
await app.services.clientInstanceService.bulkAdd();
const finalInstances = await db.stores.clientInstanceStore.getAll();
expect(finalInstances.length).toBe(1);
});