2021-10-08 10:09:22 +02:00
|
|
|
import { Knex } from 'knex';
|
|
|
|
import { Logger, LogProvider } from '../logger';
|
|
|
|
import {
|
|
|
|
IClientMetricsEnv,
|
|
|
|
IClientMetricsEnvKey,
|
|
|
|
IClientMetricsStoreV2,
|
|
|
|
} from '../types/stores/client-metrics-store-v2';
|
|
|
|
import NotFoundError from '../error/notfound-error';
|
2021-10-26 20:13:30 +02:00
|
|
|
import { startOfHour } from 'date-fns';
|
2022-08-19 10:38:26 +02:00
|
|
|
import { collapseHourlyMetrics } from '../util/collapseHourlyMetrics';
|
2021-10-08 10:09:22 +02:00
|
|
|
|
|
|
|
interface ClientMetricsEnvTable {
|
|
|
|
feature_name: string;
|
|
|
|
app_name: string;
|
|
|
|
environment: string;
|
|
|
|
timestamp: Date;
|
|
|
|
yes: number;
|
|
|
|
no: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
const TABLE = 'client_metrics_env';
|
|
|
|
|
|
|
|
const fromRow = (row: ClientMetricsEnvTable) => ({
|
|
|
|
featureName: row.feature_name,
|
|
|
|
appName: row.app_name,
|
|
|
|
environment: row.environment,
|
|
|
|
timestamp: row.timestamp,
|
2022-01-31 08:50:11 +01:00
|
|
|
yes: Number(row.yes),
|
|
|
|
no: Number(row.no),
|
2021-10-08 10:09:22 +02:00
|
|
|
});
|
|
|
|
|
2022-08-19 10:38:26 +02:00
|
|
|
const toRow = (metric: IClientMetricsEnv): ClientMetricsEnvTable => ({
|
2021-10-08 10:09:22 +02:00
|
|
|
feature_name: metric.featureName,
|
|
|
|
app_name: metric.appName,
|
|
|
|
environment: metric.environment,
|
2021-10-26 20:13:30 +02:00
|
|
|
timestamp: startOfHour(metric.timestamp),
|
2021-10-08 10:09:22 +02:00
|
|
|
yes: metric.yes,
|
|
|
|
no: metric.no,
|
|
|
|
});
|
|
|
|
|
|
|
|
export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|
|
|
private db: Knex;
|
|
|
|
|
|
|
|
private logger: Logger;
|
|
|
|
|
|
|
|
constructor(db: Knex, getLogger: LogProvider) {
|
|
|
|
this.db = db;
|
|
|
|
this.logger = getLogger('client-metrics-store-v2.js');
|
|
|
|
}
|
|
|
|
|
|
|
|
async get(key: IClientMetricsEnvKey): Promise<IClientMetricsEnv> {
|
|
|
|
const row = await this.db<ClientMetricsEnvTable>(TABLE)
|
|
|
|
.where({
|
|
|
|
feature_name: key.featureName,
|
|
|
|
app_name: key.appName,
|
|
|
|
environment: key.environment,
|
2021-10-26 20:13:30 +02:00
|
|
|
timestamp: startOfHour(key.timestamp),
|
2021-10-08 10:09:22 +02:00
|
|
|
})
|
|
|
|
.first();
|
|
|
|
if (row) {
|
|
|
|
return fromRow(row);
|
|
|
|
}
|
|
|
|
throw new NotFoundError(`Could not find metric`);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getAll(query: Object = {}): Promise<IClientMetricsEnv[]> {
|
|
|
|
const rows = await this.db<ClientMetricsEnvTable>(TABLE)
|
|
|
|
.select('*')
|
|
|
|
.where(query);
|
|
|
|
return rows.map(fromRow);
|
|
|
|
}
|
|
|
|
|
|
|
|
async exists(key: IClientMetricsEnvKey): Promise<boolean> {
|
|
|
|
try {
|
|
|
|
await this.get(key);
|
|
|
|
return true;
|
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async delete(key: IClientMetricsEnvKey): Promise<void> {
|
|
|
|
return this.db<ClientMetricsEnvTable>(TABLE)
|
|
|
|
.where({
|
|
|
|
feature_name: key.featureName,
|
|
|
|
app_name: key.appName,
|
|
|
|
environment: key.environment,
|
2021-10-26 20:13:30 +02:00
|
|
|
timestamp: startOfHour(key.timestamp),
|
2021-10-08 10:09:22 +02:00
|
|
|
})
|
|
|
|
.del();
|
|
|
|
}
|
|
|
|
|
|
|
|
deleteAll(): Promise<void> {
|
|
|
|
return this.db(TABLE).del();
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy(): void {
|
|
|
|
// Nothing to do!
|
|
|
|
}
|
|
|
|
|
|
|
|
// this function will collapse metrics before sending it to the database.
|
|
|
|
async batchInsertMetrics(metrics: IClientMetricsEnv[]): Promise<void> {
|
|
|
|
if (!metrics || metrics.length == 0) {
|
|
|
|
return;
|
|
|
|
}
|
2022-08-19 10:38:26 +02:00
|
|
|
|
|
|
|
const rows = collapseHourlyMetrics(metrics).map(toRow);
|
2021-10-08 10:09:22 +02:00
|
|
|
|
2021-11-11 16:05:29 +01:00
|
|
|
// Sort the rows to avoid deadlocks
|
2022-08-19 10:38:26 +02:00
|
|
|
const sortedRows = rows.sort(
|
2021-11-11 16:05:29 +01:00
|
|
|
(a, b) =>
|
|
|
|
a.feature_name.localeCompare(b.feature_name) ||
|
|
|
|
a.app_name.localeCompare(b.app_name) ||
|
|
|
|
a.environment.localeCompare(b.environment),
|
|
|
|
);
|
|
|
|
|
2021-10-08 10:09:22 +02:00
|
|
|
// Consider rewriting to SQL batch!
|
|
|
|
const insert = this.db<ClientMetricsEnvTable>(TABLE)
|
2022-08-19 10:38:26 +02:00
|
|
|
.insert(sortedRows)
|
2021-10-08 10:09:22 +02:00
|
|
|
.toQuery();
|
|
|
|
|
|
|
|
const query = `${insert.toString()} ON CONFLICT (feature_name, app_name, environment, timestamp) DO UPDATE SET "yes" = "client_metrics_env"."yes" + EXCLUDED.yes, "no" = "client_metrics_env"."no" + EXCLUDED.no`;
|
|
|
|
await this.db.raw(query);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getMetricsForFeatureToggle(
|
|
|
|
featureName: string,
|
|
|
|
hoursBack: number = 24,
|
|
|
|
): Promise<IClientMetricsEnv[]> {
|
|
|
|
const rows = await this.db<ClientMetricsEnvTable>(TABLE)
|
|
|
|
.select('*')
|
|
|
|
.where({ feature_name: featureName })
|
|
|
|
.andWhereRaw(`timestamp >= NOW() - INTERVAL '${hoursBack} hours'`);
|
|
|
|
return rows.map(fromRow);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getSeenAppsForFeatureToggle(
|
|
|
|
featureName: string,
|
|
|
|
hoursBack: number = 24,
|
|
|
|
): Promise<string[]> {
|
|
|
|
return this.db<ClientMetricsEnvTable>(TABLE)
|
|
|
|
.distinct()
|
|
|
|
.where({ feature_name: featureName })
|
|
|
|
.andWhereRaw(`timestamp >= NOW() - INTERVAL '${hoursBack} hours'`)
|
|
|
|
.pluck('app_name')
|
|
|
|
.orderBy('app_name');
|
|
|
|
}
|
|
|
|
|
2021-12-09 21:02:58 +01:00
|
|
|
async getSeenTogglesForApp(
|
|
|
|
appName: string,
|
|
|
|
hoursBack: number = 24,
|
|
|
|
): Promise<string[]> {
|
|
|
|
return this.db<ClientMetricsEnvTable>(TABLE)
|
|
|
|
.distinct()
|
|
|
|
.where({ app_name: appName })
|
|
|
|
.andWhereRaw(`timestamp >= NOW() - INTERVAL '${hoursBack} hours'`)
|
|
|
|
.pluck('feature_name')
|
|
|
|
.orderBy('feature_name');
|
|
|
|
}
|
|
|
|
|
2021-10-08 10:09:22 +02:00
|
|
|
async clearMetrics(hoursAgo: number): Promise<void> {
|
|
|
|
return this.db<ClientMetricsEnvTable>(TABLE)
|
|
|
|
.whereRaw(`timestamp <= NOW() - INTERVAL '${hoursAgo} hours'`)
|
|
|
|
.del();
|
|
|
|
}
|
|
|
|
}
|