metrickit 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/cache-redis.d.ts +10 -0
- package/dist/cache-redis.js +24 -0
- package/dist/cache-utils.d.ts +5 -0
- package/dist/cache.d.ts +18 -0
- package/dist/catalog.d.ts +12 -0
- package/dist/define-metric.d.ts +37 -0
- package/dist/engine.d.ts +200 -0
- package/dist/filters/default-metadata.d.ts +28 -0
- package/dist/filters/index.d.ts +3 -0
- package/dist/filters/parse.d.ts +7 -0
- package/dist/filters/types.d.ts +19 -0
- package/dist/frontend/catalog.d.ts +24 -0
- package/dist/frontend/dashboard.d.ts +12 -0
- package/dist/frontend/format.d.ts +10 -0
- package/dist/frontend/index.d.ts +10 -0
- package/dist/frontend/markers.d.ts +19 -0
- package/dist/frontend/renderers.d.ts +14 -0
- package/dist/frontend/requests.d.ts +17 -0
- package/dist/frontend/stream-state.d.ts +14 -0
- package/dist/frontend/time.d.ts +5 -0
- package/dist/frontend/transport.d.ts +19 -0
- package/dist/frontend/types.d.ts +108 -0
- package/dist/frontend.d.ts +1 -0
- package/dist/frontend.js +752 -0
- package/dist/helpers/clickhouse.d.ts +27 -0
- package/dist/helpers/distribution.d.ts +15 -0
- package/dist/helpers/index.d.ts +6 -0
- package/dist/helpers/metric-type.d.ts +21 -0
- package/dist/helpers/pivot.d.ts +15 -0
- package/dist/helpers/prisma.d.ts +20 -0
- package/dist/helpers/timeseries.d.ts +6 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +668 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1322 -0
- package/dist/orpc.d.ts +36 -0
- package/dist/orpc.js +1157 -0
- package/dist/registry.d.ts +269 -0
- package/dist/run-metrics.d.ts +14 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/inputs.d.ts +19 -0
- package/dist/schemas/metric-type.d.ts +7 -0
- package/dist/schemas/output.d.ts +842 -0
- package/dist/schemas/time.d.ts +24 -0
- package/dist/time.d.ts +6 -0
- package/dist/type-guards.d.ts +7 -0
- package/package.json +91 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1322 @@
|
|
|
1
|
+
// src/cache-redis.ts
|
|
2
|
+
function redisCacheAdapter(client) {
|
|
3
|
+
return {
|
|
4
|
+
async mget(keys) {
|
|
5
|
+
if (keys.length === 0)
|
|
6
|
+
return [];
|
|
7
|
+
return client.mget(...keys);
|
|
8
|
+
},
|
|
9
|
+
async mset(entries) {
|
|
10
|
+
if (entries.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
const pipeline = client.pipeline();
|
|
13
|
+
for (const { key, value, ttl } of entries) {
|
|
14
|
+
if (ttl > 0) {
|
|
15
|
+
pipeline.setex(key, ttl, value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
await pipeline.exec();
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/cache.ts
|
|
24
|
+
var noopCacheAdapter = {
|
|
25
|
+
async mget(keys) {
|
|
26
|
+
return keys.map(() => null);
|
|
27
|
+
},
|
|
28
|
+
async mset() {}
|
|
29
|
+
};
|
|
30
|
+
function parseCache(cached, schema) {
|
|
31
|
+
if (!cached)
|
|
32
|
+
return null;
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(cached);
|
|
35
|
+
const result = schema.safeParse(parsed);
|
|
36
|
+
if (result.success) {
|
|
37
|
+
return result.data;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/schemas/time.ts
|
|
46
|
+
import { z } from "zod";
|
|
47
|
+
var TimeGranularitySchema = z.enum([
|
|
48
|
+
"hour",
|
|
49
|
+
"day",
|
|
50
|
+
"week",
|
|
51
|
+
"month",
|
|
52
|
+
"quarter",
|
|
53
|
+
"year"
|
|
54
|
+
]);
|
|
55
|
+
var TimeRangeSchema = z.object({
|
|
56
|
+
from: z.date().optional(),
|
|
57
|
+
to: z.date().optional()
|
|
58
|
+
});
|
|
59
|
+
var RequiredTimeRangeSchema = z.object({
|
|
60
|
+
from: z.date(),
|
|
61
|
+
to: z.date()
|
|
62
|
+
});
|
|
63
|
+
var CompareSchema = z.object({
|
|
64
|
+
compareToPrevious: z.boolean().optional().default(false)
|
|
65
|
+
});
|
|
66
|
+
// src/schemas/output.ts
|
|
67
|
+
import { z as z2 } from "zod";
|
|
68
|
+
var OutputKindSchema = z2.enum([
|
|
69
|
+
"kpi",
|
|
70
|
+
"timeseries",
|
|
71
|
+
"distribution",
|
|
72
|
+
"table",
|
|
73
|
+
"leaderboard",
|
|
74
|
+
"pivot"
|
|
75
|
+
]);
|
|
76
|
+
var MetricUnitSchema = z2.enum([
|
|
77
|
+
"DKK",
|
|
78
|
+
"EUR",
|
|
79
|
+
"USD",
|
|
80
|
+
"GBP",
|
|
81
|
+
"SEK",
|
|
82
|
+
"NOK",
|
|
83
|
+
"PERCENTAGE"
|
|
84
|
+
]);
|
|
85
|
+
var KpiOutputSchema = z2.object({
|
|
86
|
+
kind: z2.literal("kpi"),
|
|
87
|
+
value: z2.number(),
|
|
88
|
+
label: z2.string().optional(),
|
|
89
|
+
unit: MetricUnitSchema.optional(),
|
|
90
|
+
prefix: z2.string().optional(),
|
|
91
|
+
suffix: z2.string().optional(),
|
|
92
|
+
trend: z2.enum(["up", "down", "flat"]).optional()
|
|
93
|
+
});
|
|
94
|
+
function defineKpiOutput(output) {
|
|
95
|
+
return {
|
|
96
|
+
kind: "kpi",
|
|
97
|
+
...output
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
var TimeSeriesPointSchema = z2.object({
|
|
101
|
+
ts: z2.date(),
|
|
102
|
+
value: z2.number(),
|
|
103
|
+
label: z2.string().optional()
|
|
104
|
+
});
|
|
105
|
+
var TimeSeriesSeriesSchema = z2.object({
|
|
106
|
+
key: z2.string(),
|
|
107
|
+
label: z2.string().optional(),
|
|
108
|
+
points: z2.array(TimeSeriesPointSchema),
|
|
109
|
+
chartType: z2.enum(["line", "bar"]).optional(),
|
|
110
|
+
axis: z2.enum(["left", "right"]).optional(),
|
|
111
|
+
meta: z2.record(z2.string(), z2.unknown()).optional()
|
|
112
|
+
});
|
|
113
|
+
var TimeSeriesOutputSchema = z2.object({
|
|
114
|
+
kind: z2.literal("timeseries"),
|
|
115
|
+
granularity: TimeGranularitySchema,
|
|
116
|
+
series: z2.array(TimeSeriesSeriesSchema)
|
|
117
|
+
});
|
|
118
|
+
function defineTimeSeriesOutput(output) {
|
|
119
|
+
if (Array.isArray(output.series)) {
|
|
120
|
+
return {
|
|
121
|
+
kind: "timeseries",
|
|
122
|
+
...output,
|
|
123
|
+
series: [...output.series]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
kind: "timeseries",
|
|
128
|
+
...output,
|
|
129
|
+
series: Object.entries(output.series).map(([key, series]) => ({
|
|
130
|
+
key,
|
|
131
|
+
...series
|
|
132
|
+
}))
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
var DistributionSegmentSchema = z2.object({
|
|
136
|
+
key: z2.string(),
|
|
137
|
+
label: z2.string(),
|
|
138
|
+
value: z2.number(),
|
|
139
|
+
percent: z2.number().optional()
|
|
140
|
+
});
|
|
141
|
+
var DistributionChartTypeSchema = z2.enum([
|
|
142
|
+
"bar",
|
|
143
|
+
"donut",
|
|
144
|
+
"pie",
|
|
145
|
+
"funnel"
|
|
146
|
+
]);
|
|
147
|
+
var DistributionOutputSchema = z2.object({
|
|
148
|
+
kind: z2.literal("distribution"),
|
|
149
|
+
total: z2.number(),
|
|
150
|
+
segments: z2.array(DistributionSegmentSchema),
|
|
151
|
+
chartType: DistributionChartTypeSchema.optional()
|
|
152
|
+
});
|
|
153
|
+
function defineDistributionOutput(output) {
|
|
154
|
+
return {
|
|
155
|
+
kind: "distribution",
|
|
156
|
+
...output,
|
|
157
|
+
segments: [...output.segments]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
var TableColumnSchema = z2.object({
|
|
161
|
+
key: z2.string(),
|
|
162
|
+
label: z2.string(),
|
|
163
|
+
type: z2.enum(["string", "number", "date", "boolean"]).optional(),
|
|
164
|
+
nullable: z2.boolean().optional()
|
|
165
|
+
});
|
|
166
|
+
var TableOutputSchema = z2.object({
|
|
167
|
+
kind: z2.literal("table"),
|
|
168
|
+
columns: z2.array(TableColumnSchema),
|
|
169
|
+
rows: z2.array(z2.record(z2.string(), z2.unknown())),
|
|
170
|
+
total: z2.number().optional()
|
|
171
|
+
});
|
|
172
|
+
function defineTableOutput(output) {
|
|
173
|
+
return {
|
|
174
|
+
kind: "table",
|
|
175
|
+
...output,
|
|
176
|
+
columns: [...output.columns],
|
|
177
|
+
rows: [...output.rows]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
var PivotDimensionSchema = z2.object({
|
|
181
|
+
key: z2.string(),
|
|
182
|
+
label: z2.string()
|
|
183
|
+
});
|
|
184
|
+
var PivotTotalsSchema = z2.object({
|
|
185
|
+
rowTotals: z2.array(z2.number()).optional(),
|
|
186
|
+
columnTotals: z2.array(z2.number()).optional(),
|
|
187
|
+
grandTotal: z2.number().optional()
|
|
188
|
+
});
|
|
189
|
+
function addGrandTotalMismatchIssue(ctx, totalsName, totals, grandTotal) {
|
|
190
|
+
const totalsSum = totals?.reduce((total, value) => total + value, 0);
|
|
191
|
+
if (totalsSum === undefined)
|
|
192
|
+
return;
|
|
193
|
+
if (Math.abs(totalsSum - grandTotal) <= Number.EPSILON)
|
|
194
|
+
return;
|
|
195
|
+
ctx.addIssue({
|
|
196
|
+
code: z2.ZodIssueCode.custom,
|
|
197
|
+
path: ["totals", "grandTotal"],
|
|
198
|
+
message: `grandTotal must equal the sum of ${totalsName}`
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function clonePivotTotals(totals) {
|
|
202
|
+
if (!totals)
|
|
203
|
+
return;
|
|
204
|
+
return {
|
|
205
|
+
rowTotals: totals.rowTotals ? [...totals.rowTotals] : undefined,
|
|
206
|
+
columnTotals: totals.columnTotals ? [...totals.columnTotals] : undefined,
|
|
207
|
+
grandTotal: totals.grandTotal
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function clonePivotCellTooltips(cellTooltips) {
|
|
211
|
+
if (!cellTooltips)
|
|
212
|
+
return;
|
|
213
|
+
return cellTooltips.map((row) => [...row]);
|
|
214
|
+
}
|
|
215
|
+
var PivotOutputSchema = z2.object({
|
|
216
|
+
kind: z2.literal("pivot"),
|
|
217
|
+
rowDimension: PivotDimensionSchema,
|
|
218
|
+
columnDimension: PivotDimensionSchema,
|
|
219
|
+
rows: z2.array(z2.string()),
|
|
220
|
+
columns: z2.array(z2.string()),
|
|
221
|
+
values: z2.array(z2.array(z2.number())),
|
|
222
|
+
cellTooltips: z2.array(z2.array(z2.string())).optional(),
|
|
223
|
+
totals: PivotTotalsSchema.optional()
|
|
224
|
+
}).superRefine((pivot, ctx) => {
|
|
225
|
+
const rowCount = pivot.rows.length;
|
|
226
|
+
const columnCount = pivot.columns.length;
|
|
227
|
+
if (pivot.values.length !== rowCount) {
|
|
228
|
+
ctx.addIssue({
|
|
229
|
+
code: z2.ZodIssueCode.custom,
|
|
230
|
+
path: ["values"],
|
|
231
|
+
message: `Pivot values must have ${rowCount} rows`
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
for (const [rowIndex, row] of pivot.values.entries()) {
|
|
235
|
+
if (row.length !== columnCount) {
|
|
236
|
+
ctx.addIssue({
|
|
237
|
+
code: z2.ZodIssueCode.custom,
|
|
238
|
+
path: ["values", rowIndex],
|
|
239
|
+
message: `Pivot row ${rowIndex} must have ${columnCount} columns`
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (pivot.cellTooltips !== undefined) {
|
|
244
|
+
if (pivot.cellTooltips.length !== rowCount) {
|
|
245
|
+
ctx.addIssue({
|
|
246
|
+
code: z2.ZodIssueCode.custom,
|
|
247
|
+
path: ["cellTooltips"],
|
|
248
|
+
message: `cellTooltips must have ${rowCount} rows`
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
for (const [rowIndex, row] of pivot.cellTooltips.entries()) {
|
|
252
|
+
if (row.length !== columnCount) {
|
|
253
|
+
ctx.addIssue({
|
|
254
|
+
code: z2.ZodIssueCode.custom,
|
|
255
|
+
path: ["cellTooltips", rowIndex],
|
|
256
|
+
message: `cellTooltips row ${rowIndex} must have ${columnCount} columns`
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (pivot.totals?.rowTotals && pivot.totals.rowTotals.length !== rowCount) {
|
|
262
|
+
ctx.addIssue({
|
|
263
|
+
code: z2.ZodIssueCode.custom,
|
|
264
|
+
path: ["totals", "rowTotals"],
|
|
265
|
+
message: `rowTotals must have ${rowCount} entries`
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (pivot.totals?.columnTotals && pivot.totals.columnTotals.length !== columnCount) {
|
|
269
|
+
ctx.addIssue({
|
|
270
|
+
code: z2.ZodIssueCode.custom,
|
|
271
|
+
path: ["totals", "columnTotals"],
|
|
272
|
+
message: `columnTotals must have ${columnCount} entries`
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (pivot.totals?.grandTotal !== undefined) {
|
|
276
|
+
addGrandTotalMismatchIssue(ctx, "rowTotals", pivot.totals.rowTotals, pivot.totals.grandTotal);
|
|
277
|
+
addGrandTotalMismatchIssue(ctx, "columnTotals", pivot.totals.columnTotals, pivot.totals.grandTotal);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
function definePivotOutput(output) {
|
|
281
|
+
return {
|
|
282
|
+
kind: "pivot",
|
|
283
|
+
...output,
|
|
284
|
+
rows: [...output.rows],
|
|
285
|
+
columns: [...output.columns],
|
|
286
|
+
values: output.values.map((row) => [...row]),
|
|
287
|
+
cellTooltips: clonePivotCellTooltips(output.cellTooltips),
|
|
288
|
+
totals: clonePivotTotals(output.totals)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
var LeaderboardItemSchema = z2.object({
|
|
292
|
+
rank: z2.number(),
|
|
293
|
+
id: z2.string(),
|
|
294
|
+
label: z2.string(),
|
|
295
|
+
value: z2.number(),
|
|
296
|
+
meta: z2.record(z2.string(), z2.unknown()).optional()
|
|
297
|
+
});
|
|
298
|
+
var LeaderboardOutputSchema = z2.object({
|
|
299
|
+
kind: z2.literal("leaderboard"),
|
|
300
|
+
items: z2.array(LeaderboardItemSchema),
|
|
301
|
+
total: z2.number().optional()
|
|
302
|
+
});
|
|
303
|
+
function defineLeaderboardOutput(output) {
|
|
304
|
+
return {
|
|
305
|
+
kind: "leaderboard",
|
|
306
|
+
...output,
|
|
307
|
+
items: [...output.items]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function defineMetricOutput(kind, output) {
|
|
311
|
+
const handlers = {
|
|
312
|
+
kpi: () => defineKpiOutput(output),
|
|
313
|
+
timeseries: () => defineTimeSeriesOutput(output),
|
|
314
|
+
distribution: () => defineDistributionOutput(output),
|
|
315
|
+
table: () => defineTableOutput(output),
|
|
316
|
+
leaderboard: () => defineLeaderboardOutput(output),
|
|
317
|
+
pivot: () => definePivotOutput(output)
|
|
318
|
+
};
|
|
319
|
+
return handlers[kind]();
|
|
320
|
+
}
|
|
321
|
+
function validateOutput(kind, output) {
|
|
322
|
+
return OutputSchemaMap[kind].parse(output);
|
|
323
|
+
}
|
|
324
|
+
var MetricOutputSchema = z2.discriminatedUnion("kind", [
|
|
325
|
+
KpiOutputSchema,
|
|
326
|
+
TimeSeriesOutputSchema,
|
|
327
|
+
DistributionOutputSchema,
|
|
328
|
+
TableOutputSchema,
|
|
329
|
+
LeaderboardOutputSchema,
|
|
330
|
+
PivotOutputSchema
|
|
331
|
+
]);
|
|
332
|
+
var OutputSchemaMap = {
|
|
333
|
+
kpi: KpiOutputSchema,
|
|
334
|
+
timeseries: TimeSeriesOutputSchema,
|
|
335
|
+
distribution: DistributionOutputSchema,
|
|
336
|
+
table: TableOutputSchema,
|
|
337
|
+
leaderboard: LeaderboardOutputSchema,
|
|
338
|
+
pivot: PivotOutputSchema
|
|
339
|
+
};
|
|
340
|
+
var MetricExecutionCacheStatusSchema = z2.enum([
|
|
341
|
+
"hit",
|
|
342
|
+
"partialHit",
|
|
343
|
+
"miss",
|
|
344
|
+
"bypassed"
|
|
345
|
+
]);
|
|
346
|
+
var MetricExecutionSchema = z2.object({
|
|
347
|
+
cacheStatus: MetricExecutionCacheStatusSchema,
|
|
348
|
+
durationMs: z2.number().nonnegative(),
|
|
349
|
+
granularity: TimeGranularitySchema.optional()
|
|
350
|
+
});
|
|
351
|
+
var MetricResultSchema = z2.object({
|
|
352
|
+
current: MetricOutputSchema,
|
|
353
|
+
previous: MetricOutputSchema.optional(),
|
|
354
|
+
supportsTimeRange: z2.boolean(),
|
|
355
|
+
execution: MetricExecutionSchema.optional()
|
|
356
|
+
});
|
|
357
|
+
// src/schemas/inputs.ts
|
|
358
|
+
import { z as z3 } from "zod";
|
|
359
|
+
var BaseFiltersSchema = z3.object({
|
|
360
|
+
organizationIds: z3.array(z3.string()).optional()
|
|
361
|
+
}).merge(TimeRangeSchema);
|
|
362
|
+
var TimeSeriesFiltersSchema = z3.object({
|
|
363
|
+
organizationIds: z3.array(z3.string()).optional()
|
|
364
|
+
}).merge(RequiredTimeRangeSchema);
|
|
365
|
+
function extendBaseFilters(shape) {
|
|
366
|
+
const optionalShape = Object.fromEntries(Object.entries(shape).map(([key, schema]) => [
|
|
367
|
+
key,
|
|
368
|
+
schema.optional()
|
|
369
|
+
]));
|
|
370
|
+
return BaseFiltersSchema.extend(optionalShape);
|
|
371
|
+
}
|
|
372
|
+
function extendTimeSeriesFilters(shape) {
|
|
373
|
+
const optionalShape = Object.fromEntries(Object.entries(shape).map(([key, schema]) => [
|
|
374
|
+
key,
|
|
375
|
+
schema.optional()
|
|
376
|
+
]));
|
|
377
|
+
return TimeSeriesFiltersSchema.extend(optionalShape);
|
|
378
|
+
}
|
|
379
|
+
// src/schemas/metric-type.ts
|
|
380
|
+
import { z as z4 } from "zod";
|
|
381
|
+
var MetricTypeSchema = z4.enum(["TOTAL", "AVG", "PER_BUCKET"]);
|
|
382
|
+
// src/define-metric.ts
|
|
383
|
+
function defineMetricWithSchema(kind, outputSchema, def) {
|
|
384
|
+
return {
|
|
385
|
+
kind,
|
|
386
|
+
outputSchema,
|
|
387
|
+
...def
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function defineKpiMetric(def) {
|
|
391
|
+
return defineMetricWithSchema("kpi", KpiOutputSchema, def);
|
|
392
|
+
}
|
|
393
|
+
function defineTimeSeriesMetric(def) {
|
|
394
|
+
return defineMetricWithSchema("timeseries", TimeSeriesOutputSchema, def);
|
|
395
|
+
}
|
|
396
|
+
function defineDistributionMetric(def) {
|
|
397
|
+
return defineMetricWithSchema("distribution", DistributionOutputSchema, def);
|
|
398
|
+
}
|
|
399
|
+
function defineTableMetric(def) {
|
|
400
|
+
return defineMetricWithSchema("table", TableOutputSchema, def);
|
|
401
|
+
}
|
|
402
|
+
function defineLeaderboardMetric(def) {
|
|
403
|
+
return defineMetricWithSchema("leaderboard", LeaderboardOutputSchema, def);
|
|
404
|
+
}
|
|
405
|
+
function definePivotMetric(def) {
|
|
406
|
+
return defineMetricWithSchema("pivot", PivotOutputSchema, def);
|
|
407
|
+
}
|
|
408
|
+
function getOutputSchema(metric) {
|
|
409
|
+
return metric.outputSchema;
|
|
410
|
+
}
|
|
411
|
+
function validateMetricOutput(metric, output2) {
|
|
412
|
+
return getOutputSchema(metric).parse(output2);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/registry.ts
|
|
416
|
+
import { z as z5 } from "zod";
|
|
417
|
+
function createRegistry(metrics, options) {
|
|
418
|
+
const metricKeys = metrics.map((m) => m.key);
|
|
419
|
+
const MetricKeySchema = z5.enum(metricKeys);
|
|
420
|
+
const MetricRequestSchema = z5.object({
|
|
421
|
+
key: MetricKeySchema,
|
|
422
|
+
requestKey: z5.string().optional(),
|
|
423
|
+
filters: z5.record(z5.string(), z5.unknown()).optional()
|
|
424
|
+
});
|
|
425
|
+
const MetricsRequestSchema = z5.object({
|
|
426
|
+
metrics: z5.array(MetricRequestSchema).min(1, "At least one metric required"),
|
|
427
|
+
granularity: TimeGranularitySchema.optional(),
|
|
428
|
+
from: z5.date().optional(),
|
|
429
|
+
to: z5.date().optional(),
|
|
430
|
+
compareToPrevious: z5.boolean().optional().default(false),
|
|
431
|
+
disableCache: z5.boolean().optional().default(false)
|
|
432
|
+
});
|
|
433
|
+
const metricsByKey = Object.fromEntries(metrics.map((metric) => [metric.key, metric]));
|
|
434
|
+
const baseFilterSchema = options?.baseFilterSchema ?? BaseFiltersSchema;
|
|
435
|
+
const kindSchemas = options?.kindSchemas ?? OutputSchemaMap;
|
|
436
|
+
function getMetricByKey(key) {
|
|
437
|
+
return metricsByKey[key];
|
|
438
|
+
}
|
|
439
|
+
function parseMetricRequestInput(metricRequest, options2) {
|
|
440
|
+
const metric = getMetricByKey(metricRequest.key);
|
|
441
|
+
if (!metric) {
|
|
442
|
+
throw new Error(`Unknown metric: ${metricRequest.key}`);
|
|
443
|
+
}
|
|
444
|
+
const baseFilters = baseFilterSchema.parse({
|
|
445
|
+
from: options2?.from,
|
|
446
|
+
to: options2?.to,
|
|
447
|
+
...metricRequest.filters
|
|
448
|
+
});
|
|
449
|
+
return {
|
|
450
|
+
metric,
|
|
451
|
+
requestKey: getMetricResultKey(metricRequest),
|
|
452
|
+
filters: metric.filterSchema.parse(baseFilters)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
metrics,
|
|
457
|
+
baseFilterSchema,
|
|
458
|
+
kindSchemas,
|
|
459
|
+
metricKeys,
|
|
460
|
+
MetricKeySchema,
|
|
461
|
+
MetricRequestSchema,
|
|
462
|
+
MetricsRequestSchema,
|
|
463
|
+
metricsByKey,
|
|
464
|
+
getMetricByKey,
|
|
465
|
+
parseMetricRequestInput,
|
|
466
|
+
getOutputSchema
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function getMetricResultKey(metricRequest) {
|
|
470
|
+
return metricRequest.requestKey ?? metricRequest.key;
|
|
471
|
+
}
|
|
472
|
+
function getMetric(result, key) {
|
|
473
|
+
return result.metrics[key];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/engine.ts
|
|
477
|
+
function createMetricsEngine(config) {
|
|
478
|
+
const cache = config?.cache ?? noopCacheAdapter;
|
|
479
|
+
const baseFilterSchema = config?.baseFilters ?? BaseFiltersSchema;
|
|
480
|
+
const kindSchemas = {
|
|
481
|
+
...OutputSchemaMap,
|
|
482
|
+
...config?.customKinds ?? {}
|
|
483
|
+
};
|
|
484
|
+
function defineMetric(kind, def) {
|
|
485
|
+
return defineMetricWithSchema(kind, kindSchemas[kind], def);
|
|
486
|
+
}
|
|
487
|
+
function defineKpiMetric2(def) {
|
|
488
|
+
return defineKpiMetric(def);
|
|
489
|
+
}
|
|
490
|
+
function defineTimeSeriesMetric2(def) {
|
|
491
|
+
return defineTimeSeriesMetric(def);
|
|
492
|
+
}
|
|
493
|
+
function defineDistributionMetric2(def) {
|
|
494
|
+
return defineDistributionMetric(def);
|
|
495
|
+
}
|
|
496
|
+
function defineTableMetric2(def) {
|
|
497
|
+
return defineTableMetric(def);
|
|
498
|
+
}
|
|
499
|
+
function defineLeaderboardMetric2(def) {
|
|
500
|
+
return defineLeaderboardMetric(def);
|
|
501
|
+
}
|
|
502
|
+
function definePivotMetric2(def) {
|
|
503
|
+
return definePivotMetric(def);
|
|
504
|
+
}
|
|
505
|
+
function createRegistry2(metrics) {
|
|
506
|
+
return createRegistry(metrics, {
|
|
507
|
+
baseFilterSchema,
|
|
508
|
+
kindSchemas
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
cache,
|
|
513
|
+
baseFilterSchema,
|
|
514
|
+
kindSchemas,
|
|
515
|
+
defineMetric,
|
|
516
|
+
defineKpiMetric: defineKpiMetric2,
|
|
517
|
+
defineTimeSeriesMetric: defineTimeSeriesMetric2,
|
|
518
|
+
defineDistributionMetric: defineDistributionMetric2,
|
|
519
|
+
defineTableMetric: defineTableMetric2,
|
|
520
|
+
defineLeaderboardMetric: defineLeaderboardMetric2,
|
|
521
|
+
definePivotMetric: definePivotMetric2,
|
|
522
|
+
createRegistry: createRegistry2
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
// src/cache-utils.ts
|
|
526
|
+
function stableHash(obj) {
|
|
527
|
+
const stable = JSON.stringify(obj, Object.keys(obj).sort());
|
|
528
|
+
let h = 2166136261;
|
|
529
|
+
for (let i = 0;i < stable.length; i++) {
|
|
530
|
+
h ^= stable.charCodeAt(i);
|
|
531
|
+
h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
|
|
532
|
+
}
|
|
533
|
+
return (h >>> 0).toString(16);
|
|
534
|
+
}
|
|
535
|
+
function normalizeForCache(obj) {
|
|
536
|
+
const result = {};
|
|
537
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
538
|
+
if (value === undefined)
|
|
539
|
+
continue;
|
|
540
|
+
if (value instanceof Date) {
|
|
541
|
+
result[key] = value.toISOString();
|
|
542
|
+
} else if (Array.isArray(value)) {
|
|
543
|
+
result[key] = value.map((v) => v instanceof Date ? v.toISOString() : v);
|
|
544
|
+
} else if (typeof value === "object" && value !== null) {
|
|
545
|
+
const nested = normalizeForCache(value);
|
|
546
|
+
for (const [nk, nv] of Object.entries(nested)) {
|
|
547
|
+
result[`${key}.${nk}`] = nv;
|
|
548
|
+
}
|
|
549
|
+
} else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
550
|
+
result[key] = value;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
function getCacheKey(metricKey, filters, period = "current", granularity) {
|
|
556
|
+
const normalized = normalizeForCache({
|
|
557
|
+
...filters,
|
|
558
|
+
granularity
|
|
559
|
+
});
|
|
560
|
+
const hash = stableHash(normalized);
|
|
561
|
+
return `metrics:${metricKey}:${period}:${hash}`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/time.ts
|
|
565
|
+
function getPreviousPeriod(range) {
|
|
566
|
+
if (!range.from || !range.to)
|
|
567
|
+
return null;
|
|
568
|
+
const durationMs = range.to.getTime() - range.from.getTime();
|
|
569
|
+
return {
|
|
570
|
+
from: new Date(range.from.getTime() - durationMs),
|
|
571
|
+
to: new Date(range.from.getTime())
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function getBucketStart(date, granularity) {
|
|
575
|
+
const d = new Date(date);
|
|
576
|
+
switch (granularity) {
|
|
577
|
+
case "hour":
|
|
578
|
+
d.setUTCMinutes(0, 0, 0);
|
|
579
|
+
break;
|
|
580
|
+
case "day":
|
|
581
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
582
|
+
break;
|
|
583
|
+
case "week": {
|
|
584
|
+
const day = d.getUTCDay();
|
|
585
|
+
const diff = d.getUTCDate() - day + (day === 0 ? -6 : 1);
|
|
586
|
+
d.setUTCDate(diff);
|
|
587
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case "month":
|
|
591
|
+
d.setUTCDate(1);
|
|
592
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
593
|
+
break;
|
|
594
|
+
case "quarter": {
|
|
595
|
+
const month = d.getUTCMonth();
|
|
596
|
+
const quarterStart = month - month % 3;
|
|
597
|
+
d.setUTCMonth(quarterStart, 1);
|
|
598
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
case "year":
|
|
602
|
+
d.setUTCMonth(0, 1);
|
|
603
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
return d;
|
|
607
|
+
}
|
|
608
|
+
function normalizeUserDate(date) {
|
|
609
|
+
const d = new Date(date);
|
|
610
|
+
const hours = d.getUTCHours();
|
|
611
|
+
if (hours >= 20) {
|
|
612
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
613
|
+
}
|
|
614
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
615
|
+
return d;
|
|
616
|
+
}
|
|
617
|
+
function generateBuckets(from, to, granularity) {
|
|
618
|
+
const buckets = [];
|
|
619
|
+
const normalizedFrom = granularity === "hour" ? from : normalizeUserDate(from);
|
|
620
|
+
const normalizedTo = granularity === "hour" ? to : normalizeUserDate(to);
|
|
621
|
+
let current = getBucketStart(normalizedFrom, granularity);
|
|
622
|
+
while (current <= normalizedTo) {
|
|
623
|
+
buckets.push(new Date(current));
|
|
624
|
+
current = getNextBucket(current, granularity);
|
|
625
|
+
}
|
|
626
|
+
return buckets;
|
|
627
|
+
}
|
|
628
|
+
function getNextBucket(date, granularity) {
|
|
629
|
+
const d = new Date(date);
|
|
630
|
+
switch (granularity) {
|
|
631
|
+
case "hour":
|
|
632
|
+
d.setUTCHours(d.getUTCHours() + 1);
|
|
633
|
+
break;
|
|
634
|
+
case "day":
|
|
635
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
636
|
+
break;
|
|
637
|
+
case "week":
|
|
638
|
+
d.setUTCDate(d.getUTCDate() + 7);
|
|
639
|
+
break;
|
|
640
|
+
case "month":
|
|
641
|
+
d.setUTCMonth(d.getUTCMonth() + 1);
|
|
642
|
+
break;
|
|
643
|
+
case "quarter":
|
|
644
|
+
d.setUTCMonth(d.getUTCMonth() + 3);
|
|
645
|
+
break;
|
|
646
|
+
case "year":
|
|
647
|
+
d.setUTCFullYear(d.getUTCFullYear() + 1);
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
return d;
|
|
651
|
+
}
|
|
652
|
+
function inferGranularity(range) {
|
|
653
|
+
if (!range.from || !range.to)
|
|
654
|
+
return "day";
|
|
655
|
+
const diffMs = range.to.getTime() - range.from.getTime();
|
|
656
|
+
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
|
657
|
+
if (diffDays <= 2)
|
|
658
|
+
return "hour";
|
|
659
|
+
if (diffDays <= 31)
|
|
660
|
+
return "day";
|
|
661
|
+
if (diffDays <= 90)
|
|
662
|
+
return "week";
|
|
663
|
+
if (diffDays <= 365)
|
|
664
|
+
return "month";
|
|
665
|
+
if (diffDays <= 730)
|
|
666
|
+
return "quarter";
|
|
667
|
+
return "year";
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/run-metrics.ts
|
|
671
|
+
function shiftTimeSeriesTimestamps(output2, offsetMs) {
|
|
672
|
+
if (!output2 || typeof output2 !== "object" || !("kind" in output2) || output2.kind !== "timeseries") {
|
|
673
|
+
return output2;
|
|
674
|
+
}
|
|
675
|
+
const timeseriesOutput = output2;
|
|
676
|
+
return {
|
|
677
|
+
...timeseriesOutput,
|
|
678
|
+
series: timeseriesOutput.series.map((s) => ({
|
|
679
|
+
...s,
|
|
680
|
+
points: s.points.map((p) => ({
|
|
681
|
+
...p,
|
|
682
|
+
ts: new Date(p.ts.getTime() + offsetMs)
|
|
683
|
+
}))
|
|
684
|
+
}))
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
async function runSingleMetric(ctx) {
|
|
688
|
+
const { metric, filters, granularity, compareToPrevious, resolverCtx } = ctx;
|
|
689
|
+
const current = await metric.resolve({
|
|
690
|
+
filters: metric.filterSchema.parse(filters),
|
|
691
|
+
ctx: { ...resolverCtx, granularity }
|
|
692
|
+
});
|
|
693
|
+
let previous;
|
|
694
|
+
if (compareToPrevious && metric.supportsTimeRange && filters.from && filters.to) {
|
|
695
|
+
const prevRange = getPreviousPeriod({ from: filters.from, to: filters.to });
|
|
696
|
+
if (prevRange) {
|
|
697
|
+
const prevFilters = {
|
|
698
|
+
...filters,
|
|
699
|
+
from: prevRange.from,
|
|
700
|
+
to: prevRange.to
|
|
701
|
+
};
|
|
702
|
+
const prevResult = await metric.resolve({
|
|
703
|
+
filters: metric.filterSchema.parse(prevFilters),
|
|
704
|
+
ctx: { ...resolverCtx, granularity }
|
|
705
|
+
});
|
|
706
|
+
const periodOffset = filters.from.getTime() - prevRange.from.getTime();
|
|
707
|
+
previous = shiftTimeSeriesTimestamps(prevResult, periodOffset);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return { current, previous, supportsTimeRange: metric.supportsTimeRange };
|
|
711
|
+
}
|
|
712
|
+
async function prepareMetrics(options) {
|
|
713
|
+
const {
|
|
714
|
+
registry,
|
|
715
|
+
createContext,
|
|
716
|
+
cache = noopCacheAdapter,
|
|
717
|
+
hasAccess
|
|
718
|
+
} = options;
|
|
719
|
+
const request = registry.MetricsRequestSchema.parse(options.request);
|
|
720
|
+
const disableCache = request.disableCache ?? false;
|
|
721
|
+
const userCtx = await createContext();
|
|
722
|
+
const granularity = request.granularity ?? inferGranularity({ from: request.from, to: request.to });
|
|
723
|
+
const resolverCtx = { ...userCtx, granularity };
|
|
724
|
+
const cacheRequests = [];
|
|
725
|
+
const validMetrics = [];
|
|
726
|
+
const errors = {};
|
|
727
|
+
for (const metricReq of request.metrics) {
|
|
728
|
+
const metric = registry.getMetricByKey(metricReq.key);
|
|
729
|
+
const requestKey = metricReq.requestKey ?? metricReq.key;
|
|
730
|
+
if (!metric) {
|
|
731
|
+
errors[requestKey] = `Unknown metric: ${metricReq.key}`;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (hasAccess && !hasAccess(metric)) {
|
|
735
|
+
errors[requestKey] = "Access denied";
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
let parsedMetricFilters;
|
|
739
|
+
try {
|
|
740
|
+
const parsedMetricRequest = registry.parseMetricRequestInput(metricReq, {
|
|
741
|
+
from: request.from,
|
|
742
|
+
to: request.to
|
|
743
|
+
});
|
|
744
|
+
parsedMetricFilters = parsedMetricRequest.filters;
|
|
745
|
+
validMetrics.push({
|
|
746
|
+
metric: parsedMetricRequest.metric,
|
|
747
|
+
filters: parsedMetricFilters,
|
|
748
|
+
requestKey: parsedMetricRequest.requestKey
|
|
749
|
+
});
|
|
750
|
+
} catch (err) {
|
|
751
|
+
errors[requestKey] = err instanceof Error ? err.message : "Invalid metric filters";
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const filters = parsedMetricFilters;
|
|
755
|
+
const ttl = metric.cacheTtl ?? 600;
|
|
756
|
+
if (!disableCache && ttl > 0) {
|
|
757
|
+
cacheRequests.push({
|
|
758
|
+
key: getCacheKey(metric.key, filters, "current", granularity),
|
|
759
|
+
metric,
|
|
760
|
+
filters
|
|
761
|
+
});
|
|
762
|
+
if (request.compareToPrevious && metric.supportsTimeRange) {
|
|
763
|
+
const prevRange = getPreviousPeriod({
|
|
764
|
+
from: filters.from,
|
|
765
|
+
to: filters.to
|
|
766
|
+
});
|
|
767
|
+
if (prevRange) {
|
|
768
|
+
cacheRequests.push({
|
|
769
|
+
key: getCacheKey(metric.key, { ...filters, ...prevRange }, "previous", granularity),
|
|
770
|
+
metric,
|
|
771
|
+
filters: { ...filters, ...prevRange }
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const cacheKeys = cacheRequests.map((r) => r.key);
|
|
778
|
+
const cachedValues = cacheKeys.length > 0 ? await cache.mget(cacheKeys) : [];
|
|
779
|
+
const cacheMap = new Map;
|
|
780
|
+
for (let i = 0;i < cacheRequests.length; i++) {
|
|
781
|
+
const req = cacheRequests[i];
|
|
782
|
+
const cached = cachedValues[i];
|
|
783
|
+
if (req && cached) {
|
|
784
|
+
const parsed = parseCache(cached, getOutputSchema(req.metric));
|
|
785
|
+
if (parsed) {
|
|
786
|
+
cacheMap.set(req.key, parsed);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
validMetrics,
|
|
792
|
+
errors,
|
|
793
|
+
cacheMap,
|
|
794
|
+
granularity,
|
|
795
|
+
resolverCtx,
|
|
796
|
+
compareToPrevious: request.compareToPrevious ?? false,
|
|
797
|
+
disableCache,
|
|
798
|
+
cache
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
async function executeMetric(validatedMetric, prepared) {
|
|
802
|
+
const { metric, filters, requestKey } = validatedMetric;
|
|
803
|
+
const {
|
|
804
|
+
cacheMap,
|
|
805
|
+
granularity,
|
|
806
|
+
resolverCtx,
|
|
807
|
+
compareToPrevious,
|
|
808
|
+
disableCache,
|
|
809
|
+
cache
|
|
810
|
+
} = prepared;
|
|
811
|
+
const startedAt = Date.now();
|
|
812
|
+
try {
|
|
813
|
+
const cachedCurrent = cacheMap.get(getCacheKey(metric.key, filters, "current", granularity));
|
|
814
|
+
const prevRange = getPreviousPeriod({ from: filters.from, to: filters.to });
|
|
815
|
+
const cachedPrevious = prevRange ? cacheMap.get(getCacheKey(metric.key, { ...filters, ...prevRange }, "previous", granularity)) : undefined;
|
|
816
|
+
if (cachedCurrent) {
|
|
817
|
+
let previous = cachedPrevious;
|
|
818
|
+
let usedLivePreviousFetch = false;
|
|
819
|
+
if (!previous && compareToPrevious && metric.supportsTimeRange && filters.from && filters.to) {
|
|
820
|
+
usedLivePreviousFetch = true;
|
|
821
|
+
const partialResult = await runSingleMetric({
|
|
822
|
+
metric,
|
|
823
|
+
filters,
|
|
824
|
+
granularity,
|
|
825
|
+
compareToPrevious: true,
|
|
826
|
+
resolverCtx
|
|
827
|
+
});
|
|
828
|
+
previous = partialResult.previous;
|
|
829
|
+
if (previous && prevRange) {
|
|
830
|
+
const ttl2 = metric.cacheTtl ?? 600;
|
|
831
|
+
if (!disableCache && ttl2 > 0) {
|
|
832
|
+
cache.mset([
|
|
833
|
+
{
|
|
834
|
+
key: getCacheKey(metric.key, { ...filters, ...prevRange }, "previous", granularity),
|
|
835
|
+
value: JSON.stringify(previous),
|
|
836
|
+
ttl: ttl2
|
|
837
|
+
}
|
|
838
|
+
]).catch((error) => console.error(error));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
key: metric.key,
|
|
844
|
+
requestKey,
|
|
845
|
+
result: {
|
|
846
|
+
current: cachedCurrent,
|
|
847
|
+
previous,
|
|
848
|
+
supportsTimeRange: metric.supportsTimeRange,
|
|
849
|
+
execution: {
|
|
850
|
+
cacheStatus: usedLivePreviousFetch ? "partialHit" : "hit",
|
|
851
|
+
durationMs: Date.now() - startedAt,
|
|
852
|
+
granularity
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
const result = await runSingleMetric({
|
|
858
|
+
metric,
|
|
859
|
+
filters,
|
|
860
|
+
granularity,
|
|
861
|
+
compareToPrevious,
|
|
862
|
+
resolverCtx
|
|
863
|
+
});
|
|
864
|
+
const ttl = metric.cacheTtl ?? 600;
|
|
865
|
+
if (!disableCache && ttl > 0) {
|
|
866
|
+
const cacheEntries = [
|
|
867
|
+
{
|
|
868
|
+
key: getCacheKey(metric.key, filters, "current", granularity),
|
|
869
|
+
value: JSON.stringify(result.current),
|
|
870
|
+
ttl
|
|
871
|
+
}
|
|
872
|
+
];
|
|
873
|
+
if (result.previous) {
|
|
874
|
+
const prevRange2 = getPreviousPeriod({
|
|
875
|
+
from: filters.from,
|
|
876
|
+
to: filters.to
|
|
877
|
+
});
|
|
878
|
+
if (prevRange2) {
|
|
879
|
+
cacheEntries.push({
|
|
880
|
+
key: getCacheKey(metric.key, { ...filters, ...prevRange2 }, "previous", granularity),
|
|
881
|
+
value: JSON.stringify(result.previous),
|
|
882
|
+
ttl
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
cache.mset(cacheEntries).catch((error) => console.error(error));
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
key: metric.key,
|
|
890
|
+
requestKey,
|
|
891
|
+
result: {
|
|
892
|
+
...result,
|
|
893
|
+
execution: {
|
|
894
|
+
cacheStatus: disableCache ? "bypassed" : "miss",
|
|
895
|
+
durationMs: Date.now() - startedAt,
|
|
896
|
+
granularity
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
} catch (err) {
|
|
901
|
+
return {
|
|
902
|
+
key: metric.key,
|
|
903
|
+
requestKey,
|
|
904
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
async function runMetrics(options) {
|
|
909
|
+
const prepared = await prepareMetrics(options);
|
|
910
|
+
const { validMetrics, errors } = prepared;
|
|
911
|
+
const results = {};
|
|
912
|
+
const outcomes = await Promise.all(validMetrics.map((vm) => executeMetric(vm, prepared)));
|
|
913
|
+
for (const outcome of outcomes) {
|
|
914
|
+
if (outcome.error) {
|
|
915
|
+
errors[outcome.requestKey ?? outcome.key] = outcome.error;
|
|
916
|
+
} else if (outcome.result) {
|
|
917
|
+
results[outcome.requestKey ?? outcome.key] = outcome.result;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
metrics: results,
|
|
922
|
+
errors
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
async function* runMetricsStream(options) {
|
|
926
|
+
const prepared = await prepareMetrics(options);
|
|
927
|
+
const { validMetrics, errors } = prepared;
|
|
928
|
+
const totalMetrics = validMetrics.length + Object.keys(errors).length;
|
|
929
|
+
let yieldedCount = 0;
|
|
930
|
+
for (const key of Object.keys(errors)) {
|
|
931
|
+
yieldedCount++;
|
|
932
|
+
yield {
|
|
933
|
+
key,
|
|
934
|
+
requestKey: key,
|
|
935
|
+
error: errors[key],
|
|
936
|
+
done: yieldedCount === totalMetrics
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
if (validMetrics.length === 0) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const pending = new Map;
|
|
943
|
+
for (const vm of validMetrics) {
|
|
944
|
+
const promise = executeMetric(vm, prepared);
|
|
945
|
+
pending.set(promise, vm.requestKey);
|
|
946
|
+
}
|
|
947
|
+
while (pending.size > 0) {
|
|
948
|
+
const settled = await Promise.race(Array.from(pending.keys()).map((p) => p.then((result) => ({ promise: p, result }))));
|
|
949
|
+
pending.delete(settled.promise);
|
|
950
|
+
yieldedCount++;
|
|
951
|
+
yield {
|
|
952
|
+
key: settled.result.key,
|
|
953
|
+
requestKey: settled.result.requestKey,
|
|
954
|
+
result: settled.result.result,
|
|
955
|
+
error: settled.result.error,
|
|
956
|
+
done: yieldedCount === totalMetrics
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// src/type-guards.ts
|
|
961
|
+
function isKpi(output2) {
|
|
962
|
+
return output2.kind === "kpi";
|
|
963
|
+
}
|
|
964
|
+
function isTimeSeries(output2) {
|
|
965
|
+
return output2.kind === "timeseries";
|
|
966
|
+
}
|
|
967
|
+
function isDistribution(output2) {
|
|
968
|
+
return output2.kind === "distribution";
|
|
969
|
+
}
|
|
970
|
+
function isTable(output2) {
|
|
971
|
+
return output2.kind === "table";
|
|
972
|
+
}
|
|
973
|
+
function isLeaderboard(output2) {
|
|
974
|
+
return output2.kind === "leaderboard";
|
|
975
|
+
}
|
|
976
|
+
function isPivot(output2) {
|
|
977
|
+
return output2.kind === "pivot";
|
|
978
|
+
}
|
|
979
|
+
// src/catalog.ts
|
|
980
|
+
function defineMetricCatalogMetadata(metadata) {
|
|
981
|
+
return metadata;
|
|
982
|
+
}
|
|
983
|
+
// src/filters/types.ts
|
|
984
|
+
function defineMetricFilterFieldMetadata(metadata) {
|
|
985
|
+
return metadata;
|
|
986
|
+
}
|
|
987
|
+
// src/filters/parse.ts
|
|
988
|
+
function getDef(zodType) {
|
|
989
|
+
return zodType._def ?? {};
|
|
990
|
+
}
|
|
991
|
+
function getTypeName(zodType) {
|
|
992
|
+
const def = getDef(zodType);
|
|
993
|
+
if (def.typeName)
|
|
994
|
+
return def.typeName;
|
|
995
|
+
if (typeof def.type === "string") {
|
|
996
|
+
const v4ToV3Map = {
|
|
997
|
+
string: "ZodString",
|
|
998
|
+
number: "ZodNumber",
|
|
999
|
+
boolean: "ZodBoolean",
|
|
1000
|
+
date: "ZodDate",
|
|
1001
|
+
enum: "ZodEnum",
|
|
1002
|
+
array: "ZodArray",
|
|
1003
|
+
object: "ZodObject",
|
|
1004
|
+
optional: "ZodOptional",
|
|
1005
|
+
nullable: "ZodNullable",
|
|
1006
|
+
default: "ZodDefault"
|
|
1007
|
+
};
|
|
1008
|
+
return v4ToV3Map[def.type] || def.type;
|
|
1009
|
+
}
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
function getEnumValues(zodType) {
|
|
1013
|
+
const def = getDef(zodType);
|
|
1014
|
+
const options = zodType.options;
|
|
1015
|
+
if (Array.isArray(options)) {
|
|
1016
|
+
return options.map(String);
|
|
1017
|
+
}
|
|
1018
|
+
if (def.entries && typeof def.entries === "object") {
|
|
1019
|
+
return Object.values(def.entries).map(String);
|
|
1020
|
+
}
|
|
1021
|
+
if (def.values && typeof def.values === "object") {
|
|
1022
|
+
return Object.values(def.values).map(String);
|
|
1023
|
+
}
|
|
1024
|
+
return [];
|
|
1025
|
+
}
|
|
1026
|
+
function getArrayElementType(zodType) {
|
|
1027
|
+
const def = getDef(zodType);
|
|
1028
|
+
if (def.element)
|
|
1029
|
+
return def.element;
|
|
1030
|
+
if (def.type && typeof def.type !== "string") {
|
|
1031
|
+
return def.type;
|
|
1032
|
+
}
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
function getShape(zodType) {
|
|
1036
|
+
return zodType.shape || {};
|
|
1037
|
+
}
|
|
1038
|
+
function unwrapZodType(zodType) {
|
|
1039
|
+
const def = getDef(zodType);
|
|
1040
|
+
const typeName = getTypeName(zodType);
|
|
1041
|
+
const wrapperTypes = ["ZodOptional", "ZodNullable", "ZodDefault"];
|
|
1042
|
+
if (typeName && wrapperTypes.includes(typeName) && def.innerType) {
|
|
1043
|
+
return unwrapZodType(def.innerType);
|
|
1044
|
+
}
|
|
1045
|
+
return zodType;
|
|
1046
|
+
}
|
|
1047
|
+
function isMinMaxRangeObject(zodType) {
|
|
1048
|
+
const shape = getShape(zodType);
|
|
1049
|
+
const keys = Object.keys(shape);
|
|
1050
|
+
const validKeys = ["min", "max"];
|
|
1051
|
+
const hasOnlyValidKeys = keys.every((key) => validKeys.includes(key));
|
|
1052
|
+
if (!hasOnlyValidKeys || keys.length === 0)
|
|
1053
|
+
return false;
|
|
1054
|
+
for (const key of keys) {
|
|
1055
|
+
const fieldType = shape[key];
|
|
1056
|
+
if (!fieldType)
|
|
1057
|
+
return false;
|
|
1058
|
+
const unwrapped = unwrapZodType(fieldType);
|
|
1059
|
+
const typeName = getTypeName(unwrapped);
|
|
1060
|
+
if (typeName !== "ZodNumber")
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
function determineTypeFromFilterObject(obj) {
|
|
1066
|
+
const shape = getShape(obj);
|
|
1067
|
+
for (const value of Object.values(shape)) {
|
|
1068
|
+
const opValue = unwrapZodType(value);
|
|
1069
|
+
const typeName = getTypeName(opValue);
|
|
1070
|
+
switch (typeName) {
|
|
1071
|
+
case "ZodNumber":
|
|
1072
|
+
return "number";
|
|
1073
|
+
case "ZodBoolean":
|
|
1074
|
+
return "boolean";
|
|
1075
|
+
case "ZodDate":
|
|
1076
|
+
return "date";
|
|
1077
|
+
case "ZodEnum":
|
|
1078
|
+
case "ZodNativeEnum":
|
|
1079
|
+
return "option";
|
|
1080
|
+
case "ZodArray": {
|
|
1081
|
+
const elementType = getArrayElementType(opValue);
|
|
1082
|
+
const elementTypeName = elementType ? getTypeName(elementType) : null;
|
|
1083
|
+
if (elementTypeName === "ZodEnum" || elementTypeName === "ZodNativeEnum") {
|
|
1084
|
+
return "multiOption";
|
|
1085
|
+
}
|
|
1086
|
+
if (elementTypeName === "ZodNumber")
|
|
1087
|
+
return "number";
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return "text";
|
|
1093
|
+
}
|
|
1094
|
+
function determineFieldType(zodType) {
|
|
1095
|
+
const unwrapped = unwrapZodType(zodType);
|
|
1096
|
+
const typeName = getTypeName(unwrapped);
|
|
1097
|
+
if (typeName === "ZodObject") {
|
|
1098
|
+
if (isMinMaxRangeObject(unwrapped))
|
|
1099
|
+
return "numberRange";
|
|
1100
|
+
return determineTypeFromFilterObject(unwrapped);
|
|
1101
|
+
}
|
|
1102
|
+
switch (typeName) {
|
|
1103
|
+
case "ZodString":
|
|
1104
|
+
return "text";
|
|
1105
|
+
case "ZodNumber":
|
|
1106
|
+
return "number";
|
|
1107
|
+
case "ZodBoolean":
|
|
1108
|
+
return "boolean";
|
|
1109
|
+
case "ZodDate":
|
|
1110
|
+
return "date";
|
|
1111
|
+
case "ZodEnum":
|
|
1112
|
+
case "ZodNativeEnum":
|
|
1113
|
+
return "option";
|
|
1114
|
+
case "ZodArray": {
|
|
1115
|
+
const elementType = getArrayElementType(unwrapped);
|
|
1116
|
+
const elementTypeName = elementType ? getTypeName(elementType) : null;
|
|
1117
|
+
if (elementTypeName === "ZodEnum" || elementTypeName === "ZodNativeEnum") {
|
|
1118
|
+
return "multiOption";
|
|
1119
|
+
}
|
|
1120
|
+
return "text";
|
|
1121
|
+
}
|
|
1122
|
+
default:
|
|
1123
|
+
return "text";
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function getDescription(zodType) {
|
|
1127
|
+
const def = getDef(zodType);
|
|
1128
|
+
if (def?.description)
|
|
1129
|
+
return def.description;
|
|
1130
|
+
const unwrapped = unwrapZodType(zodType);
|
|
1131
|
+
const unwrappedDef = getDef(unwrapped);
|
|
1132
|
+
if (unwrappedDef?.description)
|
|
1133
|
+
return unwrappedDef.description;
|
|
1134
|
+
if (getTypeName(unwrapped) === "ZodObject") {
|
|
1135
|
+
const shape = getShape(unwrapped);
|
|
1136
|
+
for (const value of Object.values(shape)) {
|
|
1137
|
+
const opDef = getDef(value);
|
|
1138
|
+
if (opDef?.description)
|
|
1139
|
+
return opDef.description;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
function formatEnumValue(value) {
|
|
1145
|
+
const label = value.toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (str) => str.toUpperCase());
|
|
1146
|
+
return { label, value };
|
|
1147
|
+
}
|
|
1148
|
+
function formatFieldDisplayName(fieldId) {
|
|
1149
|
+
return fieldId.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\b\w/g, (str) => str.toUpperCase());
|
|
1150
|
+
}
|
|
1151
|
+
function extractEnumValues(zodType) {
|
|
1152
|
+
const unwrapped = unwrapZodType(zodType);
|
|
1153
|
+
const typeName = getTypeName(unwrapped);
|
|
1154
|
+
if (typeName === "ZodEnum" || typeName === "ZodNativeEnum") {
|
|
1155
|
+
const values = getEnumValues(unwrapped);
|
|
1156
|
+
return values.map(formatEnumValue);
|
|
1157
|
+
}
|
|
1158
|
+
if (typeName === "ZodArray") {
|
|
1159
|
+
const elementType = getArrayElementType(unwrapped);
|
|
1160
|
+
if (elementType) {
|
|
1161
|
+
const elementTypeName = getTypeName(elementType);
|
|
1162
|
+
if (elementTypeName === "ZodEnum" || elementTypeName === "ZodNativeEnum") {
|
|
1163
|
+
const values = getEnumValues(elementType);
|
|
1164
|
+
return values.map(formatEnumValue);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (typeName === "ZodObject") {
|
|
1169
|
+
const shape = getShape(unwrapped);
|
|
1170
|
+
for (const value of Object.values(shape)) {
|
|
1171
|
+
const opValue = unwrapZodType(value);
|
|
1172
|
+
const opTypeName = getTypeName(opValue);
|
|
1173
|
+
if (opTypeName === "ZodEnum" || opTypeName === "ZodNativeEnum") {
|
|
1174
|
+
const values = getEnumValues(opValue);
|
|
1175
|
+
return values.map(formatEnumValue);
|
|
1176
|
+
}
|
|
1177
|
+
if (opTypeName === "ZodArray") {
|
|
1178
|
+
const elementType = getArrayElementType(opValue);
|
|
1179
|
+
if (elementType) {
|
|
1180
|
+
const elementTypeName = getTypeName(elementType);
|
|
1181
|
+
if (elementTypeName === "ZodEnum" || elementTypeName === "ZodNativeEnum") {
|
|
1182
|
+
const values = getEnumValues(elementType);
|
|
1183
|
+
return values.map(formatEnumValue);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return [];
|
|
1190
|
+
}
|
|
1191
|
+
function parseMetricFilterSchema(schema, options) {
|
|
1192
|
+
const excludeFields = new Set(options?.excludeFields ?? []);
|
|
1193
|
+
const includeFields = options?.includeFields;
|
|
1194
|
+
const fieldMetadata = options?.fieldMetadata ?? {};
|
|
1195
|
+
const fields = [];
|
|
1196
|
+
const unwrapped = unwrapZodType(schema);
|
|
1197
|
+
if (getTypeName(unwrapped) !== "ZodObject")
|
|
1198
|
+
return [];
|
|
1199
|
+
const shape = getShape(unwrapped);
|
|
1200
|
+
for (const [key, zodType] of Object.entries(shape)) {
|
|
1201
|
+
if (excludeFields.has(key))
|
|
1202
|
+
continue;
|
|
1203
|
+
if (includeFields && !includeFields.includes(key))
|
|
1204
|
+
continue;
|
|
1205
|
+
const metadata = fieldMetadata[key];
|
|
1206
|
+
const fieldType = metadata?.type ?? determineFieldType(zodType);
|
|
1207
|
+
const description = metadata?.description ?? getDescription(zodType);
|
|
1208
|
+
const enumOptions = metadata?.options ?? extractEnumValues(zodType);
|
|
1209
|
+
fields.push({
|
|
1210
|
+
id: key,
|
|
1211
|
+
displayName: metadata?.displayName ?? formatFieldDisplayName(key),
|
|
1212
|
+
type: fieldType,
|
|
1213
|
+
description,
|
|
1214
|
+
options: enumOptions.length > 0 ? enumOptions : undefined,
|
|
1215
|
+
defaultOperator: metadata?.defaultOperator
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
return fields;
|
|
1219
|
+
}
|
|
1220
|
+
// src/filters/default-metadata.ts
|
|
1221
|
+
var COMMON_METRIC_FILTER_FIELD_METADATA = defineMetricFilterFieldMetadata({
|
|
1222
|
+
metric: {
|
|
1223
|
+
displayName: "Metric",
|
|
1224
|
+
description: "Choose how the metric should be aggregated.",
|
|
1225
|
+
defaultOperator: "is"
|
|
1226
|
+
},
|
|
1227
|
+
groupBy: {
|
|
1228
|
+
displayName: "Group By",
|
|
1229
|
+
description: "Split the metric into multiple groups.",
|
|
1230
|
+
defaultOperator: "is"
|
|
1231
|
+
},
|
|
1232
|
+
limit: {
|
|
1233
|
+
displayName: "Limit",
|
|
1234
|
+
description: "Maximum number of rows to return."
|
|
1235
|
+
},
|
|
1236
|
+
view: {
|
|
1237
|
+
displayName: "View",
|
|
1238
|
+
description: "Choose which interpretation of the metric to render.",
|
|
1239
|
+
defaultOperator: "is"
|
|
1240
|
+
},
|
|
1241
|
+
chartType: {
|
|
1242
|
+
displayName: "Chart Type",
|
|
1243
|
+
description: "Choose the preferred visualization for this metric.",
|
|
1244
|
+
defaultOperator: "is"
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
function mergeMetricFilterFieldMetadata(...metadataEntries) {
|
|
1248
|
+
return Object.assign({}, ...metadataEntries);
|
|
1249
|
+
}
|
|
1250
|
+
export {
|
|
1251
|
+
validateOutput,
|
|
1252
|
+
validateMetricOutput,
|
|
1253
|
+
runMetricsStream,
|
|
1254
|
+
runMetrics,
|
|
1255
|
+
redisCacheAdapter,
|
|
1256
|
+
parseMetricFilterSchema,
|
|
1257
|
+
parseCache,
|
|
1258
|
+
normalizeUserDate,
|
|
1259
|
+
noopCacheAdapter,
|
|
1260
|
+
mergeMetricFilterFieldMetadata,
|
|
1261
|
+
isTimeSeries,
|
|
1262
|
+
isTable,
|
|
1263
|
+
isPivot,
|
|
1264
|
+
isLeaderboard,
|
|
1265
|
+
isKpi,
|
|
1266
|
+
isDistribution,
|
|
1267
|
+
inferGranularity,
|
|
1268
|
+
getPreviousPeriod,
|
|
1269
|
+
getOutputSchema,
|
|
1270
|
+
getMetric,
|
|
1271
|
+
getBucketStart,
|
|
1272
|
+
generateBuckets,
|
|
1273
|
+
extendTimeSeriesFilters,
|
|
1274
|
+
extendBaseFilters,
|
|
1275
|
+
defineTimeSeriesOutput,
|
|
1276
|
+
defineTimeSeriesMetric,
|
|
1277
|
+
defineTableOutput,
|
|
1278
|
+
defineTableMetric,
|
|
1279
|
+
definePivotOutput,
|
|
1280
|
+
definePivotMetric,
|
|
1281
|
+
defineMetricWithSchema,
|
|
1282
|
+
defineMetricOutput,
|
|
1283
|
+
defineMetricFilterFieldMetadata,
|
|
1284
|
+
defineMetricCatalogMetadata,
|
|
1285
|
+
defineLeaderboardOutput,
|
|
1286
|
+
defineLeaderboardMetric,
|
|
1287
|
+
defineKpiOutput,
|
|
1288
|
+
defineKpiMetric,
|
|
1289
|
+
defineDistributionOutput,
|
|
1290
|
+
defineDistributionMetric,
|
|
1291
|
+
createRegistry,
|
|
1292
|
+
createMetricsEngine,
|
|
1293
|
+
TimeSeriesSeriesSchema,
|
|
1294
|
+
TimeSeriesPointSchema,
|
|
1295
|
+
TimeSeriesOutputSchema,
|
|
1296
|
+
TimeSeriesFiltersSchema,
|
|
1297
|
+
TimeRangeSchema,
|
|
1298
|
+
TimeGranularitySchema,
|
|
1299
|
+
TableOutputSchema,
|
|
1300
|
+
TableColumnSchema,
|
|
1301
|
+
RequiredTimeRangeSchema,
|
|
1302
|
+
PivotTotalsSchema,
|
|
1303
|
+
PivotOutputSchema,
|
|
1304
|
+
PivotDimensionSchema,
|
|
1305
|
+
OutputSchemaMap,
|
|
1306
|
+
OutputKindSchema,
|
|
1307
|
+
MetricUnitSchema,
|
|
1308
|
+
MetricTypeSchema,
|
|
1309
|
+
MetricResultSchema,
|
|
1310
|
+
MetricOutputSchema,
|
|
1311
|
+
MetricExecutionSchema,
|
|
1312
|
+
MetricExecutionCacheStatusSchema,
|
|
1313
|
+
LeaderboardOutputSchema,
|
|
1314
|
+
LeaderboardItemSchema,
|
|
1315
|
+
KpiOutputSchema,
|
|
1316
|
+
DistributionSegmentSchema,
|
|
1317
|
+
DistributionOutputSchema,
|
|
1318
|
+
DistributionChartTypeSchema,
|
|
1319
|
+
CompareSchema,
|
|
1320
|
+
COMMON_METRIC_FILTER_FIELD_METADATA,
|
|
1321
|
+
BaseFiltersSchema
|
|
1322
|
+
};
|