@forumone/throughline-core 0.9.0 → 0.10.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/CHANGELOG.md +112 -0
- package/README.md +192 -33
- package/bin/payload-cli.mjs +443 -0
- package/bin/throughline-payload.mjs +6 -0
- package/dist/audit/collection.d.ts +3 -0
- package/dist/audit/collection.d.ts.map +1 -1
- package/dist/audit/collection.js +2 -0
- package/dist/audit/collection.js.map +1 -1
- package/dist/env/index.d.ts +54 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +113 -0
- package/dist/env/index.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/api-key-access.d.ts +50 -0
- package/dist/mcp/api-key-access.d.ts.map +1 -0
- package/dist/mcp/api-key-access.js +94 -0
- package/dist/mcp/api-key-access.js.map +1 -0
- package/dist/mcp/envelope.d.ts +6 -0
- package/dist/mcp/envelope.d.ts.map +1 -1
- package/dist/mcp/envelope.js +6 -0
- package/dist/mcp/envelope.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/payload-mcp.d.ts +1 -15
- package/dist/mcp/payload-mcp.d.ts.map +1 -1
- package/dist/mcp/payload-mcp.js +40 -1
- package/dist/mcp/payload-mcp.js.map +1 -1
- package/dist/observability/collection.d.ts +13 -0
- package/dist/observability/collection.d.ts.map +1 -0
- package/dist/observability/collection.js +76 -0
- package/dist/observability/collection.js.map +1 -0
- package/dist/observability/index.d.ts +9 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +5 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/plugin.d.ts +21 -0
- package/dist/observability/plugin.d.ts.map +1 -0
- package/dist/observability/plugin.js +51 -0
- package/dist/observability/plugin.js.map +1 -0
- package/dist/observability/report.d.ts +175 -0
- package/dist/observability/report.d.ts.map +1 -0
- package/dist/observability/report.js +274 -0
- package/dist/observability/report.js.map +1 -0
- package/dist/observability/writer.d.ts +24 -0
- package/dist/observability/writer.d.ts.map +1 -0
- package/dist/observability/writer.js +72 -0
- package/dist/observability/writer.js.map +1 -0
- package/dist/testing/anonymousAccess.d.ts +111 -0
- package/dist/testing/anonymousAccess.d.ts.map +1 -0
- package/dist/testing/anonymousAccess.js +185 -0
- package/dist/testing/anonymousAccess.js.map +1 -0
- package/dist/testing/describeAnonymousAccess.d.ts +26 -0
- package/dist/testing/describeAnonymousAccess.d.ts.map +1 -0
- package/dist/testing/describeAnonymousAccess.js +76 -0
- package/dist/testing/describeAnonymousAccess.js.map +1 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +30 -7
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { Logger } from '@forumone/throughline-plugin-contract';
|
|
2
|
+
/** Request headers copied into a report unless the caller names its own list. */
|
|
3
|
+
export declare const DEFAULT_REPORTED_HEADERS: readonly string[];
|
|
4
|
+
/**
|
|
5
|
+
* Headers that are never copied, even when a caller lists them. Each one is,
|
|
6
|
+
* or routinely carries, a credential. `x-forwarded-for` is here too: the whole
|
|
7
|
+
* address is personal data, and a report carries only its prefix.
|
|
8
|
+
*/
|
|
9
|
+
export declare const NEVER_REPORTED_HEADERS: readonly string[];
|
|
10
|
+
/** The environment variable the default reporter reads. */
|
|
11
|
+
export declare const ERROR_WEBHOOK_URL_ENV = "ERROR_WEBHOOK_URL";
|
|
12
|
+
/** Which deployment produced a report, so it is attributable after a rollback. */
|
|
13
|
+
export interface ReportDeployment {
|
|
14
|
+
environment?: string;
|
|
15
|
+
commit?: string;
|
|
16
|
+
}
|
|
17
|
+
/** An unhandled error in a request, as `instrumentation.ts` sees one. */
|
|
18
|
+
export interface RequestErrorReport {
|
|
19
|
+
kind: 'request';
|
|
20
|
+
/** ISO 8601, so a report is orderable without trusting the receiver's clock. */
|
|
21
|
+
at: string;
|
|
22
|
+
message: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
stack?: string;
|
|
25
|
+
/** React's error digest — the only handle on a minified Server Component error. */
|
|
26
|
+
digest?: string;
|
|
27
|
+
request: {
|
|
28
|
+
path: string;
|
|
29
|
+
method: string;
|
|
30
|
+
/** The allowlisted headers that were present, lower-cased. */
|
|
31
|
+
headers: Record<string, string>;
|
|
32
|
+
/** The client address reduced to a /16 (IPv4) or /64 (IPv6). */
|
|
33
|
+
clientPrefix?: string;
|
|
34
|
+
};
|
|
35
|
+
context: {
|
|
36
|
+
routePath?: string;
|
|
37
|
+
routeType?: string;
|
|
38
|
+
renderSource?: string;
|
|
39
|
+
revalidateReason?: string;
|
|
40
|
+
};
|
|
41
|
+
deployment: ReportDeployment;
|
|
42
|
+
}
|
|
43
|
+
/** A background job that ran out of retries. */
|
|
44
|
+
export interface JobFailureReport {
|
|
45
|
+
kind: 'job';
|
|
46
|
+
at: string;
|
|
47
|
+
/** The function that failed, e.g. an Inngest function id. `'unknown'` if nothing named it. */
|
|
48
|
+
functionId: string;
|
|
49
|
+
/** The run's id — the handle for finding it in the job runner's dashboard. */
|
|
50
|
+
runId?: string;
|
|
51
|
+
/** The event that triggered the run, so a failure is traceable to a cause. */
|
|
52
|
+
triggerEvent?: string;
|
|
53
|
+
message: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
stack?: string;
|
|
56
|
+
deployment: ReportDeployment;
|
|
57
|
+
}
|
|
58
|
+
/** A healthcheck run in which at least one check failed. */
|
|
59
|
+
export interface HealthcheckFailureReport {
|
|
60
|
+
kind: 'healthcheck';
|
|
61
|
+
at: string;
|
|
62
|
+
functionId: string;
|
|
63
|
+
failed: Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
details?: string;
|
|
66
|
+
}>;
|
|
67
|
+
deployment: ReportDeployment;
|
|
68
|
+
}
|
|
69
|
+
export type ErrorReport = RequestErrorReport | JobFailureReport | HealthcheckFailureReport;
|
|
70
|
+
/** Request headers as Next hands them over, or a Fetch `Headers`. */
|
|
71
|
+
export type ReportableHeaders = Record<string, string | string[] | undefined> | {
|
|
72
|
+
get(name: string): string | null;
|
|
73
|
+
};
|
|
74
|
+
export interface BuildRequestErrorReportOptions {
|
|
75
|
+
/** Headers to copy. Default: {@link DEFAULT_REPORTED_HEADERS}. {@link NEVER_REPORTED_HEADERS} are dropped regardless. */
|
|
76
|
+
headers?: readonly string[];
|
|
77
|
+
/** Default: read from the environment — see {@link currentDeployment}. */
|
|
78
|
+
deployment?: ReportDeployment;
|
|
79
|
+
now?: Date;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The deployment a report came from, read from the environment.
|
|
83
|
+
*
|
|
84
|
+
* `VERCEL_ENV` before `NODE_ENV`, because on Vercel a preview deployment is
|
|
85
|
+
* `NODE_ENV=production` too, and "production or somebody's branch" is the
|
|
86
|
+
* first question about an alert. Off Vercel, `NODE_ENV` is what there is.
|
|
87
|
+
*/
|
|
88
|
+
export declare function currentDeployment(env?: NodeJS.ProcessEnv): ReportDeployment;
|
|
89
|
+
/**
|
|
90
|
+
* An IPv4 or IPv6 address reduced to a prefix: enough to tell one host's burst
|
|
91
|
+
* from many readers hitting the same fault, and not enough to identify anyone.
|
|
92
|
+
* `x-forwarded-for` may carry a list; only the first entry is the client.
|
|
93
|
+
*/
|
|
94
|
+
export declare function clientPrefix(forwardedFor: string | undefined): string | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Build a report for an error captured during a request. Pure, so the
|
|
97
|
+
* redaction is testable without a server.
|
|
98
|
+
*
|
|
99
|
+
* `error` is `unknown` because Next types it that way and means it: the value
|
|
100
|
+
* reaching `onRequestError` may have been processed by React rather than being
|
|
101
|
+
* the instance that was thrown, which is why `digest` is read separately.
|
|
102
|
+
*/
|
|
103
|
+
export declare function buildRequestErrorReport(error: unknown, request: {
|
|
104
|
+
path: string;
|
|
105
|
+
method: string;
|
|
106
|
+
headers: ReportableHeaders;
|
|
107
|
+
}, context?: object, options?: BuildRequestErrorReportOptions): RequestErrorReport;
|
|
108
|
+
export interface BuildJobFailureReportInput {
|
|
109
|
+
/** The failing function. Omitted or empty is reported as `'unknown'` rather than dropped. */
|
|
110
|
+
functionId?: string | undefined;
|
|
111
|
+
runId?: string | undefined;
|
|
112
|
+
triggerEvent?: string | undefined;
|
|
113
|
+
error: unknown;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Build a report for a job that ran out of retries.
|
|
117
|
+
*
|
|
118
|
+
* The stack is included: this goes to a webhook the team owns, where a stack
|
|
119
|
+
* is the most useful field in it. The job-failures collection, which admins
|
|
120
|
+
* read through the CMS, stores the message and not the stack.
|
|
121
|
+
*/
|
|
122
|
+
export declare function buildJobFailureReport(input: BuildJobFailureReportInput, options?: {
|
|
123
|
+
deployment?: ReportDeployment;
|
|
124
|
+
now?: Date;
|
|
125
|
+
}): JobFailureReport;
|
|
126
|
+
/** Build a report for a healthcheck run with failing checks. */
|
|
127
|
+
export declare function buildHealthcheckFailureReport(failures: ReadonlyArray<{
|
|
128
|
+
name: string;
|
|
129
|
+
details?: string | undefined;
|
|
130
|
+
}>, options?: {
|
|
131
|
+
functionId?: string;
|
|
132
|
+
deployment?: ReportDeployment;
|
|
133
|
+
now?: Date;
|
|
134
|
+
}): HealthcheckFailureReport;
|
|
135
|
+
/**
|
|
136
|
+
* One line describing a report, for a receiver that renders rather than
|
|
137
|
+
* indexes. Reads the report structurally, so a site's own report shapes get a
|
|
138
|
+
* sensible line too.
|
|
139
|
+
*/
|
|
140
|
+
export declare function summariseReport(report: unknown): string;
|
|
141
|
+
/**
|
|
142
|
+
* Whether reports go anywhere, as a sentence for a boot-time log line. A
|
|
143
|
+
* sentence rather than a boolean because "off because this is a laptop" and
|
|
144
|
+
* "off because somebody forgot in production" look identical otherwise.
|
|
145
|
+
*/
|
|
146
|
+
export declare function describeErrorReporting(url?: string | undefined): string;
|
|
147
|
+
export interface ErrorReporterOptions {
|
|
148
|
+
/**
|
|
149
|
+
* Where to POST. Default: `process.env.ERROR_WEBHOOK_URL`, read on every
|
|
150
|
+
* call so a reporter built before the environment loads still finds it.
|
|
151
|
+
* Unset or empty means reports are not delivered.
|
|
152
|
+
*/
|
|
153
|
+
url?: string | undefined;
|
|
154
|
+
/** Extra headers sent *to the webhook*, e.g. an `authorization` the receiver expects. */
|
|
155
|
+
webhookHeaders?: Record<string, string>;
|
|
156
|
+
/** Abort the POST after this long. Default: 3000ms — shorter than any function timeout. */
|
|
157
|
+
timeoutMs?: number;
|
|
158
|
+
/** Where a failed delivery is logged. Default: the console logger. */
|
|
159
|
+
logger?: Logger;
|
|
160
|
+
/** Injected for tests. Default: the global `fetch`. */
|
|
161
|
+
fetch?: typeof fetch;
|
|
162
|
+
}
|
|
163
|
+
/** Posts a report. Never throws, never rejects. */
|
|
164
|
+
export type ErrorReporter = (report: unknown) => Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* A reporter that POSTs each report as JSON, with a `text` summary added.
|
|
167
|
+
*
|
|
168
|
+
* A delivery that fails — a refused connection, a timeout, a non-2xx — is
|
|
169
|
+
* logged as a warning and otherwise swallowed. It is never rethrown: see the
|
|
170
|
+
* note at the top of this file.
|
|
171
|
+
*/
|
|
172
|
+
export declare function createErrorReporter(options?: ErrorReporterOptions): ErrorReporter;
|
|
173
|
+
/** The default reporter: `ERROR_WEBHOOK_URL`, a 3s timeout, the console logger. */
|
|
174
|
+
export declare const reportError: ErrorReporter;
|
|
175
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/observability/report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { defaultLogger } from '../logger/index.js';
|
|
2
|
+
/*
|
|
3
|
+
What an error looks like when it leaves the application, and how it leaves.
|
|
4
|
+
|
|
5
|
+
Provider-agnostic on purpose. Which error tracker a site pays for is the site's
|
|
6
|
+
decision, so this defines the *shape* of a report and posts it to whatever URL
|
|
7
|
+
it is given — a log drain, an alerting endpoint, a Slack incoming webhook, a
|
|
8
|
+
small proxy in front of Sentry. Swapping in an SDK later replaces the reporter
|
|
9
|
+
and nothing that builds reports.
|
|
10
|
+
|
|
11
|
+
Three rules hold everywhere in this file:
|
|
12
|
+
|
|
13
|
+
- **Nothing here throws.** A reporter runs because something already went
|
|
14
|
+
wrong. One that throws turns a handled error into an unhandled one, and in an
|
|
15
|
+
Inngest `onFailure` handler it puts the run back in the retry queue for a
|
|
16
|
+
reason that has nothing to do with the original fault.
|
|
17
|
+
- **Request headers are an allowlist.** Next's `onRequestError` hands over the
|
|
18
|
+
request's headers entire, and those include `cookie` — on an admin's request,
|
|
19
|
+
a live Payload session. A report that copied headers wholesale would post a
|
|
20
|
+
credential to a third party. A handful of headers that are never secret are
|
|
21
|
+
copied, and a short list that always is a credential can never be copied,
|
|
22
|
+
whatever a caller passes.
|
|
23
|
+
- **Every report has a `text`.** Slack's incoming webhooks refuse a body
|
|
24
|
+
without one, and a delivery failure is swallowed here by design — so a Slack
|
|
25
|
+
URL without it would read as configured and deliver nothing. Other receivers
|
|
26
|
+
ignore the extra key.
|
|
27
|
+
*/
|
|
28
|
+
/** Request headers copied into a report unless the caller names its own list. */
|
|
29
|
+
export const DEFAULT_REPORTED_HEADERS = [
|
|
30
|
+
'x-request-id',
|
|
31
|
+
'x-vercel-id',
|
|
32
|
+
'user-agent',
|
|
33
|
+
'referer',
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Headers that are never copied, even when a caller lists them. Each one is,
|
|
37
|
+
* or routinely carries, a credential. `x-forwarded-for` is here too: the whole
|
|
38
|
+
* address is personal data, and a report carries only its prefix.
|
|
39
|
+
*/
|
|
40
|
+
export const NEVER_REPORTED_HEADERS = [
|
|
41
|
+
'authorization',
|
|
42
|
+
'proxy-authorization',
|
|
43
|
+
'cookie',
|
|
44
|
+
'set-cookie',
|
|
45
|
+
'x-api-key',
|
|
46
|
+
'x-forwarded-for',
|
|
47
|
+
];
|
|
48
|
+
/** The environment variable the default reporter reads. */
|
|
49
|
+
export const ERROR_WEBHOOK_URL_ENV = 'ERROR_WEBHOOK_URL';
|
|
50
|
+
const DEFAULT_TIMEOUT_MS = 3_000;
|
|
51
|
+
/**
|
|
52
|
+
* The deployment a report came from, read from the environment.
|
|
53
|
+
*
|
|
54
|
+
* `VERCEL_ENV` before `NODE_ENV`, because on Vercel a preview deployment is
|
|
55
|
+
* `NODE_ENV=production` too, and "production or somebody's branch" is the
|
|
56
|
+
* first question about an alert. Off Vercel, `NODE_ENV` is what there is.
|
|
57
|
+
*/
|
|
58
|
+
export function currentDeployment(env = process.env) {
|
|
59
|
+
const environment = nonEmpty(env['VERCEL_ENV']) ?? nonEmpty(env['NODE_ENV']);
|
|
60
|
+
const commit = nonEmpty(env['VERCEL_GIT_COMMIT_SHA']);
|
|
61
|
+
return {
|
|
62
|
+
...(environment ? { environment } : {}),
|
|
63
|
+
...(commit ? { commit: commit.slice(0, 12) } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* An IPv4 or IPv6 address reduced to a prefix: enough to tell one host's burst
|
|
68
|
+
* from many readers hitting the same fault, and not enough to identify anyone.
|
|
69
|
+
* `x-forwarded-for` may carry a list; only the first entry is the client.
|
|
70
|
+
*/
|
|
71
|
+
export function clientPrefix(forwardedFor) {
|
|
72
|
+
if (!forwardedFor)
|
|
73
|
+
return undefined;
|
|
74
|
+
const first = forwardedFor.split(',')[0]?.trim();
|
|
75
|
+
if (!first)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (first.includes(':')) {
|
|
78
|
+
const groups = first.split(':').filter(Boolean).slice(0, 4);
|
|
79
|
+
return groups.length > 0 ? `${groups.join(':')}::/64` : undefined;
|
|
80
|
+
}
|
|
81
|
+
const octets = first.split('.');
|
|
82
|
+
return octets.length === 4 ? `${octets[0]}.${octets[1]}.0.0/16` : undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build a report for an error captured during a request. Pure, so the
|
|
86
|
+
* redaction is testable without a server.
|
|
87
|
+
*
|
|
88
|
+
* `error` is `unknown` because Next types it that way and means it: the value
|
|
89
|
+
* reaching `onRequestError` may have been processed by React rather than being
|
|
90
|
+
* the instance that was thrown, which is why `digest` is read separately.
|
|
91
|
+
*/
|
|
92
|
+
export function buildRequestErrorReport(error, request, context = {}, options = {}) {
|
|
93
|
+
const prefix = clientPrefix(headerValue(request.headers, 'x-forwarded-for'));
|
|
94
|
+
const headers = {};
|
|
95
|
+
for (const name of allowedHeaders(options.headers)) {
|
|
96
|
+
const value = headerValue(request.headers, name);
|
|
97
|
+
if (value)
|
|
98
|
+
headers[name] = value;
|
|
99
|
+
}
|
|
100
|
+
const routeContext = {};
|
|
101
|
+
for (const key of ['routePath', 'routeType', 'renderSource', 'revalidateReason']) {
|
|
102
|
+
const value = context[key];
|
|
103
|
+
if (typeof value === 'string')
|
|
104
|
+
routeContext[key] = value;
|
|
105
|
+
}
|
|
106
|
+
const digest = typeof error === 'object' && error !== null && 'digest' in error
|
|
107
|
+
? String(error.digest)
|
|
108
|
+
: undefined;
|
|
109
|
+
return {
|
|
110
|
+
kind: 'request',
|
|
111
|
+
at: (options.now ?? new Date()).toISOString(),
|
|
112
|
+
...describeError(error),
|
|
113
|
+
...(digest ? { digest } : {}),
|
|
114
|
+
request: {
|
|
115
|
+
path: request.path,
|
|
116
|
+
method: request.method,
|
|
117
|
+
headers,
|
|
118
|
+
...(prefix ? { clientPrefix: prefix } : {}),
|
|
119
|
+
},
|
|
120
|
+
context: routeContext,
|
|
121
|
+
deployment: options.deployment ?? currentDeployment(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Build a report for a job that ran out of retries.
|
|
126
|
+
*
|
|
127
|
+
* The stack is included: this goes to a webhook the team owns, where a stack
|
|
128
|
+
* is the most useful field in it. The job-failures collection, which admins
|
|
129
|
+
* read through the CMS, stores the message and not the stack.
|
|
130
|
+
*/
|
|
131
|
+
export function buildJobFailureReport(input, options = {}) {
|
|
132
|
+
return {
|
|
133
|
+
kind: 'job',
|
|
134
|
+
at: (options.now ?? new Date()).toISOString(),
|
|
135
|
+
functionId: nonEmpty(input.functionId) ?? 'unknown',
|
|
136
|
+
...(nonEmpty(input.runId) ? { runId: input.runId } : {}),
|
|
137
|
+
...(nonEmpty(input.triggerEvent) ? { triggerEvent: input.triggerEvent } : {}),
|
|
138
|
+
...describeError(input.error),
|
|
139
|
+
deployment: options.deployment ?? currentDeployment(),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Build a report for a healthcheck run with failing checks. */
|
|
143
|
+
export function buildHealthcheckFailureReport(failures, options = {}) {
|
|
144
|
+
return {
|
|
145
|
+
kind: 'healthcheck',
|
|
146
|
+
at: (options.now ?? new Date()).toISOString(),
|
|
147
|
+
functionId: options.functionId ?? 'healthcheck',
|
|
148
|
+
failed: failures.map((failure) => ({
|
|
149
|
+
name: failure.name,
|
|
150
|
+
...(failure.details ? { details: failure.details } : {}),
|
|
151
|
+
})),
|
|
152
|
+
deployment: options.deployment ?? currentDeployment(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* One line describing a report, for a receiver that renders rather than
|
|
157
|
+
* indexes. Reads the report structurally, so a site's own report shapes get a
|
|
158
|
+
* sensible line too.
|
|
159
|
+
*/
|
|
160
|
+
export function summariseReport(report) {
|
|
161
|
+
const row = (typeof report === 'object' && report !== null ? report : {});
|
|
162
|
+
const deployment = (row['deployment'] ?? {});
|
|
163
|
+
const environment = nonEmpty(deployment['environment']) ?? 'unknown environment';
|
|
164
|
+
const kind = nonEmpty(row['kind']) ?? 'error';
|
|
165
|
+
const failed = Array.isArray(row['failed'])
|
|
166
|
+
? row['failed'].map((f) => String(f.name)).join(', ')
|
|
167
|
+
: undefined;
|
|
168
|
+
const headline = nonEmpty(row['message']) ?? (failed ? `failing: ${failed}` : undefined) ?? 'no message';
|
|
169
|
+
const request = (row['request'] ?? {});
|
|
170
|
+
const path = nonEmpty(request['path']);
|
|
171
|
+
const where = nonEmpty(row['functionId']) ??
|
|
172
|
+
(path ? `${nonEmpty(request['method']) ?? 'GET'} ${path}` : undefined);
|
|
173
|
+
return `[${environment}] ${kind}: ${headline}${where ? ` — ${where}` : ''}`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Whether reports go anywhere, as a sentence for a boot-time log line. A
|
|
177
|
+
* sentence rather than a boolean because "off because this is a laptop" and
|
|
178
|
+
* "off because somebody forgot in production" look identical otherwise.
|
|
179
|
+
*/
|
|
180
|
+
export function describeErrorReporting(url = readWebhookUrl()) {
|
|
181
|
+
if (!url || url.trim() === '') {
|
|
182
|
+
return `error reporting: OFF (${ERROR_WEBHOOK_URL_ENV} unset) — errors reach the log only`;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return `error reporting: on, to ${new URL(url).host}`;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return `error reporting: MISCONFIGURED (${ERROR_WEBHOOK_URL_ENV} is not a URL) — errors reach the log only`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A reporter that POSTs each report as JSON, with a `text` summary added.
|
|
193
|
+
*
|
|
194
|
+
* A delivery that fails — a refused connection, a timeout, a non-2xx — is
|
|
195
|
+
* logged as a warning and otherwise swallowed. It is never rethrown: see the
|
|
196
|
+
* note at the top of this file.
|
|
197
|
+
*/
|
|
198
|
+
export function createErrorReporter(options = {}) {
|
|
199
|
+
const logger = options.logger ?? defaultLogger;
|
|
200
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
201
|
+
return async function deliver(report) {
|
|
202
|
+
try {
|
|
203
|
+
const url = (options.url ?? readWebhookUrl())?.trim();
|
|
204
|
+
if (!url)
|
|
205
|
+
return;
|
|
206
|
+
const text = summariseReport(report);
|
|
207
|
+
// A non-object report is still delivered, wrapped, rather than dropped.
|
|
208
|
+
const body = typeof report === 'object' && report !== null
|
|
209
|
+
? { text, ...report }
|
|
210
|
+
: { text, report };
|
|
211
|
+
const doFetch = options.fetch ?? fetch;
|
|
212
|
+
const response = await doFetch(url, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: { ...options.webhookHeaders, 'content-type': 'application/json' },
|
|
215
|
+
body: JSON.stringify(body),
|
|
216
|
+
// A serverless function may be frozen once its response is sent; a
|
|
217
|
+
// report that has not left by then never does.
|
|
218
|
+
keepalive: true,
|
|
219
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
220
|
+
});
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
logger.warn('Error report was not accepted', { status: response.status, summary: text });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (deliveryError) {
|
|
226
|
+
try {
|
|
227
|
+
logger.warn('Error report delivery failed', { error: String(deliveryError) });
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// A logger that throws is the last thing that can go wrong here.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** The default reporter: `ERROR_WEBHOOK_URL`, a 3s timeout, the console logger. */
|
|
236
|
+
export const reportError = createErrorReporter();
|
|
237
|
+
function readWebhookUrl() {
|
|
238
|
+
return typeof process === 'undefined' ? undefined : process.env[ERROR_WEBHOOK_URL_ENV];
|
|
239
|
+
}
|
|
240
|
+
function allowedHeaders(requested) {
|
|
241
|
+
const never = new Set(NEVER_REPORTED_HEADERS);
|
|
242
|
+
return (requested ?? DEFAULT_REPORTED_HEADERS)
|
|
243
|
+
.map((name) => name.toLowerCase())
|
|
244
|
+
.filter((name) => !never.has(name));
|
|
245
|
+
}
|
|
246
|
+
function headerValue(headers, name) {
|
|
247
|
+
let raw;
|
|
248
|
+
if (typeof headers.get === 'function') {
|
|
249
|
+
raw = headers.get(name);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const record = headers;
|
|
253
|
+
raw = record[name] ?? Object.entries(record).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
254
|
+
}
|
|
255
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
256
|
+
return nonEmpty(value);
|
|
257
|
+
}
|
|
258
|
+
function describeError(error) {
|
|
259
|
+
if (error instanceof Error) {
|
|
260
|
+
return {
|
|
261
|
+
message: error.message || String(error),
|
|
262
|
+
...(error.name ? { name: error.name } : {}),
|
|
263
|
+
...(error.stack ? { stack: error.stack } : {}),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (typeof error === 'object' && error !== null && 'message' in error) {
|
|
267
|
+
return { message: String(error.message) };
|
|
268
|
+
}
|
|
269
|
+
return { message: String(error) };
|
|
270
|
+
}
|
|
271
|
+
function nonEmpty(value) {
|
|
272
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/observability/report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
import type { Logger } from '@forumone/throughline-plugin-contract';
|
|
3
|
+
import { type HealthcheckFailureReport, type JobFailureReport } from './report.js';
|
|
4
|
+
export interface JobFailureWriterOptions {
|
|
5
|
+
payload: Payload;
|
|
6
|
+
collectionSlug?: string | undefined;
|
|
7
|
+
logger?: Logger | undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Records one failure. Never throws, never rejects. */
|
|
10
|
+
export type JobFailureWriter = (report: JobFailureReport | HealthcheckFailureReport) => Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns a writer that turns a failure report into a `job-failures` row.
|
|
13
|
+
*
|
|
14
|
+
* It never throws: it runs inside a failure handler, and a throw there is
|
|
15
|
+
* retried by the job runner — a retry storm about the recorder while the
|
|
16
|
+
* original failure goes unrecorded. A write that fails is logged at `error`
|
|
17
|
+
* **with the failure's own summary and message**, so the one thing this
|
|
18
|
+
* writer exists to keep is never lost silently, only demoted to the log.
|
|
19
|
+
*
|
|
20
|
+
* The stack is not stored. Rows are readable through the admin, and a stack
|
|
21
|
+
* names file paths; the webhook report carries it instead.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createJobFailureWriter(options: JobFailureWriterOptions): JobFailureWriter;
|
|
24
|
+
//# sourceMappingURL=writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/observability/writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uCAAuC,CAAA;AAGnE,OAAO,EAAmB,KAAK,wBAAwB,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnG,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B;AAED,wDAAwD;AACxD,MAAM,MAAM,gBAAgB,GAAG,CAC7B,MAAM,EAAE,gBAAgB,GAAG,wBAAwB,KAChD,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CAsBzF"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { defaultLogger } from '../logger/index.js';
|
|
2
|
+
import { DEFAULT_JOB_FAILURES_SLUG } from './collection.js';
|
|
3
|
+
import { summariseReport } from './report.js';
|
|
4
|
+
/**
|
|
5
|
+
* Returns a writer that turns a failure report into a `job-failures` row.
|
|
6
|
+
*
|
|
7
|
+
* It never throws: it runs inside a failure handler, and a throw there is
|
|
8
|
+
* retried by the job runner — a retry storm about the recorder while the
|
|
9
|
+
* original failure goes unrecorded. A write that fails is logged at `error`
|
|
10
|
+
* **with the failure's own summary and message**, so the one thing this
|
|
11
|
+
* writer exists to keep is never lost silently, only demoted to the log.
|
|
12
|
+
*
|
|
13
|
+
* The stack is not stored. Rows are readable through the admin, and a stack
|
|
14
|
+
* names file paths; the webhook report carries it instead.
|
|
15
|
+
*/
|
|
16
|
+
export function createJobFailureWriter(options) {
|
|
17
|
+
const { payload, collectionSlug = DEFAULT_JOB_FAILURES_SLUG } = options;
|
|
18
|
+
const logger = options.logger ?? defaultLogger;
|
|
19
|
+
return async function recordJobFailure(report) {
|
|
20
|
+
let data;
|
|
21
|
+
try {
|
|
22
|
+
data = toRow(report);
|
|
23
|
+
await payload.create({ collection: collectionSlug, data });
|
|
24
|
+
}
|
|
25
|
+
catch (writeError) {
|
|
26
|
+
try {
|
|
27
|
+
logger.error('Job failure write failed', {
|
|
28
|
+
error: String(writeError),
|
|
29
|
+
collection: collectionSlug,
|
|
30
|
+
summary: data?.['summary'] ?? safeSummary(report),
|
|
31
|
+
message: data?.['message'],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Nothing left to tell.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function toRow(report) {
|
|
41
|
+
const base = {
|
|
42
|
+
createdAt: report.at,
|
|
43
|
+
kind: report.kind,
|
|
44
|
+
source: report.functionId,
|
|
45
|
+
summary: summariseReport(report).slice(0, 500),
|
|
46
|
+
...(report.deployment.environment ? { environment: report.deployment.environment } : {}),
|
|
47
|
+
...(report.deployment.commit ? { commit: report.deployment.commit } : {}),
|
|
48
|
+
};
|
|
49
|
+
if (report.kind === 'healthcheck') {
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
message: `${report.failed.length} check(s) failed: ${report.failed.map((f) => f.name).join(', ')}`,
|
|
53
|
+
details: report.failed,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
...base,
|
|
58
|
+
message: report.message,
|
|
59
|
+
...(report.name ? { errorName: report.name } : {}),
|
|
60
|
+
...(report.runId ? { runId: report.runId } : {}),
|
|
61
|
+
...(report.triggerEvent ? { triggerEvent: report.triggerEvent } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function safeSummary(report) {
|
|
65
|
+
try {
|
|
66
|
+
return summariseReport(report);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return '(unsummarisable failure report)';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.js","sourceRoot":"","sources":["../../src/observability/writer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,EAAE,eAAe,EAAwD,MAAM,aAAa,CAAA;AAanG;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAgC;IACrE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,yBAAyB,EAAE,GAAG,OAAO,CAAA;IACvE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,aAAa,CAAA;IAE9C,OAAO,KAAK,UAAU,gBAAgB,CAAC,MAAM;QAC3C,IAAI,IAAyC,CAAA;QAC7C,IAAI,CAAC;YACH,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;YACpB,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAA;QAC5D,CAAC;QAAC,OAAO,UAAU,EAAE,CAAC;YACpB,IAAI,CAAC;gBACH,MAAM,CAAC,KAAK,CAAC,0BAA0B,EAAE;oBACvC,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC;oBACzB,UAAU,EAAE,cAAc;oBAC1B,OAAO,EAAG,IAAI,EAAE,CAAC,SAAS,CAAwB,IAAI,WAAW,CAAC,MAAM,CAAC;oBACzE,OAAO,EAAE,IAAI,EAAE,CAAC,SAAS,CAAC;iBAC3B,CAAC,CAAA;YACJ,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;QACH,CAAC;IACH,CAAC,CAAA;AACH,CAAC;AAED,SAAS,KAAK,CAAC,MAAmD;IAChE,MAAM,IAAI,GAAG;QACX,SAAS,EAAE,MAAM,CAAC,EAAE;QACpB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,MAAM,CAAC,UAAU;QACzB,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;QAC9C,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxF,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1E,CAAA;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;QAClC,OAAO;YACL,GAAG,IAAI;YACP,OAAO,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,qBAAqB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YAClG,OAAO,EAAE,MAAM,CAAC,MAAM;SACvB,CAAA;IACH,CAAC;IAED,OAAO;QACL,GAAG,IAAI;QACP,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtE,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,MAAe;IAClC,IAAI,CAAC;QACH,OAAO,eAAe,CAAC,MAAM,CAAC,CAAA;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,iCAAiC,CAAA;IAC1C,CAAC;AACH,CAAC"}
|