@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.
- package/dist/__tests__/lib/HeapAnonymizer.test.d.ts +11 -0
- package/dist/__tests__/lib/HeapAnonymizer.test.d.ts.map +1 -0
- package/dist/__tests__/lib/HeapAnonymizer.test.js +377 -0
- package/dist/__tests__/trace-cluster/MLTraceSimilarityStrategy.test.d.ts +11 -0
- package/dist/__tests__/trace-cluster/MLTraceSimilarityStrategy.test.d.ts.map +1 -0
- package/dist/__tests__/trace-cluster/MLTraceSimilarityStrategy.test.js +50 -0
- package/dist/__tests__/utils/Utils.test.js +11 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/lib/HeapAnonymizer.d.ts +362 -0
- package/dist/lib/HeapAnonymizer.d.ts.map +1 -0
- package/dist/lib/HeapAnonymizer.js +670 -0
- package/dist/lib/HeapSerializer.d.ts +125 -0
- package/dist/lib/HeapSerializer.d.ts.map +1 -0
- package/dist/lib/HeapSerializer.js +315 -0
- package/dist/lib/Utils.d.ts.map +1 -1
- package/dist/lib/Utils.js +12 -0
- package/dist/lib/leak-filters/rules/FilterUserTaggedLeaks.rule.d.ts.map +1 -1
- package/dist/lib/leak-filters/rules/FilterUserTaggedLeaks.rule.js +17 -2
- package/dist/trace-cluster/strategies/MLTraceSimilarityStrategy.d.ts.map +1 -1
- package/dist/trace-cluster/strategies/MLTraceSimilarityStrategy.js +7 -2
- package/package.json +1 -1
|
@@ -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 */
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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* () {
|