@friggframework/core 2.0.0--canary.517.2bfd339.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.
Files changed (58) hide show
  1. package/README.md +40 -0
  2. package/application/commands/usage-commands.js +76 -0
  3. package/application/index.js +23 -9
  4. package/core/create-handler.js +103 -10
  5. package/generated/prisma-mongodb/edge.js +16 -4
  6. package/generated/prisma-mongodb/index-browser.js +13 -1
  7. package/generated/prisma-mongodb/index.d.ts +1479 -105
  8. package/generated/prisma-mongodb/index.js +16 -4
  9. package/generated/prisma-mongodb/package.json +1 -1
  10. package/generated/prisma-mongodb/schema.prisma +22 -0
  11. package/generated/prisma-mongodb/wasm.js +16 -4
  12. package/generated/prisma-postgresql/edge.js +16 -4
  13. package/generated/prisma-postgresql/index-browser.js +13 -1
  14. package/generated/prisma-postgresql/index.d.ts +1465 -91
  15. package/generated/prisma-postgresql/index.js +16 -4
  16. package/generated/prisma-postgresql/package.json +1 -1
  17. package/generated/prisma-postgresql/schema.prisma +21 -0
  18. package/generated/prisma-postgresql/wasm.js +16 -4
  19. package/handlers/app-definition-loader.js +26 -3
  20. package/handlers/integration-event-dispatcher.js +34 -15
  21. package/handlers/routers/integration-webhook-routers.js +23 -7
  22. package/index.js +11 -9
  23. package/integrations/integration-base.js +80 -20
  24. package/modules/requester/requester.js +97 -4
  25. package/package.json +12 -5
  26. package/prisma-mongodb/schema.prisma +22 -0
  27. package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +26 -0
  28. package/prisma-postgresql/schema.prisma +21 -0
  29. package/reporting/README.md +8 -1
  30. package/reporting/reporting-router.js +16 -1
  31. package/reporting/use-cases/list-integrations-report.js +61 -6
  32. package/telemetry/README.md +301 -0
  33. package/telemetry/bind-telemetry-context.js +51 -0
  34. package/telemetry/canonical-counters.js +52 -0
  35. package/telemetry/exporters/exporter-factory.js +79 -0
  36. package/telemetry/index.js +56 -0
  37. package/telemetry/instrument-handler.js +82 -0
  38. package/telemetry/no-op-telemetry.js +66 -0
  39. package/telemetry/north-star.js +103 -0
  40. package/telemetry/otel-telemetry.js +215 -0
  41. package/telemetry/plugin-subscribers-singleton.js +51 -0
  42. package/telemetry/plugin-subscribers.js +77 -0
  43. package/telemetry/telemetry-config.js +118 -0
  44. package/telemetry/telemetry-context.js +40 -0
  45. package/telemetry/telemetry-event-bus.js +58 -0
  46. package/telemetry/telemetry-service.js +44 -0
  47. package/telemetry/telemetry-singleton.js +41 -0
  48. package/telemetry/usage-rollup-singleton.js +74 -0
  49. package/telemetry/usage-rollup-subscriber.js +116 -0
  50. package/telemetry/usage-windows.js +14 -0
  51. package/usage/README.md +52 -0
  52. package/usage/index.js +19 -0
  53. package/usage/repositories/usage-repository-documentdb.js +15 -0
  54. package/usage/repositories/usage-repository-factory.js +28 -0
  55. package/usage/repositories/usage-repository-interface.js +37 -0
  56. package/usage/repositories/usage-repository-mongo.js +12 -0
  57. package/usage/repositories/usage-repository-postgres.js +142 -0
  58. package/usage/tracked-metrics.js +38 -0
package/README.md CHANGED
@@ -200,6 +200,46 @@ const secureData = cryptor.encrypt(JSON.stringify({
200
200
  }));
