analogger 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3578,6 +3578,385 @@ class ____AnaLogger
3578
3578
  return false;
3579
3579
  }
3580
3580
 
3581
+ // ─── Snapshot storage ─────────────────────────────────────────────────────
3582
+ // Browser → localStorage
3583
+ // Node.js → os.tmpdir()/analogger-snapshots/<key>.json (persists across runs)
3584
+
3585
+ /**
3586
+ * Resolve the file path used to persist a snapshot in Node.js.
3587
+ * The directory is created on first use.
3588
+ * @param {string} key – storage key, e.g. "analogger_snapshot_default_SNAP01"
3589
+ * @returns {string} – absolute file path
3590
+ */
3591
+ #_snapshotFilePath(key) {
3592
+
3593
+ const dir = path.join(os.tmpdir(), "analogger-snapshots");
3594
+ if (!fs.existsSync(dir)) {
3595
+ fs.mkdirSync(dir, { recursive: true });
3596
+ }
3597
+ // Sanitise the key so it is always a safe filename
3598
+ const safeName = key.replace(/[^a-zA-Z0-9_\-]/g, "_") + ".json";
3599
+ return path.join(dir, safeName);
3600
+
3601
+ }
3602
+
3603
+ #_snapshotGet(key) {
3604
+ // Browser path
3605
+ try {
3606
+ if (this.isBrowser() && typeof localStorage !== "undefined") {
3607
+ return localStorage.getItem(key);
3608
+ }
3609
+ } catch (e) {}
3610
+
3611
+ // Node.js path – read from temp file
3612
+
3613
+ try {
3614
+ const filePath = this.#_snapshotFilePath(key);
3615
+ if (fs.existsSync(filePath)) {
3616
+ return fs.readFileSync(filePath, "utf8");
3617
+ }
3618
+ } catch (e) {
3619
+ console.error("[AnaLogger] snapshot read error:", e.message);
3620
+ }
3621
+
3622
+
3623
+ return null;
3624
+ }
3625
+
3626
+ #_snapshotSet(key, value) {
3627
+ // Browser path
3628
+ try {
3629
+ if (this.isBrowser() && typeof localStorage !== "undefined") {
3630
+ localStorage.setItem(key, value);
3631
+ return;
3632
+ }
3633
+ } catch (e) {}
3634
+
3635
+ // Node.js path – write to temp file
3636
+
3637
+ try {
3638
+ const filePath = this.#_snapshotFilePath(key);
3639
+ fs.writeFileSync(filePath, value, "utf8");
3640
+ console.log(`[AnaLogger] Snapshot persisted to: ${filePath}`);
3641
+ } catch (e) {
3642
+ console.error("[AnaLogger] snapshot write error:", e.message);
3643
+ }
3644
+
3645
+ }
3646
+
3647
+ #_snapshotKey(snapshotID) {
3648
+ return `analogger_snapshot_${this.instanceName || "default"}_${snapshotID}`;
3649
+ }
3650
+
3651
+ /**
3652
+ * Harvest lid entries from the current log history.
3653
+ * Only entries logged after the last startSnapshotProcess() call are included
3654
+ * when a snapshot window is active.
3655
+ * @param {{ messages?: boolean, context?: boolean }} opts
3656
+ * @returns {object[]}
3657
+ */
3658
+ #_harvestSnapshot(opts = {}) {
3659
+ const { messages = true, context: includeContext = true } = opts;
3660
+ const fullHistory = this.getRawLogHistory ? this.getRawLogHistory() : (this.logHistory || []);
3661
+
3662
+ // If a snapshot window is active, slice from the marker index
3663
+ const history = this._snapshotWindowStart !== undefined
3664
+ ? fullHistory.slice(this._snapshotWindowStart)
3665
+ : fullHistory;
3666
+
3667
+ /** Deduplicate by lid - keep the last occurrence */
3668
+ const seen = new Map();
3669
+ for (const entry of history) {
3670
+ const ctx = entry.context || {};
3671
+ const lid = ctx.lid;
3672
+ if (!lid) continue;
3673
+ const record = { lid };
3674
+ if (messages) record.message = entry.message ?? "";
3675
+ if (includeContext) record.context = { ...ctx };
3676
+ seen.set(lid, record);
3677
+ }
3678
+ return Array.from(seen.values());
3679
+ }
3680
+
3681
+ /**
3682
+ * Mark the start of a snapshot window.
3683
+ * When called, any subsequent takeSnapshot() will only capture lids logged
3684
+ * from this point forward, ignoring everything already in the history.
3685
+ * Calling it again moves the window start to the current position.
3686
+ *
3687
+ * @example
3688
+ * anaLogger.keepLogHistory();
3689
+ * anaLogger.log({ lid: "WEB0001" }, "boot"); // excluded
3690
+ * anaLogger.startSnapshotProcess();
3691
+ * anaLogger.log({ lid: "WEB0002" }, "step 1"); // included
3692
+ * anaLogger.takeSnapshot("SNAP01"); // captures only WEB0002
3693
+ */
3694
+ startSnapshotProcess() {
3695
+ const history = this.getRawLogHistory ? this.getRawLogHistory() : (this.logHistory || []);
3696
+ this._snapshotWindowStart = history.length;
3697
+ console.log(
3698
+ `[AnaLogger] Snapshot window started at history index ${this._snapshotWindowStart}.`
3699
+ );
3700
+ }
3701
+
3702
+ /**
3703
+ * Capture all lids logged since the last startSnapshotProcess() (or since the
3704
+ * beginning of the session if startSnapshotProcess was never called) into a
3705
+ * named snapshot stored in localStorage / temp file.
3706
+ *
3707
+ * @param {string} snapshotID - unique label, e.g. "SNAP01"
3708
+ * @param {{ messages?: boolean, context?: boolean }} [opts]
3709
+ * messages: true - also store the log message per lid (default: true)
3710
+ * context: true - also store the full context object (default: true)
3711
+ * @returns {object[]} The array of snapshot entries that were saved
3712
+ *
3713
+ * @example
3714
+ * anaLogger.keepLogHistory();
3715
+ * anaLogger.log({ lid: "WEB0001" }, "step 1");
3716
+ * const entries = anaLogger.takeSnapshot("SNAP01");
3717
+ * anaLogger.log({ lid: "WEB0002" }, "step 2");
3718
+ * anaLogger.takeSnapshot("SNAP02", { messages: false, context: false });
3719
+ */
3720
+ takeSnapshot(snapshotID, opts = {}) {
3721
+ try {
3722
+ if (!snapshotID || typeof snapshotID !== "string") {
3723
+ console.warn("[AnaLogger] takeSnapshot: snapshotID must be a non-empty string.");
3724
+ return [];
3725
+ }
3726
+
3727
+ const { messages = true, context: includeContext = true } = opts;
3728
+ const entries = this.#_harvestSnapshot({ messages, context: includeContext });
3729
+
3730
+ const payload = {
3731
+ snapshotID,
3732
+ timestamp : Date.now(),
3733
+ options : { messages, context: includeContext },
3734
+ entries,
3735
+ };
3736
+
3737
+ this.#_snapshotSet(this.#_snapshotKey(snapshotID), JSON.stringify(payload));
3738
+
3739
+ console.log(
3740
+ `[AnaLogger] Snapshot "${snapshotID}" saved - ${entries.length} lid(s) captured.`
3741
+ );
3742
+
3743
+ return entries;
3744
+ } catch (e) {
3745
+ console.error("[AnaLogger] takeSnapshot error:", e.message);
3746
+ return [];
3747
+ }
3748
+ }
3749
+
3750
+ /**
3751
+ * Compare two previously saved snapshots and print a side-by-side diff table
3752
+ * showing which lids are shared, present only in snap1, or only in snap2.
3753
+ *
3754
+ * @param {string} snapshotID1
3755
+ * @param {string} snapshotID2
3756
+ * @param {[{ messages?: boolean }, { messages?: boolean }]} [displayOpts]
3757
+ * Optional per-snapshot display options.
3758
+ * displayOpts[0].messages = true - add a message column for snapshotID1
3759
+ * displayOpts[1].messages = true - add a message column for snapshotID2
3760
+ * @returns {{ onlyInSnap1: string[], onlyInSnap2: string[], inBoth: string[] } | null}
3761
+ *
3762
+ * @example
3763
+ * anaLogger.compareSnapshots("SNAP01", "SNAP02");
3764
+ * anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: true }]);
3765
+ * anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: false }]);
3766
+ */
3767
+ compareSnapshots(snapshotID1, snapshotID2, displayOpts = []) {
3768
+ try {
3769
+ const raw1 = this.#_snapshotGet(this.#_snapshotKey(snapshotID1));
3770
+ const raw2 = this.#_snapshotGet(this.#_snapshotKey(snapshotID2));
3771
+
3772
+ if (!raw1) console.warn(`[AnaLogger] compareSnapshots: snapshot "${snapshotID1}" not found.`);
3773
+ if (!raw2) console.warn(`[AnaLogger] compareSnapshots: snapshot "${snapshotID2}" not found.`);
3774
+ if (!raw1 || !raw2) return null;
3775
+
3776
+ const snap1 = JSON.parse(raw1);
3777
+ const snap2 = JSON.parse(raw2);
3778
+
3779
+ // Resolve per-snapshot message display flags
3780
+ const showMsg1 = !!(displayOpts[0] && displayOpts[0].messages);
3781
+ const showMsg2 = !!(displayOpts[1] && displayOpts[1].messages);
3782
+
3783
+ const map1 = new Map(snap1.entries.map((e) => [e.lid, e]));
3784
+ const map2 = new Map(snap2.entries.map((e) => [e.lid, e]));
3785
+ const allLids = new Set([...map1.keys(), ...map2.keys()]);
3786
+
3787
+ const inBoth = [];
3788
+ const onlyIn1 = [];
3789
+ const onlyIn2 = [];
3790
+
3791
+ for (const lid of allLids) {
3792
+ const in1 = map1.has(lid);
3793
+ const in2 = map2.has(lid);
3794
+ if (in1 && in2) inBoth.push(lid);
3795
+ else if (in1) onlyIn1.push(lid);
3796
+ else onlyIn2.push(lid);
3797
+ }
3798
+
3799
+ // Build display rows: shared lids paired, then staggered exclusives
3800
+ const rows = [];
3801
+ for (const lid of inBoth) {
3802
+ rows.push({
3803
+ left: lid, right: lid, status: "both",
3804
+ msgLeft : map1.get(lid)?.message ?? "",
3805
+ msgRight: map2.get(lid)?.message ?? "",
3806
+ });
3807
+ }
3808
+ const maxOnly = Math.max(onlyIn1.length, onlyIn2.length);
3809
+ for (let i = 0; i < maxOnly; i++) {
3810
+ const l = onlyIn1[i] ?? "";
3811
+ const r = onlyIn2[i] ?? "";
3812
+ rows.push({
3813
+ left: l, right: r,
3814
+ status: l && r ? "diff" : (l ? "left" : "right"),
3815
+ msgLeft : l ? (map1.get(l)?.message ?? "") : "",
3816
+ msgRight: r ? (map2.get(r)?.message ?? "") : "",
3817
+ });
3818
+ }
3819
+
3820
+ this.#_renderSnapshotTable(
3821
+ snapshotID1, snapshotID2, rows, snap1, snap2,
3822
+ { showMsg1, showMsg2 }
3823
+ );
3824
+
3825
+ return { onlyInSnap1: onlyIn1, onlyInSnap2: onlyIn2, inBoth };
3826
+ } catch (e) {
3827
+ console.error("[AnaLogger] compareSnapshots error:", e.message);
3828
+ return null;
3829
+ }
3830
+ }
3831
+
3832
+ // ─── Snapshot table renderers ─────────────────────────────────────────────
3833
+
3834
+ #_renderSnapshotTable(id1, id2, rows, snap1, snap2, { showMsg1 = false, showMsg2 = false } = {}) {
3835
+ const ts = (snap) =>
3836
+ snap.timestamp ? new Date(snap.timestamp).toLocaleTimeString() : "-";
3837
+
3838
+ // ── Header ───────────────────────────────────────────────────────────
3839
+ console.log(`\nSnapshot comparison: "${id1}" vs "${id2}"`);
3840
+ console.log(` ${id1} captured at ${ts(snap1)} - ${snap1.entries.length} lid(s)`);
3841
+ console.log(` ${id2} captured at ${ts(snap2)} - ${snap2.entries.length} lid(s)\n`);
3842
+
3843
+ // ── console.table (auto-aligned in Node and browser devtools) ────────
3844
+ // Column keys are the snapshot IDs so headers are self-documenting.
3845
+ // Message columns are appended only when requested.
3846
+ const msgKey1 = `${id1} message`;
3847
+ const msgKey2 = `${id2} message`;
3848
+
3849
+ const tableData = rows.length
3850
+ ? rows.map((r) => {
3851
+ const row = { [id1]: r.left, [id2]: r.right };
3852
+ if (showMsg1) row[msgKey1] = r.msgLeft;
3853
+ if (showMsg2) row[msgKey2] = r.msgRight;
3854
+ return row;
3855
+ })
3856
+ : [(() => {
3857
+ const row = { [id1]: "(empty)", [id2]: "(empty)" };
3858
+ if (showMsg1) row[msgKey1] = "";
3859
+ if (showMsg2) row[msgKey2] = "";
3860
+ return row;
3861
+ })()];
3862
+
3863
+ console.table(tableData);
3864
+
3865
+ // ── Browser HTML widget (progressive enhancement) ────────────────────
3866
+ if (typeof document !== "undefined") {
3867
+ this.#_renderSnapshotHtml(id1, id2, rows, snap1, snap2, ts, { showMsg1, showMsg2 });
3868
+ }
3869
+ }
3870
+
3871
+ #_renderSnapshotHtml(id1, id2, rows, snap1, snap2, ts, { showMsg1 = false, showMsg2 = false } = {}) {
3872
+ try {
3873
+ const C = {
3874
+ both: "#1e293b", bothBg: "#f8fafc",
3875
+ left: "#7f1d1d", leftBg: "#fef2f2",
3876
+ right: "#14532d", rightBg: "#f0fdf4",
3877
+ msg: "#475569", msgBg: "#f8fafc",
3878
+ header: "#1e40af", border: "#e2e8f0",
3879
+ };
3880
+
3881
+ const sharedCount = snap1.entries.filter(e => snap2.entries.some(e2 => e2.lid === e.lid)).length;
3882
+ const only1Count = snap1.entries.length - sharedCount;
3883
+ const only2Count = snap2.entries.length - sharedCount;
3884
+
3885
+ // Total visible columns: lid1, [msg1], sym, lid2, [msg2]
3886
+ const totalCols = 3 + (showMsg1 ? 1 : 0) + (showMsg2 ? 1 : 0);
3887
+ const td = (extra = "") =>
3888
+ `padding:4px 10px;font-family:monospace;font-size:12px;border-bottom:1px solid ${C.border};${extra}`;
3889
+ const tdMsg = (extra = "") =>
3890
+ `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}`;
3891
+ const thStyle = `padding:5px 10px;text-align:left;background:#f1f5f9;font-size:11px;color:#475569;border-bottom:2px solid ${C.border}`;
3892
+
3893
+ const mkRow = (row) => {
3894
+ const lBg = row.status === "left" ? C.leftBg : C.bothBg;
3895
+ const rBg = row.status === "right" ? C.rightBg : C.bothBg;
3896
+ const lCl = row.status === "left" ? C.left : row.status === "right" ? "#94a3b8" : C.both;
3897
+ const rCl = row.status === "right" ? C.right : row.status === "left" ? "#94a3b8" : C.both;
3898
+ const sym = row.status === "both" ? "=" : row.status === "left" ? "◀" : row.status === "right" ? "▶" : "≠";
3899
+
3900
+ return `<tr>
3901
+ <td style="${td(`background:${lBg};color:${lCl}`)}">${row.left || ""}</td>
3902
+ ${showMsg1 ? `<td style="${tdMsg(`background:${lBg}`)}" title="${(row.msgLeft || "").replace(/"/g, "&quot;")}">${row.msgLeft || ""}</td>` : ""}
3903
+ <td style="width:20px;text-align:center;border-bottom:1px solid ${C.border};color:#94a3b8;font-size:10px">${sym}</td>
3904
+ <td style="${td(`background:${rBg};color:${rCl}`)}">${row.right || ""}</td>
3905
+ ${showMsg2 ? `<td style="${tdMsg(`background:${rBg}`)}" title="${(row.msgRight || "").replace(/"/g, "&quot;")}">${row.msgRight || ""}</td>` : ""}
3906
+ </tr>`;
3907
+ };
3908
+
3909
+ const html = `
3910
+ <div id="analogger-snapshot-cmp" style="
3911
+ font-family:system-ui,sans-serif;font-size:13px;
3912
+ border:1px solid ${C.border};border-radius:8px;overflow:hidden;
3913
+ max-width:${showMsg1 || showMsg2 ? "820px" : "520px"};margin:12px 0;box-shadow:0 2px 8px rgba(0,0,0,.08)">
3914
+ <div style="background:${C.header};color:#fff;padding:8px 14px;font-weight:600">
3915
+ Snapshot diff &mdash;
3916
+ <code style="font-size:12px">${id1}</code>
3917
+ <span style="opacity:.7"> vs </span>
3918
+ <code style="font-size:12px">${id2}</code>
3919
+ </div>
3920
+ <div style="display:flex;background:#e2e8f0;font-size:11px;color:#64748b;padding:3px 0">
3921
+ <span style="flex:1;padding-left:10px">${id1} &middot; ${ts(snap1)} &middot; ${snap1.entries.length} lids</span>
3922
+ <span style="width:20px"></span>
3923
+ <span style="flex:1;padding-left:10px">${id2} &middot; ${ts(snap2)} &middot; ${snap2.entries.length} lids</span>
3924
+ </div>
3925
+ <table style="width:100%;border-collapse:collapse">
3926
+ <thead>
3927
+ <tr>
3928
+ <th style="${thStyle}">${id1}</th>
3929
+ ${showMsg1 ? `<th style="${thStyle};color:#94a3b8;font-style:italic">message</th>` : ""}
3930
+ <th style="width:20px;background:#f1f5f9;border-bottom:2px solid ${C.border}"></th>
3931
+ <th style="${thStyle}">${id2}</th>
3932
+ ${showMsg2 ? `<th style="${thStyle};color:#94a3b8;font-style:italic">message</th>` : ""}
3933
+ </tr>
3934
+ </thead>
3935
+ <tbody>
3936
+ ${rows.length
3937
+ ? rows.map(mkRow).join("")
3938
+ : `<tr><td colspan="${totalCols}" style="padding:10px;color:#94a3b8;text-align:center">(no lids in either snapshot)</td></tr>`}
3939
+ </tbody>
3940
+ </table>
3941
+ <div style="background:#f8fafc;padding:5px 10px;font-size:11px;color:#64748b;display:flex;gap:16px">
3942
+ <span>&#x2714; ${sharedCount} shared</span>
3943
+ <span style="color:${C.left}">&#x25C4; ${only1Count} only in ${id1}</span>
3944
+ <span style="color:${C.right}">&#x25BA; ${only2Count} only in ${id2}</span>
3945
+ </div>
3946
+ </div>`;
3947
+
3948
+ const $old = document.getElementById("analogger-snapshot-cmp");
3949
+ if ($old) $old.remove();
3950
+
3951
+ const $mount = document.querySelector("#analogger") || document.body;
3952
+ const $wrapper = document.createElement("div");
3953
+ $wrapper.innerHTML = html;
3954
+ $mount.appendChild($wrapper.firstElementChild);
3955
+ } catch (e) {
3956
+ // Non-fatal - plain-text already printed
3957
+ }
3958
+ }
3959
+
3581
3960
  /**
3582
3961
  * Set default targets, contexts and log levels
3583
3962
  * @returns {boolean}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "analogger",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Js Logger",
5
5
  "main": "./src/ana-logger.cjs",
6
6
  "module": "./esm/src/ana-logger.mjs",