@memlab/core 2.0.4 → 2.0.5

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.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=HeapAnonymizer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HeapAnonymizer.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/lib/HeapAnonymizer.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,377 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ * @format
9
+ * @oncall memory_lab
10
+ */
11
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
12
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
13
+ return new (P || (P = Promise))(function (resolve, reject) {
14
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
15
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
16
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
17
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
18
+ });
19
+ };
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ const Config_1 = __importDefault(require("../../lib/Config"));
25
+ const fs_1 = __importDefault(require("fs"));
26
+ const path_1 = __importDefault(require("path"));
27
+ const HeapParser_1 = __importDefault(require("../../lib/HeapParser"));
28
+ const HeapAnonymizer_1 = require("../../lib/HeapAnonymizer");
29
+ const NodeHeap_1 = require("../../lib/NodeHeap");
30
+ const HeapSerializer_1 = require("../../lib/HeapSerializer");
31
+ beforeEach(() => {
32
+ Config_1.default.isTest = true;
33
+ });
34
+ const timeout = 5 * 60 * 1000;
35
+ /**
36
+ * The canary values are ASSEMBLED AT RUNTIME rather than written as literals.
37
+ *
38
+ * This test file's own source text ends up in the snapshot's string table — V8
39
+ * keeps script sources on the heap like any other string. A literal canary
40
+ * would therefore be present in the capture twice: once as the object's value,
41
+ * and once inside the script source. Both get redacted, so the test would still
42
+ * pass — but it would pass without ever proving that the OBJECT's value was
43
+ * handled, which is the thing under test. Assembling at runtime means the full
44
+ * value exists only as a heap string.
45
+ */
46
+ function canaryValue() {
47
+ return ['MEMLAB', 'ANON', 'CANARY', 'a7f3c1d9e2b4'].join('-');
48
+ }
49
+ /** An identifier-shaped map key: structurally a label, semantically a payload. */
50
+ function canaryIdKey() {
51
+ return ['77', '3105482913'].join('');
52
+ }
53
+ /**
54
+ * Build a canary variant.
55
+ *
56
+ * Uses `join` rather than a template literal on purpose. Template
57
+ * concatenation produces a `concatenated string`, and V8 stores those as a pair
58
+ * of pointers to their halves — the joined text never becomes an entry in the
59
+ * string table, so a canary built that way is not actually in the capture and
60
+ * the positive controls below correctly refuse to pass. `join` flattens.
61
+ */
62
+ function canaryVariant(suffix) {
63
+ return [canaryValue(), suffix].join('-');
64
+ }
65
+ class AnonymizerCanaryHolder {
66
+ constructor(secret, idKey) {
67
+ this.anonymizerCanaryDescriptiveKey = 'a value under a descriptive name';
68
+ this.byContactId = {};
69
+ this.secretValue = secret;
70
+ this.byContactId[idKey] = 1;
71
+ }
72
+ }
73
+ test('anonymizing a heap snapshot removes heap string values', () => __awaiter(void 0, void 0, void 0, function* () {
74
+ const secret = canaryValue();
75
+ const idKey = canaryIdKey();
76
+ const holder = new AnonymizerCanaryHolder(secret, idKey);
77
+ const sourceFile = (0, NodeHeap_1.dumpNodeHeapSnapshot)();
78
+ const outputFile = `${sourceFile}.anonymized`;
79
+ try {
80
+ // POSITIVE CONTROL. Without this the whole test can pass for the wrong
81
+ // reason: if the canary were optimized away or collected before the dump,
82
+ // "absent afterwards" would be trivially true and the assertions below
83
+ // would prove nothing.
84
+ const before = fs_1.default.readFileSync(sourceFile, 'utf8');
85
+ expect(before.includes(secret)).toBe(true);
86
+ expect(before.includes(idKey)).toBe(true);
87
+ const heap = yield HeapParser_1.default.parse(sourceFile, {});
88
+ const report = (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(heap);
89
+ (0, HeapSerializer_1.serializeHeapSnapshot)(heap, outputFile);
90
+ // Scan the RAW BYTES rather than the parsed string table, so a value that
91
+ // survived somewhere unexpected — an edge name, a node name — is caught
92
+ // too. Absence from the file is the property being claimed.
93
+ const after = fs_1.default.readFileSync(outputFile, 'utf8');
94
+ expect(after.includes(secret)).toBe(false);
95
+ expect(after.includes(idKey)).toBe(false);
96
+ // ... and it must be redaction, not destruction: the labels a retainer
97
+ // trace is built from have to survive, or the capture is unanalyzable.
98
+ expect(after.includes('anonymizerCanaryDescriptiveKey')).toBe(true);
99
+ expect(after.includes('AnonymizerCanaryHolder')).toBe(true);
100
+ expect(report.valuesRedacted).toBeGreaterThan(0);
101
+ // The result still has to be a readable snapshot describing the same
102
+ // graph — redaction must not change the shape of the heap.
103
+ const reparsed = yield HeapParser_1.default.parse(outputFile, {});
104
+ expect(reparsed.nodes.length).toBe(heap.nodes.length);
105
+ expect(reparsed.edges.length).toBe(heap.edges.length);
106
+ }
107
+ finally {
108
+ // Every run creates its own snapshot directory, and both files are
109
+ // removed here, so re-running neither accumulates captures nor lets a
110
+ // previous run's output satisfy this one's assertions.
111
+ for (const file of [sourceFile, outputFile]) {
112
+ if (fs_1.default.existsSync(file)) {
113
+ fs_1.default.unlinkSync(file);
114
+ }
115
+ }
116
+ }
117
+ // Keep the holder reachable until after the dump; an unreferenced local
118
+ // can be collected before the snapshot is taken, which would silently
119
+ // remove the very object being tested.
120
+ expect(holder.secretValue).toBe(secret);
121
+ }), timeout);
122
+ test('stable mode keeps distinct values distinct, uniform mode does not', () => __awaiter(void 0, void 0, void 0, function* () {
123
+ const holder = {
124
+ distinctValues: [
125
+ canaryVariant('one'),
126
+ canaryVariant('two'),
127
+ canaryVariant('six'),
128
+ ],
129
+ };
130
+ const sourceFile = (0, NodeHeap_1.dumpNodeHeapSnapshot)();
131
+ try {
132
+ const before = fs_1.default.readFileSync(sourceFile, 'utf8');
133
+ expect(before.includes(holder.distinctValues[0])).toBe(true);
134
+ const stableHeap = yield HeapParser_1.default.parse(sourceFile, {});
135
+ (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(stableHeap, { mode: 'stable' });
136
+ const uniformHeap = yield HeapParser_1.default.parse(sourceFile, {});
137
+ (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(uniformHeap, { mode: 'uniform' });
138
+ // Same length, three different values. `stable` must map them to three
139
+ // different tokens; `uniform` collapses them to one, which is exactly the
140
+ // behaviour that manufactures fake string duplication.
141
+ const lengths = holder.distinctValues.map(v => v.length);
142
+ expect(new Set(lengths).size).toBe(1);
143
+ const tokensOfLength = (heap) => new Set(heap.snapshot.strings.filter(s => s.length === lengths[0]));
144
+ const stableTokens = tokensOfLength(stableHeap);
145
+ const uniformTokens = tokensOfLength(uniformHeap);
146
+ expect(uniformTokens.has('?'.repeat(lengths[0]))).toBe(true);
147
+ expect(stableTokens.size).toBeGreaterThan(uniformTokens.size);
148
+ }
149
+ finally {
150
+ if (fs_1.default.existsSync(sourceFile)) {
151
+ fs_1.default.unlinkSync(sourceFile);
152
+ }
153
+ }
154
+ }), timeout);
155
+ test('anonymization is deterministic and repeatable for the same input', () => __awaiter(void 0, void 0, void 0, function* () {
156
+ const holder = { value: canaryVariant('repeatable') };
157
+ const sourceFile = (0, NodeHeap_1.dumpNodeHeapSnapshot)();
158
+ try {
159
+ expect(fs_1.default.readFileSync(sourceFile, 'utf8').includes(holder.value)).toBe(true);
160
+ const first = yield HeapParser_1.default.parse(sourceFile, {});
161
+ const second = yield HeapParser_1.default.parse(sourceFile, {});
162
+ const firstReport = (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(first, { salt: 'test-salt' });
163
+ const secondReport = (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(second, { salt: 'test-salt' });
164
+ expect(secondReport.valuesRedacted).toBe(firstReport.valuesRedacted);
165
+ expect(first.snapshot.strings).toEqual(second.snapshot.strings);
166
+ // A different salt must produce different tokens, or the salt is not
167
+ // doing the one job it has.
168
+ const third = yield HeapParser_1.default.parse(sourceFile, {});
169
+ (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(third, { salt: 'another-salt' });
170
+ expect(third.snapshot.strings).not.toEqual(first.snapshot.strings);
171
+ }
172
+ finally {
173
+ if (fs_1.default.existsSync(sourceFile)) {
174
+ fs_1.default.unlinkSync(sourceFile);
175
+ }
176
+ }
177
+ }), timeout);
178
+ /**
179
+ * Field names, not computed keys, because only fast-mode properties become
180
+ * LABELS.
181
+ *
182
+ * `obj[key] = v` on a plain object puts it in dictionary mode, where V8 stores
183
+ * the key as a value inside a name dictionary rather than as a named edge — so
184
+ * such keys are already covered by the node-type rule and prove nothing about
185
+ * the callback. A declared field is emitted as a property edge name, which is
186
+ * the case that needs a caller-supplied decision.
187
+ */
188
+ class AnonymizerCallbackHolder {
189
+ constructor() {
190
+ // An application-private identifier scheme: no digits, not an address, not
191
+ // base64, not markup. Every built-in rule is blind to it BY DESIGN — memlab
192
+ // must not ship other people's formats.
193
+ //
194
+ // The values are objects, not numbers, and that is load-bearing: V8 emits no
195
+ // edge for a small-integer property because there is no heap object to point
196
+ // at, so an `= 1` field name never becomes a label at all and the test would
197
+ // silently exercise the wrong path.
198
+ this.ORD_kfmqvzhtbewx = { n: 1 };
199
+ // ... and one a built-in rule DOES take, to test the other direction: the
200
+ // callback has to be able to protect as well as redact.
201
+ this.col_987654321012 = { n: 2 };
202
+ }
203
+ }
204
+ test('a shouldRedact callback covers a scheme no built-in rule knows', () => __awaiter(void 0, void 0, void 0, function* () {
205
+ const appKey = 'ORD_kfmqvzhtbewx';
206
+ const builtInKey = 'col_987654321012';
207
+ const holder = new AnonymizerCallbackHolder();
208
+ const sourceFile = (0, NodeHeap_1.dumpNodeHeapSnapshot)();
209
+ const defaultOut = `${sourceFile}.default`;
210
+ const customOut = `${sourceFile}.custom`;
211
+ try {
212
+ const before = fs_1.default.readFileSync(sourceFile, 'utf8');
213
+ expect(before.includes(appKey)).toBe(true);
214
+ expect(before.includes(builtInKey)).toBe(true);
215
+ // POSITIVE CONTROL for the callback's reason to exist: with defaults the
216
+ // app-private key survives because nothing knows it, while the key with a
217
+ // long digit run does not. If this ever flips, the assertions below stop
218
+ // proving that the callback reached something the rules could not.
219
+ const plain = yield HeapParser_1.default.parse(sourceFile, {});
220
+ (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(plain);
221
+ (0, HeapSerializer_1.serializeHeapSnapshot)(plain, defaultOut);
222
+ const plainText = fs_1.default.readFileSync(defaultOut, 'utf8');
223
+ expect(plainText.includes(appKey)).toBe(true);
224
+ expect(plainText.includes(builtInKey)).toBe(false);
225
+ const custom = yield HeapParser_1.default.parse(sourceFile, {});
226
+ const shapes = [];
227
+ const report = (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(custom, {
228
+ shouldRedact: (value, context) => {
229
+ if (value === appKey) {
230
+ shapes.push(context.shape);
231
+ return true;
232
+ }
233
+ if (value === builtInKey) {
234
+ return false;
235
+ }
236
+ return undefined;
237
+ },
238
+ });
239
+ (0, HeapSerializer_1.serializeHeapSnapshot)(custom, customOut);
240
+ const customText = fs_1.default.readFileSync(customOut, 'utf8');
241
+ expect(customText.includes(appKey)).toBe(false);
242
+ expect(customText.includes(builtInKey)).toBe(true);
243
+ expect(report.contentRedactedByRule.some(r => r.rule === 'custom-callback')).toBe(true);
244
+ // The context has to carry something usable for matching a whole scheme
245
+ // by shape rather than value by value.
246
+ expect(shapes.length).toBeGreaterThan(0);
247
+ expect(shapes[0]).toBe('A_a');
248
+ }
249
+ finally {
250
+ for (const file of [sourceFile, defaultOut, customOut]) {
251
+ if (fs_1.default.existsSync(file)) {
252
+ fs_1.default.unlinkSync(file);
253
+ }
254
+ }
255
+ }
256
+ expect(holder.ORD_kfmqvzhtbewx.n).toBe(1);
257
+ }), timeout);
258
+ test('the file API anonymizes without building the object graph', () => __awaiter(void 0, void 0, void 0, function* () {
259
+ const secret = canaryVariant('fileapi');
260
+ const holder = { value: secret };
261
+ const sourceFile = (0, NodeHeap_1.dumpNodeHeapSnapshot)();
262
+ const outputFile = `${sourceFile}.fileapi`;
263
+ try {
264
+ expect(fs_1.default.readFileSync(sourceFile, 'utf8').includes(secret)).toBe(true);
265
+ const audit = yield (0, HeapAnonymizer_1.auditHeapSnapshotFile)(sourceFile);
266
+ expect(audit.valuesRedacted).toBeGreaterThan(0);
267
+ // auditing must not write anything, including over the input
268
+ expect(fs_1.default.existsSync(outputFile)).toBe(false);
269
+ expect(fs_1.default.readFileSync(sourceFile, 'utf8').includes(secret)).toBe(true);
270
+ const report = yield (0, HeapAnonymizer_1.anonymizeHeapSnapshotFile)(sourceFile, outputFile);
271
+ expect(report.valuesRedacted).toBe(audit.valuesRedacted);
272
+ expect(fs_1.default.readFileSync(outputFile, 'utf8').includes(secret)).toBe(false);
273
+ // and the file it wrote still has to be a readable snapshot
274
+ const reparsed = yield HeapParser_1.default.parse(outputFile, {});
275
+ expect(reparsed.nodes.length).toBeGreaterThan(0);
276
+ }
277
+ finally {
278
+ for (const file of [sourceFile, outputFile]) {
279
+ if (fs_1.default.existsSync(file)) {
280
+ fs_1.default.unlinkSync(file);
281
+ }
282
+ }
283
+ }
284
+ expect(holder.value).toBe(secret);
285
+ }), timeout);
286
+ test('the file API refuses to overwrite its own input, however it is spelled', () => __awaiter(void 0, void 0, void 0, function* () {
287
+ const sourceFile = (0, NodeHeap_1.dumpNodeHeapSnapshot)();
288
+ const dir = path_1.default.dirname(sourceFile);
289
+ const base = path_1.default.basename(sourceFile);
290
+ const linkPath = `${sourceFile}.link`;
291
+ const before = fs_1.default.readFileSync(sourceFile, 'utf8');
292
+ // Every one of these names the same file as `sourceFile`. A plain string
293
+ // comparison catches none of them, and the cost of missing one is the only
294
+ // unredacted copy of the capture.
295
+ fs_1.default.symlinkSync(sourceFile, linkPath);
296
+ const aliases = [
297
+ sourceFile,
298
+ path_1.default.join(dir, '.', base),
299
+ path_1.default.join(dir, 'x', '..', base),
300
+ linkPath,
301
+ ];
302
+ try {
303
+ for (const alias of aliases) {
304
+ yield expect((0, HeapAnonymizer_1.anonymizeHeapSnapshotFile)(sourceFile, alias)).rejects.toThrow(/same file/);
305
+ }
306
+ // and the input is untouched by the refusals
307
+ expect(fs_1.default.readFileSync(sourceFile, 'utf8')).toBe(before);
308
+ // a genuinely different path still works
309
+ const out = `${sourceFile}.out`;
310
+ yield (0, HeapAnonymizer_1.anonymizeHeapSnapshotFile)(sourceFile, out);
311
+ expect(fs_1.default.existsSync(out)).toBe(true);
312
+ fs_1.default.unlinkSync(out);
313
+ }
314
+ finally {
315
+ for (const file of [linkPath, sourceFile]) {
316
+ if (fs_1.default.existsSync(file) ||
317
+ fs_1.default.lstatSync(file, { throwIfNoEntry: false })) {
318
+ fs_1.default.unlinkSync(file);
319
+ }
320
+ }
321
+ }
322
+ }), timeout);
323
+ test('keepPatterns and shouldRedact(false) protect string VALUES, not just labels', () => __awaiter(void 0, void 0, void 0, function* () {
324
+ // Both options promise an entry is never redacted. The string-VALUE pass
325
+ // does not consult the content rules, so honouring them only there would
326
+ // silently break the promise for exactly the entries a caller named.
327
+ const keptByPattern = canaryVariant('keepme');
328
+ const keptByCallback = canaryVariant('callbackkeep');
329
+ const notKept = canaryVariant('notkept');
330
+ const holder = { a: keptByPattern, b: keptByCallback, c: notKept };
331
+ const sourceFile = (0, NodeHeap_1.dumpNodeHeapSnapshot)();
332
+ const out = `${sourceFile}.kept`;
333
+ try {
334
+ const before = fs_1.default.readFileSync(sourceFile, 'utf8');
335
+ for (const v of [keptByPattern, keptByCallback, notKept]) {
336
+ expect(before.includes(v)).toBe(true);
337
+ }
338
+ const heap = yield HeapParser_1.default.parse(sourceFile, {});
339
+ (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(heap, {
340
+ keepPatterns: [new RegExp(`${canaryValue()}-keepme$`)],
341
+ shouldRedact: value => (value === keptByCallback ? false : undefined),
342
+ });
343
+ (0, HeapSerializer_1.serializeHeapSnapshot)(heap, out);
344
+ const after = fs_1.default.readFileSync(out, 'utf8');
345
+ expect(after.includes(keptByPattern)).toBe(true);
346
+ expect(after.includes(keptByCallback)).toBe(true);
347
+ // the control: an unprotected value of the same shape is still redacted,
348
+ // so the assertions above cannot pass by anonymization simply not running
349
+ expect(after.includes(notKept)).toBe(false);
350
+ }
351
+ finally {
352
+ for (const file of [sourceFile, out]) {
353
+ if (fs_1.default.existsSync(file)) {
354
+ fs_1.default.unlinkSync(file);
355
+ }
356
+ }
357
+ }
358
+ expect(holder.a).toBe(keptByPattern);
359
+ }), timeout);
360
+ test('an out-of-range minDigitRunLength is rejected rather than matching everything', () => __awaiter(void 0, void 0, void 0, function* () {
361
+ const sourceFile = (0, NodeHeap_1.dumpNodeHeapSnapshot)();
362
+ try {
363
+ const heap = yield HeapParser_1.default.parse(sourceFile, {});
364
+ // `\d{0,}` matches every string, so this would redact the entire label
365
+ // vocabulary and still look like a successful run.
366
+ for (const bad of [0, -1, 2.5]) {
367
+ expect(() => (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(heap, { minDigitRunLength: bad })).toThrow(/positive integer/);
368
+ }
369
+ // a valid value is still honoured
370
+ expect(() => (0, HeapAnonymizer_1.anonymizeHeapSnapshot)(heap, { minDigitRunLength: 4 })).not.toThrow();
371
+ }
372
+ finally {
373
+ if (fs_1.default.existsSync(sourceFile)) {
374
+ fs_1.default.unlinkSync(sourceFile);
375
+ }
376
+ }
377
+ }), timeout);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @format
8
+ * @oncall memory_lab
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=MLTraceSimilarityStrategy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MLTraceSimilarityStrategy.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/trace-cluster/MLTraceSimilarityStrategy.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ * @format
9
+ * @oncall memory_lab
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const MLTraceSimilarityStrategy_1 = __importDefault(require("../../trace-cluster/strategies/MLTraceSimilarityStrategy"));
16
+ function newTrace() {
17
+ return [
18
+ { kind: 'node', id: 1, name: 'LeakedObject', type: 'object' },
19
+ { kind: 'edge', name_or_index: 'next', type: 'property' },
20
+ ];
21
+ }
22
+ function getTotalTraceCount(clusters) {
23
+ return clusters.reduce((sum, cluster) => sum + cluster.length, 0);
24
+ }
25
+ test('diffTraces includes each trace in a cluster exactly once', () => {
26
+ const strategy = new MLTraceSimilarityStrategy_1.default();
27
+ // a single trace still forms one cluster, and the representative trace
28
+ // must not be duplicated inside it
29
+ const single = strategy.diffTraces([newTrace()]);
30
+ expect(getTotalTraceCount(single.allClusters)).toBe(1);
31
+ // identical traces are clustered together; each trace should appear in
32
+ // the result exactly once
33
+ const traces = [newTrace(), newTrace(), newTrace()];
34
+ const result = strategy.diffTraces(traces);
35
+ expect(getTotalTraceCount(result.allClusters)).toBe(traces.length);
36
+ for (const cluster of result.allClusters) {
37
+ expect(new Set(cluster).size).toBe(cluster.length);
38
+ }
39
+ });
40
+ test('diffTraces keeps the representative trace at index 0', () => {
41
+ const strategy = new MLTraceSimilarityStrategy_1.default();
42
+ const traces = [newTrace(), newTrace(), newTrace()];
43
+ const { allClusters } = strategy.diffTraces(traces);
44
+ // HAC labels every trace in a cluster with the largest index in that
45
+ // cluster, so the representative of these identical traces is the last one.
46
+ // Downstream code (e.g. NormalizedTrace.clusterPaths) reads cluster[0] as
47
+ // the representative trace of the cluster.
48
+ expect(allClusters.length).toBe(1);
49
+ expect(allClusters[0][0]).toBe(traces[traces.length - 1]);
50
+ });
@@ -57,6 +57,17 @@ test('Check getReadableBytes', () => __awaiter(void 0, void 0, void 0, function*
57
57
  expect(Utils_1.default.getReadableBytes(2212312313)).toBe('2.2GB');
58
58
  expect(Utils_1.default.getReadableBytes(5432212312313)).toBe('5.4TB');
59
59
  }));
60
+ test('Check getNumberAtPercentile', () => __awaiter(void 0, void 0, void 0, function* () {
61
+ expect(Utils_1.default.getNumberAtPercentile([], 80)).toBe(0);
62
+ expect(Utils_1.default.getNumberAtPercentile([10, 20, 30, 40, 50], 100)).toBe(50);
63
+ expect(Utils_1.default.getNumberAtPercentile([7], 100)).toBe(7);
64
+ expect(Utils_1.default.getNumberAtPercentile([10, 20, 30, 40, 50], 0)).toBe(10);
65
+ expect(Utils_1.default.getNumberAtPercentile([10, 20, 30, 40, 50], 80)).toBe(50);
66
+ expect(Utils_1.default.getNumberAtPercentile([5, 3, 1, 4, 2], 50)).toBe(3.5);
67
+ // out-of-range percentiles are clamped rather than returning undefined/NaN
68
+ expect(Utils_1.default.getNumberAtPercentile([10, 20, 30, 40, 50], -10)).toBe(10);
69
+ expect(Utils_1.default.getNumberAtPercentile([10, 20, 30, 40, 50], 120)).toBe(50);
70
+ }));
60
71
  test('Check isStackTraceFrame', () => __awaiter(void 0, void 0, void 0, function* () {
61
72
  expect(Utils_1.default.isStackTraceFrame(strackTraceFrameNodeMock)).toBe(true);
62
73
  expect(Utils_1.default.isStackTraceFrame(oddBallNodeMock)).toBe(false);
package/dist/index.d.ts CHANGED
@@ -9,6 +9,9 @@
9
9
  */
10
10
  export * from './lib/Types';
11
11
  export * from './lib/NodeHeap';
12
+ export { serializeHeapSnapshot, serializeRawHeapSnapshot, } from './lib/HeapSerializer';
13
+ export { anonymizeHeapSnapshot, anonymizeHeapSnapshotFile, anonymizeRawHeapSnapshot, auditHeapSnapshotFile, resolveForComparison, } from './lib/HeapAnonymizer';
14
+ export type { AnonymizationMode, AnonymizeOptions, AnonymizeReport, AnonymizeRuleCount, RedactionContext, UnclassifiedLabelFamily, } from './lib/HeapAnonymizer';
12
15
  /** @internal */
13
16
  export declare function registerPackage(): Promise<void>;
14
17
  /** @internal */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAE/B,gBAAgB;AAChB,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAErD;AACD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,MAAM,EAAC,MAAM,cAAc,CAAC;AAC/C,gBAAgB;AAChB,cAAc,2BAA2B,CAAC;AAC1C,gBAAgB;AAChB,cAAc,cAAc,CAAC;AAC7B,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,IAAI,EAAC,MAAM,eAAe,CAAC;AAC9C,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACvD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,KAAK,EAAC,MAAM,aAAa,CAAC;AAC7C,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,WAAW,EAAC,MAAM,mBAAmB,CAAC;AACzD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAC3D,gBAAgB;AAChB,cAAc,mBAAmB,CAAC;AAClC,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACvD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,WAAW,EAAC,MAAM,mBAAmB,CAAC;AACzD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,QAAQ,EAAC,MAAM,oBAAoB,CAAC;AACvD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,QAAQ,EAAC,MAAM,gBAAgB,CAAC;AACnD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,6BAA6B,CAAC;AACtE,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,KAAK,EAAC,MAAM,sBAAsB,CAAC;AACtD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAC/D,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,iBAAiB,EAAC,MAAM,4BAA4B,CAAC;AACxE,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,eAAe,EAAC,MAAM,6BAA6B,CAAC;AACvE,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,gBAAgB,EAAC,MAAM,mCAAmC,CAAC;AAC9E,gBAAgB;AAChB,OAAO,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AACtD,gBAAgB;AAChB,cAAc,yBAAyB,CAAC;AACxC,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,oBAAoB,EAAC,MAAM,sCAAsC,CAAC;AACrF,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,2BAA2B,EAAC,MAAM,6CAA6C,CAAC;AACnG,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,WAAW,EAAC,MAAM,qBAAqB,CAAC;AAC3D,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,UAAU,EAAC,MAAM,kCAAkC,CAAC;AACvE,gBAAgB;AAChB,cAAc,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EACL,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,sBAAsB,CAAC;AAE9B,gBAAgB;AAChB,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAErD;AACD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,MAAM,EAAC,MAAM,cAAc,CAAC;AAC/C,gBAAgB;AAChB,cAAc,2BAA2B,CAAC;AAC1C,gBAAgB;AAChB,cAAc,cAAc,CAAC;AAC7B,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,IAAI,EAAC,MAAM,eAAe,CAAC;AAC9C,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACvD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,KAAK,EAAC,MAAM,aAAa,CAAC;AAC7C,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,WAAW,EAAC,MAAM,mBAAmB,CAAC;AACzD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAC3D,gBAAgB;AAChB,cAAc,mBAAmB,CAAC;AAClC,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACvD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,WAAW,EAAC,MAAM,mBAAmB,CAAC;AACzD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,QAAQ,EAAC,MAAM,oBAAoB,CAAC;AACvD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,QAAQ,EAAC,MAAM,gBAAgB,CAAC;AACnD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,6BAA6B,CAAC;AACtE,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,KAAK,EAAC,MAAM,sBAAsB,CAAC;AACtD,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAC/D,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,iBAAiB,EAAC,MAAM,4BAA4B,CAAC;AACxE,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,eAAe,EAAC,MAAM,6BAA6B,CAAC;AACvE,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,gBAAgB,EAAC,MAAM,mCAAmC,CAAC;AAC9E,gBAAgB;AAChB,OAAO,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AACtD,gBAAgB;AAChB,cAAc,yBAAyB,CAAC;AACxC,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,oBAAoB,EAAC,MAAM,sCAAsC,CAAC;AACrF,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,2BAA2B,EAAC,MAAM,6CAA6C,CAAC;AACnG,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,WAAW,EAAC,MAAM,qBAAqB,CAAC;AAC3D,gBAAgB;AAChB,OAAO,EAAC,OAAO,IAAI,UAAU,EAAC,MAAM,kCAAkC,CAAC;AACvE,gBAAgB;AAChB,cAAc,8BAA8B,CAAC"}
package/dist/index.js CHANGED
@@ -35,12 +35,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.NumericSet = exports.TraceFinder = exports.MultiIterationSeqClustering = exports.SequentialClustering = exports.RunMetaInfoManager = exports.EvaluationMetric = exports.NormalizedTrace = exports.leakClusterLogger = exports.ProcessManager = exports.modes = exports.memoryBarChart = exports.constant = exports.analysis = exports.browserInfo = exports.serializer = exports.runInfoUtils = exports.fileManager = exports.utils = exports.BaseOption = exports.info = exports.config = void 0;
38
+ exports.NumericSet = exports.TraceFinder = exports.MultiIterationSeqClustering = exports.SequentialClustering = exports.RunMetaInfoManager = exports.EvaluationMetric = exports.NormalizedTrace = exports.leakClusterLogger = exports.ProcessManager = exports.modes = exports.memoryBarChart = exports.constant = exports.analysis = exports.browserInfo = exports.serializer = exports.runInfoUtils = exports.fileManager = exports.utils = exports.BaseOption = exports.info = exports.config = exports.resolveForComparison = exports.auditHeapSnapshotFile = exports.anonymizeRawHeapSnapshot = exports.anonymizeHeapSnapshotFile = exports.anonymizeHeapSnapshot = exports.serializeRawHeapSnapshot = exports.serializeHeapSnapshot = void 0;
39
39
  exports.registerPackage = registerPackage;
40
40
  const path_1 = __importDefault(require("path"));
41
41
  const PackageInfoLoader_1 = require("./lib/PackageInfoLoader");
42
42
  __exportStar(require("./lib/Types"), exports);
43
43
  __exportStar(require("./lib/NodeHeap"), exports);
44
+ var HeapSerializer_1 = require("./lib/HeapSerializer");
45
+ Object.defineProperty(exports, "serializeHeapSnapshot", { enumerable: true, get: function () { return HeapSerializer_1.serializeHeapSnapshot; } });
46
+ Object.defineProperty(exports, "serializeRawHeapSnapshot", { enumerable: true, get: function () { return HeapSerializer_1.serializeRawHeapSnapshot; } });
47
+ var HeapAnonymizer_1 = require("./lib/HeapAnonymizer");
48
+ Object.defineProperty(exports, "anonymizeHeapSnapshot", { enumerable: true, get: function () { return HeapAnonymizer_1.anonymizeHeapSnapshot; } });
49
+ Object.defineProperty(exports, "anonymizeHeapSnapshotFile", { enumerable: true, get: function () { return HeapAnonymizer_1.anonymizeHeapSnapshotFile; } });
50
+ Object.defineProperty(exports, "anonymizeRawHeapSnapshot", { enumerable: true, get: function () { return HeapAnonymizer_1.anonymizeRawHeapSnapshot; } });
51
+ Object.defineProperty(exports, "auditHeapSnapshotFile", { enumerable: true, get: function () { return HeapAnonymizer_1.auditHeapSnapshotFile; } });
52
+ Object.defineProperty(exports, "resolveForComparison", { enumerable: true, get: function () { return HeapAnonymizer_1.resolveForComparison; } });
44
53
  /** @internal */
45
54
  function registerPackage() {
46
55
  return __awaiter(this, void 0, void 0, function* () {