@friggframework/core 2.0.0-next.96 → 2.0.0-next.98
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.
|
@@ -315,13 +315,13 @@ class OAuth2Requester extends Requester {
|
|
|
315
315
|
console.log('[Frigg] Token refresh succeeded');
|
|
316
316
|
return true;
|
|
317
317
|
} catch (error) {
|
|
318
|
-
|
|
318
|
+
const moduleName = this.delegate?.name ?? 'unknown module';
|
|
319
|
+
console.error(`[Frigg] Token refresh failed for ${moduleName}`, {
|
|
319
320
|
error_message: error?.message,
|
|
320
321
|
error_name: error?.name,
|
|
321
322
|
response_status: error?.response?.status,
|
|
322
323
|
response_data: error?.response?.data,
|
|
323
324
|
});
|
|
324
|
-
await this.notify(this.DLGT_INVALID_AUTH);
|
|
325
325
|
return false;
|
|
326
326
|
}
|
|
327
327
|
}
|
|
@@ -119,9 +119,7 @@ class Requester extends Delegate {
|
|
|
119
119
|
if (e?.code === 'ECONNRESET' && i < this.backOff.length) {
|
|
120
120
|
clearRequestTimer();
|
|
121
121
|
const delay = this.backOff[i] * 1000;
|
|
122
|
-
await new Promise((resolve) =>
|
|
123
|
-
setTimeout(resolve, delay)
|
|
124
|
-
);
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
125
123
|
return this._request(url, options, i + 1);
|
|
126
124
|
}
|
|
127
125
|
const fetchError = await FetchError.create({
|
|
@@ -150,16 +148,24 @@ class Requester extends Delegate {
|
|
|
150
148
|
const delay = this.backOff[i] * 1000;
|
|
151
149
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
152
150
|
return this._request(url, options, i + 1);
|
|
153
|
-
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (status === 401) {
|
|
154
154
|
if (!this.isRefreshable) {
|
|
155
155
|
await this.notify(this.DLGT_INVALID_AUTH);
|
|
156
|
-
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (this.refreshCount === 0) {
|
|
157
160
|
this.refreshCount++;
|
|
158
161
|
const refreshSucceeded = await this.refreshAuth();
|
|
159
162
|
if (refreshSucceeded) {
|
|
160
163
|
clearRequestTimer();
|
|
161
164
|
return this._request(url, options, i + 1);
|
|
162
165
|
}
|
|
166
|
+
|
|
167
|
+
await this.notify(this.DLGT_INVALID_AUTH);
|
|
168
|
+
return;
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
|
|
@@ -201,8 +207,7 @@ class Requester extends Delegate {
|
|
|
201
207
|
_maybeFlagTimeoutDuringBodyRead(err, timeoutMs) {
|
|
202
208
|
if (!err || typeof err !== 'object') return err;
|
|
203
209
|
if (err.isTimeout) return err;
|
|
204
|
-
const isAbort =
|
|
205
|
-
err.name === 'AbortError' || err.type === 'aborted';
|
|
210
|
+
const isAbort = err.name === 'AbortError' || err.type === 'aborted';
|
|
206
211
|
if (!isAbort) return err;
|
|
207
212
|
err.isTimeout = true;
|
|
208
213
|
err.timeoutMs = timeoutMs;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0-next.
|
|
4
|
+
"version": "2.0.0-next.98",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
42
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
43
|
-
"@friggframework/test": "2.0.0-next.
|
|
41
|
+
"@friggframework/eslint-config": "2.0.0-next.98",
|
|
42
|
+
"@friggframework/prettier-config": "2.0.0-next.98",
|
|
43
|
+
"@friggframework/test": "2.0.0-next.98",
|
|
44
44
|
"@prisma/client": "^6.19.3",
|
|
45
45
|
"@types/lodash": "4.17.15",
|
|
46
46
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public"
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "da2c1977216b9f939121b18d8461cc4e8a5b2b94"
|
|
84
84
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Reporting
|
|
2
|
+
|
|
3
|
+
Read-only, deployment-wide reporting endpoints for `@friggframework/core`.
|
|
4
|
+
Gated by a dedicated reporting API key — **not** the per-user auth used by the
|
|
5
|
+
rest of the Management API.
|
|
6
|
+
|
|
7
|
+
## Auth
|
|
8
|
+
|
|
9
|
+
Send the reporting key in the `x-frigg-reporting-api-key` header. It is validated
|
|
10
|
+
against the `REPORTING_API_KEY` environment variable. Missing or wrong key → `401`.
|
|
11
|
+
|
|
12
|
+
## Endpoints
|
|
13
|
+
|
|
14
|
+
| Method | Path | Description |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `GET` | `/api/v2/reports` | Index of available reports. |
|
|
17
|
+
| `GET` | `/api/v2/reports/integrations` | Integrations report (see below). |
|
|
18
|
+
|
|
19
|
+
### `GET /api/v2/reports/integrations`
|
|
20
|
+
|
|
21
|
+
Optional query params (all strings): `status` (an `IntegrationStatus`), `type`
|
|
22
|
+
(`config.type` slug), `userId`.
|
|
23
|
+
|
|
24
|
+
## Response shape
|
|
25
|
+
|
|
26
|
+
```jsonc
|
|
27
|
+
{
|
|
28
|
+
"schemaVersion": 1,
|
|
29
|
+
"service": "frigg-core-api",
|
|
30
|
+
"generatedAt": "2026-06-29T20:45:05.142Z",
|
|
31
|
+
"filters": { "status": null, "type": null, "userId": null },
|
|
32
|
+
"metrics": {
|
|
33
|
+
"total": 12,
|
|
34
|
+
"byStatus": { "ENABLED": 9, "ERROR": 2, "NEEDS_CONFIG": 1, "PROCESSING": 0, "DISABLED": 0 },
|
|
35
|
+
"byType": [
|
|
36
|
+
{
|
|
37
|
+
"type": "hubspot",
|
|
38
|
+
"label": "HubSpot CRM",
|
|
39
|
+
"total": 5,
|
|
40
|
+
"byStatus": { "ENABLED": 4, "ERROR": 1, "NEEDS_CONFIG": 0, "PROCESSING": 0, "DISABLED": 0 }
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"typeLabels": { "hubspot": "HubSpot CRM" },
|
|
44
|
+
"integrations": [ /* lightweight per-integration rows */ ]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Field reference
|
|
50
|
+
|
|
51
|
+
- `schemaVersion` — contract version; branch on it. Additive changes do **not** bump it.
|
|
52
|
+
- `filters` — echoes the applied filters (nulls when omitted).
|
|
53
|
+
- `metrics.total` — count of integrations matching the filters.
|
|
54
|
+
- `metrics.byStatus` — counts keyed by `IntegrationStatus` value.
|
|
55
|
+
- `metrics.byType[]` — per `type` breakdown: `{ type, label, total, byStatus }`.
|
|
56
|
+
- `type` — the `config.type` slug. Integrations with no type bucket as `"unknown"`.
|
|
57
|
+
- `label` — human-readable name from the integration class's
|
|
58
|
+
`Definition.display.label`. Falls back to the `type` slug when no registered
|
|
59
|
+
class supplies a label (e.g. an integration that was removed, or run on an
|
|
60
|
+
older app that doesn't register it). Additive — `type` is unchanged.
|
|
61
|
+
- `metrics.typeLabels` — map of `type` slug → human-readable label, so callers can
|
|
62
|
+
label `integrations[].type` rows without bloating each row. Contains only types
|
|
63
|
+
whose registered class supplies a non-default `display.label` (classes still
|
|
64
|
+
carrying the IntegrationBase default `'Integration Name'` are excluded).
|
|
65
|
+
- `metrics.integrations[]` — lightweight per-integration rows (`id`, `type`,
|
|
66
|
+
`status`, `userId`, `version`, `moduleCount`, `errorCount`, `mappedRecordCount`,
|
|
67
|
+
`createdAt`, `updatedAt`). These rows carry `type` only — resolve display names
|
|
68
|
+
via `metrics.typeLabels`.
|
|
69
|
+
|
|
70
|
+
## How labels are sourced
|
|
71
|
+
|
|
72
|
+
`createReportingRouter()` loads the app's registered integration classes via
|
|
73
|
+
`loadAppDefinition()` and builds a `{ slug → label }` map from each class's
|
|
74
|
+
`Definition.display.label`. Loading is wrapped in try/catch, so reporting still
|
|
75
|
+
works (labels fall back to slugs) if the app definition can't be loaded. The
|
|
76
|
+
`ListIntegrationsReport` use case stays storage-agnostic — it just reads the
|
|
77
|
+
injected `typeLabels` map, so all database adapters (PostgreSQL, MongoDB,
|
|
78
|
+
DocumentDB) get labels with zero adapter changes.
|
|
79
|
+
|
|
80
|
+
## Caveats
|
|
81
|
+
|
|
82
|
+
- The response is sensitive (exposes integration types, counts, user IDs, versions).
|
|
83
|
+
Treat the reporting key as an admin secret.
|
|
84
|
+
- Read-only — no mutation endpoints.
|
|
85
|
+
- Labels only appear once the deployment runs a `@friggframework/core` version that
|
|
86
|
+
includes this field and the app registers the integration classes.
|
|
@@ -4,11 +4,40 @@ const {
|
|
|
4
4
|
createReportingRepository,
|
|
5
5
|
} = require('./repositories/reporting-repository-factory');
|
|
6
6
|
const { ListIntegrationsReport } = require('./use-cases/list-integrations-report');
|
|
7
|
+
const { loadAppDefinition } = require('../handlers/app-definition-loader');
|
|
8
|
+
|
|
9
|
+
// IntegrationBase.Definition default — skip it so the slug is used instead.
|
|
10
|
+
const PLACEHOLDER_DISPLAY_NAME = 'Integration Name';
|
|
11
|
+
|
|
12
|
+
// Map each integration's config.type slug to its human-readable display label.
|
|
13
|
+
// Wrapped so reporting still works if the app definition fails to load.
|
|
14
|
+
function buildTypeLabels() {
|
|
15
|
+
try {
|
|
16
|
+
const { integrations = [] } = loadAppDefinition();
|
|
17
|
+
const labels = {};
|
|
18
|
+
for (const IntegrationClass of integrations) {
|
|
19
|
+
const def = IntegrationClass?.Definition;
|
|
20
|
+
if (!def?.name) continue;
|
|
21
|
+
const label = def.display?.label;
|
|
22
|
+
if (label && label !== PLACEHOLDER_DISPLAY_NAME) {
|
|
23
|
+
labels[def.name] = label;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return labels;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(
|
|
29
|
+
'Reporting: failed to load integration labels:',
|
|
30
|
+
error.message
|
|
31
|
+
);
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
7
35
|
|
|
8
36
|
function createReportingRouter() {
|
|
9
37
|
const reportingRepository = createReportingRepository();
|
|
10
38
|
const listIntegrationsReport = new ListIntegrationsReport({
|
|
11
39
|
reportingRepository,
|
|
40
|
+
typeLabels: buildTypeLabels(),
|
|
12
41
|
});
|
|
13
42
|
|
|
14
43
|
const router = express.Router();
|
|
@@ -14,11 +14,12 @@ const KNOWN_STATUSES = [
|
|
|
14
14
|
];
|
|
15
15
|
|
|
16
16
|
class ListIntegrationsReport {
|
|
17
|
-
constructor({ reportingRepository } = {}) {
|
|
17
|
+
constructor({ reportingRepository, typeLabels = {} } = {}) {
|
|
18
18
|
if (!reportingRepository) {
|
|
19
19
|
throw new Error('reportingRepository is required');
|
|
20
20
|
}
|
|
21
21
|
this.reportingRepository = reportingRepository;
|
|
22
|
+
this.typeLabels = typeLabels;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async execute(query = {}) {
|
|
@@ -64,6 +65,7 @@ class ListIntegrationsReport {
|
|
|
64
65
|
if (!byTypeMap.has(integration.type)) {
|
|
65
66
|
byTypeMap.set(integration.type, {
|
|
66
67
|
type: integration.type,
|
|
68
|
+
label: this.typeLabels[integration.type] || integration.type,
|
|
67
69
|
total: 0,
|
|
68
70
|
byStatus: emptyStatusCounts(),
|
|
69
71
|
});
|
|
@@ -86,6 +88,7 @@ class ListIntegrationsReport {
|
|
|
86
88
|
total: integrations.length,
|
|
87
89
|
byStatus,
|
|
88
90
|
byType: Array.from(byTypeMap.values()),
|
|
91
|
+
typeLabels: { ...this.typeLabels },
|
|
89
92
|
integrations,
|
|
90
93
|
},
|
|
91
94
|
};
|