@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
|
@@ -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
|
-
|
|
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:
|
|
125
|
-
userId:
|
|
126
|
-
entities:
|
|
127
|
-
config:
|
|
128
|
-
status:
|
|
129
|
-
version:
|
|
130
|
-
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:
|
|
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
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
150
|
+
return this._request(url, options, i + 1);
|
|
254
151
|
}
|
|
255
152
|
|
|
256
153
|
if (status === 401) {
|
|
257
154
|
if (!this.isRefreshable) {
|
|
258
|
-
|
|
259
|
-
|
|
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.
|
|
183
|
+
return this._request(url, options, i + 1);
|
|
268
184
|
}
|
|
269
185
|
|
|
270
|
-
await this.
|
|
271
|
-
|
|
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.
|
|
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.
|
|
49
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
50
|
-
"@friggframework/test": "2.0.0--canary.
|
|
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": "
|
|
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
|
-
}
|
package/reporting/README.md
CHANGED
|
@@ -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
|
|
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
|
|