@milaboratories/pl-flight-recorder 0.2.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 +119 -0
- package/dist/analyze.d.ts +133 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +525 -0
- package/dist/analyze.js.map +1 -0
- package/dist/data_summary.d.ts +28 -0
- package/dist/data_summary.d.ts.map +1 -0
- package/dist/data_summary.js +73 -0
- package/dist/data_summary.js.map +1 -0
- package/dist/digest.d.ts +28 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +55 -0
- package/dist/digest.js.map +1 -0
- package/dist/events.d.ts +89 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/instrument.d.ts +82 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +313 -0
- package/dist/instrument.js.map +1 -0
- package/dist/recorder.d.ts +82 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +293 -0
- package/dist/recorder.js.map +1 -0
- package/dist/redact.d.ts +53 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +145 -0
- package/dist/redact.js.map +1 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +377 -0
- package/dist/report.js.map +1 -0
- package/dist/rules.d.ts +73 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +245 -0
- package/dist/rules.js.map +1 -0
- package/dist/sampler.d.ts +22 -0
- package/dist/sampler.d.ts.map +1 -0
- package/dist/sampler.js +33 -0
- package/dist/sampler.js.map +1 -0
- package/dist/sampler_thread.d.ts +1 -0
- package/dist/sampler_thread.js +41 -0
- package/dist/sampler_thread.js.map +1 -0
- package/dist/session.d.ts +40 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/supervisor.d.ts +58 -0
- package/dist/supervisor.d.ts.map +1 -0
- package/dist/supervisor.js +108 -0
- package/dist/supervisor.js.map +1 -0
- package/package.json +43 -0
- package/src/analyze.test.ts +539 -0
- package/src/analyze.ts +795 -0
- package/src/data_summary.ts +110 -0
- package/src/digest.ts +49 -0
- package/src/events.ts +102 -0
- package/src/index.ts +104 -0
- package/src/instrument.ts +442 -0
- package/src/recorder.ts +397 -0
- package/src/redact.test.ts +155 -0
- package/src/redact.ts +213 -0
- package/src/report.ts +512 -0
- package/src/rules.test.ts +182 -0
- package/src/rules.ts +383 -0
- package/src/sampler.ts +40 -0
- package/src/sampler_thread.ts +45 -0
- package/src/session.ts +69 -0
- package/src/supervisor.ts +150 -0
package/src/redact.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { summarizeData, type DataSummary } from "./data_summary";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Structure-preserving redaction for anything a driver seam is handed.
|
|
6
|
+
*
|
|
7
|
+
* The definition a block model builds is recorded by *shape* rather than by a
|
|
8
|
+
* hand-written digest per definition type. Keys, numbers and the small set of
|
|
9
|
+
* strings that are schema survive verbatim; every other string is replaced by a
|
|
10
|
+
* hash and a length. That keeps the record useful for diagnosis, keeps customer
|
|
11
|
+
* data out of it by default rather than by enumeration, and works unchanged for
|
|
12
|
+
* definition shapes this code has never seen — the V2 query API included.
|
|
13
|
+
*
|
|
14
|
+
* The default for an unrecognised string is to hash it. A new field can
|
|
15
|
+
* therefore make a report less informative, but never make it leak.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Keys whose string value is schema, kept as written. */
|
|
19
|
+
export const SCHEMA_KEYS = new Set(["type", "name", "valueType", "kind", "operator", "mode"]);
|
|
20
|
+
|
|
21
|
+
/** Keys under which every string is schema, at any depth (axis identity). */
|
|
22
|
+
export const SCHEMA_SUBTREE_KEYS = new Set(["domain", "contextDomain"]);
|
|
23
|
+
|
|
24
|
+
/** Keys never descended into; summarised by counts instead. */
|
|
25
|
+
export const SUMMARISED_KEYS = new Set(["data", "dataInfo"]);
|
|
26
|
+
|
|
27
|
+
/** Keys reduced to a cardinality, because their contents are values. */
|
|
28
|
+
export const COUNTED_KEYS = new Set(["references", "parts"]);
|
|
29
|
+
|
|
30
|
+
export type RedactionStats = {
|
|
31
|
+
hashedStrings: number;
|
|
32
|
+
truncatedArrays: number;
|
|
33
|
+
omittedItems: number;
|
|
34
|
+
depthCapped: number;
|
|
35
|
+
opaqueObjects: number;
|
|
36
|
+
budgetExhausted: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type RedactOptions = {
|
|
40
|
+
maxDepth?: number;
|
|
41
|
+
maxArrayItems?: number;
|
|
42
|
+
maxStringLength?: number;
|
|
43
|
+
/** Ceiling on emitted values, so one pathological definition cannot fill the log. */
|
|
44
|
+
maxNodes?: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type HashedString = { h: string; n: number };
|
|
48
|
+
|
|
49
|
+
/** Redacts a definition, returning the new value and what had to be elided. */
|
|
50
|
+
export function redact(
|
|
51
|
+
value: unknown,
|
|
52
|
+
options: RedactOptions = {},
|
|
53
|
+
): { value: unknown; stats: RedactionStats } {
|
|
54
|
+
const limits = {
|
|
55
|
+
maxDepth: options.maxDepth ?? 32,
|
|
56
|
+
maxArrayItems: options.maxArrayItems ?? 64,
|
|
57
|
+
maxStringLength: options.maxStringLength ?? 128,
|
|
58
|
+
maxNodes: options.maxNodes ?? 20_000,
|
|
59
|
+
};
|
|
60
|
+
const stats: RedactionStats = {
|
|
61
|
+
hashedStrings: 0,
|
|
62
|
+
truncatedArrays: 0,
|
|
63
|
+
omittedItems: 0,
|
|
64
|
+
depthCapped: 0,
|
|
65
|
+
opaqueObjects: 0,
|
|
66
|
+
budgetExhausted: false,
|
|
67
|
+
};
|
|
68
|
+
const state = { nodes: 0, seen: new WeakSet<object>() };
|
|
69
|
+
return {
|
|
70
|
+
value: walk(value, { key: undefined, schemaSubtree: false, depth: 0 }, limits, stats, state),
|
|
71
|
+
stats,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Stable short hash plus the original length. Never reversible to the value. */
|
|
76
|
+
export function hashString(value: string): HashedString {
|
|
77
|
+
return {
|
|
78
|
+
h: crypto.createHash("sha256").update(value).digest("hex").slice(0, 12),
|
|
79
|
+
n: value.length,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** True for the object form produced in place of a redacted string. */
|
|
84
|
+
export function isHashedString(value: unknown): value is HashedString {
|
|
85
|
+
return typeof value === "object" && value !== null && "h" in value && "n" in value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Internals
|
|
89
|
+
|
|
90
|
+
type Position = { key: string | undefined; schemaSubtree: boolean; depth: number };
|
|
91
|
+
type Limits = Required<RedactOptions>;
|
|
92
|
+
type State = { nodes: number; seen: WeakSet<object> };
|
|
93
|
+
|
|
94
|
+
function walk(
|
|
95
|
+
value: unknown,
|
|
96
|
+
at: Position,
|
|
97
|
+
limits: Limits,
|
|
98
|
+
stats: RedactionStats,
|
|
99
|
+
state: State,
|
|
100
|
+
): unknown {
|
|
101
|
+
if (state.nodes++ > limits.maxNodes) {
|
|
102
|
+
stats.budgetExhausted = true;
|
|
103
|
+
return { $budget: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (value === null || value === undefined) return value ?? null;
|
|
107
|
+
if (typeof value === "bigint") return Number(value);
|
|
108
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
109
|
+
if (typeof value === "string") return redactString(value, at, limits, stats);
|
|
110
|
+
if (typeof value !== "object") return { $type: typeof value };
|
|
111
|
+
|
|
112
|
+
if (at.depth >= limits.maxDepth) {
|
|
113
|
+
stats.depthCapped++;
|
|
114
|
+
return { $depth: at.depth };
|
|
115
|
+
}
|
|
116
|
+
// A definition can carry live accessors and other class instances whose
|
|
117
|
+
// internals reference each other; walking those is neither safe nor useful.
|
|
118
|
+
if (state.seen.has(value)) return { $cycle: true };
|
|
119
|
+
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
state.seen.add(value);
|
|
122
|
+
return walkArray(value, at, limits, stats, state);
|
|
123
|
+
}
|
|
124
|
+
if (!isPlainObject(value)) {
|
|
125
|
+
stats.opaqueObjects++;
|
|
126
|
+
return { $opaque: className(value) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
state.seen.add(value);
|
|
130
|
+
const out: Record<string, unknown> = {};
|
|
131
|
+
for (const [key, child] of Object.entries(value)) {
|
|
132
|
+
if (SUMMARISED_KEYS.has(key)) {
|
|
133
|
+
out[key] = summarizeData(child);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (COUNTED_KEYS.has(key)) {
|
|
137
|
+
out[key] = { $count: countOf(child) };
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
out[key] = walk(
|
|
141
|
+
child,
|
|
142
|
+
{
|
|
143
|
+
key,
|
|
144
|
+
schemaSubtree: at.schemaSubtree || SCHEMA_SUBTREE_KEYS.has(key),
|
|
145
|
+
depth: at.depth + 1,
|
|
146
|
+
},
|
|
147
|
+
limits,
|
|
148
|
+
stats,
|
|
149
|
+
state,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function walkArray(
|
|
156
|
+
value: unknown[],
|
|
157
|
+
at: Position,
|
|
158
|
+
limits: Limits,
|
|
159
|
+
stats: RedactionStats,
|
|
160
|
+
state: State,
|
|
161
|
+
): unknown[] {
|
|
162
|
+
const kept = value
|
|
163
|
+
.slice(0, limits.maxArrayItems)
|
|
164
|
+
.map((item) =>
|
|
165
|
+
walk(
|
|
166
|
+
item,
|
|
167
|
+
{ key: at.key, schemaSubtree: at.schemaSubtree, depth: at.depth + 1 },
|
|
168
|
+
limits,
|
|
169
|
+
stats,
|
|
170
|
+
state,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
const omitted = value.length - kept.length;
|
|
174
|
+
if (omitted <= 0) return kept;
|
|
175
|
+
// Arrays stay arrays so the rules can still walk join entries; the loss is
|
|
176
|
+
// recorded in the array itself rather than in a side channel.
|
|
177
|
+
stats.truncatedArrays++;
|
|
178
|
+
stats.omittedItems += omitted;
|
|
179
|
+
return [...kept, { $omitted: omitted }];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function redactString(
|
|
183
|
+
value: string,
|
|
184
|
+
at: Position,
|
|
185
|
+
limits: Limits,
|
|
186
|
+
stats: RedactionStats,
|
|
187
|
+
): string | HashedString {
|
|
188
|
+
if (at.schemaSubtree || (at.key !== undefined && SCHEMA_KEYS.has(at.key))) {
|
|
189
|
+
return value.length > limits.maxStringLength
|
|
190
|
+
? `${value.slice(0, limits.maxStringLength)}…`
|
|
191
|
+
: value;
|
|
192
|
+
}
|
|
193
|
+
stats.hashedStrings++;
|
|
194
|
+
return hashString(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function countOf(value: unknown): number | undefined {
|
|
198
|
+
if (Array.isArray(value)) return value.length;
|
|
199
|
+
if (isPlainObject(value)) return Object.keys(value).length;
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
204
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
205
|
+
const proto = Object.getPrototypeOf(value) as object | null;
|
|
206
|
+
return proto === Object.prototype || proto === null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function className(value: object): string {
|
|
210
|
+
return (value as { constructor?: { name?: string } }).constructor?.name ?? "unknown";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export type { DataSummary };
|