trickle-observe 0.1.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.
Files changed (57) hide show
  1. package/auto-env.js +13 -0
  2. package/auto-esm.mjs +128 -0
  3. package/auto.js +3 -0
  4. package/dist/auto-codegen.d.ts +29 -0
  5. package/dist/auto-codegen.js +999 -0
  6. package/dist/auto-register.d.ts +16 -0
  7. package/dist/auto-register.js +99 -0
  8. package/dist/cache.d.ts +27 -0
  9. package/dist/cache.js +52 -0
  10. package/dist/env-detect.d.ts +5 -0
  11. package/dist/env-detect.js +35 -0
  12. package/dist/express.d.ts +44 -0
  13. package/dist/express.js +342 -0
  14. package/dist/fetch-observer.d.ts +24 -0
  15. package/dist/fetch-observer.js +217 -0
  16. package/dist/index.d.ts +64 -0
  17. package/dist/index.js +172 -0
  18. package/dist/observe-register.d.ts +29 -0
  19. package/dist/observe-register.js +455 -0
  20. package/dist/observe.d.ts +44 -0
  21. package/dist/observe.js +109 -0
  22. package/dist/proxy-tracker.d.ts +15 -0
  23. package/dist/proxy-tracker.js +172 -0
  24. package/dist/register.d.ts +21 -0
  25. package/dist/register.js +105 -0
  26. package/dist/transport.d.ts +22 -0
  27. package/dist/transport.js +228 -0
  28. package/dist/type-hash.d.ts +5 -0
  29. package/dist/type-hash.js +60 -0
  30. package/dist/type-inference.d.ts +14 -0
  31. package/dist/type-inference.js +259 -0
  32. package/dist/types.d.ts +78 -0
  33. package/dist/types.js +2 -0
  34. package/dist/wrap.d.ts +10 -0
  35. package/dist/wrap.js +247 -0
  36. package/observe-esm-hooks.mjs +367 -0
  37. package/observe-esm.mjs +40 -0
  38. package/observe.js +2 -0
  39. package/package.json +26 -0
  40. package/register.js +2 -0
  41. package/src/auto-codegen.ts +1058 -0
  42. package/src/auto-register.ts +102 -0
  43. package/src/cache.ts +53 -0
  44. package/src/env-detect.ts +22 -0
  45. package/src/express.ts +386 -0
  46. package/src/fetch-observer.ts +226 -0
  47. package/src/index.ts +199 -0
  48. package/src/observe-register.ts +453 -0
  49. package/src/observe.ts +127 -0
  50. package/src/proxy-tracker.ts +208 -0
  51. package/src/register.ts +110 -0
  52. package/src/transport.ts +207 -0
  53. package/src/type-hash.ts +71 -0
  54. package/src/type-inference.ts +285 -0
  55. package/src/types.ts +61 -0
  56. package/src/wrap.ts +289 -0
  57. package/tsconfig.json +8 -0
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inferType = inferType;
4
+ exports.unifyTypes = unifyTypes;
5
+ const MAX_ARRAY_SAMPLE = 20;
6
+ const DEFAULT_MAX_DEPTH = 5;
7
+ /**
8
+ * Infer the TypeNode representation of a runtime JavaScript value.
9
+ * Uses a WeakSet to detect circular references.
10
+ * Samples at most the first 20 elements of arrays for performance.
11
+ */
12
+ function inferType(value, maxDepth = DEFAULT_MAX_DEPTH) {
13
+ const seen = new WeakSet();
14
+ return infer(value, maxDepth, seen);
15
+ }
16
+ function infer(value, depth, seen) {
17
+ // Null
18
+ if (value === null) {
19
+ return { kind: 'primitive', name: 'null' };
20
+ }
21
+ // Undefined
22
+ if (value === undefined) {
23
+ return { kind: 'primitive', name: 'undefined' };
24
+ }
25
+ const t = typeof value;
26
+ // Primitives
27
+ if (t === 'string')
28
+ return { kind: 'primitive', name: 'string' };
29
+ if (t === 'number')
30
+ return { kind: 'primitive', name: 'number' };
31
+ if (t === 'boolean')
32
+ return { kind: 'primitive', name: 'boolean' };
33
+ if (t === 'bigint')
34
+ return { kind: 'primitive', name: 'bigint' };
35
+ if (t === 'symbol')
36
+ return { kind: 'primitive', name: 'symbol' };
37
+ // Functions
38
+ if (t === 'function') {
39
+ return {
40
+ kind: 'function',
41
+ params: new Array(value.length).fill({ kind: 'unknown' }),
42
+ returnType: { kind: 'unknown' },
43
+ };
44
+ }
45
+ // Object types — check depth and circular refs
46
+ if (t === 'object') {
47
+ const obj = value;
48
+ // Circular reference detection
49
+ if (seen.has(obj)) {
50
+ return { kind: 'unknown' };
51
+ }
52
+ if (depth <= 0) {
53
+ return { kind: 'unknown' };
54
+ }
55
+ seen.add(obj);
56
+ try {
57
+ return inferObject(obj, depth, seen);
58
+ }
59
+ finally {
60
+ // Don't remove from seen — keeps circular detection intact for the full traversal
61
+ }
62
+ }
63
+ return { kind: 'unknown' };
64
+ }
65
+ function inferObject(obj, depth, seen) {
66
+ // Promise
67
+ if (obj instanceof Promise) {
68
+ return { kind: 'promise', resolved: { kind: 'unknown' } };
69
+ }
70
+ // Map
71
+ if (obj instanceof Map) {
72
+ let keyType = { kind: 'unknown' };
73
+ let valType = { kind: 'unknown' };
74
+ let count = 0;
75
+ const keyTypes = [];
76
+ const valTypes = [];
77
+ for (const [k, v] of obj) {
78
+ if (count >= MAX_ARRAY_SAMPLE)
79
+ break;
80
+ keyTypes.push(infer(k, depth - 1, seen));
81
+ valTypes.push(infer(v, depth - 1, seen));
82
+ count++;
83
+ }
84
+ keyType = unifyTypes(keyTypes);
85
+ valType = unifyTypes(valTypes);
86
+ return { kind: 'map', key: keyType, value: valType };
87
+ }
88
+ // Set
89
+ if (obj instanceof Set) {
90
+ const elementTypes = [];
91
+ let count = 0;
92
+ for (const item of obj) {
93
+ if (count >= MAX_ARRAY_SAMPLE)
94
+ break;
95
+ elementTypes.push(infer(item, depth - 1, seen));
96
+ count++;
97
+ }
98
+ return { kind: 'set', element: unifyTypes(elementTypes) };
99
+ }
100
+ // Array (including TypedArrays)
101
+ if (Array.isArray(obj)) {
102
+ return inferArray(obj, depth, seen);
103
+ }
104
+ // TypedArrays (not real arrays)
105
+ if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
106
+ return {
107
+ kind: 'object',
108
+ properties: {
109
+ __typedArray: { kind: 'primitive', name: 'string' },
110
+ length: { kind: 'primitive', name: 'number' },
111
+ },
112
+ };
113
+ }
114
+ // Buffer (Node.js)
115
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(obj)) {
116
+ return {
117
+ kind: 'object',
118
+ properties: {
119
+ __buffer: { kind: 'primitive', name: 'string' },
120
+ length: { kind: 'primitive', name: 'number' },
121
+ },
122
+ };
123
+ }
124
+ // Date
125
+ if (obj instanceof Date) {
126
+ return {
127
+ kind: 'object',
128
+ properties: {
129
+ __date: { kind: 'primitive', name: 'string' },
130
+ },
131
+ };
132
+ }
133
+ // RegExp
134
+ if (obj instanceof RegExp) {
135
+ return {
136
+ kind: 'object',
137
+ properties: {
138
+ __regexp: { kind: 'primitive', name: 'string' },
139
+ source: { kind: 'primitive', name: 'string' },
140
+ flags: { kind: 'primitive', name: 'string' },
141
+ },
142
+ };
143
+ }
144
+ // Error
145
+ if (obj instanceof Error) {
146
+ return {
147
+ kind: 'object',
148
+ properties: {
149
+ __error: { kind: 'primitive', name: 'string' },
150
+ name: { kind: 'primitive', name: 'string' },
151
+ message: { kind: 'primitive', name: 'string' },
152
+ stack: { kind: 'primitive', name: 'string' },
153
+ },
154
+ };
155
+ }
156
+ // Plain objects
157
+ return inferPlainObject(obj, depth, seen);
158
+ }
159
+ function inferArray(arr, depth, seen) {
160
+ if (arr.length === 0) {
161
+ return { kind: 'array', element: { kind: 'unknown' } };
162
+ }
163
+ const sampleSize = Math.min(arr.length, MAX_ARRAY_SAMPLE);
164
+ const elementTypes = [];
165
+ for (let i = 0; i < sampleSize; i++) {
166
+ elementTypes.push(infer(arr[i], depth - 1, seen));
167
+ }
168
+ return { kind: 'array', element: unifyTypes(elementTypes) };
169
+ }
170
+ function inferPlainObject(obj, depth, seen) {
171
+ const properties = {};
172
+ const keys = Object.keys(obj);
173
+ for (const key of keys) {
174
+ try {
175
+ properties[key] = infer(obj[key], depth - 1, seen);
176
+ }
177
+ catch {
178
+ properties[key] = { kind: 'unknown' };
179
+ }
180
+ }
181
+ return { kind: 'object', properties };
182
+ }
183
+ /**
184
+ * Unify an array of TypeNodes into a single TypeNode.
185
+ * If all types are structurally identical, returns that type.
186
+ * Otherwise, returns a union of the distinct types.
187
+ */
188
+ function unifyTypes(types) {
189
+ if (types.length === 0)
190
+ return { kind: 'unknown' };
191
+ if (types.length === 1)
192
+ return types[0];
193
+ // Deduplicate by serialization
194
+ const uniqueMap = new Map();
195
+ for (const t of types) {
196
+ const key = canonicalStringify(t);
197
+ if (!uniqueMap.has(key)) {
198
+ uniqueMap.set(key, t);
199
+ }
200
+ }
201
+ const unique = Array.from(uniqueMap.values());
202
+ if (unique.length === 1)
203
+ return unique[0];
204
+ // Flatten nested unions
205
+ const members = [];
206
+ for (const u of unique) {
207
+ if (u.kind === 'union') {
208
+ members.push(...u.members);
209
+ }
210
+ else {
211
+ members.push(u);
212
+ }
213
+ }
214
+ // Deduplicate again after flattening
215
+ const finalMap = new Map();
216
+ for (const m of members) {
217
+ const key = canonicalStringify(m);
218
+ if (!finalMap.has(key)) {
219
+ finalMap.set(key, m);
220
+ }
221
+ }
222
+ const finalMembers = Array.from(finalMap.values());
223
+ if (finalMembers.length === 1)
224
+ return finalMembers[0];
225
+ return { kind: 'union', members: finalMembers };
226
+ }
227
+ function canonicalStringify(node) {
228
+ if (node.kind === 'object') {
229
+ const sorted = Object.keys(node.properties).sort();
230
+ const entries = sorted.map(k => `${JSON.stringify(k)}:${canonicalStringify(node.properties[k])}`);
231
+ return `{object:{${entries.join(',')}}}`;
232
+ }
233
+ if (node.kind === 'union') {
234
+ const sorted = node.members.map(canonicalStringify).sort();
235
+ return `{union:[${sorted.join(',')}]}`;
236
+ }
237
+ if (node.kind === 'array') {
238
+ return `{array:${canonicalStringify(node.element)}}`;
239
+ }
240
+ if (node.kind === 'primitive') {
241
+ return `{prim:${node.name}}`;
242
+ }
243
+ if (node.kind === 'function') {
244
+ return `{fn:[${node.params.map(canonicalStringify).join(',')}]->${canonicalStringify(node.returnType)}}`;
245
+ }
246
+ if (node.kind === 'promise') {
247
+ return `{promise:${canonicalStringify(node.resolved)}}`;
248
+ }
249
+ if (node.kind === 'map') {
250
+ return `{map:${canonicalStringify(node.key)},${canonicalStringify(node.value)}}`;
251
+ }
252
+ if (node.kind === 'set') {
253
+ return `{set:${canonicalStringify(node.element)}}`;
254
+ }
255
+ if (node.kind === 'tuple') {
256
+ return `{tuple:[${node.elements.map(canonicalStringify).join(',')}]}`;
257
+ }
258
+ return '{unknown}';
259
+ }
@@ -0,0 +1,78 @@
1
+ export type TypeNode = {
2
+ kind: "primitive";
3
+ name: "string" | "number" | "boolean" | "null" | "undefined" | "bigint" | "symbol";
4
+ } | {
5
+ kind: "array";
6
+ element: TypeNode;
7
+ } | {
8
+ kind: "object";
9
+ properties: Record<string, TypeNode>;
10
+ } | {
11
+ kind: "union";
12
+ members: TypeNode[];
13
+ } | {
14
+ kind: "function";
15
+ params: TypeNode[];
16
+ returnType: TypeNode;
17
+ } | {
18
+ kind: "promise";
19
+ resolved: TypeNode;
20
+ } | {
21
+ kind: "map";
22
+ key: TypeNode;
23
+ value: TypeNode;
24
+ } | {
25
+ kind: "set";
26
+ element: TypeNode;
27
+ } | {
28
+ kind: "tuple";
29
+ elements: TypeNode[];
30
+ } | {
31
+ kind: "unknown";
32
+ };
33
+ export interface IngestPayload {
34
+ functionName: string;
35
+ module: string;
36
+ language: "js" | "python";
37
+ environment: string;
38
+ typeHash: string;
39
+ argsType: TypeNode;
40
+ returnType: TypeNode;
41
+ isAsync?: boolean;
42
+ paramNames?: string[];
43
+ sampleInput?: unknown;
44
+ sampleOutput?: unknown;
45
+ error?: {
46
+ type: string;
47
+ message: string;
48
+ stackTrace?: string;
49
+ argsSnapshot?: unknown;
50
+ };
51
+ }
52
+ export interface GlobalOpts {
53
+ backendUrl: string;
54
+ batchIntervalMs: number;
55
+ enabled: boolean;
56
+ environment: string | undefined;
57
+ maxBatchSize?: number;
58
+ debug?: boolean;
59
+ }
60
+ export interface TrickleOpts {
61
+ name?: string;
62
+ module?: string;
63
+ trackArgs?: boolean;
64
+ trackReturn?: boolean;
65
+ sampleRate?: number;
66
+ maxDepth?: number;
67
+ }
68
+ export interface WrapOptions {
69
+ functionName: string;
70
+ module: string;
71
+ trackArgs: boolean;
72
+ trackReturn: boolean;
73
+ sampleRate: number;
74
+ maxDepth: number;
75
+ environment: string;
76
+ enabled: boolean;
77
+ paramNames?: string[];
78
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/wrap.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { WrapOptions } from './types';
2
+ import { TypeCache } from './cache';
3
+ declare const typeCache: TypeCache;
4
+ /**
5
+ * Wrap a function to capture runtime type information on each call.
6
+ * The wrapper is completely transparent: same name, same length, same behavior.
7
+ * Errors are always re-thrown after capturing type context.
8
+ */
9
+ export declare function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: WrapOptions): T;
10
+ export { typeCache };
package/dist/wrap.js ADDED
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.typeCache = void 0;
4
+ exports.wrapFunction = wrapFunction;
5
+ const type_inference_1 = require("./type-inference");
6
+ const type_hash_1 = require("./type-hash");
7
+ const proxy_tracker_1 = require("./proxy-tracker");
8
+ const cache_1 = require("./cache");
9
+ const transport_1 = require("./transport");
10
+ const typeCache = new cache_1.TypeCache();
11
+ exports.typeCache = typeCache;
12
+ /** Symbol to mark already-wrapped functions, preventing double-wrap. */
13
+ const TRICKLE_WRAPPED = Symbol.for('__trickle_wrapped');
14
+ /**
15
+ * Wrap a function to capture runtime type information on each call.
16
+ * The wrapper is completely transparent: same name, same length, same behavior.
17
+ * Errors are always re-thrown after capturing type context.
18
+ */
19
+ function wrapFunction(fn, opts) {
20
+ if (!opts.enabled)
21
+ return fn;
22
+ // Prevent double-wrapping (compile hook + load hook may both see the same function)
23
+ if (fn[TRICKLE_WRAPPED])
24
+ return fn;
25
+ const functionKey = `${opts.module}::${opts.functionName}`;
26
+ // Create wrapper with same length using a dynamic approach
27
+ const wrapper = function (...args) {
28
+ // Sample rate check
29
+ if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
30
+ return fn.apply(this, args);
31
+ }
32
+ // Set up arg tracking
33
+ const trackers = [];
34
+ const proxiedArgs = args.map((arg, i) => {
35
+ if (arg !== null && arg !== undefined && typeof arg === 'object') {
36
+ const tracker = (0, proxy_tracker_1.createTracker)(arg);
37
+ trackers.push(tracker);
38
+ return tracker.proxy;
39
+ }
40
+ return arg;
41
+ });
42
+ let result;
43
+ let threwError = false;
44
+ let caughtError;
45
+ try {
46
+ result = fn.apply(this, proxiedArgs);
47
+ }
48
+ catch (err) {
49
+ threwError = true;
50
+ caughtError = err;
51
+ // Capture error context
52
+ try {
53
+ captureErrorPayload(functionKey, opts, args, trackers, err);
54
+ }
55
+ catch {
56
+ // Never let our instrumentation interfere
57
+ }
58
+ // CRITICAL: always re-throw
59
+ throw err;
60
+ }
61
+ // Handle async functions (Promise return)
62
+ if (result !== null && result !== undefined && typeof result === 'object' && typeof result.then === 'function') {
63
+ return result.then((resolved) => {
64
+ try {
65
+ capturePayload(functionKey, opts, args, trackers, resolved, true);
66
+ }
67
+ catch {
68
+ // Never let our instrumentation interfere
69
+ }
70
+ return resolved;
71
+ }, (err) => {
72
+ try {
73
+ captureErrorPayload(functionKey, opts, args, trackers, err);
74
+ }
75
+ catch {
76
+ // Never let our instrumentation interfere
77
+ }
78
+ // Re-throw the original rejection
79
+ throw err;
80
+ });
81
+ }
82
+ // Synchronous return
83
+ try {
84
+ capturePayload(functionKey, opts, args, trackers, result);
85
+ }
86
+ catch {
87
+ // Never let our instrumentation interfere
88
+ }
89
+ return result;
90
+ };
91
+ // Preserve function name and length
92
+ Object.defineProperty(wrapper, 'name', { value: fn.name || opts.functionName, configurable: true });
93
+ Object.defineProperty(wrapper, 'length', { value: fn.length, configurable: true });
94
+ // Mark as wrapped to prevent double-wrapping
95
+ wrapper[TRICKLE_WRAPPED] = true;
96
+ return wrapper;
97
+ }
98
+ /**
99
+ * Capture and enqueue a successful invocation's type data.
100
+ */
101
+ function capturePayload(functionKey, opts, originalArgs, trackers, returnValue, isAsync = false) {
102
+ // Build args type as a tuple
103
+ const argsType = buildArgsType(originalArgs, trackers, opts.maxDepth);
104
+ const returnType = (0, type_inference_1.inferType)(returnValue, opts.maxDepth);
105
+ const hash = (0, type_hash_1.hashType)(argsType, returnType);
106
+ // Check cache
107
+ if (!typeCache.shouldSend(functionKey, hash)) {
108
+ return;
109
+ }
110
+ typeCache.markSent(functionKey, hash);
111
+ const payload = {
112
+ functionName: opts.functionName,
113
+ module: opts.module,
114
+ language: 'js',
115
+ environment: opts.environment,
116
+ typeHash: hash,
117
+ argsType,
118
+ returnType,
119
+ sampleInput: sanitizeSample(originalArgs),
120
+ sampleOutput: sanitizeSample(returnValue),
121
+ };
122
+ if (isAsync) {
123
+ payload.isAsync = true;
124
+ }
125
+ if (opts.paramNames && opts.paramNames.length > 0) {
126
+ payload.paramNames = opts.paramNames;
127
+ }
128
+ (0, transport_1.enqueue)(payload);
129
+ }
130
+ /**
131
+ * Capture type context for a failed invocation.
132
+ */
133
+ function captureErrorPayload(functionKey, opts, originalArgs, trackers, error) {
134
+ const argsType = buildArgsType(originalArgs, trackers, opts.maxDepth);
135
+ const returnType = { kind: 'unknown' };
136
+ const hash = (0, type_hash_1.hashType)(argsType, returnType);
137
+ // Always send error payloads (don't cache-skip them)
138
+ const errorInfo = extractErrorInfo(error);
139
+ // Collect accessed paths from trackers as "variable" context
140
+ const accessedPaths = {};
141
+ for (const tracker of trackers) {
142
+ const paths = tracker.getAccessedPaths();
143
+ for (const [path, type] of paths) {
144
+ accessedPaths[path] = type;
145
+ }
146
+ }
147
+ const payload = {
148
+ functionName: opts.functionName,
149
+ module: opts.module,
150
+ language: 'js',
151
+ environment: opts.environment,
152
+ typeHash: hash,
153
+ argsType,
154
+ returnType,
155
+ sampleInput: sanitizeSample(originalArgs),
156
+ error: {
157
+ type: errorInfo.type,
158
+ message: errorInfo.message,
159
+ stackTrace: errorInfo.stack,
160
+ argsSnapshot: sanitizeSample(originalArgs),
161
+ },
162
+ };
163
+ (0, transport_1.enqueue)(payload);
164
+ }
165
+ /**
166
+ * Build a tuple TypeNode for the args array.
167
+ * Uses tracker data for objects/arrays that were proxied (captures the "shape" actually used),
168
+ * and falls back to full inference for primitives and untracked args.
169
+ */
170
+ function buildArgsType(originalArgs, trackers, maxDepth) {
171
+ const elements = originalArgs.map(arg => (0, type_inference_1.inferType)(arg, maxDepth));
172
+ if (elements.length === 0) {
173
+ return { kind: 'tuple', elements: [] };
174
+ }
175
+ return { kind: 'tuple', elements };
176
+ }
177
+ /**
178
+ * Extract error information safely.
179
+ */
180
+ function extractErrorInfo(err) {
181
+ if (err instanceof Error) {
182
+ return {
183
+ type: err.constructor.name || 'Error',
184
+ message: err.message,
185
+ stack: err.stack,
186
+ };
187
+ }
188
+ if (typeof err === 'string') {
189
+ return { type: 'String', message: err };
190
+ }
191
+ return {
192
+ type: typeof err,
193
+ message: String(err),
194
+ };
195
+ }
196
+ /**
197
+ * Sanitize a sample value for safe serialization.
198
+ * Truncates large strings, limits array lengths, strips functions.
199
+ */
200
+ function sanitizeSample(value, depth = 3) {
201
+ if (depth <= 0)
202
+ return '[truncated]';
203
+ if (value === null || value === undefined)
204
+ return value;
205
+ const t = typeof value;
206
+ if (t === 'string') {
207
+ const s = value;
208
+ return s.length > 200 ? s.substring(0, 200) + '...' : s;
209
+ }
210
+ if (t === 'number' || t === 'boolean')
211
+ return value;
212
+ if (t === 'bigint')
213
+ return String(value);
214
+ if (t === 'symbol')
215
+ return String(value);
216
+ if (t === 'function')
217
+ return `[Function: ${value.name || 'anonymous'}]`;
218
+ if (Array.isArray(value)) {
219
+ const sample = value.slice(0, 5);
220
+ return sample.map(item => sanitizeSample(item, depth - 1));
221
+ }
222
+ if (t === 'object') {
223
+ if (value instanceof Date)
224
+ return value.toISOString();
225
+ if (value instanceof RegExp)
226
+ return String(value);
227
+ if (value instanceof Error)
228
+ return { error: value.message };
229
+ if (value instanceof Map)
230
+ return `[Map: ${value.size} entries]`;
231
+ if (value instanceof Set)
232
+ return `[Set: ${value.size} items]`;
233
+ const obj = value;
234
+ const result = {};
235
+ const keys = Object.keys(obj).slice(0, 20);
236
+ for (const key of keys) {
237
+ try {
238
+ result[key] = sanitizeSample(obj[key], depth - 1);
239
+ }
240
+ catch {
241
+ result[key] = '[unreadable]';
242
+ }
243
+ }
244
+ return result;
245
+ }
246
+ return String(value);
247
+ }