@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.
Files changed (59) hide show
  1. package/README.md +0 -40
  2. package/application/index.js +10 -15
  3. package/core/create-handler.js +10 -114
  4. package/generated/prisma-mongodb/edge.js +4 -16
  5. package/generated/prisma-mongodb/index-browser.js +1 -13
  6. package/generated/prisma-mongodb/index.d.ts +96 -1495
  7. package/generated/prisma-mongodb/index.js +4 -16
  8. package/generated/prisma-mongodb/package.json +1 -1
  9. package/generated/prisma-mongodb/schema.prisma +0 -24
  10. package/generated/prisma-mongodb/wasm.js +4 -16
  11. package/generated/prisma-postgresql/edge.js +4 -16
  12. package/generated/prisma-postgresql/index-browser.js +1 -13
  13. package/generated/prisma-postgresql/index.d.ts +91 -1541
  14. package/generated/prisma-postgresql/index.js +4 -16
  15. package/generated/prisma-postgresql/package.json +1 -1
  16. package/generated/prisma-postgresql/schema.prisma +0 -23
  17. package/generated/prisma-postgresql/wasm.js +4 -16
  18. package/handlers/app-definition-loader.js +3 -26
  19. package/handlers/integration-event-dispatcher.js +15 -34
  20. package/handlers/routers/integration-webhook-routers.js +7 -23
  21. package/index.js +9 -11
  22. package/integrations/integration-base.js +23 -88
  23. package/modules/module.js +15 -2
  24. package/modules/requester/requester.js +46 -111
  25. package/package.json +5 -12
  26. package/prisma-mongodb/schema.prisma +0 -24
  27. package/prisma-postgresql/schema.prisma +0 -23
  28. package/reporting/README.md +1 -8
  29. package/reporting/reporting-router.js +1 -16
  30. package/reporting/use-cases/list-integrations-report.js +6 -61
  31. package/application/commands/usage-commands.js +0 -77
  32. package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +0 -26
  33. package/telemetry/README.md +0 -317
  34. package/telemetry/bind-telemetry-context.js +0 -51
  35. package/telemetry/canonical-counters.js +0 -52
  36. package/telemetry/exporters/exporter-factory.js +0 -79
  37. package/telemetry/index.js +0 -56
  38. package/telemetry/instrument-handler.js +0 -82
  39. package/telemetry/no-op-telemetry.js +0 -66
  40. package/telemetry/north-star.js +0 -103
  41. package/telemetry/otel-telemetry.js +0 -215
  42. package/telemetry/plugin-subscribers-singleton.js +0 -51
  43. package/telemetry/plugin-subscribers.js +0 -77
  44. package/telemetry/telemetry-config.js +0 -118
  45. package/telemetry/telemetry-context.js +0 -40
  46. package/telemetry/telemetry-event-bus.js +0 -58
  47. package/telemetry/telemetry-service.js +0 -44
  48. package/telemetry/telemetry-singleton.js +0 -41
  49. package/telemetry/usage-rollup-singleton.js +0 -74
  50. package/telemetry/usage-rollup-subscriber.js +0 -116
  51. package/telemetry/usage-windows.js +0 -14
  52. package/usage/README.md +0 -54
  53. package/usage/index.js +0 -19
  54. package/usage/repositories/usage-repository-documentdb.js +0 -199
  55. package/usage/repositories/usage-repository-factory.js +0 -28
  56. package/usage/repositories/usage-repository-interface.js +0 -37
  57. package/usage/repositories/usage-repository-mongo.js +0 -12
  58. package/usage/repositories/usage-repository-postgres.js +0 -152
  59. package/usage/tracked-metrics.js +0 -38
@@ -18,9 +18,6 @@ const {
18
18
  UpdateIntegrationConfig,
19
19
  } = require('./use-cases/update-integration-config');
20
20
  const { validateExtensionBinding } = require('./extension');
21
- const { getTelemetry } = require('../telemetry/telemetry-singleton');
22
- const { instrumentHandler } = require('../telemetry/instrument-handler');
23
- const { bindTelemetryContext } = require('../telemetry/bind-telemetry-context');
24
21
 
