@friggframework/core 2.0.0--canary.623.71305f2.0 → 2.0.0--canary.622.fe9da2d.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 (56) hide show
  1. package/README.md +40 -0
  2. package/application/commands/usage-commands.js +56 -0
  3. package/application/index.js +10 -9
  4. package/core/create-handler.js +112 -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 +1503 -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 +23 -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 +1540 -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 +22 -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 +29 -15
  21. package/handlers/routers/integration-webhook-routers.js +20 -7
  22. package/index.js +16 -9
  23. package/integrations/integration-base.js +65 -14
  24. package/modules/module.js +2 -15
  25. package/modules/requester/requester.js +102 -46
  26. package/package.json +12 -5
  27. package/prisma-mongodb/schema.prisma +23 -0
  28. package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +26 -0
  29. package/prisma-postgresql/schema.prisma +22 -0
  30. package/reporting/README.md +8 -1
  31. package/reporting/reporting-router.js +8 -1
  32. package/reporting/use-cases/list-integrations-report.js +53 -6
  33. package/telemetry/README.md +331 -0
  34. package/telemetry/bind-telemetry-context.js +73 -0
  35. package/telemetry/canonical-counters.js +52 -0
  36. package/telemetry/exporters.js +85 -0
  37. package/telemetry/index.js +26 -0
  38. package/telemetry/instrument-handler.js +87 -0
  39. package/telemetry/no-op-telemetry.js +67 -0
  40. package/telemetry/north-star.js +103 -0
  41. package/telemetry/otel-telemetry.js +213 -0
  42. package/telemetry/plugin-subscribers.js +77 -0
  43. package/telemetry/telemetry-config.js +120 -0
  44. package/telemetry/telemetry-context.js +40 -0
  45. package/telemetry/telemetry-event-bus.js +58 -0
  46. package/telemetry/telemetry-runtime.js +147 -0
  47. package/telemetry/telemetry-service.js +51 -0
  48. package/telemetry/usage-rollup-subscriber.js +116 -0
  49. package/usage/README.md +54 -0
  50. package/usage/index.js +17 -0
  51. package/usage/repositories/usage-repository-documentdb.js +194 -0
  52. package/usage/repositories/usage-repository-factory.js +25 -0
  53. package/usage/repositories/usage-repository-interface.js +37 -0
  54. package/usage/repositories/usage-repository-prisma.js +146 -0
  55. package/usage/tracked-metrics.js +38 -0
  56. package/usage/usage-windows.js +24 -0
package/modules/module.js CHANGED
@@ -147,24 +147,15 @@ 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(object);
150
+ await this.markCredentialsInvalid();
151
151
  }
152
152
  }
153
153
 