201
201
  ```
202
202
 
203
+ ### 4b. Telemetry & Usage Tracking (`/telemetry`, `/usage`)
204
+
205
+ Vendor-neutral OpenTelemetry observability plus durable, per-integration usage
206
+ counters (ADR-011). No-op by default (zero cold-start cost); framework seams are
207
+ auto-instrumented so integrations get handler/API-module/webhook metrics for free.
208
+
209
+ **Usage:**
210
+ ```javascript
211
+ // App definition — turn on export + declare a North Star:
212
+ const Definition = {
213
+ name: 'my-app',
214
+ telemetry: {
215
+ exporter: { type: 'otlp', endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT },
216
+ northStar: { default: { name: 'records.synced' } },
217
+ },
218
+ };
219
+
220
+ // Integration code — custom metrics/spans (this.telemetry is auto-tagged):
221
+ await this.telemetry.span('delta_sync', async () => {
222
+ this.telemetry.count('records.synced', batch.length, { entity: 'contact' });
223
+ });
224
+
225
+ // Declare which usage counters an integration reports (opts into reporting):
226
+ class HubSpotIntegration extends IntegrationBase {
227
+ static Definition = {
228
+ name: 'hubspot',
229
+ usage: { canonical: ['records.synced', 'api.requests'] },
230
+ };
231
+ }
232
+
233
+ // Read the durable usage store (never an APM):
234
+ const frigg = createFriggCommands({ integrationClass: HubSpotIntegration });
235
+ await frigg.usage.totals({ metric: 'records.synced', groupBy: 'integrationType' });
236
+ ```
237
+
238
+ **See:** [`telemetry/README.md`](telemetry/README.md) for the full guide
239
+ (exporters, custom metrics, the Usage-Counter contract, North Star, the plugin
240
+ tap, cardinality rules, and caveats) and [`usage/README.md`](usage/README.md) for
241
+ the store internals.
242
+
203
243
  ### 5. Error Handling (`/errors`)
204
244
 
205
245
  Standardized error types with proper HTTP status codes.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Usage Commands
3
+ *
4
+ * Application Layer — the read/write surface over the durable usage store
5
+ * (ADR-011 §5). Exposed as `frigg.usage.*` on the unified command object so
6
+ * reports and integration code query usage without touching the repository.
7
+ *
8
+ * @example
9
+ * const frigg = createFriggCommands({ integrationClass: MyIntegration });
10
+ * await frigg.usage.totals({ metric: 'records.synced', groupBy: 'integrationType', since });
11
+ * await frigg.usage.series({ metric: 'records.synced', integrationType: 'hubspot', bucket: 'day' });
12
+ */
13
+ const {
14
+ createUsageRepository,
15
+ } = require('../../usage/repositories/usage-repository-factory');
16
+ const { computeUsageWindows } = require('../../telemetry/usage-windows');
17
+ const { resolveNorthStarEntry } = require('../../telemetry/north-star');
18
+
19
+ function createUsageCommands({ usageRepository, northStar = null } = {}) {
20
+ const repository = usageRepository || createUsageRepository();
21
+
22
+ return {
23
+ /**
24
+ * Record a usage counter for a point in time. Callers pass `at` (a Date,
25
+ * default now) — NOT a raw window key — and both the day and hour windows
26
+ * are derived, matching how the auto-rollup persists so series() reads
27
+ * back consistently at either granularity.
28
+ */
29
+ async recordUsageCounter({
30
+ integrationId,
31
+ integrationType,
32
+ metric,
33
+ value = 1,
34
+ at = new Date(),
35
+ }) {
36
+ for (const window of computeUsageWindows(at)) {
37
+ await repository.increment({
38
+ integrationId,
39
+ integrationType,
40
+ metric,
41
+ window,
42
+ value,
43
+ });
44
+ }
45
+ },
46
+
47
+ async totals(args) {
48
+ return repository.totals(args);
49
+ },
50
+
51
+ async series(args) {
52
+ return repository.series(args);
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
+ },
73
+ };
74
+ }
75
+
76
+ module.exports = { createUsageCommands };
@@ -4,15 +4,25 @@ const {
4
4
  } = require('./commands/integration-commands');
5
5
  const { createUserCommands } = require('./commands/user-commands');
6
6
  const { createEntityCommands } = require('./commands/entity-commands');
7
- const {
8
- createCredentialCommands,
9
- } = require('./commands/credential-commands');
10
- const {
11
- createProcessCommands,
12
- } = require('./commands/process-commands');
13
- const {
14
- createSchedulerCommands,
15
- } = require('./commands/scheduler-commands');
7
+ const { createCredentialCommands } = require('./commands/credential-commands');
8
+ const { createProcessCommands } = require('./commands/process-commands');
9
+ const { createSchedulerCommands } = require('./commands/scheduler-commands');
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
+ }
16
26
 
17
27
  /**
18
28
  * Create a unified command factory with all CRUD operations
@@ -56,6 +66,9 @@ function createFriggCommands({ integrationClass }) {
56
66
 
57
67
  // Process commands
58
68
  ...processCommands,
69
+
70
+ // Usage read/write (ADR-011) — nested to match `frigg.usage.*`
71
+ usage: createUsageCommands({ northStar: resolveNorthStarConfig() }),
59
72
  };
60
73
  }
61
74
 
@@ -70,6 +83,7 @@ module.exports = {
70
83
  createCredentialCommands,
71
84
  createProcessCommands,
72
85
  createSchedulerCommands,
86
+ createUsageCommands,
73
87
 
74
88
  // Legacy standalone function
75
89
  findIntegrationContextByExternalEntityId,
@@ -4,6 +4,82 @@
4
4
 
5
5
  const { initDebugLog, flushDebugLog } = require('../logs');
6
6
  const { secretsToEnv } = require('./secrets-to-env');
7
+ const { getTelemetry } = require('../telemetry/telemetry-singleton');
8
+ const {
9
+ getUsageRollupSubscriber,
10
+ } = require('../telemetry/usage-rollup-singleton');
11
+ const {
12
+ getPluginTelemetrySubscribers,
13
+ } = require('../telemetry/plugin-subscribers-singleton');
14
+
15
+ // Bounds the tail latency telemetry adds to every warm invocation. Kept low so
16
+ // an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
17
+ // most this, not multiple seconds. Override with OTEL_FLUSH_TIMEOUT_MS.
18
+ const DEFAULT_FLUSH_TIMEOUT_MS =
19
+ Number(process.env.OTEL_FLUSH_TIMEOUT_MS) || 500;
20
+
21
+ /**
22
+ * Fold the invocation's buffered usage counters into the durable store, then
23
+ * clear the buffer. On an SQS redelivery (ApproximateReceiveCount > 1) we
24
+ * DISCARD rather than flush — the prior delivery already counted, and the usage
25
+ * accuracy contract is "approximate, skip obvious redeliveries". Fully guarded.
26
+ */
27
+ async function flushUsageRollup(subscriber, eventSummary, shouldUseDatabase) {
28
+ if (!subscriber) return;
29
+ try {
30
+ // Persisting usage requires a DB connection. DB-free handlers (e.g. the
31
+ // webhook-receipt route) never called connectPrisma, so drop the buffer
32
+ // instead of issuing a connectionless Prisma write.
33
+ if (!shouldUseDatabase) {
34
+ subscriber.discard();
35
+ return;
36
+ }
37
+ const redelivered =
38
+ Array.isArray(eventSummary?.records) &&
39
+ eventSummary.records.some((r) => Number(r.receiveCount) > 1);
40
+ if (redelivered) {
41
+ subscriber.discard();
42
+ } else {
43
+ await subscriber.flush();
44
+ }
45
+ } catch (_) {
46
+ // Usage rollup must never break the handler.
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Flush telemetry before the Lambda container freezes. Because
52
+ * `callbackWaitsForEmptyEventLoop=false` (below) stops the event loop the moment
53
+ * the handler returns, OTel's timer-driven batch processors would never fire —
54
+ * so spans/metrics must be flushed synchronously here. Bounded by a timeout so a
55
+ * stalled exporter can never block the response, and fully guarded so a flush
56
+ * failure never breaks the handler.
57
+ */
58
+ async function flushTelemetry(telemetry, timeoutMs) {
59
+ try {
60
+ if (
61
+ !telemetry ||
62
+ typeof telemetry.isEnabled !== 'function' ||
63
+ !telemetry.isEnabled()
64
+ ) {
65
+ return;
66
+ }
67
+ let timer;
68
+ const deadline = new Promise((resolve) => {
69
+ timer = setTimeout(resolve, timeoutMs);
70
+ });
71
+ try {
72
+ await Promise.race([
73
+ Promise.resolve(telemetry.forceFlush()),
74
+ deadline,
75
+ ]);
76
+ } finally {
77
+ clearTimeout(timer);
78
+ }
79
+ } catch (_) {
80
+ // Telemetry flush must never break the handler.
81
+ }
82
+ }
7
83
 
8
84
  // Best-effort extraction of correlation identifiers from a Lambda event.
9
85
  // For SQS: pulls messageIds + parsed event/processId/integrationId from each
@@ -36,8 +112,7 @@ const summarizeLambdaEvent = (event) => {
36
112
  if (event.httpMethod || event.requestContext?.http) {
37
113
  return {
38
114
  source: 'http',
39
- method:
40
- event.httpMethod || event.requestContext?.http?.method,
115
+ method: event.httpMethod || event.requestContext?.http?.method,
41
116
  path: event.path || event.rawPath,
42
117
  };
43
118
  }
@@ -50,6 +125,9 @@ const createHandler = (optionByName = {}) => {
50
125
  isUserFacingResponse = true,
51
126
  method,
52
127
  shouldUseDatabase = true,
128
+ telemetry,
129
+ flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,
130
+ usageRollup,
53
131
  } = optionByName;
54
132
 
55
133
  if (!method) {
@@ -58,16 +136,23 @@ const createHandler = (optionByName = {}) => {
58
136
 
59
137
  return async (event, context) => {
60
138
  const eventSummary = summarizeLambdaEvent(event);
139
+ const activeTelemetry = telemetry || getTelemetry();
140
+ const activeUsageRollup =
141
+ usageRollup !== undefined
142
+ ? usageRollup
143
+ : getUsageRollupSubscriber();
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();
61
149
 
62
150
  try {
63
- console.info(
64
- `[createHandler] ${eventName}: handler entry`,
65
- {
66
- eventName,
67
- awsRequestId: context?.awsRequestId,
68
- ...eventSummary,
69
- }
70
- );
151
+ console.info(`[createHandler] ${eventName}: handler entry`, {
152
+ eventName,
153
+ awsRequestId: context?.awsRequestId,
154
+ ...eventSummary,
155
+ });
71
156
 
72
157
  initDebugLog(eventName, event);
73
158
 
@@ -139,6 +224,14 @@ const createHandler = (optionByName = {}) => {
139
224
 
140
225
  // Here we can just rethrow and let AWS build the response.
141
226
  throw error;
227
+ } finally {
228
+ // Flush telemetry + usage before the container freezes.
229
+ await flushTelemetry(activeTelemetry, flushTimeoutMs);
230
+ await flushUsageRollup(
231
+ activeUsageRollup,
232
+ eventSummary,
233
+ shouldUseDatabase
234
+ );
142
235
  }
143
236
  };
144
237
  };