@friggframework/core 2.0.0-next.102 → 2.0.0-next.103
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 +40 -0
- package/application/commands/usage-commands.js +56 -0
- package/application/index.js +10 -9
- package/core/create-handler.js +112 -10
- package/generated/prisma-mongodb/edge.js +16 -4
- package/generated/prisma-mongodb/index-browser.js +13 -1
- package/generated/prisma-mongodb/index.d.ts +1503 -105
- package/generated/prisma-mongodb/index.js +16 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +23 -0
- package/generated/prisma-mongodb/wasm.js +16 -4
- package/generated/prisma-postgresql/edge.js +16 -4
- package/generated/prisma-postgresql/index-browser.js +13 -1
- package/generated/prisma-postgresql/index.d.ts +1540 -91
- package/generated/prisma-postgresql/index.js +16 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +22 -0
- package/generated/prisma-postgresql/wasm.js +16 -4
- package/handlers/app-definition-loader.js +26 -3
- package/handlers/integration-event-dispatcher.js +29 -15
- package/handlers/routers/integration-webhook-routers.js +20 -7
- package/index.js +16 -9
- package/integrations/integration-base.js +64 -7
- package/modules/requester/requester.js +106 -5
- package/package.json +12 -5
- package/prisma-mongodb/schema.prisma +23 -0
- package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +26 -0
- package/prisma-postgresql/schema.prisma +22 -0
- package/reporting/README.md +8 -1
- package/reporting/reporting-router.js +8 -1
- package/reporting/use-cases/list-integrations-report.js +53 -6
- package/telemetry/README.md +331 -0
- package/telemetry/bind-telemetry-context.js +73 -0
- package/telemetry/canonical-counters.js +52 -0
- package/telemetry/exporters.js +85 -0
- package/telemetry/index.js +26 -0
- package/telemetry/instrument-handler.js +87 -0
- package/telemetry/no-op-telemetry.js +67 -0
- package/telemetry/north-star.js +103 -0
- package/telemetry/otel-telemetry.js +213 -0
- package/telemetry/plugin-subscribers.js +77 -0
- package/telemetry/telemetry-config.js +120 -0
- package/telemetry/telemetry-context.js +40 -0
- package/telemetry/telemetry-event-bus.js +58 -0
- package/telemetry/telemetry-runtime.js +147 -0
- package/telemetry/telemetry-service.js +51 -0
- package/telemetry/usage-rollup-subscriber.js +116 -0
- package/usage/README.md +54 -0
- package/usage/index.js +17 -0
- package/usage/repositories/usage-repository-documentdb.js +194 -0
- package/usage/repositories/usage-repository-factory.js +25 -0
- package/usage/repositories/usage-repository-interface.js +37 -0
- package/usage/repositories/usage-repository-prisma.js +146 -0
- package/usage/tracked-metrics.js +38 -0
- package/usage/usage-windows.js +24 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const { UsageRepositoryInterface } = require('./usage-repository-interface');
|
|
3
|
+
const { windowKey } = require('../usage-windows');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prisma-backed usage store — the single implementation for both mongodb and
|
|
7
|
+
* postgresql (Prisma abstracts the store; integrationId is a plain string in
|
|
8
|
+
* both). DocumentDB extends this but overrides the write/read ops with raw
|
|
9
|
+
* commands. All queries touch only the isolated `UsageCounter` model — never
|
|
10
|
+
* user/integration-scoped tables.
|
|
11
|
+
*/
|
|
12
|
+
class UsageRepositoryPrisma extends UsageRepositoryInterface {
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.prisma = prisma;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async increment({
|
|
19
|
+
integrationId,
|
|
20
|
+
integrationType,
|
|
21
|
+
metric,
|
|
22
|
+
window,
|
|
23
|
+
value = 1,
|
|
24
|
+
}) {
|
|
25
|
+
const where = {
|
|
26
|
+
integrationId_integrationType_metric_window: {
|
|
27
|
+
integrationId,
|
|
28
|
+
integrationType,
|
|
29
|
+
metric,
|
|
30
|
+
window,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const upsertArgs = {
|
|
34
|
+
where,
|
|
35
|
+
create: { integrationId, integrationType, metric, window, value },
|
|
36
|
+
update: { value: { increment: value } },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Concurrent first-insert race: two workers both INSERT and one hits the
|
|
40
|
+
// unique constraint (P2002). After the winner's insert the row exists, so
|
|
41
|
+
// a retry takes the atomic UPDATE (increment) path. Bounded so a
|
|
42
|
+
// pathological repeated race can't throw unexpectedly out of the public
|
|
43
|
+
// `recordUsageCounter` write; non-conflict errors surface immediately.
|
|
44
|
+
for (let attempt = 1; ; attempt++) {
|
|
45
|
+
try {
|
|
46
|
+
await this.prisma.usageCounter.upsert(upsertArgs);
|
|
47
|
+
return;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err && err.code === 'P2002' && attempt < MAX_UPSERT_ATTEMPTS) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getTotalsByDimension({
|
|
58
|
+
metric,
|
|
59
|
+
groupBy = 'integrationType',
|
|
60
|
+
since,
|
|
61
|
+
bucket = 'day',
|
|
62
|
+
} = {}) {
|
|
63
|
+
if (!metric) {
|
|
64
|
+
throw new Error('getTotalsByDimension requires a metric (units are per-metric)');
|
|
65
|
+
}
|
|
66
|
+
assertGroupBy(groupBy);
|
|
67
|
+
assertBucket(bucket);
|
|
68
|
+
|
|
69
|
+
// Filter to ONE window granularity — every event is written to both a
|
|
70
|
+
// day: and an hour: row, so summing across granularities would double
|
|
71
|
+
// (or worse) the true count.
|
|
72
|
+
// Bound `since` on the WINDOW key (mirrors getTimeSeries) — write-time
|
|
73
|
+
// updatedAt would misplace a late/redelivered increment for an earlier
|
|
74
|
+
// window, over- or under-counting the time-bounded total.
|
|
75
|
+
const where = { metric, window: { startsWith: `${bucket}:` } };
|
|
76
|
+
if (since) where.window.gte = windowKey(bucket, since);
|
|
77
|
+
|
|
78
|
+
const groups = await this.prisma.usageCounter.groupBy({
|
|
79
|
+
by: [groupBy],
|
|
80
|
+
where,
|
|
81
|
+
_sum: { value: true },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// value is a BigInt column — coerce the sum to a JSON-safe Number
|
|
85
|
+
// (JSON.stringify throws on BigInt; counts never approach 2^53).
|
|
86
|
+
return groups.map((group) => ({
|
|
87
|
+
[groupBy]: group[groupBy],
|
|
88
|
+
value: Number(group._sum?.value ?? 0),
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getTimeSeries({ metric, integrationType, from, to, bucket = 'day' } = {}) {
|
|
93
|
+
assertBucket(bucket);
|
|
94
|
+
if (!integrationType) {
|
|
95
|
+
throw new Error('getTimeSeries requires an integrationType');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Range-filter on the WINDOW key (write-time `updatedAt` would misplace a
|
|
99
|
+
// late increment for an earlier window). Window keys are ISO-lexicographic
|
|
100
|
+
// within a granularity, so string gte/lte gives the correct range.
|
|
101
|
+
const window = { startsWith: `${bucket}:` };
|
|
102
|
+
if (from) window.gte = windowKey(bucket, from);
|
|
103
|
+
if (to) window.lte = windowKey(bucket, to);
|
|
104
|
+
|
|
105
|
+
// Aggregate ACROSS integration instances: there is one row per
|
|
106
|
+
// (integrationId, integrationType, metric, window), so a type with many
|
|
107
|
+
// instances has many rows per window — sum them into one point.
|
|
108
|
+
const groups = await this.prisma.usageCounter.groupBy({
|
|
109
|
+
by: ['window'],
|
|
110
|
+
where: { metric, integrationType, window },
|
|
111
|
+
_sum: { value: true },
|
|
112
|
+
orderBy: { window: 'asc' },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return groups.map((group) => ({
|
|
116
|
+
bucket: group.window,
|
|
117
|
+
value: Number(group._sum?.value ?? 0),
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const MAX_UPSERT_ATTEMPTS = 3;
|
|
123
|
+
const VALID_GROUP_BY = new Set(['integrationType', 'metric']);
|
|
124
|
+
const VALID_BUCKETS = new Set(['day', 'hour']);
|
|
125
|
+
|
|
126
|
+
function assertGroupBy(groupBy) {
|
|
127
|
+
if (!VALID_GROUP_BY.has(groupBy)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Invalid groupBy "${groupBy}". Allowed: ${[...VALID_GROUP_BY].join(
|
|
130
|
+
', '
|
|
131
|
+
)}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertBucket(bucket) {
|
|
137
|
+
if (!VALID_BUCKETS.has(bucket)) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Invalid bucket "${bucket}". Allowed: ${[...VALID_BUCKETS].join(
|
|
140
|
+
', '
|
|
141
|
+
)}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { UsageRepositoryPrisma };
|
|
@@ -0,0 +1,38 @@
|
|
|
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 };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollup window keys. A window key is `<granularity>:<UTC-truncated-timestamp>`
|
|
3
|
+
* (e.g. `day:2026-07-05`, `hour:2026-07-05T14`), prefixed by granularity so the
|
|
4
|
+
* store can filter a series by bucket (`window startsWith 'day:'`) and range on
|
|
5
|
+
* the ISO-lexicographic keys. Shared by the rollup writer and the read
|
|
6
|
+
* repositories so the write/read formats can never drift.
|
|
7
|
+
*/
|
|
8
|
+
function windowKey(bucket, date = new Date()) {
|
|
9
|
+
const iso = new Date(date).toISOString();
|
|
10
|
+
return `${bucket}:${
|
|
11
|
+
bucket === 'hour' ? iso.slice(0, 13) : iso.slice(0, 10)
|
|
12
|
+
}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The window keys a usage event falls into — one per granularity.
|
|
17
|
+
* @param {Date} [date] Defaults to now.
|
|
18
|
+
* @returns {string[]} e.g. ['day:2026-07-05', 'hour:2026-07-05T14']
|
|
19
|
+
*/
|
|
20
|
+
function computeUsageWindows(date = new Date()) {
|
|
21
|
+
return [windowKey('day', date), windowKey('hour', date)];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { computeUsageWindows, windowKey };
|