@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,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Declarative plugin/extension telemetry taps.
|
|
3
|
-
*
|
|
4
|
-
* The built-in usage rollup and North Star derivation subscribe to the telemetry
|
|
5
|
-
* bus in framework code; this lets an ADOPTER subscribe declaratively from the
|
|
6
|
-
* app definition (`Definition.telemetry.subscribers: [...]`) — forward to a
|
|
7
|
-
* custom sink, compute aggregates, persist counters — without writing wiring code.
|
|
8
|
-
*
|
|
9
|
-
* A subscriber is one of:
|
|
10
|
-
* - a **factory** `fn(telemetry)` that registers itself (e.g. calls
|
|
11
|
-
* `telemetry.on('metric', ...)`) and MAY return an unsubscribe function; or
|
|
12
|
-
* - a **declarative object** `{ event?: 'metric'|'event', handler(payload, eventType) }`
|
|
13
|
-
* — when `event` is omitted the handler receives both metric and event payloads.
|
|
14
|
-
*
|
|
15
|
-
* Every attach and every handler invocation is guarded so a misbehaving
|
|
16
|
-
* subscriber can never break emission, its siblings, or a handler (mirrors the
|
|
17
|
-
* bus's own per-subscriber isolation).
|
|
18
|
-
*
|
|
19
|
-
* @param {object} params
|
|
20
|
-
* @param {object} params.telemetry Telemetry service exposing `on(eventType, cb)`.
|
|
21
|
-
* @param {Array<Function|{event?: string, handler: Function}>} [params.subscribers]
|
|
22
|
-
* @returns {Array<Function>} unsubscribe functions for the wired subscribers.
|
|
23
|
-
*/
|
|
24
|
-
const EVENT_TYPES = ['metric', 'event'];
|
|
25
|
-
|
|
26
|
-
function wireTelemetrySubscribers({ telemetry, subscribers = [] } = {}) {
|
|
27
|
-
if (
|
|
28
|
-
!telemetry ||
|
|
29
|
-
typeof telemetry.on !== 'function' ||
|
|
30
|
-
!Array.isArray(subscribers)
|
|
31
|
-
) {
|
|
32
|
-
return [];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const unsubscribes = [];
|
|
36
|
-
|
|
37
|
-
for (const subscriber of subscribers) {
|
|
38
|
-
try {
|
|
39
|
-
if (typeof subscriber === 'function') {
|
|
40
|
-
const off = subscriber(telemetry);
|
|
41
|
-
if (typeof off === 'function') unsubscribes.push(off);
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (subscriber && typeof subscriber.handler === 'function') {
|
|
46
|
-
const events = subscriber.event
|
|
47
|
-
? [subscriber.event]
|
|
48
|
-
: EVENT_TYPES;
|
|
49
|
-
for (const eventType of events) {
|
|
50
|
-
const off = telemetry.on(eventType, (payload) => {
|
|
51
|
-
try {
|
|
52
|
-
subscriber.handler(payload, eventType);
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.warn(
|
|
55
|
-
`[Frigg][telemetry] subscriber handler for "${eventType}" threw: ${
|
|
56
|
-
error && error.message
|
|
57
|
-
}`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
if (typeof off === 'function') unsubscribes.push(off);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
// A single bad subscriber must never break wiring of the others.
|
|
66
|
-
console.warn(
|
|
67
|
-
`[Frigg][telemetry] failed to wire a telemetry subscriber: ${
|
|
68
|
-
error && error.message
|
|
69
|
-
}`
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return unsubscribes;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
module.exports = { wireTelemetrySubscribers };
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolves and validates the telemetry section of an app definition
|
|
3
|
-
* into a normalized config the TelemetryService and
|
|
4
|
-
* usage rollup consume: `{ exporter, northStar, sampleRatio }`.
|
|
5
|
-
*
|
|
6
|
-
* Exporter default is keyed off STAGE: only a genuinely LOCAL run (STAGE=local)
|
|
7
|
-
* gets `console` (visible, free); every deployed stage — including `dev` —
|
|
8
|
-
* defaults to `none`. `dev` is a deployed AWS stage, and `console` there would
|
|
9
|
-
* write full spans to CloudWatch by default (cost + a data-exposure surface).
|
|
10
|
-
* Adopters opt into `console`/`otlp` explicitly for deployed stages.
|
|
11
|
-
*/
|
|
12
|
-
const VALID_EXPORTER_TYPES = [
|
|
13
|
-
'none',
|
|
14
|
-
'noop',
|
|
15
|
-
'console',
|
|
16
|
-
'otlp',
|
|
17
|
-
'honeycomb',
|
|
18
|
-
'datadog',
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
function resolveExporter(exporter, stage) {
|
|
22
|
-
if (!exporter) {
|
|
23
|
-
return stage === 'local' ? { type: 'console' } : { type: 'none' };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Pre-built exporter instances (tests / advanced adopters) bypass type checks.
|
|
27
|
-
if (exporter.traceExporter || exporter.metricExporter) {
|
|
28
|
-
return exporter;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (!VALID_EXPORTER_TYPES.includes(exporter.type)) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
`[Frigg][telemetry] invalid exporter type "${exporter.type}". ` +
|
|
34
|
-
`Supported: ${VALID_EXPORTER_TYPES.join(', ')}`
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return exporter;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function resolveSampleRatio(sampleRatio) {
|
|
41
|
-
if (sampleRatio === undefined || sampleRatio === null) return 1;
|
|
42
|
-
if (
|
|
43
|
-
typeof sampleRatio !== 'number' ||
|
|
44
|
-
Number.isNaN(sampleRatio) ||
|
|
45
|
-
sampleRatio < 0 ||
|
|
46
|
-
sampleRatio > 1
|
|
47
|
-
) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`[Frigg][telemetry] sampleRatio must be a number in [0,1], got ${sampleRatio}`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
return sampleRatio;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function validateNorthStarEntry(entry, where) {
|
|
56
|
-
if (!entry || typeof entry.name !== 'string' || !entry.name) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`[Frigg][telemetry] northStar.${where} must reference a counter { name }`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Adopter-declared telemetry subscribers (Decision 6). Each is either a factory
|
|
65
|
-
* function `fn(telemetry)` or a declarative `{ event?, handler }` object. Kept as
|
|
66
|
-
* an array so `wireTelemetrySubscribers` can attach them to the bus at runtime.
|
|
67
|
-
*/
|
|
68
|
-
function resolveSubscribers(subscribers) {
|
|
69
|
-
if (subscribers === undefined || subscribers === null) return [];
|
|
70
|
-
if (!Array.isArray(subscribers)) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
'[Frigg][telemetry] subscribers must be an array of functions or { handler } objects'
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
for (const subscriber of subscribers) {
|
|
76
|
-
const ok =
|
|
77
|
-
typeof subscriber === 'function' ||
|
|
78
|
-
(subscriber && typeof subscriber.handler === 'function');
|
|
79
|
-
if (!ok) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
'[Frigg][telemetry] each subscriber must be a function or an object with a handler function'
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return subscribers;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function resolveNorthStar(northStar) {
|
|
89
|
-
if (!northStar) return null;
|
|
90
|
-
if (northStar.default) validateNorthStarEntry(northStar.default, 'default');
|
|
91
|
-
if (northStar.byType) {
|
|
92
|
-
for (const [type, entry] of Object.entries(northStar.byType)) {
|
|
93
|
-
validateNorthStarEntry(entry, `byType.${type}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return northStar;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @param {object} appDefinition The backend `Definition` export.
|
|
101
|
-
* @param {object} [ctx]
|
|
102
|
-
* @param {string} [ctx.stage] Deployment stage (defaults to STAGE/NODE_ENV).
|
|
103
|
-
* @returns {{ exporter: object, northStar: object|null, sampleRatio: number }}
|
|
104
|
-
*/
|
|
105
|
-
function resolveTelemetryConfig(appDefinition = {}, ctx = {}) {
|
|
106
|
-
const stage =
|
|
107
|
-
ctx.stage || process.env.STAGE || process.env.NODE_ENV || 'production';
|
|
108
|
-
const telemetry = appDefinition.telemetry || {};
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
exporter: resolveExporter(telemetry.exporter, stage),
|
|
112
|
-
northStar: resolveNorthStar(telemetry.northStar),
|
|
113
|
-
sampleRatio: resolveSampleRatio(telemetry.sampleRatio),
|
|
114
|
-
subscribers: resolveSubscribers(telemetry.subscribers),
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
module.exports = { resolveTelemetryConfig };
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Ambient telemetry context. A single process-wide AsyncLocalStorage
|
|
5
|
-
* holds the standard identifiers ({integrationId, integrationType, userId,
|
|
6
|
-
* version, ...}) for the duration of a handler, set once at the handler seam via
|
|
7
|
-
* `telemetry.withContext(...)`. Any emission during that async scope — including
|
|
8
|
-
* deep calls like an API-module request — reads the ambient context, so the
|
|
9
|
-
* usage rollup can attribute counters per-integration WITHOUT threading context
|
|
10
|
-
* through every call and WITHOUT relying on OTel baggage (which does not exist
|
|
11
|
-
* on the no-op path). This is the propagation mechanism the plan intended.
|
|
12
|
-
*
|
|
13
|
-
* Only bus-payload `context` is populated from here; high-cardinality ids never
|
|
14
|
-
* touch OTel metric labels.
|
|
15
|
-
*/
|
|
16
|
-
const store = new AsyncLocalStorage();
|
|
17
|
-
|
|
18
|
-
function runWithTelemetryContext(context, fn) {
|
|
19
|
-
return store.run(context || {}, fn);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getTelemetryContextStore() {
|
|
23
|
-
return store.getStore() || null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Merge the ambient context with an explicit per-call context (explicit keys
|
|
28
|
-
* win, e.g. a request `url`). Returns undefined when there is nothing to attach.
|
|
29
|
-
*/
|
|
30
|
-
function mergeTelemetryContext(explicit) {
|
|
31
|
-
const ambient = store.getStore();
|
|
32
|
-
if (!ambient && !explicit) return undefined;
|
|
33
|
-
return { ...(ambient || {}), ...(explicit || {}) };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
module.exports = {
|
|
37
|
-
runWithTelemetryContext,
|
|
38
|
-
getTelemetryContextStore,
|
|
39
|
-
mergeTelemetryContext,
|
|
40
|
-
};
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TelemetryEventBus — the internal event stream telemetry flows onto.
|
|
3
|
-
* Plugins/extensions and the built-in usage rollup
|
|
4
|
-
* subscribe here; it is deliberately independent of OTel export (Decision 7),
|
|
5
|
-
* so usage counters persist even when the OTel exporter is a no-op.
|
|
6
|
-
*
|
|
7
|
-
* This is intentionally a **synchronous** in-process fan-out (Frigg runs one
|
|
8
|
-
* invocation per Lambda container): a plain subscriber list with per-subscriber
|
|
9
|
-
* `try/catch` so a misbehaving subscriber can never break emission or its
|
|
10
|
-
* siblings. No queue/backpressure machinery — emission volume is bounded by a
|
|
11
|
-
* single invocation's work.
|
|
12
|
-
*
|
|
13
|
-
* ## Public event contract (semver-stable)
|
|
14
|
-
* Event types and their payload shapes:
|
|
15
|
-
* - `'metric'` → `{ name: string, value: number, attributes: object, context?: object }`
|
|
16
|
-
* - `'event'` → `{ name: string, attributes: object, context?: object }`
|
|
17
|
-
* `attributes` are the bounded OTel metric labels. `context` (present when an
|
|
18
|
-
* ambient telemetry context is active) carries the high-cardinality identifiers
|
|
19
|
-
* — {integrationId, integrationType, userId, version, ...} plus per-call extras
|
|
20
|
-
* like a request `url` — which the usage rollup reads for attribution. Those ids
|
|
21
|
-
* NEVER appear in `attributes` (Cardinality note).
|
|
22
|
-
*/
|
|
23
|
-
function createTelemetryEventBus() {
|
|
24
|
-
/** @type {Map<string, Set<Function>>} */
|
|
25
|
-
const subscribers = new Map();
|
|
26
|
-
|
|
27
|
-
function on(eventType, callback) {
|
|
28
|
-
if (!subscribers.has(eventType)) {
|
|
29
|
-
subscribers.set(eventType, new Set());
|
|
30
|
-
}
|
|
31
|
-
const set = subscribers.get(eventType);
|
|
32
|
-
set.add(callback);
|
|
33
|
-
return function off() {
|
|
34
|
-
set.delete(callback);
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function emit(eventType, payload) {
|
|
39
|
-
const set = subscribers.get(eventType);
|
|
40
|
-
if (!set || set.size === 0) return;
|
|
41
|
-
for (const callback of set) {
|
|
42
|
-
try {
|
|
43
|
-
callback(payload);
|
|
44
|
-
} catch (error) {
|
|
45
|
-
// A subscriber must never break emission or its siblings.
|
|
46
|
-
console.warn(
|
|
47
|
-
`[Frigg][telemetry] subscriber for "${eventType}" threw: ${
|
|
48
|
-
error && error.message
|
|
49
|
-
}`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return { on, emit };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
module.exports = { createTelemetryEventBus };
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
const { createNoOpTelemetry } = require('./no-op-telemetry');
|
|
2
|
-
const { createTelemetryEventBus } = require('./telemetry-event-bus');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Determine whether an exporter descriptor means "emit nothing".
|
|
6
|
-
* Absent config or `{ type: 'none' }` (or `'noop'`) → no-op.
|
|
7
|
-
*/
|
|
8
|
-
function isNoOpExporter(exporter) {
|
|
9
|
-
if (!exporter) return true;
|
|
10
|
-
const type = exporter.type;
|
|
11
|
-
return !type || type === 'none' || type === 'noop';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Create the vendor-neutral telemetry service.
|
|
16
|
-
*
|
|
17
|
-
* Integration code uses the returned object (`this.telemetry`) and never imports
|
|
18
|
-
* a backend SDK. When no exporter is configured the service is a pure no-op that
|
|
19
|
-
* loads zero OpenTelemetry modules; a real exporter lazily initialises the OTel
|
|
20
|
-
* SDK (see `./otel-telemetry`).
|
|
21
|
-
*
|
|
22
|
-
* @param {object} [options]
|
|
23
|
-
* @param {object} [options.exporter] Exporter descriptor, e.g. `{ type: 'otlp', endpoint }`.
|
|
24
|
-
* @param {object} [options.resource] Resource attributes, e.g. `{ service, stage }`.
|
|
25
|
-
* @param {number} [options.sampleRatio] Parent-based sampling ratio (0..1).
|
|
26
|
-
* @param {object} [options.bus] Event bus to reuse (defaults to a fresh one).
|
|
27
|
-
*/
|
|
28
|
-
function createTelemetry(options = {}) {
|
|
29
|
-
// The internal event stream is always on and independent of OTel export:
|
|
30
|
-
// the usage rollup + plugin taps must work even when
|
|
31
|
-
// the OTel exporter is a no-op. The bus is pure JS, so this does not load
|
|
32
|
-
// any OTel module on the no-op path.
|
|
33
|
-
const bus = options.bus || createTelemetryEventBus();
|
|
34
|
-
|
|
35
|
-
if (isNoOpExporter(options.exporter)) {
|
|
36
|
-
return createNoOpTelemetry({ bus });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Lazy-required so the no-op path never loads the OTel SDK.
|
|
40
|
-
const { createOtelTelemetry } = require('./otel-telemetry');
|
|
41
|
-
return createOtelTelemetry({ ...options, bus });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
module.exports = { createTelemetry, isNoOpExporter };
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
const { createTelemetry } = require('./telemetry-service');
|
|
2
|
-
const { createNoOpTelemetry } = require('./no-op-telemetry');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Process-wide telemetry singleton. Created lazily on first use and
|
|
6
|
-
* reused for the life of the Lambda container, so the OTel SDK initialises at
|
|
7
|
-
* most once per cold start. Building it never throws — any failure (e.g. no app
|
|
8
|
-
* definition on disk in a unit test) falls back to the no-op so a handler is
|
|
9
|
-
* never broken by telemetry setup.
|
|
10
|
-
*/
|
|
11
|
-
let instance = null;
|
|
12
|
-
|
|
13
|
-
function getTelemetry() {
|
|
14
|
-
if (instance) return instance;
|
|
15
|
-
try {
|
|
16
|
-
// Lazy-required to avoid a load-time cycle with the handlers layer.
|
|
17
|
-
const {
|
|
18
|
-
loadAppDefinition,
|
|
19
|
-
} = require('../handlers/app-definition-loader');
|
|
20
|
-
const { telemetry = {} } = loadAppDefinition();
|
|
21
|
-
const stage = process.env.STAGE || process.env.NODE_ENV || 'production';
|
|
22
|
-
instance = createTelemetry({
|
|
23
|
-
exporter: telemetry.exporter,
|
|
24
|
-
sampleRatio: telemetry.sampleRatio,
|
|
25
|
-
resource: {
|
|
26
|
-
service: process.env.FRIGG_STACK || 'frigg',
|
|
27
|
-
stage,
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
} catch (_) {
|
|
31
|
-
instance = createNoOpTelemetry();
|
|
32
|
-
}
|
|
33
|
-
return instance;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Test-only: drop the cached instance so the next getTelemetry rebuilds it. */
|
|
37
|
-
function resetTelemetryForTests() {
|
|
38
|
-
instance = null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
module.exports = { getTelemetry, resetTelemetryForTests };
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
const { createUsageRollupSubscriber } = require('./usage-rollup-subscriber');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Process-wide usage-rollup subscriber. Built once per cold start,
|
|
5
|
-
* subscribed to the telemetry singleton's bus, and reused across invocations.
|
|
6
|
-
* Returns null when usage is disabled (no integration declares Definition.usage)
|
|
7
|
-
* or when the app definition can't be loaded — the handler then skips the flush.
|
|
8
|
-
* Never throws.
|
|
9
|
-
*/
|
|
10
|
-
let subscriber = null;
|
|
11
|
-
let initialized = false;
|
|
12
|
-
|
|
13
|
-
function getUsageRollupSubscriber() {
|
|
14
|
-
if (initialized) return subscriber;
|
|
15
|
-
initialized = true;
|
|
16
|
-
try {
|
|
17
|
-
const {
|
|
18
|
-
loadAppDefinition,
|
|
19
|
-
} = require('../handlers/app-definition-loader');
|
|
20
|
-
const { computeTrackedMetrics } = require('../usage/tracked-metrics');
|
|
21
|
-
const {
|
|
22
|
-
northStarKeys,
|
|
23
|
-
createNorthStarDerivationSubscriber,
|
|
24
|
-
} = require('./north-star');
|
|
25
|
-
const { integrations = [], telemetry = {} } = loadAppDefinition();
|
|
26
|
-
|
|
27
|
-
// Track both Definition.usage opt-ins and any North Star counter keys,
|
|
28
|
-
// so a derived North Star persists even without an explicit usage decl.
|
|
29
|
-
const trackedMetrics = computeTrackedMetrics(integrations);
|
|
30
|
-
for (const key of northStarKeys(telemetry.northStar)) {
|
|
31
|
-
trackedMetrics.add(key);
|
|
32
|
-
}
|
|
33
|
-
if (trackedMetrics.size === 0) {
|
|
34
|
-
subscriber = null;
|
|
35
|
-
return subscriber;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const { getTelemetry } = require('./telemetry-singleton');
|
|
39
|
-
const {
|
|
40
|
-
createUsageRepository,
|
|
41
|
-
} = require('../usage/repositories/usage-repository-factory');
|
|
42
|
-
const telemetryService = getTelemetry();
|
|
43
|
-
|
|
44
|
-
subscriber = createUsageRollupSubscriber({
|
|
45
|
-
telemetry: telemetryService,
|
|
46
|
-
usageRepository: createUsageRepository(),
|
|
47
|
-
trackedMetrics,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// North Star derived-from-trace: emits the North Star counter when a
|
|
51
|
-
// configured signal matches; the emission flows to the rollup above.
|
|
52
|
-
if (telemetry.northStar) {
|
|
53
|
-
createNorthStarDerivationSubscriber({
|
|
54
|
-
telemetry: telemetryService,
|
|
55
|
-
northStar: telemetry.northStar,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
} catch (error) {
|
|
59
|
-
// Usage disabled for this process — surface it so operators can tell
|
|
60
|
-
// "usage off" from "usage broken" (e.g. unresolved DB_TYPE).
|
|
61
|
-
console.warn(
|
|
62
|
-
`[Frigg][usage] rollup disabled: ${error && error.message}`
|
|
63
|
-
);
|
|
64
|
-
subscriber = null;
|
|
65
|
-
}
|
|
66
|
-
return subscriber;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function resetUsageRollupForTests() {
|
|
70
|
-
subscriber = null;
|
|
71
|
-
initialized = false;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
module.exports = { getUsageRollupSubscriber, resetUsageRollupForTests };
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
const { computeUsageWindows } = require('./usage-windows');
|
|
2
|
-
|
|
3
|
-
const WEBHOOK_EVENT_NAMES = new Set(['ON_WEBHOOK']);
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Framework auto-signal metric names → the canonical usage key they feed.
|
|
7
|
-
* Resolvers receive (attributes, context) and may return null to
|
|
8
|
-
* decline. Handler invocations map by event: USER_ACTION → user_actions; the
|
|
9
|
-
* DB-connected `ON_WEBHOOK` queue dispatch → webhooks.received (per-integration,
|
|
10
|
-
* and where a durable write is actually possible — the HTTP receipt handler is
|
|
11
|
-
* DB-free so its buffer is discarded).
|
|
12
|
-
*/
|
|
13
|
-
const METRIC_TO_CANONICAL = {
|
|
14
|
-
'frigg.apimodule.requests': () => 'api.requests',
|
|
15
|
-
'frigg.handler.invocations': (attrs, ctx) => {
|
|
16
|
-
if (attrs && attrs.event === 'USER_ACTION') return 'user_actions';
|
|
17
|
-
if (ctx && WEBHOOK_EVENT_NAMES.has(ctx.event_name)) {
|
|
18
|
-
return 'webhooks.received';
|
|
19
|
-
}
|
|
20
|
-
return null;
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The built-in usage-rollup subscriber. Subscribes to the
|
|
26
|
-
* telemetry event bus and folds *declared* counters (canonical or custom) into
|
|
27
|
-
* the durable usage store. It buffers within an invocation and writes on
|
|
28
|
-
* `flush()` (no timers — Lambda-safe), or drops the buffer on `discard()` for an
|
|
29
|
-
* SQS redelivery so at-least-once delivery does not double-count (approximate
|
|
30
|
-
* accuracy contract).
|
|
31
|
-
*
|
|
32
|
-
* Attribution: high-cardinality ids never ride metric labels, so integrationId
|
|
33
|
-
* comes from the bus-only `context` (populated from the ambient handler context)
|
|
34
|
-
* and otherwise falls back to the bounded integration_type.
|
|
35
|
-
*/
|
|
36
|
-
function createUsageRollupSubscriber({
|
|
37
|
-
telemetry,
|
|
38
|
-
usageRepository,
|
|
39
|
-
trackedMetrics = new Set(),
|
|
40
|
-
now = () => new Date(),
|
|
41
|
-
}) {
|
|
42
|
-
// Buffer key is a JSON tuple [integrationId, integrationType, metric, window]
|
|
43
|
-
// — collision-proof for developer-defined custom keys / names.
|
|
44
|
-
let buffer = new Map();
|
|
45
|
-
|
|
46
|
-
function resolveUsageKey(name, attributes, context) {
|
|
47
|
-
if (trackedMetrics.has(name)) return name;
|
|
48
|
-
const resolver = METRIC_TO_CANONICAL[name];
|
|
49
|
-
const canonical = resolver ? resolver(attributes || {}, context) : null;
|
|
50
|
-
return canonical && trackedMetrics.has(canonical) ? canonical : null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function onMetric(payload) {
|
|
54
|
-
try {
|
|
55
|
-
const { name, value = 1, attributes = {}, context } = payload;
|
|
56
|
-
const metric = resolveUsageKey(name, attributes, context);
|
|
57
|
-
if (!metric) return;
|
|
58
|
-
|
|
59
|
-
const integrationType =
|
|
60
|
-
context?.integrationType || attributes.integration_type || null;
|
|
61
|
-
if (!integrationType) return; // cannot attribute — skip
|
|
62
|
-
|
|
63
|
-
const integrationId = context?.integrationId || integrationType;
|
|
64
|
-
|
|
65
|
-
for (const window of computeUsageWindows(now())) {
|
|
66
|
-
const key = JSON.stringify([
|
|
67
|
-
integrationId,
|
|
68
|
-
integrationType,
|
|
69
|
-
metric,
|
|
70
|
-
window,
|
|
71
|
-
]);
|
|
72
|
-
buffer.set(key, (buffer.get(key) || 0) + value);
|
|
73
|
-
}
|
|
74
|
-
} catch (_) {
|
|
75
|
-
// Rollup must never break emission.
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const unsubscribe = telemetry.on('metric', onMetric);
|
|
80
|
-
|
|
81
|
-
async function flush() {
|
|
82
|
-
if (buffer.size === 0) return;
|
|
83
|
-
const pending = buffer;
|
|
84
|
-
buffer = new Map();
|
|
85
|
-
for (const [key, value] of pending) {
|
|
86
|
-
const [integrationId, integrationType, metric, window] =
|
|
87
|
-
JSON.parse(key);
|
|
88
|
-
try {
|
|
89
|
-
await usageRepository.increment({
|
|
90
|
-
integrationId,
|
|
91
|
-
integrationType,
|
|
92
|
-
metric,
|
|
93
|
-
window,
|
|
94
|
-
value,
|
|
95
|
-
});
|
|
96
|
-
} catch (_) {
|
|
97
|
-
// A single failed write must not abort the rest of the flush.
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function discard() {
|
|
103
|
-
buffer = new Map();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
flush,
|
|
108
|
-
discard,
|
|
109
|
-
unsubscribe,
|
|
110
|
-
get bufferSize() {
|
|
111
|
-
return buffer.size;
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
module.exports = { createUsageRollupSubscriber, METRIC_TO_CANONICAL };
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compute the rollup window keys a usage event falls into.
|
|
3
|
-
* Windows are UTC and prefixed by their granularity so the store can filter a
|
|
4
|
-
* series by bucket (`window startsWith 'day:'`).
|
|
5
|
-
*
|
|
6
|
-
* @param {Date} [date] Defaults to now.
|
|
7
|
-
* @returns {string[]} e.g. ['day:2026-07-05', 'hour:2026-07-05T14']
|
|
8
|
-
*/
|
|
9
|
-
function computeUsageWindows(date = new Date()) {
|
|
10
|
-
const iso = date.toISOString(); // 2026-07-05T14:23:00.000Z
|
|
11
|
-
return [`day:${iso.slice(0, 10)}`, `hour:${iso.slice(0, 13)}`];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
module.exports = { computeUsageWindows };
|
package/usage/README.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# Usage Store
|
|
2
|
-
|
|
3
|
-
The durable, Frigg-owned store behind ADR-011 feature-usage tracking. Holds
|
|
4
|
-
per-integration counters that the reporting endpoint reads for cross-integration
|
|
5
|
-
comparison and trend series.
|
|
6
|
-
|
|
7
|
-
Populated by the telemetry usage rollup and read via `frigg.usage.*` — see
|
|
8
|
-
[`telemetry/README.md`](../telemetry/README.md) for the full guide, config, and
|
|
9
|
-
usage examples. This README covers the store internals only.
|
|
10
|
-
|
|
11
|
-
## Fact row
|
|
12
|
-
|
|
13
|
-
`UsageCounter { integrationId, integrationType, metric, window, value, updatedAt }`,
|
|
14
|
-
uniquely keyed by `(integrationId, integrationType, metric, window)`. `window` is
|
|
15
|
-
`day:YYYY-MM-DD` or `hour:YYYY-MM-DDTHH` (UTC).
|
|
16
|
-
|
|
17
|
-
## Isolation (ADR-010 Decision 3)
|
|
18
|
-
|
|
19
|
-
The store is deliberately isolated from user/integration-scoped data:
|
|
20
|
-
|
|
21
|
-
- **No `userId`** and **no foreign key** to `Integration` — a user-scoped query
|
|
22
|
-
can never return a usage row, and usage history survives integration deletion.
|
|
23
|
-
- **Not** in the encryption registry — dimensions are bounded and non-sensitive.
|
|
24
|
-
|
|
25
|
-
## Repository triad
|
|
26
|
-
|
|
27
|
-
Mirrors the reporting/process pattern — an interface plus PostgreSQL, MongoDB and
|
|
28
|
-
DocumentDB adapters selected by `DB_TYPE`:
|
|
29
|
-
|
|
30
|
-
```js
|
|
31
|
-
const { createUsageRepository } = require('@friggframework/core'); // usage-repository-factory
|
|
32
|
-
|
|
33
|
-
class UsageRepositoryInterface {
|
|
34
|
-
async increment({ integrationId, integrationType, metric, window, value }) {} // atomic upsert
|
|
35
|
-
async totals({ metric, groupBy, since, bucket }) {} // comparison (one window granularity)
|
|
36
|
-
async series({ metric, integrationType, from, to, bucket }) {} // trend (aggregated across instances)
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
- **`increment`** is atomic: PostgreSQL uses Prisma `upsert` with
|
|
41
|
-
`{ value: { increment } }`; a concurrent first-insert race (`P2002`) retries
|
|
42
|
-
once onto the atomic update path.
|
|
43
|
-
- **`totals`** filters to a single window granularity (default `day`) so day and
|
|
44
|
-
hour rows are never double-summed. Requires a `metric`; `groupBy` is
|
|
45
|
-
allow-listed to `integrationType` / `metric`.
|
|
46
|
-
- **`series`** aggregates across integration instances (`groupBy(window) + sum`)
|
|
47
|
-
and range-filters on the window key. Requires an `integrationType`.
|
|
48
|
-
|
|
49
|
-
> **DocumentDB:** the adapter overrides increment/totals/series with raw commands
|
|
50
|
-
> (`$runCommandRaw`: a `$inc` upsert via `documentdb-utils.updateOne`, and a
|
|
51
|
-
> cursor-drained `$aggregate` `$group/$sum`) — matching every other DocumentDB
|
|
52
|
-
> adapter, since Prisma's Mongo engine emits upsert/groupBy shapes DocumentDB
|
|
53
|
-
> rejects and cursor reads truncate at ~101 docs. Command shapes are unit-tested;
|
|
54
|
-
> run an end-to-end check against a real cluster before GA.
|
package/usage/index.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
createUsageRepository,
|
|
3
|
-
UsageRepositoryMongo,
|
|
4
|
-
UsageRepositoryPostgres,
|
|
5
|
-
UsageRepositoryDocumentDB,
|
|
6
|
-
} = require('./repositories/usage-repository-factory');
|
|
7
|
-
const {
|
|
8
|
-
UsageRepositoryInterface,
|
|
9
|
-
} = require('./repositories/usage-repository-interface');
|
|
10
|
-
const { computeTrackedMetrics } = require('./tracked-metrics');
|
|
11
|
-
|
|
12
|
-
module.exports = {
|
|
13
|
-
createUsageRepository,
|
|
14
|
-
computeTrackedMetrics,
|
|
15
|
-
UsageRepositoryInterface,
|
|
16
|
-
UsageRepositoryMongo,
|
|
17
|
-
UsageRepositoryPostgres,
|
|
18
|
-
UsageRepositoryDocumentDB,
|
|
19
|
-
};
|