@dudousxd/nestjs-telescope-inertia-watcher 1.7.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # @dudousxd/nestjs-telescope-inertia-watcher
2
+
3
+ ## 1.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`b35cc6b`](https://github.com/DavideCarvalho/nestjs-telescope/commit/b35cc6b1508e98f75f2ed78b8d08b93569f47089) - Add `@dudousxd/nestjs-telescope-inertia-watcher`: an `InertiaWatcher` that
8
+ subscribes to the `nestjs-inertia:render` diagnostics channel and records one
9
+ `inertia` entry per Inertia render (component, resolved props, deferred/merge
10
+ classification, partial-reload decision, asset version + 409 version-mismatch,
11
+ history flags, page size), correlated to the request batch. Adds the
12
+ `EntryType.Inertia` constant to core. Fully decoupled from `nestjs-inertia` —
13
+ the channel name and payload shape (`v: 1`) are the only contract; malformed or
14
+ wrong-version messages are dropped.
15
+
16
+ ### Patch Changes
17
+
18
+ - [`10c76f1`](https://github.com/DavideCarvalho/nestjs-telescope/commit/10c76f1c0d9c1034e5bffca2d9775cd16e553200) - Inertia panel polish: log on record failure (was silently swallowed) and warn once on
19
+ an unsupported diagnostic version; collapse `InertiaContent` to
20
+ `Omit<InertiaRenderDiagnostic, …>` with a destructure-rest copy; make the
21
+ `isInertiaDiagnostic` guard honest about what it proves; render the Inertia badge in
22
+ the entries table (parity with the cache badge); data-drive the props rows. Adds a
23
+ committed cross-repo wire-contract fixture (`inertia-render.v1.json`) so producer
24
+ drift fails a test.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davi Carvalho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @dudousxd/nestjs-telescope-inertia-watcher
2
+
3
+ Inertia.js render watcher for [`@dudousxd/nestjs-telescope`](../../README.md).
4
+ Subscribes to the `nestjs-inertia:render` diagnostics channel that
5
+ [`@dudousxd/nestjs-inertia`](https://github.com/DavideCarvalho/nestjs-inertia)
6
+ publishes once per response, and records one `inertia` entry per render
7
+ (rendered component, resolved props, deferred/merge classification, the
8
+ partial-reload decision, asset version + 409 version-mismatch, history flags and
9
+ page size), correlated to the request that produced it.
10
+
11
+ The bridge is Node's core `node:diagnostics_channel` — telescope **subscribes**,
12
+ inertia **publishes**. The two libraries stay fully decoupled: this package does
13
+ NOT depend on `nestjs-inertia`. The channel name (`nestjs-inertia:render`) and
14
+ the payload shape (`v: 1`) are the only contract; malformed or wrong-version
15
+ messages are dropped, never thrown.
16
+
17
+ When this watcher isn't installed, inertia's `channel.hasSubscribers` stays
18
+ `false` and it publishes nothing — so the integration costs ~zero when off.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ pnpm add @dudousxd/nestjs-telescope-inertia-watcher
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```ts
29
+ import { TelescopeModule } from '@dudousxd/nestjs-telescope';
30
+ import { InertiaWatcher } from '@dudousxd/nestjs-telescope-inertia-watcher';
31
+
32
+ TelescopeModule.forRoot({
33
+ watchers: [new InertiaWatcher()],
34
+ });
35
+ ```
36
+
37
+ On bootstrap the watcher subscribes to the channel (flipping inertia's
38
+ `hasSubscribers` to `true`), so events start flowing. Each captured entry has
39
+ type `inertia` and `InertiaContent`. The heavy `resolvedProps` field is passed
40
+ to the Recorder by reference, so its `redactBounded` clips + masks it with the
41
+ same budget every other entry's content gets (secret keys → `[REDACTED]`,
42
+ oversized trees truncated).
43
+
44
+ ## License
45
+
46
+ MIT © Davi Carvalho
@@ -0,0 +1,4 @@
1
+ export { InertiaWatcher } from './inertia.watcher.js';
2
+ export type { InertiaContent, InertiaRenderDiagnostic } from './inertia-content.js';
3
+ export { buildInertiaContent, isInertiaDiagnostic } from './inertia-content.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // packages/inertia-watcher/src/index.ts
2
+ export { InertiaWatcher } from './inertia.watcher.js';
3
+ export { buildInertiaContent, isInertiaDiagnostic } from './inertia-content.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,80 @@
1
+ import { type RecordInput } from '@dudousxd/nestjs-telescope';
2
+ /**
3
+ * The persisted content of one captured Inertia render: the wire
4
+ * `InertiaRenderDiagnostic` (below) minus the transport-only `v` and the
5
+ * always-true `isInertia` discriminator. Defined as an `Omit` so it can never
6
+ * drift from the wire shape. The heavy `resolvedProps` field is passed THROUGH
7
+ * to the Recorder by reference so its `redactBounded` clips + masks it with the
8
+ * standard budget — we do nothing special for size/secrets here.
9
+ */
10
+ export type InertiaContent = Omit<InertiaRenderDiagnostic, 'v' | 'isInertia'>;
11
+ /**
12
+ * The wire payload published by `@dudousxd/nestjs-inertia` on the
13
+ * `nestjs-inertia:render` channel. This shape is the source of truth in
14
+ * nestjs-inertia's `src/diagnostics.ts` (`InertiaRenderDiagnostic`, spec §1.3);
15
+ * it is duplicated here STRUCTURALLY (never imported) so the two libraries stay
16
+ * decoupled. Validate with `isInertiaDiagnostic` before trusting it.
17
+ */
18
+ export interface InertiaRenderDiagnostic {
19
+ v: number;
20
+ component: string;
21
+ url: string;
22
+ method: string;
23
+ isInertia: boolean;
24
+ isPartial: boolean;
25
+ partial: {
26
+ only: string[];
27
+ except: string[];
28
+ reset: string[];
29
+ resetOnce: string[];
30
+ };
31
+ props: {
32
+ sharedKeys: string[];
33
+ finalKeys: string[];
34
+ deferred: Record<string, string[]>;
35
+ merge: string[];
36
+ deepMerge: string[];
37
+ matchPropsOn: Record<string, string>;
38
+ optionalKeys: string[];
39
+ onceKeys: string[];
40
+ excludedKeys: string[];
41
+ };
42
+ resolvedProps: unknown;
43
+ assetVersion: string;
44
+ versionMismatch: boolean;
45
+ clientVersion: string | null;
46
+ encryptHistory: boolean;
47
+ clearHistory: boolean;
48
+ statusCode: number;
49
+ pageBytes: number;
50
+ ssr: boolean;
51
+ }
52
+ /**
53
+ * True when a message is shaped like our diagnostic — it carries a numeric `v`
54
+ * and the load-bearing render fields — REGARDLESS of version. Lets the watcher
55
+ * tell "Inertia-shaped but unsupported `v`" (warn once) apart from ordinary
56
+ * non-Inertia channel noise (drop silently). Never throws.
57
+ */
58
+ export declare function isInertiaShaped(msg: unknown): msg is {
59
+ v: number;
60
+ } & Partial<InertiaRenderDiagnostic>;
61
+ /**
62
+ * Defensive structural validation of an untrusted channel message: Inertia-shaped
63
+ * AND a supported `v === 1`. The watcher records only these (fail-safe across
64
+ * producer version bumps). Narrowed return type — this proves the version and the
65
+ * load-bearing fields it actually checks, not the whole payload; the UI re-reads
66
+ * every other field defensively. Never throws.
67
+ */
68
+ export declare function isInertiaDiagnostic(msg: unknown): msg is {
69
+ v: 1;
70
+ } & Partial<InertiaRenderDiagnostic>;
71
+ /**
72
+ * Map a validated diagnostic to a `RecordInput`. `familyHash` groups all renders
73
+ * of a component (`inertia:<component>`), mirroring `event:<name>` / `redis:<CMD>`.
74
+ * `durationMs` is null — render time belongs to the sibling `request` entry. The
75
+ * content is the wire payload minus `v`/`isInertia` via destructure-rest, so the
76
+ * heavy `resolvedProps` is carried BY REFERENCE (no clone/stringify) for the
77
+ * Recorder's `redactBounded` to traverse, clip, and mask.
78
+ */
79
+ export declare function buildInertiaContent(d: InertiaRenderDiagnostic): RecordInput<InertiaContent>;
80
+ //# sourceMappingURL=inertia-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inertia-content.d.ts","sourceRoot":"","sources":["../src/inertia-content.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzE;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,uBAAuB,EAAE,GAAG,GAAG,WAAW,CAAC,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,CAAC,EAAE,MAAM,CAAC;IACV,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACpF,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACnC,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACrC,YAAY,EAAE,MAAM,EAAE,CAAC;QACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,YAAY,EAAE,MAAM,EAAE,CAAC;KACxB,CAAC;IACF,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,OAAO,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,GACX,GAAG,IAAI;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAazD;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,GACX,GAAG,IAAI;IAAE,CAAC,EAAE,CAAC,CAAA;CAAE,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAEpD;AAYD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,uBAAuB,GAAG,WAAW,CAAC,cAAc,CAAC,CAS3F"}
@@ -0,0 +1,63 @@
1
+ // packages/inertia-watcher/src/inertia-content.ts
2
+ import { EntryType } from '@dudousxd/nestjs-telescope';
3
+ /**
4
+ * True when a message is shaped like our diagnostic — it carries a numeric `v`
5
+ * and the load-bearing render fields — REGARDLESS of version. Lets the watcher
6
+ * tell "Inertia-shaped but unsupported `v`" (warn once) apart from ordinary
7
+ * non-Inertia channel noise (drop silently). Never throws.
8
+ */
9
+ export function isInertiaShaped(msg) {
10
+ if (typeof msg !== 'object' || msg === null)
11
+ return false;
12
+ const m = msg;
13
+ return (typeof m.v === 'number' &&
14
+ typeof m.component === 'string' &&
15
+ typeof m.url === 'string' &&
16
+ typeof m.method === 'string' &&
17
+ typeof m.props === 'object' &&
18
+ m.props !== null &&
19
+ typeof m.partial === 'object' &&
20
+ m.partial !== null);
21
+ }
22
+ /**
23
+ * Defensive structural validation of an untrusted channel message: Inertia-shaped
24
+ * AND a supported `v === 1`. The watcher records only these (fail-safe across
25
+ * producer version bumps). Narrowed return type — this proves the version and the
26
+ * load-bearing fields it actually checks, not the whole payload; the UI re-reads
27
+ * every other field defensively. Never throws.
28
+ */
29
+ export function isInertiaDiagnostic(msg) {
30
+ return isInertiaShaped(msg) && msg.v === 1;
31
+ }
32
+ /** Build the tag list for one render: base tags + partial/mismatch/deferred/merge flags. */
33
+ function buildTags(d) {
34
+ const tags = ['inertia', `inertia:component:${d.component}`];
35
+ if (d.isPartial)
36
+ tags.push('inertia:partial');
37
+ if (d.versionMismatch)
38
+ tags.push('inertia:version-mismatch');
39
+ if (Object.keys(d.props.deferred ?? {}).length)
40
+ tags.push('inertia:deferred');
41
+ if ((d.props.merge?.length ?? 0) || (d.props.deepMerge?.length ?? 0))
42
+ tags.push('inertia:merge');
43
+ return tags;
44
+ }
45
+ /**
46
+ * Map a validated diagnostic to a `RecordInput`. `familyHash` groups all renders
47
+ * of a component (`inertia:<component>`), mirroring `event:<name>` / `redis:<CMD>`.
48
+ * `durationMs` is null — render time belongs to the sibling `request` entry. The
49
+ * content is the wire payload minus `v`/`isInertia` via destructure-rest, so the
50
+ * heavy `resolvedProps` is carried BY REFERENCE (no clone/stringify) for the
51
+ * Recorder's `redactBounded` to traverse, clip, and mask.
52
+ */
53
+ export function buildInertiaContent(d) {
54
+ const { v, isInertia, ...content } = d;
55
+ return {
56
+ type: EntryType.Inertia,
57
+ familyHash: `inertia:${d.component}`,
58
+ durationMs: null,
59
+ tags: buildTags(d),
60
+ content,
61
+ };
62
+ }
63
+ //# sourceMappingURL=inertia-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inertia-content.js","sourceRoot":"","sources":["../src/inertia-content.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,OAAO,EAAE,SAAS,EAAoB,MAAM,4BAA4B,CAAC;AAiDzE;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAY;IAEZ,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,OAAO,CACL,OAAO,CAAC,CAAC,CAAC,KAAK,QAAQ;QACvB,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ;QAC/B,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ;QACzB,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;QAC5B,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;QAC3B,CAAC,CAAC,KAAK,KAAK,IAAI;QAChB,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;QAC7B,CAAC,CAAC,OAAO,KAAK,IAAI,CACnB,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,GAAY;IAEZ,OAAO,eAAe,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;AAC7C,CAAC;AAED,4FAA4F;AAC5F,SAAS,SAAS,CAAC,CAA0B;IAC3C,MAAM,IAAI,GAAG,CAAC,SAAS,EAAE,qBAAqB,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;IAC7D,IAAI,CAAC,CAAC,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAC9C,IAAI,CAAC,CAAC,eAAe;QAAE,IAAI,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IAC7D,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM;QAAE,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAC9E,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC,CAAC;QAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACjG,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,CAA0B;IAC5D,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,GAAG,CAAC,CAAC;IACvC,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,OAAO;QACvB,UAAU,EAAE,WAAW,CAAC,CAAC,SAAS,EAAE;QACpC,UAAU,EAAE,IAAI;QAChB,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;QAClB,OAAO;KACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { type Watcher, type WatcherContext } from '@dudousxd/nestjs-telescope';
2
+ /**
3
+ * Records every Inertia response published on the `nestjs-inertia:render`
4
+ * diagnostics channel as one `inertia` entry, correlated to the active
5
+ * request/job batch.
6
+ *
7
+ * ## How it works
8
+ * On `register` the watcher subscribes a listener to the channel. `inertia`
9
+ * publishes synchronously inside `InertiaService.render()`, so the listener runs
10
+ * on the same call stack / async context as the render — `ctx.record(...)` lands
11
+ * in the request's ALS batch (no batch is opened here, no request-id plumbing).
12
+ * Subscribing also flips inertia's `channel.hasSubscribers` to `true`, which is
13
+ * what makes the producer start building + publishing payloads at all.
14
+ *
15
+ * ## Resilience
16
+ * Each message is structurally validated (`isInertiaDiagnostic`, `v === 1`);
17
+ * malformed or wrong-version payloads are dropped. All recording is wrapped so a
18
+ * telescope error can never break a render. Registration is idempotent.
19
+ */
20
+ export declare class InertiaWatcher implements Watcher {
21
+ readonly type: "inertia";
22
+ private registered;
23
+ private onMessage;
24
+ register(ctx: WatcherContext): void;
25
+ /** Unsubscribe the listener. Safe to call when never registered. */
26
+ cleanup(): void;
27
+ /** Validate + record, swallowing any failure so a render can never break. */
28
+ private safeRecord;
29
+ }
30
+ //# sourceMappingURL=inertia.watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inertia.watcher.d.ts","sourceRoot":"","sources":["../src/inertia.watcher.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAiB1F;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,cAAe,YAAW,OAAO;IAC5C,QAAQ,CAAC,IAAI,YAAqB;IAClC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAyC;IAE1D,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI;IASnC,oEAAoE;IACpE,OAAO,IAAI,IAAI;IAQf,6EAA6E;IAC7E,OAAO,CAAC,UAAU;CAmBnB"}
@@ -0,0 +1,75 @@
1
+ // packages/inertia-watcher/src/inertia.watcher.ts
2
+ import diagnostics_channel from 'node:diagnostics_channel';
3
+ import { EntryType } from '@dudousxd/nestjs-telescope';
4
+ import { buildInertiaContent, isInertiaDiagnostic, isInertiaShaped } from './inertia-content.js';
5
+ /**
6
+ * The channel name is the cross-repo contract with `@dudousxd/nestjs-inertia`
7
+ * (exported there as `INERTIA_DIAG_CHANNEL`). Hardcoded here — telescope must NOT
8
+ * import inertia at runtime. Keep this string byte-identical on both sides.
9
+ */
10
+ const INERTIA_CHANNEL = 'nestjs-inertia:render';
11
+ /**
12
+ * One-time guard so an unsupported producer version (`v !== 1`) is surfaced once
13
+ * per process instead of every render — distinct from ordinary non-Inertia noise,
14
+ * which stays silent.
15
+ */
16
+ let warnedUnsupportedVersion = false;
17
+ /**
18
+ * Records every Inertia response published on the `nestjs-inertia:render`
19
+ * diagnostics channel as one `inertia` entry, correlated to the active
20
+ * request/job batch.
21
+ *
22
+ * ## How it works
23
+ * On `register` the watcher subscribes a listener to the channel. `inertia`
24
+ * publishes synchronously inside `InertiaService.render()`, so the listener runs
25
+ * on the same call stack / async context as the render — `ctx.record(...)` lands
26
+ * in the request's ALS batch (no batch is opened here, no request-id plumbing).
27
+ * Subscribing also flips inertia's `channel.hasSubscribers` to `true`, which is
28
+ * what makes the producer start building + publishing payloads at all.
29
+ *
30
+ * ## Resilience
31
+ * Each message is structurally validated (`isInertiaDiagnostic`, `v === 1`);
32
+ * malformed or wrong-version payloads are dropped. All recording is wrapped so a
33
+ * telescope error can never break a render. Registration is idempotent.
34
+ */
35
+ export class InertiaWatcher {
36
+ type = EntryType.Inertia;
37
+ registered = false;
38
+ onMessage = null;
39
+ register(ctx) {
40
+ if (this.registered)
41
+ return;
42
+ this.registered = true;
43
+ const channel = diagnostics_channel.channel(INERTIA_CHANNEL);
44
+ this.onMessage = (msg) => this.safeRecord(ctx, msg);
45
+ channel.subscribe(this.onMessage);
46
+ }
47
+ /** Unsubscribe the listener. Safe to call when never registered. */
48
+ cleanup() {
49
+ if (this.onMessage) {
50
+ diagnostics_channel.channel(INERTIA_CHANNEL).unsubscribe(this.onMessage);
51
+ this.onMessage = null;
52
+ }
53
+ this.registered = false;
54
+ }
55
+ /** Validate + record, swallowing any failure so a render can never break. */
56
+ safeRecord(ctx, msg) {
57
+ try {
58
+ if (!isInertiaDiagnostic(msg)) {
59
+ // Inertia-shaped but an unsupported producer version: warn once so the
60
+ // drift is visible. Genuine non-Inertia messages fall through silently.
61
+ if (!warnedUnsupportedVersion && isInertiaShaped(msg)) {
62
+ warnedUnsupportedVersion = true;
63
+ console.warn(`InertiaWatcher: dropping unsupported diagnostic version v=${msg.v} (expected 1) — upgrade @dudousxd/nestjs-telescope to match @dudousxd/nestjs-inertia`);
64
+ }
65
+ return;
66
+ }
67
+ ctx.record(buildInertiaContent(msg));
68
+ }
69
+ catch (err) {
70
+ // NOT rethrown — telescope must never break an Inertia render.
71
+ console.error('InertiaWatcher: failed to record inertia render:', err);
72
+ }
73
+ }
74
+ }
75
+ //# sourceMappingURL=inertia.watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inertia.watcher.js","sourceRoot":"","sources":["../src/inertia.watcher.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,OAAO,mBAAmB,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAqC,MAAM,4BAA4B,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEjG;;;;GAIG;AACH,MAAM,eAAe,GAAG,uBAAuB,CAAC;AAEhD;;;;GAIG;AACH,IAAI,wBAAwB,GAAG,KAAK,CAAC;AAErC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,cAAc;IAChB,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC;IAC1B,UAAU,GAAG,KAAK,CAAC;IACnB,SAAS,GAAoC,IAAI,CAAC;IAE1D,QAAQ,CAAC,GAAmB;QAC1B,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QAEvB,MAAM,OAAO,GAAG,mBAAmB,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QAC7D,IAAI,CAAC,SAAS,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACpD,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACpC,CAAC;IAED,oEAAoE;IACpE,OAAO;QACL,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,mBAAmB,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACzE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;QACD,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;IAC1B,CAAC;IAED,6EAA6E;IACrE,UAAU,CAAC,GAAmB,EAAE,GAAY;QAClD,IAAI,CAAC;YACH,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC9B,uEAAuE;gBACvE,wEAAwE;gBACxE,IAAI,CAAC,wBAAwB,IAAI,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtD,wBAAwB,GAAG,IAAI,CAAC;oBAChC,OAAO,CAAC,IAAI,CACV,6DAA6D,GAAG,CAAC,CAAC,sFAAsF,CACzJ,CAAC;gBACJ,CAAC;gBACD,OAAO;YACT,CAAC;YACD,GAAG,CAAC,MAAM,CAAC,mBAAmB,CAAC,GAAgD,CAAC,CAAC,CAAC;QACpF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,+DAA+D;YAC/D,OAAO,CAAC,KAAK,CAAC,kDAAkD,EAAE,GAAG,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@dudousxd/nestjs-telescope-inertia-watcher",
3
+ "version": "1.7.0",
4
+ "description": "Inertia.js render watcher for @dudousxd/nestjs-telescope.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/DavideCarvalho/nestjs-telescope.git",
9
+ "directory": "packages/inertia-watcher"
10
+ },
11
+ "author": "Davi Carvalho <davi@goflip.ai>",
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist/",
24
+ "README.md",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "peerDependencies": {
28
+ "@dudousxd/nestjs-telescope": "^1.7.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0",
32
+ "reflect-metadata": "^0.2.2",
33
+ "typescript": "^5.4.0",
34
+ "vitest": "^3.0.0",
35
+ "@dudousxd/nestjs-telescope": "1.7.0"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "keywords": [
41
+ "nestjs",
42
+ "telescope",
43
+ "inertia",
44
+ "inertia.js",
45
+ "observability"
46
+ ],
47
+ "scripts": {
48
+ "build": "tsc -p tsconfig.json",
49
+ "test": "vitest run --passWithNoTests",
50
+ "test:watch": "vitest",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit"
52
+ }
53
+ }