25
22
  const constantsToBeMigrated = {
26
23
  defaultEvents: {
@@ -79,11 +76,6 @@ class IntegrationBase {
79
76
  // Tier 3 Integration Extensions — see packages/core/integrations/EXTENSIONS.md
80
77
  // Shape: { [bindingName]: { extension, handlers?: { [eventName]: methodName } } }
81
78
  extensions: {},
82
- // Usage-counter opt-in. Declaring a canonical key opts into the
83
- // cross-integration comparison report + durable rollup; custom keys are
84
- // comparable within this integration type. Shape:
85
- // usage: { canonical: ['records.synced', ...], custom: { 'deals.enriched': { unit, label } } }
86
- usage: {},
87
79
  display: {
88
80
  name: 'Integration Name',
89
81
  logo: '',
@@ -108,28 +100,18 @@ class IntegrationBase {
108
100
  this.messages = { errors: [], warnings: [] };
109
101
  this._isHydrated = false;
110
102
 
111
- // Telemetry: every instance carries the service so integration
112
- // code can call `this.telemetry.*`. Extracted before the record check so
113
- // passing only `telemetry` never triggers a hollow hydration. Bound to
114
- // this instance so emissions auto-carry `integration_type` for the usage
115
- // rollup (see bind-telemetry-context).
116
- const { telemetry, ...recordParams } = params;
117
- this.telemetry = bindTelemetryContext(telemetry || getTelemetry(), () =>
118
- this.getTelemetryContext()
119
- );
120
-
121
- if (Object.keys(recordParams).length > 0) {
103
+ if (params && Object.keys(params).length > 0) {
122
104
  this.setIntegrationRecord({
123
105
  record: {
124
- id: recordParams.id,
125
- userId: recordParams.userId,
126
- entities: recordParams.entities,
127
- config: recordParams.config,
128
- status: recordParams.status,
129
- version: recordParams.version,
130
- messages: recordParams.messages,
106
+ id: params.id,
107
+ userId: params.userId,
108
+ entities: params.entities,
109
+ config: params.config,
110
+ status: params.status,
111
+ version: params.version,
112
+ messages: params.messages,
131
113
  },
132
- modules: recordParams.modules || [],
114
+ modules: params.modules || [],
133
115
  });
134
116
  }
135
117
 
@@ -222,25 +204,6 @@ class IntegrationBase {
222
204
  };
223
205
 
224
206
  this._isHydrated = Boolean(this.id);
225
-
226
- // Log the instance-open exactly once per hydrated
227
- // instance, carrying the standard identifier set — so an integration is
228
- // visible in telemetry even on a path that never dispatches a handler.
229
- // High-cardinality ids ride the bus context (3rd arg), never metric
230
- // labels; only the bounded integration_type is an attribute.
231
- if (this._isHydrated && !this._instantiationLogged) {
232
- this._instantiationLogged = true;
233
- try {
234
- this.telemetry.event(
235
- 'frigg.integration.instantiated',
236
- { integration_type: this.constructor?.Definition?.name },
237
- this.getTelemetryContext()
238
- );
239
- } catch (_) {
240
- // Telemetry must never break integration hydration.
241
- }
242
- }
243
-
244
207
  return this;
245
208
  }
246
209
 
@@ -248,25 +211,6 @@ class IntegrationBase {
248
211
  return this._isHydrated;
249
212
  }
250
213
 
251
- /**
252
- * Standard telemetry identifier set. Assembled from the
253
- * hydrated record (integrationId, userId, version), the static Definition
254
- * (integrationType, version fallback), and the environment (stage, appName).
255
- * High-cardinality ids (integrationId, userId) ride span baggage only — never
256
- * metric labels.
257
- */
258
- getTelemetryContext() {
259
- const Definition = this.constructor.Definition || {};
260
- return {
261
- integrationId: this.id ?? null,
262
- integrationType: Definition.name ?? null,
263
- userId: this.userId ?? null,
264
- version: this.version ?? Definition.version ?? null,
265
- stage: process.env.STAGE || process.env.NODE_ENV || null,
266
- appName: process.env.FRIGG_STACK || null,
267
- };
268
- }
269
-
270
214
  assertHydrated(message = 'Integration instance is not hydrated') {
271
215
  if (!this.isHydrated) {
272
216
  throw new Error(message);
@@ -292,16 +236,13 @@ class IntegrationBase {
292
236
  // e.g., 'quo-attio' → 'quo', 'attio' → 'attio'
293
237
  const moduleNameToKey = {};
294
238
  if (this.constructor.Definition?.modules) {
295
- for (const [key, moduleConfig] of Object.entries(
296
- this.constructor.Definition.modules
297
- )) {
239
+ for (const [key, moduleConfig] of Object.entries(this.constructor.Definition.modules)) {
298
240
  const definition = moduleConfig.definition;
299
241
  if (definition) {
300
242
  // Use getName() if available, fallback to moduleName
301
- const definitionName =
302
- typeof definition.getName === 'function'
303
- ? definition.getName()
304
- : definition.moduleName;
243
+ const definitionName = typeof definition.getName === 'function'
244
+ ? definition.getName()
245
+ : definition.moduleName;
305
246
  if (definitionName) {
306
247
  moduleNameToKey[definitionName] = key;
307
248
  }
@@ -386,9 +327,7 @@ class IntegrationBase {
386
327
  try {
387
328
  const authPassed = await this[module].testAuth();
388
329
  if (!authPassed) {
389
- throw new Error(
390
- `testAuth returned false for module ${module}`
391
- );
330
+ throw new Error(`testAuth returned false for module ${module}`);
392
331
  }
393
332
  } catch {
394
333
  didAuthPass = false;
@@ -645,9 +584,7 @@ class IntegrationBase {
645
584
  async persistStatus(status) {
646
585
  await this.updateIntegrationStatus.execute(this.id, status);
647
586
  this.status = status;
648
- console.log(
649
- `[Frigg] Integration ${this.id} status changed to ${status}`
650
- );
587
+ console.log(`[Frigg] Integration ${this.id} status changed to ${status}`);
651
588
  }
652
589
 
653
590
  /**
@@ -812,15 +749,7 @@ class IntegrationBase {
812
749
  `Event ${event} is not defined in the Integration event object`
813
750
  );
814
751
  }
815
- // Auto-instrument. This is the seam for user
816
- // actions, config-options, and lifecycle events dispatched via `this.on`
817
- // (the queue/webhook/route paths go through IntegrationEventDispatcher).
818
- return instrumentHandler(
819
- this.telemetry,
820
- this.getTelemetryContext(),
821
- { event, eventType: this.on[event].type },
822
- () => this.on[event].handler.call(this, object)
823
- );
752
+ return this.on[event].handler.call(this, object);
824
753
  }
825
754
 
826
755
  getOptionDetails() {
@@ -862,12 +791,18 @@ class IntegrationBase {
862
791
  if (!this.id) return;
863
792
 
864
793
  if (delegateString === 'CREDENTIAL_INVALIDATED') {
794
+ const detail =
795
+ object?.reason || object?.statusCode
796
+ ? ` (status ${object?.statusCode ?? '?'}: ${
797
+ object?.reason ?? 'no reason given'
798
+ })`
799
+ : '';
865
800
  console.log(
866
801
  `[Frigg] Module ${
867
802
  notifier?.name || '?'
868
803
  } reported invalid credentials for integration ${
869
804
  this.id
870
- } — marking ERROR`
805
+ } — marking ERROR${detail}`
871
806
  );
872
807
  await this.persistStatus('ERROR');
873
808
  return;
package/modules/module.js CHANGED
@@ -147,15 +147,24 @@ class Module extends Delegate {
147
147
  } else if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) {
148
148
  await this.deauthorize();
149
149
  } else if (delegateString === this.api.DLGT_INVALID_AUTH) {
150
- await this.markCredentialsInvalid();
150
+ await this.markCredentialsInvalid(object);
151
151
  }
152
152
  }
153
153
 
154
- async markCredentialsInvalid() {
154
+ async markCredentialsInvalid(diagnosticInfo = null) {
155
155
  if (!this.credential) return;
156
156
 
157
157
  if (!this.credential.id) return;
158
158
 
159
+ if (diagnosticInfo) {
160
+ console.error(
161
+ `[Frigg] Module ${this.name} credentials rejected (status ${
162
+ diagnosticInfo.statusCode ?? '?'
163
+ }):`,
164
+ diagnosticInfo.message ?? diagnosticInfo
165
+ );
166
+ }
167
+
159
168
  await this.credentialRepository.updateAuthenticationStatus(
160
169
  this.credential.id,
161
170
  false
@@ -181,6 +190,10 @@ class Module extends Delegate {
181
190
  await this.notify(this.DLGT_CREDENTIAL_INVALIDATED, {
182
191
  credentialId: this.credential.id,
183
192
  moduleName: this.name,
193
+ ...(diagnosticInfo && {
194
+ reason: diagnosticInfo.message,
195
+ statusCode: diagnosticInfo.statusCode,
196
+ }),
184
197
  });
185
198
  } catch (err) {
186
199
  console.error(
@@ -2,7 +2,6 @@ const fetch = require('node-fetch');
2
2
  const { Delegate } = require('../../core');
3
3
  const { FetchError } = require('../../errors');
4
4
  const { get } = require('../../assertions');
5
- const { getTelemetry } = require('../../telemetry/telemetry-singleton');
6
5
 
7
6
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
8
7
 
@@ -43,49 +42,6 @@ class Requester extends Delegate {
43
42
  // Allow passing in the fetch function
44
43
  // Instance methods can use this.fetch without differentiating
45
44
  this.fetch = get(params, 'fetch', fetch);
46
-
47
- // Telemetry. Defaults to the process singleton; overridable
48
- // for tests. Outbound requests are instrumented in `_request`.
49
- //
50
- // Attribution boundary: the `frigg.apimodule.requests` OTel metric fires
51
- // for EVERY request (observability is complete). The durable per-
52
- // integration `api.requests` usage counter, however, only rolls up
53
- // requests carrying an integration context — set by the handler seams via
54
- // AsyncLocalStorage (dispatched USER_ACTION/CRON/QUEUE/WEBHOOK paths). A
55
- // request made before an integration exists (OAuth/token exchange, entity
56
- // discovery during connection setup) has no integration to attribute to,
57
- // so it is observable but not rolled up. Pass a context-bound `telemetry`
58
- // (an integration's `this.telemetry`) to attribute out-of-band requests.
59
- this.telemetry = (params && params.telemetry) || getTelemetry();
60
- }
61
-
62
- /**
63
- * Redact secrets/PII from a URL before it touches telemetry. Many API
64
- * modules embed credentials in the query string (?api_key=, ?token=,
65
- * presigned signatures) or in userinfo — those must never reach a span,
66
- * the bus, or an exporter. Keep only protocol + host + path (enough for
67
- * North Star endpoint matching).
68
- */
69
- _sanitizeUrl(url) {
70
- const raw = String(url);
71
- try {
72
- const u = new URL(raw);
73
- return `${u.protocol}//${u.host}${u.pathname}`;
74
- } catch (_) {
75
- // Relative/opaque URL: drop the query string at minimum.
76
- return raw.split('?')[0];
77
- }
78
- }
79
-
80
- /** Bounded module label for the apimodule.requests metric. */
81
- _telemetryModuleLabel() {
82
- return (
83
- this.moduleName ||
84
- this.delegate?.name ||
85
- this.delegate?.constructor?.name ||
86
- this.constructor?.name ||
87
- 'unknown'
88
- );
89
45
  }
90
46
 
91
47
  parsedBody = async (resp) => {
@@ -102,66 +58,7 @@ class Requester extends Delegate {
102
58
  return resp.text();
103
59
  };
104
60
 
105
- /**
106
- * Instrumenting entry point. Wraps the whole logical request —
107
- * including retry/refresh recursion — in a single span + one
108
- * `frigg.apimodule.requests` counter, emitted on the `i === 0` boundary so
109
- * retries are never double-counted. The full URL rides the span only; the
110
- * metric carries bounded labels {module, method, status} — never endpoint.
111
- */
112
- async _request(url, options = {}, i = 0) {
113
- if (i !== 0) {
114
- return this._rawRequest(url, options, i);
115
- }
116
-
117
- const telemetry = this.telemetry;
118
- if (!telemetry || typeof telemetry.span !== 'function') {
119
- return this._rawRequest(url, options, 0);
120
- }
121
-
122
- const module = this._telemetryModuleLabel();
123
- const method = (options.method || 'GET').toUpperCase();
124
- const safeUrl = this._sanitizeUrl(url);
125
-
126
- return telemetry.span('frigg.apimodule.request', async (span) => {
127
- if (span && typeof span.setAttributes === 'function') {
128
- span.setAttributes({
129
- 'frigg.module': module,
130
- 'http.request.method': method,
131
- // Redacted (no query/userinfo) — never emit raw URLs.
132
- 'url.path': safeUrl,
133
- });
134
- }
135
- // Redacted url (unbounded) rides the bus-only context for North Star
136
- // derived-from-trace matching — never a metric label.
137
- const busContext = { url: safeUrl };
138
- try {
139
- const result = await this._rawRequest(url, options, 0);
140
- telemetry.count(
141
- 'frigg.apimodule.requests',
142
- 1,
143
- { module, method, status: 'ok' },
144
- busContext
145
- );
146
- return result;
147
- } catch (err) {
148
- const code = err?.status ?? err?.statusCode;
149
- const status = code ? String(code) : 'error';
150
- if (span && typeof span.setAttribute === 'function' && code) {
151
- span.setAttribute('http.response.status_code', code);
152
- }
153
- telemetry.count(
154
- 'frigg.apimodule.requests',
155
- 1,
156
- { module, method, status },
157
- busContext
158
- );
159
- throw err;
160
- }
161
- });
162
- }
163
-
164
- async _rawRequest(url, options, i = 0) {
61
+ async _request(url, options, i = 0) {
165
62
  let encodedUrl = encodeURI(url);
166
63
  if (options.query) {
167
64
  let queryBuild = '?';
@@ -223,7 +120,7 @@ class Requester extends Delegate {
223
120
  clearRequestTimer();
224
121
  const delay = this.backOff[i] * 1000;
225
122
  await new Promise((resolve) => setTimeout(resolve, delay));
226
- return this._rawRequest(url, options, i + 1);
123
+ return this._request(url, options, i + 1);
227
124
  }
228
125
  const fetchError = await FetchError.create({
229
126
  resource: encodedUrl,
@@ -250,13 +147,32 @@ class Requester extends Delegate {
250
147
  clearRequestTimer();
251
148
  const delay = this.backOff[i] * 1000;
252
149
  await new Promise((resolve) => setTimeout(resolve, delay));
253
- return this._rawRequest(url, options, i + 1);
150
+ return this._request(url, options, i + 1);
254
151
  }
255
152
 
256
153
  if (status === 401) {
257
154
  if (!this.isRefreshable) {
258
- await this.notify(this.DLGT_INVALID_AUTH);
259
- return;
155
+ // A non-refreshable requester (ApiKeyRequester,
156
+ // BasicAuthRequester) has no token to renew, but a single
157
+ // 401 is not necessarily proof the credential itself is
158
+ // bad — it may be a transient edge/gateway hiccup. Grant
159
+ // one grace retry of the same request before concluding
160
+ // the credential is truly invalid, mirroring the single
161
+ // refresh attempt OAuth2Requester gets below.
162
+ if (i === 0) {
163
+ clearRequestTimer();
164
+ const delay = this.backOff[0] * 1000;
165
+ await new Promise((resolve) =>
166
+ setTimeout(resolve, delay)
167
+ );
168
+ return this._request(url, options, i + 1);
169
+ }
170
+
171
+ throw await this._invalidateAuth(
172
+ encodedUrl,
173
+ options,
174
+ response
175
+ );
260
176
  }
261
177
 
262
178
  if (this.refreshCount === 0) {
@@ -264,11 +180,14 @@ class Requester extends Delegate {
264
180
  const refreshSucceeded = await this.refreshAuth();
265
181
  if (refreshSucceeded) {
266
182
  clearRequestTimer();
267
- return this._rawRequest(url, options, i + 1);
183
+ return this._request(url, options, i + 1);
268
184
  }
269
185
 
270
- await this.notify(this.DLGT_INVALID_AUTH);
271
- return;
186
+ throw await this._invalidateAuth(
187
+ encodedUrl,
188
+ options,
189
+ response
190
+ );
272
191
  }
273
192
  }
274
193
 
@@ -307,6 +226,22 @@ class Requester extends Delegate {
307
226
  }
308
227
  }
309
228
 
229
+ // Builds a diagnostic FetchError from the failed 401 response, notifies
230
+ // delegates with it attached (so the credential-invalidation chain can
231
+ // log real detail instead of a bare "invalid credentials" with no
232
+ // evidence), and returns the error for the caller to throw. Notifying
233
+ // and throwing are split so notify() always fires even though `await`ing
234
+ // it happens before the throw.
235
+ async _invalidateAuth(encodedUrl, options, response) {
236
+ const fetchError = await FetchError.create({
237
+ resource: encodedUrl,
238
+ init: options,
239
+ response,
240
+ });
241
+ await this.notify(this.DLGT_INVALID_AUTH, fetchError);
242
+ return fetchError;
243
+ }
244
+
310
245
  _maybeFlagTimeoutDuringBodyRead(err, timeoutMs) {
311
246
  if (!err || typeof err !== 'object') return err;
312
247
  if (err.isTimeout) return err;
package/package.json CHANGED
@@ -1,20 +1,13 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.622.faffe5f.0",
4
+ "version": "2.0.0--canary.623.71305f2.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
8
8
  "@aws-sdk/client-lambda": "^3.714.0",
9
9
  "@aws-sdk/client-sqs": "^3.588.0",
10
10
  "@hapi/boom": "^10.0.1",
11
- "@opentelemetry/api": "^1.9.1",
12
- "@opentelemetry/context-async-hooks": "^2.9.0",
13
- "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0",
14
- "@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
15
- "@opentelemetry/resources": "^2.9.0",
16
- "@opentelemetry/sdk-metrics": "^2.9.0",
17
- "@opentelemetry/sdk-trace-base": "^2.9.0",
18
11
  "bcryptjs": "^2.4.3",
19
12
  "body-parser": "^1.20.5",
20
13
  "bson": "^4.7.2",
@@ -45,9 +38,9 @@
45
38
  }
46
39
  },
47
40
  "devDependencies": {
48
- "@friggframework/eslint-config": "2.0.0--canary.622.faffe5f.0",
49
- "@friggframework/prettier-config": "2.0.0--canary.622.faffe5f.0",
50
- "@friggframework/test": "2.0.0--canary.622.faffe5f.0",
41
+ "@friggframework/eslint-config": "2.0.0--canary.623.71305f2.0",
42
+ "@friggframework/prettier-config": "2.0.0--canary.623.71305f2.0",
43
+ "@friggframework/test": "2.0.0--canary.623.71305f2.0",
51
44
  "@prisma/client": "^6.19.3",
52
45
  "@types/lodash": "4.17.15",
53
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -87,5 +80,5 @@
87
80
  "publishConfig": {
88
81
  "access": "public"
89
82
  },
90
- "gitHead": "faffe5ff8b3fea6bbb46a52502cbd5b5ecb35559"
83
+ "gitHead": "71305f2dfed42de7be90e012a383437a46404159"
91
84
  }
@@ -430,27 +430,3 @@ model ScriptSchedule {
430
430
  @@index([enabled])
431
431
  @@map("ScriptSchedule")
432
432
  }
433
-
434
- /// ADR-011 durable usage counters. Deliberately isolated (ADR-010 Decision 3):
435
- /// NO userId and NO foreign key to Integration, so a user-scoped query can never
436
- /// return a usage row and usage history survives integration deletion. Not in
437
- /// the encryption registry — dimensions are bounded, non-sensitive.
438
- model UsageCounter {
439
- id String @id @default(auto()) @map("_id") @db.ObjectId
440
-
441
- integrationId String
442
- integrationType String
443
- metric String
444
- window String // e.g. "day:2026-07-05" or "hour:2026-07-05T14"
445
- value BigInt @default(0) // BigInt: a busy day/hour counter can exceed 32-bit
446
-
447
- createdAt DateTime @default(now())
448
- updatedAt DateTime @updatedAt
449
-
450
- @@unique([integrationId, integrationType, metric, window])
451
- // Covers totals() (metric + window prefix/range, no integrationType filter).
452
- @@index([metric, window])
453
- // Covers series() (metric + integrationType + window range).
454
- @@index([metric, integrationType, window])
455
- @@map("UsageCounter")
456
- }
@@ -412,26 +412,3 @@ model ScriptSchedule {
412
412
 
413
413
  @@index([enabled])
414
414
  }
415
-
416
- /// ADR-011 durable usage counters. Deliberately isolated (ADR-010 Decision 3):
417
- /// NO userId and NO foreign key to Integration, so a user-scoped query can never
418
- /// return a usage row and usage history survives integration deletion. Not in
419
- /// the encryption registry — dimensions are bounded, non-sensitive.
420
- model UsageCounter {
421
- id Int @id @default(autoincrement())
422
-
423
- integrationId String
424
- integrationType String
425
- metric String
426
- window String // e.g. "day:2026-07-05" or "hour:2026-07-05T14"
427
- value BigInt @default(0) // BigInt: a busy day/hour counter can exceed 32-bit
428
-
429
- createdAt DateTime @default(now())
430
- updatedAt DateTime @updatedAt
431
-
432
- @@unique([integrationId, integrationType, metric, window])
433
- // Covers totals() (metric + window prefix/range, no integrationType filter).
434
- @@index([metric, window])
435
- // Covers series() (metric + integrationType + window range).
436
- @@index([metric, integrationType, window])
437
- }
@@ -52,15 +52,8 @@ Optional query params (all strings): `status` (an `IntegrationStatus`), `type`
52
52
  - `filters` — echoes the applied filters (nulls when omitted).
53
53
  - `metrics.total` — count of integrations matching the filters.
54
54
  - `metrics.byStatus` — counts keyed by `IntegrationStatus` value.
55
- - `metrics.byType[]` — per `type` breakdown: `{ type, label, total, byStatus, usage? }`.
55
+ - `metrics.byType[]` — per `type` breakdown: `{ type, label, total, byStatus }`.
56
56
  - `type` — the `config.type` slug. Integrations with no type bucket as `"unknown"`.
57
- - `usage` — **additive** (ADR-011): present only when the deployment has the
58
- usage store configured. A map of canonical counter → total for that type,
59
- e.g. `{ "records.synced": 4200, "webhooks.received": 118, "api.requests": 9004 }`
60
- (`0` when a type has no data). Read only from the Frigg usage store, never an
61
- external APM; a usage-store read failure never fails the structural report.
62
- Populated by the counters an integration opts into via `Definition.usage` —
63
- see [`../telemetry/README.md`](../telemetry/README.md).
64
57
  - `label` — human-readable name from the integration class's
65
58
  `Definition.display.label`. Falls back to the `type` slug when no registered
66
59
  class supplies a label (e.g. an integration that was removed, or run on an
@@ -3,12 +3,7 @@ const catchAsyncError = require('express-async-handler');
3
3
  const {
4
4
  createReportingRepository,
5
5
  } = require('./repositories/reporting-repository-factory');
6
- const {
7
- createUsageRepository,
8
- } = require('../usage/repositories/usage-repository-factory');
9
- const {
10
- ListIntegrationsReport,
11
- } = require('./use-cases/list-integrations-report');
6
+ const { ListIntegrationsReport } = require('./use-cases/list-integrations-report');
12
7
  const { loadAppDefinition } = require('../handlers/app-definition-loader');
13
8
 
14
9
  // IntegrationBase.Definition default — skip it so the slug is used instead.
@@ -40,18 +35,8 @@ function buildTypeLabels() {
40
35
 
41
36
  function createReportingRouter() {
42
37
  const reportingRepository = createReportingRepository();
43
- // Enrich the report with feature-usage columns read from
44
- // the Frigg-owned usage store. Best-effort — a failure to construct it never
45
- // blocks the structural report.
46
- let usageRepository = null;
47
- try {
48
- usageRepository = createUsageRepository();
49
- } catch (_) {
50
- usageRepository = null;
51
- }
52
38
  const listIntegrationsReport = new ListIntegrationsReport({
53
39
  reportingRepository,
54
- usageRepository,
55
40
  typeLabels: buildTypeLabels(),
56
41
  });
57
42