@beignet/devtools 0.0.3 → 0.0.4
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 +79 -0
- package/README.md +116 -71
- package/dist/events.d.ts +15 -3
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +1 -0
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +13 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -13
- package/dist/index.js.map +1 -1
- package/dist/persistence.d.ts +1 -1
- package/dist/persistence.d.ts.map +1 -1
- package/dist/provider-instrumentation.d.ts +1 -1
- package/dist/provider-instrumentation.d.ts.map +1 -1
- package/dist/provider.d.ts +5 -5
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +10 -5
- package/dist/provider.js.map +1 -1
- package/dist/redaction.d.ts +1 -1
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +1 -0
- package/dist/redaction.js.map +1 -1
- package/dist/routes.d.ts +2 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +3 -3
- package/dist/routes.js.map +1 -1
- package/dist/ui-model.d.ts +38 -0
- package/dist/ui-model.d.ts.map +1 -0
- package/dist/ui-model.js +1039 -0
- package/dist/ui-model.js.map +1 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +310 -164
- package/dist/ui.js.map +1 -1
- package/dist/watchers.d.ts +2 -2
- package/dist/watchers.d.ts.map +1 -1
- package/dist/watchers.js +10 -2
- package/dist/watchers.js.map +1 -1
- package/package.json +22 -2
- package/src/events.ts +25 -1
- package/src/index.ts +13 -15
- package/src/persistence.ts +1 -1
- package/src/provider-instrumentation.ts +1 -1
- package/src/provider.ts +12 -7
- package/src/redaction.ts +2 -1
- package/src/routes.ts +4 -4
- package/src/ui-model.ts +1233 -0
- package/src/ui.ts +310 -164
- package/src/watchers.ts +12 -3
- package/dist/audit.d.ts +0 -31
- package/dist/audit.d.ts.map +0 -1
- package/dist/audit.js +0 -55
- package/dist/audit.js.map +0 -1
- package/dist/instrumentation.d.ts +0 -114
- package/dist/instrumentation.d.ts.map +0 -1
- package/dist/instrumentation.js +0 -303
- package/dist/instrumentation.js.map +0 -1
- package/dist/trace-context.d.ts +0 -80
- package/dist/trace-context.d.ts.map +0 -1
- package/dist/trace-context.js +0 -92
- package/dist/trace-context.js.map +0 -1
- package/src/audit.ts +0 -92
- package/src/instrumentation.ts +0 -491
- package/src/trace-context.ts +0 -166
package/dist/trace-context.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
const TRACEPARENT_PATTERN = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
2
|
-
function createHexId(length) {
|
|
3
|
-
const bytes = new Uint8Array(length / 2);
|
|
4
|
-
if (typeof crypto !== "undefined" && "getRandomValues" in crypto) {
|
|
5
|
-
crypto.getRandomValues(bytes);
|
|
6
|
-
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7
|
-
}
|
|
8
|
-
let value = "";
|
|
9
|
-
while (value.length < length) {
|
|
10
|
-
value += Math.floor(Math.random() * 16).toString(16);
|
|
11
|
-
}
|
|
12
|
-
return value.slice(0, length);
|
|
13
|
-
}
|
|
14
|
-
function isNonZeroHex(value) {
|
|
15
|
-
return !/^0+$/.test(value);
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Create a non-zero W3C trace ID.
|
|
19
|
-
*/
|
|
20
|
-
export function createTraceId() {
|
|
21
|
-
let traceId = createHexId(32);
|
|
22
|
-
while (!isNonZeroHex(traceId)) {
|
|
23
|
-
traceId = createHexId(32);
|
|
24
|
-
}
|
|
25
|
-
return traceId;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Create a non-zero W3C span ID.
|
|
29
|
-
*/
|
|
30
|
-
export function createSpanId() {
|
|
31
|
-
let spanId = createHexId(16);
|
|
32
|
-
while (!isNonZeroHex(spanId)) {
|
|
33
|
-
spanId = createHexId(16);
|
|
34
|
-
}
|
|
35
|
-
return spanId;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Create a W3C traceparent header value.
|
|
39
|
-
*/
|
|
40
|
-
export function createTraceparent(args) {
|
|
41
|
-
return `00-${args.traceId}-${args.spanId}-${args.traceFlags ?? "01"}`;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Parse and validate a W3C traceparent header value.
|
|
45
|
-
*/
|
|
46
|
-
export function parseTraceparent(value) {
|
|
47
|
-
if (!value)
|
|
48
|
-
return undefined;
|
|
49
|
-
const normalized = value.trim().toLowerCase();
|
|
50
|
-
const match = TRACEPARENT_PATTERN.exec(normalized);
|
|
51
|
-
if (!match)
|
|
52
|
-
return undefined;
|
|
53
|
-
const [, traceId, spanId, traceFlags] = match;
|
|
54
|
-
if (!isNonZeroHex(traceId) || !isNonZeroHex(spanId))
|
|
55
|
-
return undefined;
|
|
56
|
-
return {
|
|
57
|
-
traceId,
|
|
58
|
-
spanId,
|
|
59
|
-
traceFlags,
|
|
60
|
-
traceparent: normalized,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Create devtools trace context from explicit IDs or an existing traceparent.
|
|
65
|
-
*/
|
|
66
|
-
export function createDevtoolsTraceContext(input = {}) {
|
|
67
|
-
const parsed = parseTraceparent(input.traceparent);
|
|
68
|
-
const traceId = input.traceId ?? parsed?.traceId ?? createTraceId();
|
|
69
|
-
const parentSpanId = input.parentSpanId ?? parsed?.spanId;
|
|
70
|
-
const spanId = input.spanId ?? createSpanId();
|
|
71
|
-
return {
|
|
72
|
-
traceId,
|
|
73
|
-
spanId,
|
|
74
|
-
...(parentSpanId ? { parentSpanId } : {}),
|
|
75
|
-
traceparent: createTraceparent({
|
|
76
|
-
traceId,
|
|
77
|
-
spanId,
|
|
78
|
-
traceFlags: parsed?.traceFlags,
|
|
79
|
-
}),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Create child trace context from a parent context.
|
|
84
|
-
*/
|
|
85
|
-
export function createChildDevtoolsTraceContext(parent) {
|
|
86
|
-
return createDevtoolsTraceContext({
|
|
87
|
-
traceId: parent.traceId,
|
|
88
|
-
parentSpanId: parent.spanId,
|
|
89
|
-
traceparent: parent.traceparent,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
//# sourceMappingURL=trace-context.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"trace-context.js","sourceRoot":"","sources":["../src/trace-context.ts"],"names":[],"mappings":"AAAA,MAAM,mBAAmB,GAAG,kDAAkD,CAAC;AAwD/E,SAAS,WAAW,CAAC,MAAc;IACjC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,iBAAiB,IAAI,MAAM,EAAE,CAAC;QACjE,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAC9B,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CACzE,EAAE,CACH,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,OAAO,KAAK,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC;QAC7B,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,IAAI,OAAO,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC9B,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9B,OAAO,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY;IAC1B,IAAI,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC7B,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAIjC;IACC,OAAO,MAAM,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;AACxE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAgC;IAEhC,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACnD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAE7B,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,GAAG,KAAK,CAAC;IAC9C,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAEtE,OAAO;QACL,OAAO;QACP,MAAM;QACN,UAAU;QACV,WAAW,EAAE,UAAU;KACxB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,0BAA0B,CACxC,QAAmC,EAAE;IAErC,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,MAAM,EAAE,OAAO,IAAI,aAAa,EAAE,CAAC;IACpE,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,IAAI,MAAM,EAAE,MAAM,CAAC;IAC1D,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,YAAY,EAAE,CAAC;IAE9C,OAAO;QACL,OAAO;QACP,MAAM;QACN,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,WAAW,EAAE,iBAAiB,CAAC;YAC7B,OAAO;YACP,MAAM;YACN,UAAU,EAAE,MAAM,EAAE,UAAU;SAC/B,CAAC;KACH,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,+BAA+B,CAC7C,MAAiC;IAEjC,OAAO,0BAA0B,CAAC;QAChC,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,YAAY,EAAE,MAAM,CAAC,MAAM;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAC;AACL,CAAC"}
|
package/src/audit.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AuditLogEntry,
|
|
3
|
-
type AuditLogEntryInput,
|
|
4
|
-
type AuditLogPort,
|
|
5
|
-
normalizeAuditLogEntry,
|
|
6
|
-
redactAuditLogEntry,
|
|
7
|
-
} from "@beignet/core/ports";
|
|
8
|
-
import type { DevtoolsPort } from "./index";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Options for wrapping an audit log with devtools emission.
|
|
12
|
-
*/
|
|
13
|
-
export interface DevtoolsAuditLogOptions {
|
|
14
|
-
/**
|
|
15
|
-
* Durable audit log to write first.
|
|
16
|
-
*/
|
|
17
|
-
audit: AuditLogPort;
|
|
18
|
-
/**
|
|
19
|
-
* Optional devtools port that receives audit events.
|
|
20
|
-
*/
|
|
21
|
-
devtools?: DevtoolsPort;
|
|
22
|
-
/**
|
|
23
|
-
* Whether to emit devtools events. Defaults to true.
|
|
24
|
-
*/
|
|
25
|
-
emit?: boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Optional app-owned redactor applied after Beignet's audit redaction.
|
|
28
|
-
*/
|
|
29
|
-
redact?: (entry: AuditLogEntry) => AuditLogEntry;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function prepareEntry(
|
|
33
|
-
input: AuditLogEntryInput,
|
|
34
|
-
redact?: (entry: AuditLogEntry) => AuditLogEntry,
|
|
35
|
-
): AuditLogEntry {
|
|
36
|
-
const redacted = redactAuditLogEntry(normalizeAuditLogEntry(input));
|
|
37
|
-
return redact ? redact(redacted) : redacted;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function auditSummary(entry: AuditLogEntry): string {
|
|
41
|
-
const resource = entry.resource?.id
|
|
42
|
-
? `${entry.resource.type}:${entry.resource.id}`
|
|
43
|
-
: entry.resource?.type;
|
|
44
|
-
const outcome = entry.outcome === "failure" ? "failed" : "succeeded";
|
|
45
|
-
return resource
|
|
46
|
-
? `${entry.action} ${outcome} for ${resource}`
|
|
47
|
-
: `${entry.action} ${outcome}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Wrap an audit log so durable audit writes can also appear in devtools.
|
|
52
|
-
*
|
|
53
|
-
* Devtools observer failures are ignored so audit persistence remains the
|
|
54
|
-
* source of truth.
|
|
55
|
-
*/
|
|
56
|
-
export function createDevtoolsAuditLog(
|
|
57
|
-
options: DevtoolsAuditLogOptions,
|
|
58
|
-
): AuditLogPort {
|
|
59
|
-
return {
|
|
60
|
-
async record(input) {
|
|
61
|
-
const entry = prepareEntry(input, options.redact);
|
|
62
|
-
|
|
63
|
-
await options.audit.record(entry);
|
|
64
|
-
|
|
65
|
-
if (options.emit === false || !options.devtools) return;
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
options.devtools.record({
|
|
69
|
-
type: "custom",
|
|
70
|
-
watcher: "audit",
|
|
71
|
-
name: entry.action,
|
|
72
|
-
label: "Audit",
|
|
73
|
-
summary: auditSummary(entry),
|
|
74
|
-
requestId: entry.requestId,
|
|
75
|
-
traceId: entry.traceId,
|
|
76
|
-
details: {
|
|
77
|
-
action: entry.action,
|
|
78
|
-
actor: entry.actor,
|
|
79
|
-
tenant: entry.tenant,
|
|
80
|
-
resource: entry.resource,
|
|
81
|
-
outcome: entry.outcome,
|
|
82
|
-
message: entry.message,
|
|
83
|
-
metadata: entry.metadata,
|
|
84
|
-
occurredAt: entry.occurredAt.toISOString(),
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
} catch {
|
|
88
|
-
// Devtools is an observer; durable audit writes must not depend on it.
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
}
|
package/src/instrumentation.ts
DELETED
|
@@ -1,491 +0,0 @@
|
|
|
1
|
-
import type { AnyPorts } from "@beignet/core/ports";
|
|
2
|
-
import type {
|
|
3
|
-
HttpContractConfig,
|
|
4
|
-
HttpRequestLike,
|
|
5
|
-
HttpResponseLike,
|
|
6
|
-
ServerHook,
|
|
7
|
-
} from "@beignet/core/server";
|
|
8
|
-
import type { DevtoolsEventInput, DevtoolsRedactor } from "./events";
|
|
9
|
-
import type { DevtoolsPort } from "./index";
|
|
10
|
-
import { createDevtoolsEvent } from "./provider";
|
|
11
|
-
import {
|
|
12
|
-
createChildDevtoolsTraceContext,
|
|
13
|
-
createDevtoolsTraceContext,
|
|
14
|
-
type DevtoolsTraceContext,
|
|
15
|
-
type DevtoolsTraceContextInput,
|
|
16
|
-
parseTraceparent,
|
|
17
|
-
} from "./trace-context";
|
|
18
|
-
import type { BuiltInDevtoolsWatcherName } from "./watchers";
|
|
19
|
-
|
|
20
|
-
type PortsWithDevtools = AnyPorts & {
|
|
21
|
-
devtools?: DevtoolsPort;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type ContextWithPorts = {
|
|
25
|
-
requestId?: string;
|
|
26
|
-
traceId?: string;
|
|
27
|
-
spanId?: string;
|
|
28
|
-
parentSpanId?: string;
|
|
29
|
-
traceparent?: string;
|
|
30
|
-
ports?: {
|
|
31
|
-
devtools?: DevtoolsPort;
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Options for server hooks that record request/error events to devtools.
|
|
37
|
-
*/
|
|
38
|
-
export interface DevtoolsHooksOptions<Ctx> {
|
|
39
|
-
/**
|
|
40
|
-
* Devtools route prefix. Requests under this path are ignored to avoid
|
|
41
|
-
* filling the timeline with its own polling traffic.
|
|
42
|
-
*
|
|
43
|
-
* @default "/api/devtools"
|
|
44
|
-
*/
|
|
45
|
-
basePath?: string;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Resolve the request ID used for event correlation.
|
|
49
|
-
*/
|
|
50
|
-
getRequestId?: (args: {
|
|
51
|
-
req: HttpRequestLike;
|
|
52
|
-
ctx?: Ctx;
|
|
53
|
-
response?: HttpResponseLike;
|
|
54
|
-
}) => string | undefined;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Response header used to expose the resolved request ID.
|
|
58
|
-
*
|
|
59
|
-
* Pass `false` to avoid writing a header.
|
|
60
|
-
*
|
|
61
|
-
* @default "x-request-id"
|
|
62
|
-
*/
|
|
63
|
-
requestIdHeader?: string | false;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* W3C trace context header used to correlate devtools events with distributed
|
|
67
|
-
* traces.
|
|
68
|
-
*
|
|
69
|
-
* Pass `false` to avoid writing a header.
|
|
70
|
-
*
|
|
71
|
-
* @default "traceparent"
|
|
72
|
-
*/
|
|
73
|
-
traceContextHeader?: string | false;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Resolve a trace context from request/context/response data.
|
|
77
|
-
*/
|
|
78
|
-
getTraceContext?: (args: {
|
|
79
|
-
req: HttpRequestLike;
|
|
80
|
-
ctx?: Ctx;
|
|
81
|
-
response?: HttpResponseLike;
|
|
82
|
-
}) => DevtoolsTraceContextInput | string | undefined;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Apply a custom redactor to events produced by these hooks. The default
|
|
86
|
-
* devtools redactor still runs when events are stored.
|
|
87
|
-
*/
|
|
88
|
-
redact?: DevtoolsRedactor;
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Decide whether to capture a completed request event.
|
|
92
|
-
*/
|
|
93
|
-
shouldCapture?: (args: {
|
|
94
|
-
req: HttpRequestLike;
|
|
95
|
-
ctx?: Ctx;
|
|
96
|
-
contract: HttpContractConfig;
|
|
97
|
-
response: HttpResponseLike;
|
|
98
|
-
error?: unknown;
|
|
99
|
-
}) => boolean;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Use-case run event shape consumed by the devtools observer.
|
|
104
|
-
*/
|
|
105
|
-
export interface DevtoolsUseCaseRunEvent<Ctx> {
|
|
106
|
-
name: string;
|
|
107
|
-
kind: "command" | "query";
|
|
108
|
-
phase: "start" | "end" | "error";
|
|
109
|
-
durationMs?: number;
|
|
110
|
-
error?: unknown;
|
|
111
|
-
ctx: Ctx;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Options for `createDevtoolsUseCaseObserver(...)`.
|
|
116
|
-
*/
|
|
117
|
-
export interface DevtoolsUseCaseObserverOptions<Ctx> {
|
|
118
|
-
/**
|
|
119
|
-
* Resolve a devtools port from use-case context.
|
|
120
|
-
*/
|
|
121
|
-
getDevtools?: (ctx: Ctx) => DevtoolsPort | undefined;
|
|
122
|
-
/**
|
|
123
|
-
* Resolve the request ID used for event correlation.
|
|
124
|
-
*/
|
|
125
|
-
getRequestId?: (ctx: Ctx) => string | undefined;
|
|
126
|
-
/**
|
|
127
|
-
* Whether use-case error phases should also emit `error` events.
|
|
128
|
-
*/
|
|
129
|
-
logErrorEvents?: boolean;
|
|
130
|
-
/**
|
|
131
|
-
* Optional redactor applied before events are recorded.
|
|
132
|
-
*/
|
|
133
|
-
redact?: DevtoolsRedactor;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getContextRequestId(ctx: unknown): string | undefined {
|
|
137
|
-
if (!ctx || typeof ctx !== "object") return undefined;
|
|
138
|
-
const requestId = (ctx as { requestId?: unknown }).requestId;
|
|
139
|
-
return typeof requestId === "string" ? requestId : undefined;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function getContextDevtools(ctx: unknown): DevtoolsPort | undefined {
|
|
143
|
-
if (!ctx || typeof ctx !== "object") return undefined;
|
|
144
|
-
const ports = (ctx as ContextWithPorts).ports;
|
|
145
|
-
return ports?.devtools;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function getContextTraceContext(
|
|
149
|
-
ctx: unknown,
|
|
150
|
-
): DevtoolsTraceContextInput | undefined {
|
|
151
|
-
if (!ctx || typeof ctx !== "object") return undefined;
|
|
152
|
-
const context = ctx as ContextWithPorts;
|
|
153
|
-
if (
|
|
154
|
-
!context.traceId &&
|
|
155
|
-
!context.spanId &&
|
|
156
|
-
!context.parentSpanId &&
|
|
157
|
-
!context.traceparent
|
|
158
|
-
) {
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
traceId: context.traceId,
|
|
163
|
-
spanId: context.spanId,
|
|
164
|
-
parentSpanId: context.parentSpanId,
|
|
165
|
-
traceparent: context.traceparent,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function getErrorMessage(error: unknown): string {
|
|
170
|
-
if (error instanceof Error) return error.message;
|
|
171
|
-
if (typeof error === "string") return error;
|
|
172
|
-
return "Unknown error";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function getErrorStack(error: unknown): string | undefined {
|
|
176
|
-
return error instanceof Error ? error.stack : undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function createRequestId(): string {
|
|
180
|
-
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
181
|
-
return crypto.randomUUID();
|
|
182
|
-
}
|
|
183
|
-
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function getPathname(req: HttpRequestLike): string {
|
|
187
|
-
try {
|
|
188
|
-
return new URL(req.url).pathname;
|
|
189
|
-
} catch {
|
|
190
|
-
return req.url;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function isDevtoolsPath(pathname: string, basePath: string): boolean {
|
|
195
|
-
const normalizedBase = basePath.replace(/\/+$/, "");
|
|
196
|
-
return (
|
|
197
|
-
pathname === normalizedBase || pathname.startsWith(`${normalizedBase}/`)
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function isWatcherEnabled(
|
|
202
|
-
devtools: DevtoolsPort,
|
|
203
|
-
name: BuiltInDevtoolsWatcherName,
|
|
204
|
-
): boolean {
|
|
205
|
-
return devtools.isWatcherEnabled(name);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Create server hooks that record Beignet request and error activity.
|
|
210
|
-
*
|
|
211
|
-
* Devtools routes under `basePath` are ignored so polling and SSE traffic do not
|
|
212
|
-
* fill the event buffer. Request IDs and trace context are also written to
|
|
213
|
-
* response headers unless disabled.
|
|
214
|
-
*/
|
|
215
|
-
export function createDevtoolsHooks<
|
|
216
|
-
Ctx,
|
|
217
|
-
Ports extends PortsWithDevtools = PortsWithDevtools,
|
|
218
|
-
>(options: DevtoolsHooksOptions<Ctx> = {}): ServerHook<Ctx, Ports> {
|
|
219
|
-
let devtools: DevtoolsPort | undefined;
|
|
220
|
-
const basePath = options.basePath ?? "/api/devtools";
|
|
221
|
-
const requestIdHeader = options.requestIdHeader ?? "x-request-id";
|
|
222
|
-
const traceContextHeader = options.traceContextHeader ?? "traceparent";
|
|
223
|
-
const generatedRequestIds = new WeakMap<HttpRequestLike, string>();
|
|
224
|
-
const traceContexts = new WeakMap<HttpRequestLike, DevtoolsTraceContext>();
|
|
225
|
-
|
|
226
|
-
const resolveRequestId = (args: {
|
|
227
|
-
req: HttpRequestLike;
|
|
228
|
-
ctx?: Ctx;
|
|
229
|
-
response?: HttpResponseLike;
|
|
230
|
-
}) => {
|
|
231
|
-
const headerRequestId =
|
|
232
|
-
requestIdHeader === false
|
|
233
|
-
? undefined
|
|
234
|
-
: args.req.headers.get(requestIdHeader);
|
|
235
|
-
const existing =
|
|
236
|
-
options.getRequestId?.(args) ??
|
|
237
|
-
getContextRequestId(args.ctx) ??
|
|
238
|
-
headerRequestId ??
|
|
239
|
-
generatedRequestIds.get(args.req);
|
|
240
|
-
|
|
241
|
-
if (existing) return existing;
|
|
242
|
-
|
|
243
|
-
const requestId = createRequestId();
|
|
244
|
-
generatedRequestIds.set(args.req, requestId);
|
|
245
|
-
return requestId;
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const resolveTraceContext = (args: {
|
|
249
|
-
req: HttpRequestLike;
|
|
250
|
-
ctx?: Ctx;
|
|
251
|
-
response?: HttpResponseLike;
|
|
252
|
-
}) => {
|
|
253
|
-
const configured = options.getTraceContext?.(args);
|
|
254
|
-
const configuredContext =
|
|
255
|
-
typeof configured === "string" ? { traceparent: configured } : configured;
|
|
256
|
-
const contextTrace = getContextTraceContext(args.ctx);
|
|
257
|
-
const headerTraceparent =
|
|
258
|
-
traceContextHeader === false
|
|
259
|
-
? undefined
|
|
260
|
-
: args.req.headers.get(traceContextHeader);
|
|
261
|
-
const existing = configuredContext ?? contextTrace;
|
|
262
|
-
const existingTraceparent =
|
|
263
|
-
existing?.traceparent ?? parseTraceparent(headerTraceparent)?.traceparent;
|
|
264
|
-
|
|
265
|
-
if (existing) {
|
|
266
|
-
const traceContext = createDevtoolsTraceContext({
|
|
267
|
-
...existing,
|
|
268
|
-
traceparent: existingTraceparent,
|
|
269
|
-
});
|
|
270
|
-
traceContexts.set(args.req, traceContext);
|
|
271
|
-
return traceContext;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const cached = traceContexts.get(args.req);
|
|
275
|
-
if (cached) return cached;
|
|
276
|
-
|
|
277
|
-
const traceContext = createDevtoolsTraceContext({
|
|
278
|
-
traceparent: existingTraceparent,
|
|
279
|
-
});
|
|
280
|
-
traceContexts.set(args.req, traceContext);
|
|
281
|
-
return traceContext;
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const record = (event: DevtoolsEventInput) => {
|
|
285
|
-
if (!devtools) return;
|
|
286
|
-
const normalized = createDevtoolsEvent(event);
|
|
287
|
-
try {
|
|
288
|
-
devtools.record(options.redact ? options.redact(normalized) : normalized);
|
|
289
|
-
} catch (error) {
|
|
290
|
-
devtools.record({
|
|
291
|
-
type: "error",
|
|
292
|
-
message: "Devtools hook redactor failed",
|
|
293
|
-
details: {
|
|
294
|
-
message: error instanceof Error ? error.message : String(error),
|
|
295
|
-
},
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
name: "devtools",
|
|
302
|
-
onRequest: ({ ports }) => {
|
|
303
|
-
devtools = ports.devtools;
|
|
304
|
-
return undefined;
|
|
305
|
-
},
|
|
306
|
-
beforeHandle: ({ req, ctx }) => {
|
|
307
|
-
if (!ctx || typeof ctx !== "object" || Array.isArray(ctx)) {
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const traceContext = resolveTraceContext({ req, ctx });
|
|
312
|
-
return {
|
|
313
|
-
ctx: {
|
|
314
|
-
...ctx,
|
|
315
|
-
traceId: traceContext.traceId,
|
|
316
|
-
spanId: traceContext.spanId,
|
|
317
|
-
parentSpanId: traceContext.parentSpanId,
|
|
318
|
-
traceparent: traceContext.traceparent,
|
|
319
|
-
} as Ctx,
|
|
320
|
-
};
|
|
321
|
-
},
|
|
322
|
-
beforeSend: async ({ req, ctx, response }) => {
|
|
323
|
-
if (requestIdHeader === false && traceContextHeader === false) {
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const requestId = resolveRequestId({ req, ctx, response });
|
|
328
|
-
const traceContext = resolveTraceContext({ req, ctx, response });
|
|
329
|
-
const headers = {
|
|
330
|
-
...response.headers,
|
|
331
|
-
...(requestIdHeader === false ? {} : { [requestIdHeader]: requestId }),
|
|
332
|
-
...(traceContextHeader === false
|
|
333
|
-
? {}
|
|
334
|
-
: { [traceContextHeader]: traceContext.traceparent }),
|
|
335
|
-
};
|
|
336
|
-
return {
|
|
337
|
-
...response,
|
|
338
|
-
headers,
|
|
339
|
-
};
|
|
340
|
-
},
|
|
341
|
-
afterSend: async ({ req, ctx, contract, response, error, durationMs }) => {
|
|
342
|
-
if (!devtools) return;
|
|
343
|
-
|
|
344
|
-
const path = getPathname(req);
|
|
345
|
-
if (isDevtoolsPath(path, basePath)) return;
|
|
346
|
-
|
|
347
|
-
const shouldCaptureRequest = isWatcherEnabled(devtools, "requests");
|
|
348
|
-
const shouldCaptureError =
|
|
349
|
-
Boolean(error) && isWatcherEnabled(devtools, "errors");
|
|
350
|
-
|
|
351
|
-
if (!shouldCaptureRequest && !shouldCaptureError) return;
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
options.shouldCapture &&
|
|
355
|
-
!options.shouldCapture({ req, ctx, contract, response, error })
|
|
356
|
-
) {
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const requestId = resolveRequestId({ req, ctx, response });
|
|
361
|
-
const traceContext = resolveTraceContext({ req, ctx, response });
|
|
362
|
-
|
|
363
|
-
if (shouldCaptureRequest) {
|
|
364
|
-
record({
|
|
365
|
-
type: "request",
|
|
366
|
-
requestId,
|
|
367
|
-
traceId: traceContext.traceId,
|
|
368
|
-
spanId: traceContext.spanId,
|
|
369
|
-
parentSpanId: traceContext.parentSpanId,
|
|
370
|
-
traceparent: traceContext.traceparent,
|
|
371
|
-
method: req.method,
|
|
372
|
-
path,
|
|
373
|
-
contractName: contract.name,
|
|
374
|
-
status: response.status,
|
|
375
|
-
durationMs,
|
|
376
|
-
details: {
|
|
377
|
-
headers: Object.fromEntries(req.headers.entries()),
|
|
378
|
-
},
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (error && shouldCaptureError) {
|
|
383
|
-
record({
|
|
384
|
-
type: "error",
|
|
385
|
-
requestId,
|
|
386
|
-
traceId: traceContext.traceId,
|
|
387
|
-
spanId: traceContext.spanId,
|
|
388
|
-
parentSpanId: traceContext.parentSpanId,
|
|
389
|
-
traceparent: traceContext.traceparent,
|
|
390
|
-
message: getErrorMessage(error),
|
|
391
|
-
stack: getErrorStack(error),
|
|
392
|
-
contractName: contract.name,
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
},
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Create a use-case observer compatible with `createUseCase({ onRun })`.
|
|
401
|
-
*/
|
|
402
|
-
export function createDevtoolsUseCaseObserver<Ctx>(
|
|
403
|
-
options: DevtoolsUseCaseObserverOptions<Ctx> = {},
|
|
404
|
-
): (event: DevtoolsUseCaseRunEvent<Ctx>) => void {
|
|
405
|
-
const logErrorEvents = options.logErrorEvents ?? true;
|
|
406
|
-
const activeTraceContexts = new Map<string, DevtoolsTraceContext>();
|
|
407
|
-
|
|
408
|
-
return (event) => {
|
|
409
|
-
const devtools =
|
|
410
|
-
options.getDevtools?.(event.ctx) ?? getContextDevtools(event.ctx);
|
|
411
|
-
if (!devtools) return;
|
|
412
|
-
|
|
413
|
-
const requestId =
|
|
414
|
-
options.getRequestId?.(event.ctx) ?? getContextRequestId(event.ctx);
|
|
415
|
-
const errorMessage =
|
|
416
|
-
event.phase === "error" ? getErrorMessage(event.error) : undefined;
|
|
417
|
-
const shouldCaptureUseCase = isWatcherEnabled(devtools, "useCases");
|
|
418
|
-
const shouldCaptureError =
|
|
419
|
-
event.phase === "error" &&
|
|
420
|
-
logErrorEvents &&
|
|
421
|
-
isWatcherEnabled(devtools, "errors");
|
|
422
|
-
|
|
423
|
-
if (!shouldCaptureUseCase && !shouldCaptureError) return;
|
|
424
|
-
|
|
425
|
-
const parentTraceContext = getContextTraceContext(event.ctx);
|
|
426
|
-
const useCaseKey = [
|
|
427
|
-
requestId ?? "no-request",
|
|
428
|
-
parentTraceContext?.traceId ?? "no-trace",
|
|
429
|
-
event.kind,
|
|
430
|
-
event.name,
|
|
431
|
-
].join(":");
|
|
432
|
-
const traceContext =
|
|
433
|
-
event.phase === "start"
|
|
434
|
-
? createChildDevtoolsTraceContext(parentTraceContext ?? {})
|
|
435
|
-
: (activeTraceContexts.get(useCaseKey) ??
|
|
436
|
-
createChildDevtoolsTraceContext(parentTraceContext ?? {}));
|
|
437
|
-
|
|
438
|
-
if (event.phase === "start") {
|
|
439
|
-
activeTraceContexts.set(useCaseKey, traceContext);
|
|
440
|
-
} else {
|
|
441
|
-
activeTraceContexts.delete(useCaseKey);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const record = (input: DevtoolsEventInput) => {
|
|
445
|
-
const normalized = createDevtoolsEvent(input);
|
|
446
|
-
try {
|
|
447
|
-
devtools.record(
|
|
448
|
-
options.redact ? options.redact(normalized) : normalized,
|
|
449
|
-
);
|
|
450
|
-
} catch (error) {
|
|
451
|
-
devtools.record({
|
|
452
|
-
type: "error",
|
|
453
|
-
message: "Devtools use case redactor failed",
|
|
454
|
-
details: {
|
|
455
|
-
message: error instanceof Error ? error.message : String(error),
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
if (shouldCaptureUseCase) {
|
|
462
|
-
record({
|
|
463
|
-
type: "usecase",
|
|
464
|
-
requestId,
|
|
465
|
-
traceId: traceContext.traceId,
|
|
466
|
-
spanId: traceContext.spanId,
|
|
467
|
-
parentSpanId: traceContext.parentSpanId,
|
|
468
|
-
traceparent: traceContext.traceparent,
|
|
469
|
-
name: event.name,
|
|
470
|
-
kind: event.kind,
|
|
471
|
-
phase: event.phase,
|
|
472
|
-
durationMs: event.durationMs,
|
|
473
|
-
error: errorMessage,
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (shouldCaptureError) {
|
|
478
|
-
record({
|
|
479
|
-
type: "error",
|
|
480
|
-
requestId,
|
|
481
|
-
traceId: traceContext.traceId,
|
|
482
|
-
spanId: traceContext.spanId,
|
|
483
|
-
parentSpanId: traceContext.parentSpanId,
|
|
484
|
-
traceparent: traceContext.traceparent,
|
|
485
|
-
message: errorMessage ?? "Use case failed",
|
|
486
|
-
stack: getErrorStack(event.error),
|
|
487
|
-
useCaseName: event.name,
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
}
|