analogger 2.8.1 → 2.9.1
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 +187 -0
- package/ana-logger.d.cts +63 -0
- package/browser/ana-logger.mjs +359 -0
- package/dist/analogger-browser.min.mjs +51 -8
- package/dist/html-to-image-plugin.min.mjs +51 -8
- package/esm/ana-logger.mjs +384 -0
- package/package.json +1 -1
- package/src/ana-logger.cjs +384 -0
package/README.md
CHANGED
|
@@ -1436,6 +1436,190 @@ anaLogger.log("lid: USR_LOGIN, color: purple", "User logged in");
|
|
|
1436
1436
|
---
|
|
1437
1437
|
|
|
1438
1438
|
|
|
1439
|
+
---
|
|
1440
|
+
|
|
1441
|
+
## Snapshots
|
|
1442
|
+
|
|
1443
|
+
Snapshots let you capture the set of log IDs (lids) seen at a specific point in time, persist them, and later compare two captures side by side. This is useful for regression checks — verifying that the same lids fire in two different runs, or that a code change does not add or remove unexpected log calls.
|
|
1444
|
+
|
|
1445
|
+
Snapshots work in both **Node.js** (persisted as JSON files under `os.tmpdir()/analogger-snapshots/`) and the **browser** (persisted in `localStorage`).
|
|
1446
|
+
|
|
1447
|
+
> `keepLogHistory()` must be called before any logging starts, as snapshots read from the in-memory log history.
|
|
1448
|
+
|
|
1449
|
+
---
|
|
1450
|
+
|
|
1451
|
+
### startSnapshotProcess()
|
|
1452
|
+
|
|
1453
|
+
Marks the start of a snapshot window. Any subsequent `takeSnapshot()` call will only capture lids logged **from this point forward**, ignoring everything already in the history. Calling it again moves the window start to the current position.
|
|
1454
|
+
|
|
1455
|
+
```javascript
|
|
1456
|
+
anaLogger.keepLogHistory();
|
|
1457
|
+
|
|
1458
|
+
anaLogger.log({ lid: "BOOT_001" }, "application boot"); // excluded
|
|
1459
|
+
anaLogger.log({ lid: "BOOT_002" }, "config loaded"); // excluded
|
|
1460
|
+
|
|
1461
|
+
anaLogger.startSnapshotProcess();
|
|
1462
|
+
|
|
1463
|
+
anaLogger.log({ lid: "WEB_001" }, "request received"); // included
|
|
1464
|
+
anaLogger.log({ lid: "WEB_002" }, "response sent"); // included
|
|
1465
|
+
|
|
1466
|
+
anaLogger.takeSnapshot("SNAP01"); // captures only WEB_001 and WEB_002
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
If `startSnapshotProcess()` is never called, `takeSnapshot()` captures the entire history.
|
|
1470
|
+
|
|
1471
|
+
<br/>
|
|
1472
|
+
|
|
1473
|
+
---
|
|
1474
|
+
|
|
1475
|
+
### takeSnapshot()
|
|
1476
|
+
|
|
1477
|
+
Captures all lids logged since the last `startSnapshotProcess()` (or since session start) and persists the snapshot under a unique ID.
|
|
1478
|
+
|
|
1479
|
+
Returns the array of captured entries so you can inspect or assert on them programmatically.
|
|
1480
|
+
|
|
1481
|
+
```javascript
|
|
1482
|
+
takeSnapshot(snapshotID, options?)
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
| Parameter | Type | Default | Description |
|
|
1486
|
+
|---|---|---|---|
|
|
1487
|
+
| `snapshotID` | `string` | — | Unique label for this snapshot, e.g. `"SNAP01"` |
|
|
1488
|
+
| `options.messages` | `boolean` | `true` | Store the log message alongside each lid |
|
|
1489
|
+
| `options.context` | `boolean` | `true` | Store the full context object alongside each lid |
|
|
1490
|
+
|
|
1491
|
+
```javascript
|
|
1492
|
+
anaLogger.keepLogHistory();
|
|
1493
|
+
anaLogger.log({ lid: "API_001" }, "initialised");
|
|
1494
|
+
anaLogger.log({ lid: "WEB_002" }, "request in");
|
|
1495
|
+
|
|
1496
|
+
// Capture lids + messages + context (defaults)
|
|
1497
|
+
const entries = anaLogger.takeSnapshot("SNAP01");
|
|
1498
|
+
// entries = [{ lid: "API_001", message: "initialised", context: {...} }, ...]
|
|
1499
|
+
|
|
1500
|
+
// Capture lids only (lighter footprint)
|
|
1501
|
+
anaLogger.takeSnapshot("SNAP02", { messages: false, context: false });
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
In Node.js the snapshot is written to a file and its path is logged:
|
|
1505
|
+
|
|
1506
|
+
```
|
|
1507
|
+
[AnaLogger] Snapshot "SNAP01" saved - 2 lid(s) captured.
|
|
1508
|
+
[AnaLogger] Snapshot persisted to: /tmp/analogger-snapshots/analogger_snapshot_default_SNAP01.json
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
Because the file lives in `os.tmpdir()`, snapshots persist across Node process restarts — you can take `SNAP01` in one run and compare it in a later one.
|
|
1512
|
+
|
|
1513
|
+
<br/>
|
|
1514
|
+
|
|
1515
|
+
---
|
|
1516
|
+
|
|
1517
|
+
### compareSnapshots()
|
|
1518
|
+
|
|
1519
|
+
Loads two previously saved snapshots and prints a side-by-side diff table using `console.table`, which gives automatic column alignment in both Node.js terminals and browser devtools.
|
|
1520
|
+
|
|
1521
|
+
```javascript
|
|
1522
|
+
compareSnapshots(snapshotID1, snapshotID2, displayOpts?)
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
| Parameter | Type | Default | Description |
|
|
1526
|
+
|---|---|---|---|
|
|
1527
|
+
| `snapshotID1` | `string` | — | ID of the first snapshot |
|
|
1528
|
+
| `snapshotID2` | `string` | — | ID of the second snapshot |
|
|
1529
|
+
| `displayOpts[0].messages` | `boolean` | `false` | Show a message column for snapshot 1 |
|
|
1530
|
+
| `displayOpts[1].messages` | `boolean` | `false` | Show a message column for snapshot 2 |
|
|
1531
|
+
|
|
1532
|
+
Returns `{ onlyInSnap1, onlyInSnap2, inBoth }` for programmatic use, or `null` if either snapshot is not found.
|
|
1533
|
+
|
|
1534
|
+
#### Basic comparison (lids only)
|
|
1535
|
+
|
|
1536
|
+
```javascript
|
|
1537
|
+
anaLogger.compareSnapshots("SNAP01", "SNAP02");
|
|
1538
|
+
```
|
|
1539
|
+
|
|
1540
|
+
```
|
|
1541
|
+
Snapshot comparison: "SNAP01" vs "SNAP02"
|
|
1542
|
+
SNAP01 captured at 09:04:28 - 3 lid(s)
|
|
1543
|
+
SNAP02 captured at 09:04:31 - 4 lid(s)
|
|
1544
|
+
|
|
1545
|
+
┌─────────┬─────────┬─────────┐
|
|
1546
|
+
│ (index) │ SNAP01 │ SNAP02 │
|
|
1547
|
+
├─────────┼─────────┼─────────┤
|
|
1548
|
+
│ 0 │ API_001 │ API_001 │
|
|
1549
|
+
│ 1 │ WEB_002 │ WEB_002 │
|
|
1550
|
+
│ 2 │ DB_003 │ │
|
|
1551
|
+
│ 3 │ │ WEB_004 │
|
|
1552
|
+
└─────────┴─────────┴─────────┘
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
#### With message columns
|
|
1556
|
+
|
|
1557
|
+
```javascript
|
|
1558
|
+
anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: true }]);
|
|
1559
|
+
```
|
|
1560
|
+
|
|
1561
|
+
```
|
|
1562
|
+
┌─────────┬─────────┬──────────────────────┬─────────┬──────────────────────┐
|
|
1563
|
+
│ (index) │ SNAP01 │ SNAP01 message │ SNAP02 │ SNAP02 message │
|
|
1564
|
+
├─────────┼─────────┼──────────────────────┼─────────┼──────────────────────┤
|
|
1565
|
+
│ 0 │ API_001 │ 'initialised' │ API_001 │ 'initialised' │
|
|
1566
|
+
│ 1 │ WEB_002 │ 'request in' │ │ │
|
|
1567
|
+
│ 2 │ │ │ WEB_004 │ 'new endpoint added' │
|
|
1568
|
+
└─────────┴─────────┴──────────────────────┴─────────┴──────────────────────┘
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
You can also enable messages for only one side:
|
|
1572
|
+
|
|
1573
|
+
```javascript
|
|
1574
|
+
// Messages only for SNAP02
|
|
1575
|
+
anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: false }, { messages: true }]);
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
In the browser, `compareSnapshots` also injects a colour-coded HTML widget into the `#analogger` DOM container (or `document.body` as a fallback): lids present in both snapshots are shown in neutral, snap1-only rows are highlighted in red, and snap2-only rows in green.
|
|
1579
|
+
|
|
1580
|
+
#### Using the return value
|
|
1581
|
+
|
|
1582
|
+
```javascript
|
|
1583
|
+
const { onlyInSnap1, onlyInSnap2, inBoth } = anaLogger.compareSnapshots("SNAP01", "SNAP02");
|
|
1584
|
+
|
|
1585
|
+
if (onlyInSnap2.length > 0) {
|
|
1586
|
+
console.warn("New lids appeared in SNAP02:", onlyInSnap2);
|
|
1587
|
+
}
|
|
1588
|
+
```
|
|
1589
|
+
|
|
1590
|
+
#### Full workflow example
|
|
1591
|
+
|
|
1592
|
+
```javascript
|
|
1593
|
+
const { anaLogger } = require("analogger");
|
|
1594
|
+
|
|
1595
|
+
anaLogger.keepLogHistory();
|
|
1596
|
+
|
|
1597
|
+
// --- First run (or first phase) ---
|
|
1598
|
+
anaLogger.startSnapshotProcess();
|
|
1599
|
+
anaLogger.log({ lid: "API_001" }, "initialised");
|
|
1600
|
+
anaLogger.log({ lid: "WEB_002" }, "request received");
|
|
1601
|
+
anaLogger.log({ lid: "DB_003" }, "query executed");
|
|
1602
|
+
anaLogger.takeSnapshot("SNAP01");
|
|
1603
|
+
|
|
1604
|
+
// --- Second run (or second phase, e.g. after a code change) ---
|
|
1605
|
+
anaLogger.startSnapshotProcess();
|
|
1606
|
+
anaLogger.log({ lid: "API_001" }, "initialised");
|
|
1607
|
+
anaLogger.log({ lid: "WEB_002" }, "request received");
|
|
1608
|
+
// DB_003 is gone, WEB_004 is new
|
|
1609
|
+
anaLogger.log({ lid: "WEB_004" }, "cache hit");
|
|
1610
|
+
anaLogger.takeSnapshot("SNAP02");
|
|
1611
|
+
|
|
1612
|
+
// --- Compare ---
|
|
1613
|
+
const diff = anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: true }]);
|
|
1614
|
+
// diff.onlyInSnap1 => ["DB_003"] (removed)
|
|
1615
|
+
// diff.onlyInSnap2 => ["WEB_004"] (added)
|
|
1616
|
+
// diff.inBoth => ["API_001", "WEB_002"]
|
|
1617
|
+
```
|
|
1618
|
+
|
|
1619
|
+
<br/>
|
|
1620
|
+
|
|
1621
|
+
---
|
|
1622
|
+
|
|
1439
1623
|
## Changelog
|
|
1440
1624
|
|
|
1441
1625
|
##### current:
|
|
@@ -1445,6 +1629,9 @@ anaLogger.log("lid: USR_LOGIN, color: purple", "User logged in");
|
|
|
1445
1629
|
* Add `order` option to detect and warn on out-of-sequence log calls
|
|
1446
1630
|
* Add `maxSeen` option to warn when a lid is logged more times than allowed
|
|
1447
1631
|
* Add `test` context option and `report()` method for inline assertions and test summaries
|
|
1632
|
+
* Add `startSnapshotProcess()` to mark the start of a snapshot window
|
|
1633
|
+
* Add `takeSnapshot(id, opts?)` to capture lids at a point in time; persists to `localStorage` (browser) or `os.tmpdir()` (Node); returns captured entries
|
|
1634
|
+
* Add `compareSnapshots(id1, id2, displayOpts?)` to diff two snapshots side by side using `console.table`; optional message columns via `[{ messages: true }, { messages: true }]`
|
|
1448
1635
|
|
|
1449
1636
|
|
|
1450
1637
|
##### 1.23.2:
|
package/ana-logger.d.cts
CHANGED
|
@@ -423,6 +423,69 @@ declare class ____AnaLogger {
|
|
|
423
423
|
}): Promise<any>;
|
|
424
424
|
alert(...args: any[]): void;
|
|
425
425
|
assert(condition: any, expected?: boolean, ...args: any[]): boolean;
|
|
426
|
+
/**
|
|
427
|
+
* Mark the start of a snapshot window.
|
|
428
|
+
* When called, any subsequent takeSnapshot() will only capture lids logged
|
|
429
|
+
* from this point forward, ignoring everything already in the history.
|
|
430
|
+
* Calling it again moves the window start to the current position.
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* anaLogger.keepLogHistory();
|
|
434
|
+
* anaLogger.log({ lid: "WEB0001" }, "boot"); // excluded
|
|
435
|
+
* anaLogger.startSnapshotProcess();
|
|
436
|
+
* anaLogger.log({ lid: "WEB0002" }, "step 1"); // included
|
|
437
|
+
* anaLogger.takeSnapshot("SNAP01"); // captures only WEB0002
|
|
438
|
+
*/
|
|
439
|
+
startSnapshotProcess(): void;
|
|
440
|
+
_snapshotWindowStart: number;
|
|
441
|
+
/**
|
|
442
|
+
* Capture all lids logged since the last startSnapshotProcess() (or since the
|
|
443
|
+
* beginning of the session if startSnapshotProcess was never called) into a
|
|
444
|
+
* named snapshot stored in localStorage / temp file.
|
|
445
|
+
*
|
|
446
|
+
* @param {string} snapshotID - unique label, e.g. "SNAP01"
|
|
447
|
+
* @param {{ messages?: boolean, context?: boolean }} [opts]
|
|
448
|
+
* messages: true - also store the log message per lid (default: true)
|
|
449
|
+
* context: true - also store the full context object (default: true)
|
|
450
|
+
* @returns {object[]} The array of snapshot entries that were saved
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* anaLogger.keepLogHistory();
|
|
454
|
+
* anaLogger.log({ lid: "WEB0001" }, "step 1");
|
|
455
|
+
* const entries = anaLogger.takeSnapshot("SNAP01");
|
|
456
|
+
* anaLogger.log({ lid: "WEB0002" }, "step 2");
|
|
457
|
+
* anaLogger.takeSnapshot("SNAP02", { messages: false, context: false });
|
|
458
|
+
*/
|
|
459
|
+
takeSnapshot(snapshotID: string, opts?: {
|
|
460
|
+
messages?: boolean;
|
|
461
|
+
context?: boolean;
|
|
462
|
+
}): object[];
|
|
463
|
+
/**
|
|
464
|
+
* Compare two previously saved snapshots and print a side-by-side diff table
|
|
465
|
+
* showing which lids are shared, present only in snap1, or only in snap2.
|
|
466
|
+
*
|
|
467
|
+
* @param {string} snapshotID1
|
|
468
|
+
* @param {string} snapshotID2
|
|
469
|
+
* @param {[{ messages?: boolean }, { messages?: boolean }]} [displayOpts]
|
|
470
|
+
* Optional per-snapshot display options.
|
|
471
|
+
* displayOpts[0].messages = true - add a message column for snapshotID1
|
|
472
|
+
* displayOpts[1].messages = true - add a message column for snapshotID2
|
|
473
|
+
* @returns {{ onlyInSnap1: string[], onlyInSnap2: string[], inBoth: string[] } | null}
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* anaLogger.compareSnapshots("SNAP01", "SNAP02");
|
|
477
|
+
* anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: true }]);
|
|
478
|
+
* anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: false }]);
|
|
479
|
+
*/
|
|
480
|
+
compareSnapshots(snapshotID1: string, snapshotID2: string, displayOpts?: [{
|
|
481
|
+
messages?: boolean;
|
|
482
|
+
}, {
|
|
483
|
+
messages?: boolean;
|
|
484
|
+
}]): {
|
|
485
|
+
onlyInSnap1: string[];
|
|
486
|
+
onlyInSnap2: string[];
|
|
487
|
+
inBoth: string[];
|
|
488
|
+
} | null;
|
|
426
489
|
/**
|
|
427
490
|
* Set standard Analogger format
|
|
428
491
|
* @example
|
package/browser/ana-logger.mjs
CHANGED
|
@@ -3565,6 +3565,365 @@ class ____AnaLogger
|
|
|
3565
3565
|
return false;
|
|
3566
3566
|
}
|
|
3567
3567
|
|
|
3568
|
+
// ─── Snapshot storage ─────────────────────────────────────────────────────
|
|
3569
|
+
// Browser → localStorage
|
|
3570
|
+
// Node.js → os.tmpdir()/analogger-snapshots/<key>.json (persists across runs)
|
|
3571
|
+
|
|
3572
|
+
/**
|
|
3573
|
+
* Resolve the file path used to persist a snapshot in Node.js.
|
|
3574
|
+
* The directory is created on first use.
|
|
3575
|
+
* @param {string} key – storage key, e.g. "analogger_snapshot_default_SNAP01"
|
|
3576
|
+
* @returns {string} – absolute file path
|
|
3577
|
+
*/
|
|
3578
|
+
#_snapshotFilePath(key) {
|
|
3579
|
+
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
#_snapshotGet(key) {
|
|
3583
|
+
// Browser path
|
|
3584
|
+
try {
|
|
3585
|
+
if (this.isBrowser() && typeof localStorage !== "undefined") {
|
|
3586
|
+
return localStorage.getItem(key);
|
|
3587
|
+
}
|
|
3588
|
+
} catch (e) {}
|
|
3589
|
+
|
|
3590
|
+
// Node.js path – read from temp file
|
|
3591
|
+
|
|
3592
|
+
|
|
3593
|
+
return null;
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
#_snapshotSet(key, value) {
|
|
3597
|
+
// Browser path
|
|
3598
|
+
try {
|
|
3599
|
+
if (this.isBrowser() && typeof localStorage !== "undefined") {
|
|
3600
|
+
localStorage.setItem(key, value);
|
|
3601
|
+
return;
|
|
3602
|
+
}
|
|
3603
|
+
} catch (e) {}
|
|
3604
|
+
|
|
3605
|
+
// Node.js path – write to temp file
|
|
3606
|
+
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
#_snapshotKey(snapshotID) {
|
|
3610
|
+
return `analogger_snapshot_${this.instanceName || "default"}_${snapshotID}`;
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
/**
|
|
3614
|
+
* Harvest lid entries from the current log history.
|
|
3615
|
+
* Only entries logged after the last startSnapshotProcess() call are included
|
|
3616
|
+
* when a snapshot window is active.
|
|
3617
|
+
* @param {{ messages?: boolean, context?: boolean }} opts
|
|
3618
|
+
* @returns {object[]}
|
|
3619
|
+
*/
|
|
3620
|
+
#_harvestSnapshot(opts = {}) {
|
|
3621
|
+
const { messages = true, context: includeContext = true } = opts;
|
|
3622
|
+
const fullHistory = this.getRawLogHistory ? this.getRawLogHistory() : (this.logHistory || []);
|
|
3623
|
+
|
|
3624
|
+
// If a snapshot window is active, slice from the marker index
|
|
3625
|
+
const history = this._snapshotWindowStart !== undefined
|
|
3626
|
+
? fullHistory.slice(this._snapshotWindowStart)
|
|
3627
|
+
: fullHistory;
|
|
3628
|
+
|
|
3629
|
+
/** Deduplicate by lid - keep the last occurrence */
|
|
3630
|
+
const seen = new Map();
|
|
3631
|
+
for (const entry of history) {
|
|
3632
|
+
const ctx = entry.context || {};
|
|
3633
|
+
const lid = ctx.lid;
|
|
3634
|
+
if (!lid) continue;
|
|
3635
|
+
const record = { lid };
|
|
3636
|
+
if (messages) record.message = entry.message ?? "";
|
|
3637
|
+
if (includeContext) record.context = { ...ctx };
|
|
3638
|
+
seen.set(lid, record);
|
|
3639
|
+
}
|
|
3640
|
+
return Array.from(seen.values());
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
/**
|
|
3644
|
+
* Mark the start of a snapshot window.
|
|
3645
|
+
* When called, any subsequent takeSnapshot() will only capture lids logged
|
|
3646
|
+
* from this point forward, ignoring everything already in the history.
|
|
3647
|
+
* Calling it again moves the window start to the current position.
|
|
3648
|
+
*
|
|
3649
|
+
* @example
|
|
3650
|
+
* anaLogger.keepLogHistory();
|
|
3651
|
+
* anaLogger.log({ lid: "WEB0001" }, "boot"); // excluded
|
|
3652
|
+
* anaLogger.startSnapshotProcess();
|
|
3653
|
+
* anaLogger.log({ lid: "WEB0002" }, "step 1"); // included
|
|
3654
|
+
* anaLogger.takeSnapshot("SNAP01"); // captures only WEB0002
|
|
3655
|
+
*/
|
|
3656
|
+
startSnapshotProcess() {
|
|
3657
|
+
const history = this.getRawLogHistory ? this.getRawLogHistory() : (this.logHistory || []);
|
|
3658
|
+
this._snapshotWindowStart = history.length;
|
|
3659
|
+
console.log(
|
|
3660
|
+
`[AnaLogger] Snapshot window started at history index ${this._snapshotWindowStart}.`
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
/**
|
|
3665
|
+
* Capture all lids logged since the last startSnapshotProcess() (or since the
|
|
3666
|
+
* beginning of the session if startSnapshotProcess was never called) into a
|
|
3667
|
+
* named snapshot stored in localStorage / temp file.
|
|
3668
|
+
*
|
|
3669
|
+
* @param {string} snapshotID - unique label, e.g. "SNAP01"
|
|
3670
|
+
* @param {{ messages?: boolean, context?: boolean }} [opts]
|
|
3671
|
+
* messages: true - also store the log message per lid (default: true)
|
|
3672
|
+
* context: true - also store the full context object (default: true)
|
|
3673
|
+
* @returns {object[]} The array of snapshot entries that were saved
|
|
3674
|
+
*
|
|
3675
|
+
* @example
|
|
3676
|
+
* anaLogger.keepLogHistory();
|
|
3677
|
+
* anaLogger.log({ lid: "WEB0001" }, "step 1");
|
|
3678
|
+
* const entries = anaLogger.takeSnapshot("SNAP01");
|
|
3679
|
+
* anaLogger.log({ lid: "WEB0002" }, "step 2");
|
|
3680
|
+
* anaLogger.takeSnapshot("SNAP02", { messages: false, context: false });
|
|
3681
|
+
*/
|
|
3682
|
+
takeSnapshot(snapshotID, opts = {}) {
|
|
3683
|
+
try {
|
|
3684
|
+
if (!snapshotID || typeof snapshotID !== "string") {
|
|
3685
|
+
console.warn("[AnaLogger] takeSnapshot: snapshotID must be a non-empty string.");
|
|
3686
|
+
return [];
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
const { messages = true, context: includeContext = true } = opts;
|
|
3690
|
+
const entries = this.#_harvestSnapshot({ messages, context: includeContext });
|
|
3691
|
+
|
|
3692
|
+
const payload = {
|
|
3693
|
+
snapshotID,
|
|
3694
|
+
timestamp : Date.now(),
|
|
3695
|
+
options : { messages, context: includeContext },
|
|
3696
|
+
entries,
|
|
3697
|
+
};
|
|
3698
|
+
|
|
3699
|
+
this.#_snapshotSet(this.#_snapshotKey(snapshotID), JSON.stringify(payload));
|
|
3700
|
+
|
|
3701
|
+
console.log(
|
|
3702
|
+
`[AnaLogger] Snapshot "${snapshotID}" saved - ${entries.length} lid(s) captured.`
|
|
3703
|
+
);
|
|
3704
|
+
|
|
3705
|
+
return entries;
|
|
3706
|
+
} catch (e) {
|
|
3707
|
+
console.error("[AnaLogger] takeSnapshot error:", e.message);
|
|
3708
|
+
return [];
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
/**
|
|
3713
|
+
* Compare two previously saved snapshots and print a side-by-side diff table
|
|
3714
|
+
* showing which lids are shared, present only in snap1, or only in snap2.
|
|
3715
|
+
*
|
|
3716
|
+
* @param {string} snapshotID1
|
|
3717
|
+
* @param {string} snapshotID2
|
|
3718
|
+
* @param {[{ messages?: boolean }, { messages?: boolean }]} [displayOpts]
|
|
3719
|
+
* Optional per-snapshot display options.
|
|
3720
|
+
* displayOpts[0].messages = true - add a message column for snapshotID1
|
|
3721
|
+
* displayOpts[1].messages = true - add a message column for snapshotID2
|
|
3722
|
+
* @returns {{ onlyInSnap1: string[], onlyInSnap2: string[], inBoth: string[] } | null}
|
|
3723
|
+
*
|
|
3724
|
+
* @example
|
|
3725
|
+
* anaLogger.compareSnapshots("SNAP01", "SNAP02");
|
|
3726
|
+
* anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: true }]);
|
|
3727
|
+
* anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: false }]);
|
|
3728
|
+
*/
|
|
3729
|
+
compareSnapshots(snapshotID1, snapshotID2, displayOpts = []) {
|
|
3730
|
+
try {
|
|
3731
|
+
const raw1 = this.#_snapshotGet(this.#_snapshotKey(snapshotID1));
|
|
3732
|
+
const raw2 = this.#_snapshotGet(this.#_snapshotKey(snapshotID2));
|
|
3733
|
+
|
|
3734
|
+
if (!raw1) console.warn(`[AnaLogger] compareSnapshots: snapshot "${snapshotID1}" not found.`);
|
|
3735
|
+
if (!raw2) console.warn(`[AnaLogger] compareSnapshots: snapshot "${snapshotID2}" not found.`);
|
|
3736
|
+
if (!raw1 || !raw2) return null;
|
|
3737
|
+
|
|
3738
|
+
const snap1 = JSON.parse(raw1);
|
|
3739
|
+
const snap2 = JSON.parse(raw2);
|
|
3740
|
+
|
|
3741
|
+
// Resolve per-snapshot message display flags
|
|
3742
|
+
const showMsg1 = !!(displayOpts[0] && displayOpts[0].messages);
|
|
3743
|
+
const showMsg2 = !!(displayOpts[1] && displayOpts[1].messages);
|
|
3744
|
+
|
|
3745
|
+
const map1 = new Map(snap1.entries.map((e) => [e.lid, e]));
|
|
3746
|
+
const map2 = new Map(snap2.entries.map((e) => [e.lid, e]));
|
|
3747
|
+
const allLids = new Set([...map1.keys(), ...map2.keys()]);
|
|
3748
|
+
|
|
3749
|
+
const inBoth = [];
|
|
3750
|
+
const onlyIn1 = [];
|
|
3751
|
+
const onlyIn2 = [];
|
|
3752
|
+
|
|
3753
|
+
for (const lid of allLids) {
|
|
3754
|
+
const in1 = map1.has(lid);
|
|
3755
|
+
const in2 = map2.has(lid);
|
|
3756
|
+
if (in1 && in2) inBoth.push(lid);
|
|
3757
|
+
else if (in1) onlyIn1.push(lid);
|
|
3758
|
+
else onlyIn2.push(lid);
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
// Build display rows: shared lids paired, then each exclusive lid on its own row
|
|
3762
|
+
const rows = [];
|
|
3763
|
+
for (const lid of inBoth) {
|
|
3764
|
+
rows.push({
|
|
3765
|
+
left: lid, right: lid, status: "both",
|
|
3766
|
+
msgLeft : map1.get(lid)?.message ?? "",
|
|
3767
|
+
msgRight: map2.get(lid)?.message ?? "",
|
|
3768
|
+
});
|
|
3769
|
+
}
|
|
3770
|
+
for (const lid of onlyIn1) {
|
|
3771
|
+
rows.push({
|
|
3772
|
+
left: lid, right: "",
|
|
3773
|
+
status: "left",
|
|
3774
|
+
msgLeft : map1.get(lid)?.message ?? "",
|
|
3775
|
+
msgRight: "",
|
|
3776
|
+
});
|
|
3777
|
+
}
|
|
3778
|
+
for (const lid of onlyIn2) {
|
|
3779
|
+
rows.push({
|
|
3780
|
+
left: "", right: lid,
|
|
3781
|
+
status: "right",
|
|
3782
|
+
msgLeft : "",
|
|
3783
|
+
msgRight: map2.get(lid)?.message ?? "",
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
this.#_renderSnapshotTable(
|
|
3788
|
+
snapshotID1, snapshotID2, rows, snap1, snap2,
|
|
3789
|
+
{ showMsg1, showMsg2 }
|
|
3790
|
+
);
|
|
3791
|
+
|
|
3792
|
+
return { onlyInSnap1: onlyIn1, onlyInSnap2: onlyIn2, inBoth };
|
|
3793
|
+
} catch (e) {
|
|
3794
|
+
console.error("[AnaLogger] compareSnapshots error:", e.message);
|
|
3795
|
+
return null;
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
// ─── Snapshot table renderers ─────────────────────────────────────────────
|
|
3800
|
+
|
|
3801
|
+
#_renderSnapshotTable(id1, id2, rows, snap1, snap2, { showMsg1 = false, showMsg2 = false } = {}) {
|
|
3802
|
+
const ts = (snap) =>
|
|
3803
|
+
snap.timestamp ? new Date(snap.timestamp).toLocaleTimeString() : "-";
|
|
3804
|
+
|
|
3805
|
+
// ── Header ───────────────────────────────────────────────────────────
|
|
3806
|
+
console.log(`\nSnapshot comparison: "${id1}" vs "${id2}"`);
|
|
3807
|
+
console.log(` ${id1} captured at ${ts(snap1)} - ${snap1.entries.length} lid(s)`);
|
|
3808
|
+
console.log(` ${id2} captured at ${ts(snap2)} - ${snap2.entries.length} lid(s)\n`);
|
|
3809
|
+
|
|
3810
|
+
// ── console.table (auto-aligned in Node and browser devtools) ────────
|
|
3811
|
+
// Column keys are the snapshot IDs so headers are self-documenting.
|
|
3812
|
+
// Message columns are appended only when requested.
|
|
3813
|
+
const msgKey1 = `${id1} message`;
|
|
3814
|
+
const msgKey2 = `${id2} message`;
|
|
3815
|
+
|
|
3816
|
+
const tableData = rows.length
|
|
3817
|
+
? rows.map((r) => {
|
|
3818
|
+
const row = { [id1]: r.left, [id2]: r.right };
|
|
3819
|
+
if (showMsg1) row[msgKey1] = r.msgLeft;
|
|
3820
|
+
if (showMsg2) row[msgKey2] = r.msgRight;
|
|
3821
|
+
return row;
|
|
3822
|
+
})
|
|
3823
|
+
: [(() => {
|
|
3824
|
+
const row = { [id1]: "(empty)", [id2]: "(empty)" };
|
|
3825
|
+
if (showMsg1) row[msgKey1] = "";
|
|
3826
|
+
if (showMsg2) row[msgKey2] = "";
|
|
3827
|
+
return row;
|
|
3828
|
+
})()];
|
|
3829
|
+
|
|
3830
|
+
console.table(tableData);
|
|
3831
|
+
|
|
3832
|
+
// ── Browser HTML widget (progressive enhancement) ────────────────────
|
|
3833
|
+
if (typeof document !== "undefined") {
|
|
3834
|
+
this.#_renderSnapshotHtml(id1, id2, rows, snap1, snap2, ts, { showMsg1, showMsg2 });
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
#_renderSnapshotHtml(id1, id2, rows, snap1, snap2, ts, { showMsg1 = false, showMsg2 = false } = {}) {
|
|
3839
|
+
try {
|
|
3840
|
+
const C = {
|
|
3841
|
+
both: "#1e293b", bothBg: "#f8fafc",
|
|
3842
|
+
left: "#7f1d1d", leftBg: "#fef2f2",
|
|
3843
|
+
right: "#14532d", rightBg: "#f0fdf4",
|
|
3844
|
+
msg: "#475569", msgBg: "#f8fafc",
|
|
3845
|
+
header: "#1e40af", border: "#e2e8f0",
|
|
3846
|
+
};
|
|
3847
|
+
|
|
3848
|
+
const sharedCount = snap1.entries.filter(e => snap2.entries.some(e2 => e2.lid === e.lid)).length;
|
|
3849
|
+
const only1Count = snap1.entries.length - sharedCount;
|
|
3850
|
+
const only2Count = snap2.entries.length - sharedCount;
|
|
3851
|
+
|
|
3852
|
+
// Total visible columns: lid1, [msg1], sym, lid2, [msg2]
|
|
3853
|
+
const totalCols = 3 + (showMsg1 ? 1 : 0) + (showMsg2 ? 1 : 0);
|
|
3854
|
+
const td = (extra = "") =>
|
|
3855
|
+
`padding:4px 10px;font-family:monospace;font-size:12px;border-bottom:1px solid ${C.border};${extra}`;
|
|
3856
|
+
const tdMsg = (extra = "") =>
|
|
3857
|
+
`padding:4px 10px;font-size:11px;color:${C.msg};border-bottom:1px solid ${C.border};max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;${extra}`;
|
|
3858
|
+
const thStyle = `padding:5px 10px;text-align:left;background:#f1f5f9;font-size:11px;color:#475569;border-bottom:2px solid ${C.border}`;
|
|
3859
|
+
|
|
3860
|
+
const mkRow = (row) => {
|
|
3861
|
+
const lBg = row.status === "left" ? C.leftBg : C.bothBg;
|
|
3862
|
+
const rBg = row.status === "right" ? C.rightBg : C.bothBg;
|
|
3863
|
+
const lCl = row.status === "left" ? C.left : row.status === "right" ? "#94a3b8" : C.both;
|
|
3864
|
+
const rCl = row.status === "right" ? C.right : row.status === "left" ? "#94a3b8" : C.both;
|
|
3865
|
+
const sym = row.status === "both" ? "=" : row.status === "left" ? "◀" : row.status === "right" ? "▶" : "≠";
|
|
3866
|
+
|
|
3867
|
+
return `<tr>
|
|
3868
|
+
<td style="${td(`background:${lBg};color:${lCl}`)}">${row.left || ""}</td>
|
|
3869
|
+
${showMsg1 ? `<td style="${tdMsg(`background:${lBg}`)}" title="${(row.msgLeft || "").replace(/"/g, """)}">${row.msgLeft || ""}</td>` : ""}
|
|
3870
|
+
<td style="width:20px;text-align:center;border-bottom:1px solid ${C.border};color:#94a3b8;font-size:10px">${sym}</td>
|
|
3871
|
+
<td style="${td(`background:${rBg};color:${rCl}`)}">${row.right || ""}</td>
|
|
3872
|
+
${showMsg2 ? `<td style="${tdMsg(`background:${rBg}`)}" title="${(row.msgRight || "").replace(/"/g, """)}">${row.msgRight || ""}</td>` : ""}
|
|
3873
|
+
</tr>`;
|
|
3874
|
+
};
|
|
3875
|
+
|
|
3876
|
+
const html = `
|
|
3877
|
+
<div id="analogger-snapshot-cmp" style="
|
|
3878
|
+
font-family:system-ui,sans-serif;font-size:13px;
|
|
3879
|
+
border:1px solid ${C.border};border-radius:8px;overflow:hidden;
|
|
3880
|
+
max-width:${showMsg1 || showMsg2 ? "820px" : "520px"};margin:12px 0;box-shadow:0 2px 8px rgba(0,0,0,.08)">
|
|
3881
|
+
<div style="background:${C.header};color:#fff;padding:8px 14px;font-weight:600">
|
|
3882
|
+
Snapshot diff —
|
|
3883
|
+
<code style="font-size:12px">${id1}</code>
|
|
3884
|
+
<span style="opacity:.7"> vs </span>
|
|
3885
|
+
<code style="font-size:12px">${id2}</code>
|
|
3886
|
+
</div>
|
|
3887
|
+
<div style="display:flex;background:#e2e8f0;font-size:11px;color:#64748b;padding:3px 0">
|
|
3888
|
+
<span style="flex:1;padding-left:10px">${id1} · ${ts(snap1)} · ${snap1.entries.length} lids</span>
|
|
3889
|
+
<span style="width:20px"></span>
|
|
3890
|
+
<span style="flex:1;padding-left:10px">${id2} · ${ts(snap2)} · ${snap2.entries.length} lids</span>
|
|
3891
|
+
</div>
|
|
3892
|
+
<table style="width:100%;border-collapse:collapse">
|
|
3893
|
+
<thead>
|
|
3894
|
+
<tr>
|
|
3895
|
+
<th style="${thStyle}">${id1}</th>
|
|
3896
|
+
${showMsg1 ? `<th style="${thStyle};color:#94a3b8;font-style:italic">message</th>` : ""}
|
|
3897
|
+
<th style="width:20px;background:#f1f5f9;border-bottom:2px solid ${C.border}"></th>
|
|
3898
|
+
<th style="${thStyle}">${id2}</th>
|
|
3899
|
+
${showMsg2 ? `<th style="${thStyle};color:#94a3b8;font-style:italic">message</th>` : ""}
|
|
3900
|
+
</tr>
|
|
3901
|
+
</thead>
|
|
3902
|
+
<tbody>
|
|
3903
|
+
${rows.length
|
|
3904
|
+
? rows.map(mkRow).join("")
|
|
3905
|
+
: `<tr><td colspan="${totalCols}" style="padding:10px;color:#94a3b8;text-align:center">(no lids in either snapshot)</td></tr>`}
|
|
3906
|
+
</tbody>
|
|
3907
|
+
</table>
|
|
3908
|
+
<div style="background:#f8fafc;padding:5px 10px;font-size:11px;color:#64748b;display:flex;gap:16px">
|
|
3909
|
+
<span>✔ ${sharedCount} shared</span>
|
|
3910
|
+
<span style="color:${C.left}">◄ ${only1Count} only in ${id1}</span>
|
|
3911
|
+
<span style="color:${C.right}">► ${only2Count} only in ${id2}</span>
|
|
3912
|
+
</div>
|
|
3913
|
+
</div>`;
|
|
3914
|
+
|
|
3915
|
+
const $old = document.getElementById("analogger-snapshot-cmp");
|
|
3916
|
+
if ($old) $old.remove();
|
|
3917
|
+
|
|
3918
|
+
const $mount = document.querySelector("#analogger") || document.body;
|
|
3919
|
+
const $wrapper = document.createElement("div");
|
|
3920
|
+
$wrapper.innerHTML = html;
|
|
3921
|
+
$mount.appendChild($wrapper.firstElementChild);
|
|
3922
|
+
} catch (e) {
|
|
3923
|
+
// Non-fatal - plain-text already printed
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3568
3927
|
/**
|
|
3569
3928
|
* Set default targets, contexts and log levels
|
|
3570
3929
|
* @returns {boolean}
|