154
- async markCredentialsInvalid(diagnosticInfo = null) {
154
+ async markCredentialsInvalid() {
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
-
168
159
  await this.credentialRepository.updateAuthenticationStatus(
169
160
  this.credential.id,
170
161
  false
@@ -190,10 +181,6 @@ class Module extends Delegate {
190
181
  await this.notify(this.DLGT_CREDENTIAL_INVALIDATED, {
191
182
  credentialId: this.credential.id,
192
183
  moduleName: this.name,
193
- ...(diagnosticInfo && {
194
- reason: diagnosticInfo.message,
195
- statusCode: diagnosticInfo.statusCode,
196
- }),
197
184
  });
198
185
  } catch (err) {
199
186
  console.error(
@@ -2,6 +2,7 @@ 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-runtime');
5
6
 
6
7
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
7
8
 
@@ -42,6 +43,40 @@ class Requester extends Delegate {
42
43
  // Allow passing in the fetch function
43
44
  // Instance methods can use this.fetch without differentiating
44
45
  this.fetch = get(params, 'fetch', fetch);
46
+
47
+ // Defaults to the process singleton. Pass an integration's bound
48
+ // `this.telemetry` to attribute out-of-band requests — setup/OAuth calls
49
+ // made before an integration context exists aren't rolled up otherwise.
50
+ this.telemetry = (params && params.telemetry) || getTelemetry();
51
+ }
52
+
53
+ /**
54
+ * Redact secrets/PII from a URL before it touches telemetry. Many API
55
+ * modules embed credentials in the query string (?api_key=, ?token=,
56
+ * presigned signatures) or in userinfo — those must never reach a span,
57
+ * the bus, or an exporter. Keep only protocol + host + path (enough for
58
+ * North Star endpoint matching).
59
+ */
60
+ _sanitizeUrl(url) {
61
+ const raw = String(url);
62
+ try {
63
+ const u = new URL(raw);
64
+ return `${u.protocol}//${u.host}${u.pathname}`;
65
+ } catch (_) {
66
+ // Relative/opaque URL: drop the query string at minimum.
67
+ return raw.split('?')[0];
68
+ }
69
+ }
70
+
71
+ /** Bounded module label for the apimodule.requests metric. */
72
+ _telemetryModuleLabel() {
73
+ return (
74
+ this.moduleName ||
75
+ this.delegate?.name ||
76
+ this.delegate?.constructor?.name ||
77
+ this.constructor?.name ||
78
+ 'unknown'
79
+ );
45
80
  }
46
81
 
47
82
  parsedBody = async (resp) => {
@@ -58,7 +93,66 @@ class Requester extends Delegate {
58
93
  return resp.text();
59
94
  };
60
95
 
61
- async _request(url, options, i = 0) {
96
+ /**
97
+ * Instrumenting entry point. Wraps the whole logical request —
98
+ * including retry/refresh recursion — in a single span + one
99
+ * `frigg.apimodule.requests` counter, emitted on the `i === 0` boundary so
100
+ * retries are never double-counted. The full URL rides the span only; the
101
+ * metric carries bounded labels {module, method, status} — never endpoint.
102
+ */
103
+ async _request(url, options = {}, i = 0) {
104
+ if (i !== 0) {
105
+ return this._rawRequest(url, options, i);
106
+ }
107
+
108
+ const telemetry = this.telemetry;
109
+ if (!telemetry || typeof telemetry.span !== 'function') {
110
+ return this._rawRequest(url, options, 0);
111
+ }
112
+
113
+ const module = this._telemetryModuleLabel();
114
+ const method = (options.method || 'GET').toUpperCase();
115
+ const safeUrl = this._sanitizeUrl(url);
116
+
117
+ return telemetry.span('frigg.apimodule.request', async (span) => {
118
+ if (span && typeof span.setAttributes === 'function') {
119
+ span.setAttributes({
120
+ 'frigg.module': module,
121
+ 'http.request.method': method,
122
+ // Redacted (no query/userinfo) — never emit raw URLs.
123
+ 'url.path': safeUrl,
124
+ });
125
+ }
126
+ // Redacted url (unbounded) rides the bus-only context for North Star
127
+ // derived-from-trace matching — never a metric label.
128
+ const busContext = { url: safeUrl };
129
+ try {
130
+ const result = await this._rawRequest(url, options, 0);
131
+ telemetry.count(
132
+ 'frigg.apimodule.requests',
133
+ 1,
134
+ { module, method, status: 'ok' },
135
+ busContext
136
+ );
137
+ return result;
138
+ } catch (err) {
139
+ const code = err?.status ?? err?.statusCode;
140
+ const status = code ? String(code) : 'error';
141
+ if (span && typeof span.setAttribute === 'function' && code) {
142
+ span.setAttribute('http.response.status_code', code);
143
+ }
144
+ telemetry.count(
145
+ 'frigg.apimodule.requests',
146
+ 1,
147
+ { module, method, status },
148
+ busContext
149
+ );
150
+ throw err;
151
+ }
152
+ });
153
+ }
154
+
155
+ async _rawRequest(url, options, i = 0) {
62
156
  let encodedUrl = encodeURI(url);
63
157
  if (options.query) {
64
158
  let queryBuild = '?';
@@ -120,7 +214,7 @@ class Requester extends Delegate {
120
214
  clearRequestTimer();
121
215
  const delay = this.backOff[i] * 1000;
122
216
  await new Promise((resolve) => setTimeout(resolve, delay));
123
- return this._request(url, options, i + 1);
217
+ return this._rawRequest(url, options, i + 1);
124
218
  }
125
219
  const fetchError = await FetchError.create({
126
220
  resource: encodedUrl,
@@ -147,32 +241,13 @@ class Requester extends Delegate {
147
241
  clearRequestTimer();
148
242
  const delay = this.backOff[i] * 1000;
149
243
  await new Promise((resolve) => setTimeout(resolve, delay));
150
- return this._request(url, options, i + 1);
244
+ return this._rawRequest(url, options, i + 1);
151
245
  }
152
246
 
153
247
  if (status === 401) {
154
248
  if (!this.isRefreshable) {
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
- );
249
+ await this.notify(this.DLGT_INVALID_AUTH);
250
+ return;
176
251
  }
177
252
 
178
253
  if (this.refreshCount === 0) {
@@ -180,14 +255,11 @@ class Requester extends Delegate {
180
255
  const refreshSucceeded = await this.refreshAuth();
181
256
  if (refreshSucceeded) {
182
257
  clearRequestTimer();
183
- return this._request(url, options, i + 1);
258
+ return this._rawRequest(url, options, i + 1);
184
259
  }
185
260
 
186
- throw await this._invalidateAuth(
187
- encodedUrl,
188
- options,
189
- response
190
- );
261
+ await this.notify(this.DLGT_INVALID_AUTH);
262
+ return;
191
263
  }
192
264
  }
193
265
 
@@ -226,22 +298,6 @@ class Requester extends Delegate {
226
298
  }
227
299
  }
228
300
 
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
-
245
301
  _maybeFlagTimeoutDuringBodyRead(err, timeoutMs) {
246
302
  if (!err || typeof err !== 'object') return err;
247
303
  if (err.isTimeout) return err;
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.623.71305f2.0",
4
+ "version": "2.0.0--canary.622.fe9da2d.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",
11
18
  "bcryptjs": "^2.4.3",
12
19
  "body-parser": "^1.20.5",
13
20
  "bson": "^4.7.2",
@@ -38,9 +45,9 @@
38
45
  }
39
46
  },
40
47
  "devDependencies": {
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",
48
+ "@friggframework/eslint-config": "2.0.0--canary.622.fe9da2d.0",
49
+ "@friggframework/prettier-config": "2.0.0--canary.622.fe9da2d.0",
50
+ "@friggframework/test": "2.0.0--canary.622.fe9da2d.0",
44
51
  "@prisma/client": "^6.19.3",
45
52
  "@types/lodash": "4.17.15",
46
53
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +87,5 @@
80
87
  "publishConfig": {
81
88
  "access": "public"
82
89
  },
83
- "gitHead": "71305f2dfed42de7be90e012a383437a46404159"
90
+ "gitHead": "fe9da2d26354e6118a6f14350cdf18394408b3a4"
84
91
  }
@@ -430,3 +430,26 @@ model ScriptSchedule {
430
430
  @@index([enabled])
431
431
  @@map("ScriptSchedule")
432
432
  }
433
+
434
+ /// Durable usage counters, deliberately isolated: no userId and no foreign key
435
+ /// to Integration, so a user-scoped query can never return a usage row and usage
436
+ /// history survives integration deletion.
437
+ model UsageCounter {
438
+ id String @id @default(auto()) @map("_id") @db.ObjectId
439
+
440
+ integrationId String
441
+ integrationType String
442
+ metric String
443
+ window String // e.g. "day:2026-07-05" or "hour:2026-07-05T14"
444
+ value BigInt @default(0) // BigInt: a busy day/hour counter can exceed 32-bit
445
+
446
+ createdAt DateTime @default(now())
447
+ updatedAt DateTime @updatedAt
448
+
449
+ @@unique([integrationId, integrationType, metric, window])
450
+ // Covers totals() (metric + window prefix/range, no integrationType filter).
451
+ @@index([metric, window])
452
+ // Covers series() (metric + integrationType + window range).
453
+ @@index([metric, integrationType, window])
454
+ @@map("UsageCounter")
455
+ }
@@ -0,0 +1,26 @@
1
+ -- CreateTable
2
+ -- Durable usage counters, isolated from the user/integration-scoped tables (no
3
+ -- userId, no FK to Integration) so usage history survives integration deletion
4
+ -- and can never surface in a user-scoped query. "window" is quoted throughout —
5
+ -- it is a reserved SQL keyword.
6
+ CREATE TABLE "UsageCounter" (
7
+ "id" SERIAL NOT NULL,
8
+ "integrationId" TEXT NOT NULL,
9
+ "integrationType" TEXT NOT NULL,
10
+ "metric" TEXT NOT NULL,
11
+ "window" TEXT NOT NULL,
12
+ "value" BIGINT NOT NULL DEFAULT 0,
13
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
14
+ "updatedAt" TIMESTAMP(3) NOT NULL,
15
+
16
+ CONSTRAINT "UsageCounter_pkey" PRIMARY KEY ("id")
17
+ );
18
+
19
+ -- CreateIndex
20
+ CREATE UNIQUE INDEX "UsageCounter_integrationId_integrationType_metric_window_key" ON "UsageCounter"("integrationId", "integrationType", "metric", "window");
21
+
22
+ -- CreateIndex
23
+ CREATE INDEX "UsageCounter_metric_window_idx" ON "UsageCounter"("metric", "window");
24
+
25
+ -- CreateIndex
26
+ CREATE INDEX "UsageCounter_metric_integrationType_window_idx" ON "UsageCounter"("metric", "integrationType", "window");
@@ -412,3 +412,25 @@ model ScriptSchedule {
412
412
 
413
413
  @@index([enabled])
414
414
  }
415
+
416
+ /// Durable usage counters, deliberately isolated: no userId and no foreign key
417
+ /// to Integration, so a user-scoped query can never return a usage row and usage
418
+ /// history survives integration deletion.
419
+ model UsageCounter {
420
+ id Int @id @default(autoincrement())
421
+
422
+ integrationId String
423
+ integrationType String
424
+ metric String
425
+ window String // e.g. "day:2026-07-05" or "hour:2026-07-05T14"
426
+ value BigInt @default(0) // BigInt: a busy day/hour counter can exceed 32-bit
427
+
428
+ createdAt DateTime @default(now())
429
+ updatedAt DateTime @updatedAt
430
+
431
+ @@unique([integrationId, integrationType, metric, window])
432
+ // Covers totals() (metric + window prefix/range, no integrationType filter).
433
+ @@index([metric, window])
434
+ // Covers series() (metric + integrationType + window range).
435
+ @@index([metric, integrationType, window])
436
+ }
@@ -52,8 +52,15 @@ 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 }`.
55
+ - `metrics.byType[]` — per `type` breakdown: `{ type, label, total, byStatus, usage? }`.
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).
57
64
  - `label` — human-readable name from the integration class's
58
65
  `Definition.display.label`. Falls back to the `type` slug when no registered
59
66
  class supplies a label (e.g. an integration that was removed, or run on an
@@ -3,7 +3,12 @@ const catchAsyncError = require('express-async-handler');
3
3
  const {
4
4
  createReportingRepository,
5
5
  } = require('./repositories/reporting-repository-factory');
6
- const { ListIntegrationsReport } = require('./use-cases/list-integrations-report');
6
+ const {
7
+ createUsageRepository,
8
+ } = require('../usage/repositories/usage-repository-factory');
9
+ const {
10
+ ListIntegrationsReport,
11
+ } = require('./use-cases/list-integrations-report');
7
12
  const { loadAppDefinition } = require('../handlers/app-definition-loader');
8
13
 
9
14
  // IntegrationBase.Definition default — skip it so the slug is used instead.
@@ -35,8 +40,10 @@ function buildTypeLabels() {
35
40
 
36
41
  function createReportingRouter() {
37
42
  const reportingRepository = createReportingRepository();
43
+ const usageRepository = createUsageRepository();
38
44
  const listIntegrationsReport = new ListIntegrationsReport({
39
45
  reportingRepository,
46
+ usageRepository,
40
47
  typeLabels: buildTypeLabels(),
41
48
  });
42
49
 
@@ -1,4 +1,5 @@
1
1
  const Boom = require('@hapi/boom');
2
+ const { CANONICAL_COUNTERS } = require('../../telemetry/canonical-counters');
2
3
 
3
4
  const SCHEMA_VERSION = 1;
4
5
  const SERVICE = 'frigg-core-api';
@@ -16,11 +17,19 @@ const KNOWN_STATUSES = [
16
17
  ];
17
18
 
18
19
  class ListIntegrationsReport {
19
- constructor({ reportingRepository, typeLabels = {} } = {}) {
20
+ constructor({
21
+ reportingRepository,
22
+ usageRepository,
23
+ typeLabels = {},
24
+ } = {}) {
20
25
  if (!reportingRepository) {
21
26
  throw new Error('reportingRepository is required');
22
27
  }
28
+ if (!usageRepository) {
29
+ throw new Error('usageRepository is required');
30
+ }
23
31
  this.reportingRepository = reportingRepository;
32
+ this.usageRepository = usageRepository;
24
33
  this.typeLabels = typeLabels;
25
34
  }
26
35
 
@@ -67,7 +76,8 @@ class ListIntegrationsReport {
67
76
  if (!byTypeMap.has(integration.type)) {
68
77
  byTypeMap.set(integration.type, {
69
78
  type: integration.type,
70
- label: this.typeLabels[integration.type] || integration.type,
79
+ label:
80
+ this.typeLabels[integration.type] || integration.type,
71
81
  total: 0,
72
82
  byStatus: emptyStatusCounts(),
73
83
  });
@@ -77,6 +87,8 @@ class ListIntegrationsReport {
77
87
  bucket.byStatus[statusKey] = (bucket.byStatus[statusKey] ?? 0) + 1;
78
88
  }
79
89
 
90
+ await this._attachUsageColumns(byTypeMap);
91
+
80
92
  return {
81
93
  schemaVersion: SCHEMA_VERSION,
82
94
  service: SERVICE,
@@ -96,9 +108,44 @@ class ListIntegrationsReport {
96
108
  };
97
109
  }
98
110
 
111
+ async _attachUsageColumns(byTypeMap) {
112
+ const metrics = Object.keys(CANONICAL_COUNTERS);
113
+ try {
114
+ const totalsByMetric = await Promise.all(
115
+ metrics.map(async (metric) => {
116
+ const totals = await this.usageRepository.getTotalsByDimension({
117
+ metric,
118
+ groupBy: 'integrationType',
119
+ });
120
+ const map = new Map(
121
+ (totals || []).map((t) => [t.integrationType, t.value])
122
+ );
123
+ return [metric, map];
124
+ })
125
+ );
126
+
127
+ for (const bucket of byTypeMap.values()) {
128
+ bucket.usage = {};
129
+ for (const [metric, map] of totalsByMetric) {
130
+ bucket.usage[metric] = map.get(bucket.type) ?? 0;
131
+ }
132
+ }
133
+ } catch (error) {
134
+ console.warn(
135
+ `[Frigg][reporting] usage columns unavailable: ${
136
+ error && error.message
137
+ }`
138
+ );
139
+ }
140
+ }
141
+
99
142
  _validateQuery({ status, type, userId } = {}) {
100
143
  for (const [key, value] of Object.entries({ status, type, userId })) {
101
- if (value !== undefined && value !== null && typeof value !== 'string') {
144
+ if (
145
+ value !== undefined &&
146
+ value !== null &&
147
+ typeof value !== 'string'
148
+ ) {
102
149
  throw Boom.badRequest(
103
150
  `Invalid query parameter '${key}': expected a string`
104
151
  );
@@ -112,9 +159,9 @@ class ListIntegrationsReport {
112
159
  };
113
160
  if (normalized.status && !KNOWN_STATUSES.includes(normalized.status)) {
114
161
  throw Boom.badRequest(
115
- `Invalid status '${normalized.status}'. Expected one of: ${KNOWN_STATUSES.join(
116
- ', '
117
- )}`
162
+ `Invalid status '${
163
+ normalized.status
164
+ }'. Expected one of: ${KNOWN_STATUSES.join(', ')}`
118
165
  );
119
166
  }
120
167
  return normalized;