@friggframework/core 2.0.0--canary.622.5e1352f.0 → 2.0.0--canary.622.16ee19d.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.
@@ -14,8 +14,9 @@ const {
14
14
  createUsageRepository,
15
15
  } = require('../../usage/repositories/usage-repository-factory');
16
16
  const { computeUsageWindows } = require('../../telemetry/usage-windows');
17
+ const { resolveNorthStarEntry } = require('../../telemetry/north-star');
17
18
 
18
- function createUsageCommands({ usageRepository } = {}) {
19
+ function createUsageCommands({ usageRepository, northStar = null } = {}) {
19
20
  const repository = usageRepository || createUsageRepository();
20
21
 
21
22
  return {
@@ -50,6 +51,25 @@ function createUsageCommands({ usageRepository } = {}) {
50
51
  async series(args) {
51
52
  return repository.series(args);
52
53
  },
54
+
55
+ /**
56
+ * First-class North Star read (ADR-011 Decision 5). Resolves the
57
+ * configured counter for an integration type (byType wins over default),
58
+ * then returns its totals from the durable usage store. Returns `null`
59
+ * when no North Star is configured, so callers can branch without
60
+ * knowing the counter key. Trends read via `series({ metric })`.
61
+ */
62
+ async northStar({ integrationType, since, groupBy = 'integrationType', bucket } = {}) {
63
+ const entry = resolveNorthStarEntry(northStar, integrationType);
64
+ if (!entry) return null;
65
+ const totals = await repository.totals({
66
+ metric: entry.name,
67
+ groupBy,
68
+ since,
69
+ bucket,
70
+ });
71
+ return { metric: entry.name, totals };
72
+ },
53
73
  };
54
74
  }
55
75
 
@@ -8,6 +8,21 @@ const { createCredentialCommands } = require('./commands/credential-commands');
8
8
  const { createProcessCommands } = require('./commands/process-commands');
9
9
  const { createSchedulerCommands } = require('./commands/scheduler-commands');
10
10
  const { createUsageCommands } = require('./commands/usage-commands');
11
+ const { loadAppDefinition } = require('../handlers/app-definition-loader');
12
+
13
+ /**
14
+ * Resolve the adopter's North Star config from the app definition so
15
+ * `frigg.usage.northStar(...)` can read it (ADR-011 Decision 5). Guarded: a
16
+ * missing/unloadable app definition (e.g. in unit tests) must never break the
17
+ * command factory — usage reads simply have no North Star.
18
+ */
19
+ function resolveNorthStarConfig() {
20
+ try {
21
+ return loadAppDefinition().telemetry?.northStar ?? null;
22
+ } catch (_) {
23
+ return null;
24
+ }
25
+ }
11
26
 
12
27
  /**
13
28
  * Create a unified command factory with all CRUD operations
@@ -53,7 +68,7 @@ function createFriggCommands({ integrationClass }) {
53
68
  ...processCommands,
54
69
 
55
70
  // Usage read/write (ADR-011) — nested to match `frigg.usage.*`
56
- usage: createUsageCommands(),
71
+ usage: createUsageCommands({ northStar: resolveNorthStarConfig() }),
57
72
  };
58
73
  }
59
74
 
@@ -8,6 +8,9 @@ const { getTelemetry } = require('../telemetry/telemetry-singleton');
8
8
  const {
9
9
  getUsageRollupSubscriber,
10
10
  } = require('../telemetry/usage-rollup-singleton');
11
+ const {
12
+ getPluginTelemetrySubscribers,
13
+ } = require('../telemetry/plugin-subscribers-singleton');
11
14
 
12
15
  // Bounds the tail latency telemetry adds to every warm invocation. Kept low so
13
16
  // an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
@@ -139,6 +142,11 @@ const createHandler = (optionByName = {}) => {
139
142
  ? usageRollup
140
143
  : getUsageRollupSubscriber();
141
144
 
145
+ // Wire adopter-declared telemetry subscribers once per cold start
146
+ // (ADR-011 Decision 6). Memoized in the singleton, so this is a cheap
147
+ // no-op after the first invocation.
148
+ getPluginTelemetrySubscribers();
149
+
142
150
  try {
143
151
  console.info(`[createHandler] ${eventName}: handler entry`, {
144
152
  eventName,
@@ -54,6 +54,7 @@ function loadAppDefinition() {
54
54
  exporter: { type: 'none' },
55
55
  northStar: null,
56
56
  sampleRatio: 1,
57
+ subscribers: [],
57
58
  };
58
59
  }
59
60
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.622.5e1352f.0",
4
+ "version": "2.0.0--canary.622.16ee19d.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -45,9 +45,9 @@
45
45
  }
46
46
  },
47
47
  "devDependencies": {
48
- "@friggframework/eslint-config": "2.0.0--canary.622.5e1352f.0",
49
- "@friggframework/prettier-config": "2.0.0--canary.622.5e1352f.0",
50
- "@friggframework/test": "2.0.0--canary.622.5e1352f.0",
48
+ "@friggframework/eslint-config": "2.0.0--canary.622.16ee19d.0",
49
+ "@friggframework/prettier-config": "2.0.0--canary.622.16ee19d.0",
50
+ "@friggframework/test": "2.0.0--canary.622.16ee19d.0",
51
51
  "@prisma/client": "^6.19.3",
52
52
  "@types/lodash": "4.17.15",
53
53
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -87,5 +87,5 @@
87
87
  "publishConfig": {
88
88
  "access": "public"
89
89
  },
90
- "gitHead": "5e1352f5b615a8d48df800bcb9559d3fa9b0178b"
90
+ "gitHead": "16ee19d11c975c256a996503bafc70c062440ff0"
91
91
  }
@@ -200,12 +200,45 @@ telemetry: {
200
200
  }
201
201
  ```
202
202
 
203
- Read it like any counter: `frigg.usage.totals({ metric: 'contacts_synced' })`.
203
+ Read it as a first-class metric without knowing the configured key — the North
204
+ Star resolves per integration type (`byType` wins over `default`):
205
+
206
+ ```js
207
+ // Resolves the configured counter for the type, then returns its totals.
208
+ await frigg.usage.northStar({ integrationType: 'hubspot', since: daysAgo(30) });
209
+ // → { metric: 'contacts_synced', totals: [{ integrationType: 'hubspot', value: 900 }] }
210
+ // → null when no North Star is configured (caller branches without knowing keys)
211
+ ```
212
+
213
+ Or read it like any counter once you know the key:
214
+ `frigg.usage.totals({ metric: 'contacts_synced' })`; trends via `frigg.usage.series({ metric })`.
204
215
 
205
216
  ## Plugin / extension tap
206
217
 
207
- Telemetry flows onto an internal event stream. Subscribe to forward to a custom
208
- sink or compute aggregates:
218
+ Telemetry flows onto an internal event stream (independent of OTel export, so
219
+ taps fire even with `exporter: none`). Two ways to subscribe:
220
+
221
+ **Declarative (app definition)** — the framework wires these once per cold start,
222
+ each guarded so a bad subscriber can't break emission or its siblings:
223
+
224
+ ```js
225
+ // backend/index.js
226
+ const Definition = {
227
+ telemetry: {
228
+ subscribers: [
229
+ // (a) declarative object — `event` optional; omit to receive both:
230
+ { event: 'metric', handler: ({ name, value, attributes, context }) => {
231
+ forwardToStatsd(name, value, attributes);
232
+ } },
233
+ // (b) factory — gets the telemetry service, registers itself, may
234
+ // return an unsubscribe:
235
+ (telemetry) => telemetry.on('event', (payload) => auditSink.write(payload)),
236
+ ],
237
+ },
238
+ };
239
+ ```
240
+
241
+ **Imperative** — subscribe from anywhere that runs at startup:
209
242
 
210
243
  ```js
211
244
  const { getTelemetry } = require('@friggframework/core');
@@ -24,6 +24,11 @@ const {
24
24
  CANONICAL_COUNTERS,
25
25
  isCanonicalCounter,
26
26
  } = require('./canonical-counters');
27
+ const { wireTelemetrySubscribers } = require('./plugin-subscribers');
28
+ const {
29
+ getPluginTelemetrySubscribers,
30
+ resetPluginTelemetrySubscribersForTests,
31
+ } = require('./plugin-subscribers-singleton');
27
32
 
28
33
  module.exports = {
29
34
  createTelemetry,
@@ -45,4 +50,7 @@ module.exports = {
45
50
  createNorthStarDerivationSubscriber,
46
51
  CANONICAL_COUNTERS,
47
52
  isCanonicalCounter,
53
+ wireTelemetrySubscribers,
54
+ getPluginTelemetrySubscribers,
55
+ resetPluginTelemetrySubscribersForTests,
48
56
  };
@@ -0,0 +1,51 @@
1
+ const { wireTelemetrySubscribers } = require('./plugin-subscribers');
2
+
3
+ /**
4
+ * Process-wide wiring of adopter-declared telemetry subscribers (ADR-011
5
+ * Decision 6). Built once per cold start and reused for the life of the Lambda
6
+ * container, mirroring the usage-rollup singleton. Independent of the usage
7
+ * rollup: adopter taps wire even when no integration declares `Definition.usage`.
8
+ * Never throws — a bad/absent app definition just means no adopter taps.
9
+ */
10
+ let wired = null;
11
+ let initialized = false;
12
+
13
+ function getPluginTelemetrySubscribers() {
14
+ if (initialized) return wired;
15
+ initialized = true;
16
+ try {
17
+ const {
18
+ loadAppDefinition,
19
+ } = require('../handlers/app-definition-loader');
20
+ const { telemetry = {} } = loadAppDefinition();
21
+ const subscribers = telemetry.subscribers || [];
22
+ if (subscribers.length === 0) {
23
+ wired = [];
24
+ return wired;
25
+ }
26
+
27
+ const { getTelemetry } = require('./telemetry-singleton');
28
+ wired = wireTelemetrySubscribers({
29
+ telemetry: getTelemetry(),
30
+ subscribers,
31
+ });
32
+ } catch (error) {
33
+ console.warn(
34
+ `[Frigg][telemetry] plugin subscribers disabled: ${
35
+ error && error.message
36
+ }`
37
+ );
38
+ wired = [];
39
+ }
40
+ return wired;
41
+ }
42
+
43
+ function resetPluginTelemetrySubscribersForTests() {
44
+ wired = null;
45
+ initialized = false;
46
+ }
47
+
48
+ module.exports = {
49
+ getPluginTelemetrySubscribers,
50
+ resetPluginTelemetrySubscribersForTests,
51
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Declarative plugin/extension telemetry taps (ADR-011 Decision 6).
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 };
@@ -60,6 +60,31 @@ function validateNorthStarEntry(entry, where) {
60
60
  }
61
61
  }
62
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
+
63
88
  function resolveNorthStar(northStar) {
64
89
  if (!northStar) return null;
65
90
  if (northStar.default) validateNorthStarEntry(northStar.default, 'default');
@@ -86,6 +111,7 @@ function resolveTelemetryConfig(appDefinition = {}, ctx = {}) {
86
111
  exporter: resolveExporter(telemetry.exporter, stage),
87
112
  northStar: resolveNorthStar(telemetry.northStar),
88
113
  sampleRatio: resolveSampleRatio(telemetry.sampleRatio),
114
+ subscribers: resolveSubscribers(telemetry.subscribers),
89
115
  };
90
116
  }
91
117