@friggframework/core 2.0.0--canary.622.faffe5f.0 → 2.0.0--canary.623.71305f2.0
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/README.md +0 -40
- package/application/index.js +10 -15
- package/core/create-handler.js +10 -114
- package/generated/prisma-mongodb/edge.js +4 -16
- package/generated/prisma-mongodb/index-browser.js +1 -13
- package/generated/prisma-mongodb/index.d.ts +96 -1495
- package/generated/prisma-mongodb/index.js +4 -16
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -24
- package/generated/prisma-mongodb/wasm.js +4 -16
- package/generated/prisma-postgresql/edge.js +4 -16
- package/generated/prisma-postgresql/index-browser.js +1 -13
- package/generated/prisma-postgresql/index.d.ts +91 -1541
- package/generated/prisma-postgresql/index.js +4 -16
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -23
- package/generated/prisma-postgresql/wasm.js +4 -16
- package/handlers/app-definition-loader.js +3 -26
- package/handlers/integration-event-dispatcher.js +15 -34
- package/handlers/routers/integration-webhook-routers.js +7 -23
- package/index.js +9 -11
- package/integrations/integration-base.js +23 -88
- package/modules/module.js +15 -2
- package/modules/requester/requester.js +46 -111
- package/package.json +5 -12
- package/prisma-mongodb/schema.prisma +0 -24
- package/prisma-postgresql/schema.prisma +0 -23
- package/reporting/README.md +1 -8
- package/reporting/reporting-router.js +1 -16
- package/reporting/use-cases/list-integrations-report.js +6 -61
- package/application/commands/usage-commands.js +0 -77
- package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +0 -26
- package/telemetry/README.md +0 -317
- package/telemetry/bind-telemetry-context.js +0 -51
- package/telemetry/canonical-counters.js +0 -52
- package/telemetry/exporters/exporter-factory.js +0 -79
- package/telemetry/index.js +0 -56
- package/telemetry/instrument-handler.js +0 -82
- package/telemetry/no-op-telemetry.js +0 -66
- package/telemetry/north-star.js +0 -103
- package/telemetry/otel-telemetry.js +0 -215
- package/telemetry/plugin-subscribers-singleton.js +0 -51
- package/telemetry/plugin-subscribers.js +0 -77
- package/telemetry/telemetry-config.js +0 -118
- package/telemetry/telemetry-context.js +0 -40
- package/telemetry/telemetry-event-bus.js +0 -58
- package/telemetry/telemetry-service.js +0 -44
- package/telemetry/telemetry-singleton.js +0 -41
- package/telemetry/usage-rollup-singleton.js +0 -74
- package/telemetry/usage-rollup-subscriber.js +0 -116
- package/telemetry/usage-windows.js +0 -14
- package/usage/README.md +0 -54
- package/usage/index.js +0 -19
- package/usage/repositories/usage-repository-documentdb.js +0 -199
- package/usage/repositories/usage-repository-factory.js +0 -28
- package/usage/repositories/usage-repository-interface.js +0 -37
- package/usage/repositories/usage-repository-mongo.js +0 -12
- package/usage/repositories/usage-repository-postgres.js +0 -152
- package/usage/tracked-metrics.js +0 -38
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
const { prisma } = require('../../database/prisma');
|
|
2
|
-
const { updateOne } = require('../../database/documentdb-utils');
|
|
3
|
-
const { UsageRepositoryMongo } = require('./usage-repository-mongo');
|
|
4
|
-
|
|
5
|
-
const COLLECTION = 'UsageCounter';
|
|
6
|
-
const DRAIN_BATCH_SIZE = 1000;
|
|
7
|
-
const MAX_BATCHES = 100000;
|
|
8
|
-
const VALID_GROUP_BY = new Set(['integrationType', 'metric']);
|
|
9
|
-
const VALID_BUCKETS = new Set(['day', 'hour']);
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* DocumentDB usage store. Amazon DocumentDB does not accept the command shapes
|
|
13
|
-
* Prisma's Mongo engine emits for `upsert({ update: { value: { increment } } })`
|
|
14
|
-
* and `groupBy({ _sum })`, and cursor reads truncate at ~101 docs — so, like
|
|
15
|
-
* every other DocumentDB adapter in this repo, these operations are issued as
|
|
16
|
-
* raw commands (`$runCommandRaw`) via the validated documentdb-utils helpers and
|
|
17
|
-
* a drained aggregate cursor. No timestamps are managed: totals/series filter on
|
|
18
|
-
* the window KEY (not write-time), so createdAt/updatedAt are unnecessary here.
|
|
19
|
-
*/
|
|
20
|
-
class UsageRepositoryDocumentDB extends UsageRepositoryMongo {
|
|
21
|
-
constructor() {
|
|
22
|
-
super();
|
|
23
|
-
this.prisma = prisma;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async increment({ integrationId, integrationType, metric, window, value = 1 }) {
|
|
27
|
-
// Atomic upsert-increment: $inc creates the field at the increment value
|
|
28
|
-
// on insert; $setOnInsert stamps the identity on first write. The compound
|
|
29
|
-
// filter is the unique key, so concurrent writers converge on one row.
|
|
30
|
-
await updateOne(
|
|
31
|
-
this.prisma,
|
|
32
|
-
COLLECTION,
|
|
33
|
-
{ integrationId, integrationType, metric, window },
|
|
34
|
-
{
|
|
35
|
-
$inc: { value },
|
|
36
|
-
$setOnInsert: {
|
|
37
|
-
integrationId,
|
|
38
|
-
integrationType,
|
|
39
|
-
metric,
|
|
40
|
-
window,
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
{ upsert: true }
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async totals({
|
|
48
|
-
metric,
|
|
49
|
-
groupBy = 'integrationType',
|
|
50
|
-
since,
|
|
51
|
-
bucket = 'day',
|
|
52
|
-
} = {}) {
|
|
53
|
-
if (!metric) {
|
|
54
|
-
throw new Error('totals requires a metric (units are per-metric)');
|
|
55
|
-
}
|
|
56
|
-
assertGroupBy(groupBy);
|
|
57
|
-
assertBucket(bucket);
|
|
58
|
-
|
|
59
|
-
// Single window granularity (day: OR hour:) — never sum across both.
|
|
60
|
-
// `since` bounds on the window KEY (mirrors series / the Prisma adapter).
|
|
61
|
-
const rows = await this._aggregateDrained([
|
|
62
|
-
{
|
|
63
|
-
$match: {
|
|
64
|
-
metric,
|
|
65
|
-
window: windowMatch(bucket, {
|
|
66
|
-
gte: since ? windowKey(bucket, since) : undefined,
|
|
67
|
-
}),
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
{ $group: { _id: `$${groupBy}`, value: { $sum: '$value' } } },
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
return rows.map((row) => ({
|
|
74
|
-
[groupBy]: row._id,
|
|
75
|
-
value: toNumber(row.value),
|
|
76
|
-
}));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async series({ metric, integrationType, from, to, bucket = 'day' } = {}) {
|
|
80
|
-
assertBucket(bucket);
|
|
81
|
-
if (!integrationType) {
|
|
82
|
-
throw new Error('series requires an integrationType');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const rows = await this._aggregateDrained([
|
|
86
|
-
{
|
|
87
|
-
$match: {
|
|
88
|
-
metric,
|
|
89
|
-
integrationType,
|
|
90
|
-
window: windowMatch(bucket, {
|
|
91
|
-
gte: from ? windowKey(bucket, from) : undefined,
|
|
92
|
-
lte: to ? windowKey(bucket, to) : undefined,
|
|
93
|
-
}),
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
{ $group: { _id: '$window', value: { $sum: '$value' } } },
|
|
97
|
-
{ $sort: { _id: 1 } },
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
return rows.map((row) => ({
|
|
101
|
-
bucket: row._id,
|
|
102
|
-
value: toNumber(row.value),
|
|
103
|
-
}));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async _aggregateDrained(pipeline) {
|
|
107
|
-
const first = await this.prisma.$runCommandRaw({
|
|
108
|
-
aggregate: COLLECTION,
|
|
109
|
-
pipeline,
|
|
110
|
-
cursor: { batchSize: DRAIN_BATCH_SIZE },
|
|
111
|
-
});
|
|
112
|
-
return drainCursor(this.prisma, COLLECTION, first);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** A `$match` window clause: prefix by granularity, optionally range-bounded. */
|
|
117
|
-
function windowMatch(bucket, { gte, lte } = {}) {
|
|
118
|
-
const clause = { $regex: `^${bucket}:` };
|
|
119
|
-
if (gte) clause.$gte = gte;
|
|
120
|
-
if (lte) clause.$lte = lte;
|
|
121
|
-
return clause;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Window key for a date at a granularity (mirrors telemetry/usage-windows). */
|
|
125
|
-
function windowKey(bucket, date) {
|
|
126
|
-
const iso = new Date(date).toISOString();
|
|
127
|
-
return `${bucket}:${bucket === 'hour' ? iso.slice(0, 13) : iso.slice(0, 10)}`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Coerce an aggregate `$sum` result to a JS Number. Prisma $runCommandRaw returns
|
|
132
|
-
* extended JSON, so a 64-bit sum can arrive as { $numberLong: "..." } (or
|
|
133
|
-
* $numberInt/$numberDouble); counts never approach 2^53.
|
|
134
|
-
*/
|
|
135
|
-
function toNumber(value) {
|
|
136
|
-
if (value === null || value === undefined) return 0;
|
|
137
|
-
if (typeof value === 'number') return value;
|
|
138
|
-
if (typeof value === 'bigint') return Number(value);
|
|
139
|
-
if (typeof value === 'object') {
|
|
140
|
-
const raw =
|
|
141
|
-
value.$numberLong ?? value.$numberInt ?? value.$numberDouble;
|
|
142
|
-
if (raw !== undefined) return Number(raw);
|
|
143
|
-
}
|
|
144
|
-
return Number(value) || 0;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function drainCursor(client, collection, firstResult) {
|
|
148
|
-
const cursor = firstResult?.cursor || {};
|
|
149
|
-
const docs = [...(cursor.firstBatch || [])];
|
|
150
|
-
let cursorId = cursor.id;
|
|
151
|
-
let batches = 0;
|
|
152
|
-
|
|
153
|
-
while (isCursorOpen(cursorId) && batches < MAX_BATCHES) {
|
|
154
|
-
batches += 1;
|
|
155
|
-
const next = await client.$runCommandRaw({
|
|
156
|
-
getMore: cursorId,
|
|
157
|
-
collection,
|
|
158
|
-
batchSize: DRAIN_BATCH_SIZE,
|
|
159
|
-
});
|
|
160
|
-
const nextCursor = next?.cursor || {};
|
|
161
|
-
const nextBatch = nextCursor.nextBatch || [];
|
|
162
|
-
docs.push(...nextBatch);
|
|
163
|
-
cursorId = nextCursor.id;
|
|
164
|
-
if (nextBatch.length === 0) break;
|
|
165
|
-
}
|
|
166
|
-
return docs;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function isCursorOpen(id) {
|
|
170
|
-
if (id === undefined || id === null) return false;
|
|
171
|
-
if (typeof id === 'number') return id !== 0;
|
|
172
|
-
if (typeof id === 'bigint') return id !== 0n;
|
|
173
|
-
if (typeof id === 'object' && id.$numberLong !== undefined) {
|
|
174
|
-
return id.$numberLong !== '0';
|
|
175
|
-
}
|
|
176
|
-
return String(id) !== '0';
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function assertGroupBy(groupBy) {
|
|
180
|
-
if (!VALID_GROUP_BY.has(groupBy)) {
|
|
181
|
-
throw new Error(
|
|
182
|
-
`Invalid groupBy "${groupBy}". Allowed: ${[...VALID_GROUP_BY].join(
|
|
183
|
-
', '
|
|
184
|
-
)}`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function assertBucket(bucket) {
|
|
190
|
-
if (!VALID_BUCKETS.has(bucket)) {
|
|
191
|
-
throw new Error(
|
|
192
|
-
`Invalid bucket "${bucket}". Allowed: ${[...VALID_BUCKETS].join(
|
|
193
|
-
', '
|
|
194
|
-
)}`
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
module.exports = { UsageRepositoryDocumentDB };
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
const { UsageRepositoryMongo } = require('./usage-repository-mongo');
|
|
2
|
-
const { UsageRepositoryPostgres } = require('./usage-repository-postgres');
|
|
3
|
-
const { UsageRepositoryDocumentDB } = require('./usage-repository-documentdb');
|
|
4
|
-
const config = require('../../database/config');
|
|
5
|
-
|
|
6
|
-
function createUsageRepository() {
|
|
7
|
-
const dbType = config.DB_TYPE;
|
|
8
|
-
|
|
9
|
-
switch (dbType) {
|
|
10
|
-
case 'mongodb':
|
|
11
|
-
return new UsageRepositoryMongo();
|
|
12
|
-
case 'postgresql':
|
|
13
|
-
return new UsageRepositoryPostgres();
|
|
14
|
-
case 'documentdb':
|
|
15
|
-
return new UsageRepositoryDocumentDB();
|
|
16
|
-
default:
|
|
17
|
-
throw new Error(
|
|
18
|
-
`Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
module.exports = {
|
|
24
|
-
createUsageRepository,
|
|
25
|
-
UsageRepositoryMongo,
|
|
26
|
-
UsageRepositoryPostgres,
|
|
27
|
-
UsageRepositoryDocumentDB,
|
|
28
|
-
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Port for the durable usage-counter store.
|
|
3
|
-
*
|
|
4
|
-
* The store is deliberately isolated (ADR-010 Decision 3): its fact rows carry
|
|
5
|
-
* NO userId and NO foreign key to Integration, so a user-scoped query can never
|
|
6
|
-
* return a usage row and usage history survives integration deletion.
|
|
7
|
-
*
|
|
8
|
-
* Fact row: { integrationId, integrationType, metric, window, value, updatedAt }
|
|
9
|
-
* Dimensions must be bounded — high-cardinality ids stay on traces.
|
|
10
|
-
*/
|
|
11
|
-
class UsageRepositoryInterface {
|
|
12
|
-
/**
|
|
13
|
-
* Atomically add `value` to the counter for
|
|
14
|
-
* (integrationId, integrationType, metric, window), inserting if absent.
|
|
15
|
-
*/
|
|
16
|
-
async increment(/* { integrationId, integrationType, metric, window, value } */) {
|
|
17
|
-
throw new Error('increment must be implemented by subclass');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Sum a metric grouped by a bounded dimension since an optional timestamp.
|
|
22
|
-
* @returns {Promise<Array<{[groupBy]: string, value: number}>>}
|
|
23
|
-
*/
|
|
24
|
-
async totals(/* { metric, groupBy, since } */) {
|
|
25
|
-
throw new Error('totals must be implemented by subclass');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Time series of a metric for one integration type.
|
|
30
|
-
* @returns {Promise<Array<{bucket: string, value: number}>>}
|
|
31
|
-
*/
|
|
32
|
-
async series(/* { metric, integrationType, from, to, bucket } */) {
|
|
33
|
-
throw new Error('series must be implemented by subclass');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
module.exports = { UsageRepositoryInterface };
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
const { UsageRepositoryPostgres } = require('./usage-repository-postgres');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mongo usage store. `UsageCounter` has no encrypted fields and only simple
|
|
5
|
-
* upsert/aggregate operations, and Prisma abstracts the query layer, so the
|
|
6
|
-
* Postgres implementation runs unchanged on the Mongo client (integrationId is
|
|
7
|
-
* stored as a plain string in both). Kept as a distinct class to preserve the
|
|
8
|
-
* repository-triad convention and give a seam if the paths ever diverge.
|
|
9
|
-
*/
|
|
10
|
-
class UsageRepositoryMongo extends UsageRepositoryPostgres {}
|
|
11
|
-
|
|
12
|
-
module.exports = { UsageRepositoryMongo };
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
const { prisma } = require('../../database/prisma');
|
|
2
|
-
const { UsageRepositoryInterface } = require('./usage-repository-interface');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Prisma-backed usage store. This is the canonical implementation;
|
|
6
|
-
* because Prisma abstracts the underlying database, the Mongo and DocumentDB
|
|
7
|
-
* adapters extend this class unchanged (see their files). All queries touch only
|
|
8
|
-
* the isolated `UsageCounter` model — never user/integration-scoped tables.
|
|
9
|
-
*/
|
|
10
|
-
class UsageRepositoryPostgres extends UsageRepositoryInterface {
|
|
11
|
-
constructor() {
|
|
12
|
-
super();
|
|
13
|
-
this.prisma = prisma;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async increment({
|
|
17
|
-
integrationId,
|
|
18
|
-
integrationType,
|
|
19
|
-
metric,
|
|
20
|
-
window,
|
|
21
|
-
value = 1,
|
|
22
|
-
}) {
|
|
23
|
-
const where = {
|
|
24
|
-
integrationId_integrationType_metric_window: {
|
|
25
|
-
integrationId,
|
|
26
|
-
integrationType,
|
|
27
|
-
metric,
|
|
28
|
-
window,
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
const upsertArgs = {
|
|
32
|
-
where,
|
|
33
|
-
create: { integrationId, integrationType, metric, window, value },
|
|
34
|
-
update: { value: { increment: value } },
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Concurrent first-insert race: two workers both INSERT and one hits the
|
|
38
|
-
// unique constraint (P2002). After the winner's insert the row exists, so
|
|
39
|
-
// a retry takes the atomic UPDATE (increment) path. Bounded so a
|
|
40
|
-
// pathological repeated race can't throw unexpectedly out of the public
|
|
41
|
-
// `recordUsageCounter` write; non-conflict errors surface immediately.
|
|
42
|
-
for (let attempt = 1; ; attempt++) {
|
|
43
|
-
try {
|
|
44
|
-
await this.prisma.usageCounter.upsert(upsertArgs);
|
|
45
|
-
return;
|
|
46
|
-
} catch (err) {
|
|
47
|
-
if (err && err.code === 'P2002' && attempt < MAX_UPSERT_ATTEMPTS) {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
throw err;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async totals({
|
|
56
|
-
metric,
|
|
57
|
-
groupBy = 'integrationType',
|
|
58
|
-
since,
|
|
59
|
-
bucket = 'day',
|
|
60
|
-
} = {}) {
|
|
61
|
-
if (!metric) {
|
|
62
|
-
throw new Error('totals requires a metric (units are per-metric)');
|
|
63
|
-
}
|
|
64
|
-
assertGroupBy(groupBy);
|
|
65
|
-
assertBucket(bucket);
|
|
66
|
-
|
|
67
|
-
// Filter to ONE window granularity — every event is written to both a
|
|
68
|
-
// day: and an hour: row, so summing across granularities would double
|
|
69
|
-
// (or worse) the true count.
|
|
70
|
-
// Bound `since` on the WINDOW key (mirrors series) — write-time
|
|
71
|
-
// updatedAt would misplace a late/redelivered increment for an earlier
|
|
72
|
-
// window, over- or under-counting the time-bounded total.
|
|
73
|
-
const where = { metric, window: { startsWith: `${bucket}:` } };
|
|
74
|
-
if (since) where.window.gte = windowKey(bucket, since);
|
|
75
|
-
|
|
76
|
-
const groups = await this.prisma.usageCounter.groupBy({
|
|
77
|
-
by: [groupBy],
|
|
78
|
-
where,
|
|
79
|
-
_sum: { value: true },
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// value is a BigInt column — coerce the sum to a JSON-safe Number
|
|
83
|
-
// (JSON.stringify throws on BigInt; counts never approach 2^53).
|
|
84
|
-
return groups.map((group) => ({
|
|
85
|
-
[groupBy]: group[groupBy],
|
|
86
|
-
value: Number(group._sum?.value ?? 0),
|
|
87
|
-
}));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async series({ metric, integrationType, from, to, bucket = 'day' } = {}) {
|
|
91
|
-
assertBucket(bucket);
|
|
92
|
-
if (!integrationType) {
|
|
93
|
-
throw new Error('series requires an integrationType');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Range-filter on the WINDOW key (write-time `updatedAt` would misplace a
|
|
97
|
-
// late increment for an earlier window). Window keys are ISO-lexicographic
|
|
98
|
-
// within a granularity, so string gte/lte gives the correct range.
|
|
99
|
-
const window = { startsWith: `${bucket}:` };
|
|
100
|
-
if (from) window.gte = windowKey(bucket, from);
|
|
101
|
-
if (to) window.lte = windowKey(bucket, to);
|
|
102
|
-
|
|
103
|
-
// Aggregate ACROSS integration instances: there is one row per
|
|
104
|
-
// (integrationId, integrationType, metric, window), so a type with many
|
|
105
|
-
// instances has many rows per window — sum them into one point.
|
|
106
|
-
const groups = await this.prisma.usageCounter.groupBy({
|
|
107
|
-
by: ['window'],
|
|
108
|
-
where: { metric, integrationType, window },
|
|
109
|
-
_sum: { value: true },
|
|
110
|
-
orderBy: { window: 'asc' },
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return groups.map((group) => ({
|
|
114
|
-
bucket: group.window,
|
|
115
|
-
value: Number(group._sum?.value ?? 0),
|
|
116
|
-
}));
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const MAX_UPSERT_ATTEMPTS = 3;
|
|
121
|
-
const VALID_GROUP_BY = new Set(['integrationType', 'metric']);
|
|
122
|
-
const VALID_BUCKETS = new Set(['day', 'hour']);
|
|
123
|
-
|
|
124
|
-
function assertGroupBy(groupBy) {
|
|
125
|
-
if (!VALID_GROUP_BY.has(groupBy)) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`Invalid groupBy "${groupBy}". Allowed: ${[...VALID_GROUP_BY].join(
|
|
128
|
-
', '
|
|
129
|
-
)}`
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function assertBucket(bucket) {
|
|
135
|
-
if (!VALID_BUCKETS.has(bucket)) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
`Invalid bucket "${bucket}". Allowed: ${[...VALID_BUCKETS].join(
|
|
138
|
-
', '
|
|
139
|
-
)}`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Window key for a date at a granularity (mirrors telemetry/usage-windows). */
|
|
145
|
-
function windowKey(bucket, date) {
|
|
146
|
-
const iso = new Date(date).toISOString();
|
|
147
|
-
return `${bucket}:${
|
|
148
|
-
bucket === 'hour' ? iso.slice(0, 13) : iso.slice(0, 10)
|
|
149
|
-
}`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
module.exports = { UsageRepositoryPostgres };
|
package/usage/tracked-metrics.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
const { isCanonicalCounter } = require('../telemetry/canonical-counters');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Compute the set of usage-counter keys the rollup should persist, from each
|
|
5
|
-
* integration's `Definition.usage` opt-in.
|
|
6
|
-
* Declaring a canonical key opts into cross-type comparison + the rollup; custom
|
|
7
|
-
* keys are tracked per integration type. Unknown canonical keys are dropped with
|
|
8
|
-
* a warning (lazy validation, matching how Definition is treated elsewhere).
|
|
9
|
-
*/
|
|
10
|
-
function computeTrackedMetrics(integrationClasses = []) {
|
|
11
|
-
const tracked = new Set();
|
|
12
|
-
if (!Array.isArray(integrationClasses)) return tracked;
|
|
13
|
-
|
|
14
|
-
for (const IntegrationClass of integrationClasses) {
|
|
15
|
-
const usage = IntegrationClass?.Definition?.usage;
|
|
16
|
-
if (!usage) continue;
|
|
17
|
-
const name = IntegrationClass.Definition?.name || 'integration';
|
|
18
|
-
|
|
19
|
-
for (const key of usage.canonical || []) {
|
|
20
|
-
if (!isCanonicalCounter(key)) {
|
|
21
|
-
console.warn(
|
|
22
|
-
`[Frigg][usage] "${name}" declares unknown canonical counter "${key}" — ignored. ` +
|
|
23
|
-
`Use a key from CANONICAL_COUNTERS or declare it under usage.custom.`
|
|
24
|
-
);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
tracked.add(key);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
for (const key of Object.keys(usage.custom || {})) {
|
|
31
|
-
tracked.add(key);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return tracked;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
module.exports = { computeTrackedMetrics };
|