react-render-pulse 0.0.0-stage → 1.0.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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -2
  3. package/dist/aggregate/aggregator.d.cts +27 -0
  4. package/dist/aggregate/aggregator.d.ts +28 -0
  5. package/dist/aggregate/aggregator.d.ts.map +1 -0
  6. package/dist/aggregate/histogram.d.cts +18 -0
  7. package/dist/aggregate/histogram.d.ts +19 -0
  8. package/dist/aggregate/histogram.d.ts.map +1 -0
  9. package/dist/components/RenderMonitor.d.cts +19 -0
  10. package/dist/components/RenderMonitor.d.ts +20 -0
  11. package/dist/components/RenderMonitor.d.ts.map +1 -0
  12. package/dist/context/device.d.cts +2 -0
  13. package/dist/context/device.d.ts +3 -0
  14. package/dist/context/device.d.ts.map +1 -0
  15. package/dist/context/route.d.cts +7 -0
  16. package/dist/context/route.d.ts +8 -0
  17. package/dist/context/route.d.ts.map +1 -0
  18. package/dist/core.cjs +56 -0
  19. package/dist/core.cjs.map +1 -0
  20. package/dist/core.d.cts +2 -0
  21. package/dist/core.d.ts +3 -0
  22. package/dist/core.d.ts.map +1 -0
  23. package/dist/core.js +28 -0
  24. package/dist/core.js.map +1 -0
  25. package/dist/export/console.d.cts +3 -0
  26. package/dist/export/console.d.ts +4 -0
  27. package/dist/export/console.d.ts.map +1 -0
  28. package/dist/export/otel.d.cts +34 -0
  29. package/dist/export/otel.d.ts +35 -0
  30. package/dist/export/otel.d.ts.map +1 -0
  31. package/dist/hooks/useRenderMonitor.d.cts +9 -0
  32. package/dist/hooks/useRenderMonitor.d.ts +10 -0
  33. package/dist/hooks/useRenderMonitor.d.ts.map +1 -0
  34. package/dist/index.cjs +634 -0
  35. package/dist/index.cjs.map +1 -0
  36. package/dist/index.d.cts +8 -0
  37. package/dist/index.d.ts +9 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +602 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/modes/detect.d.cts +16 -0
  42. package/dist/modes/detect.d.ts +17 -0
  43. package/dist/modes/detect.d.ts.map +1 -0
  44. package/dist/observe/longtasks.d.cts +12 -0
  45. package/dist/observe/longtasks.d.ts +13 -0
  46. package/dist/observe/longtasks.d.ts.map +1 -0
  47. package/dist/observe/renderWindows.d.cts +17 -0
  48. package/dist/observe/renderWindows.d.ts +18 -0
  49. package/dist/observe/renderWindows.d.ts.map +1 -0
  50. package/dist/otel.cjs +65 -0
  51. package/dist/otel.cjs.map +1 -0
  52. package/dist/otel.d.cts +2 -0
  53. package/dist/otel.d.ts +3 -0
  54. package/dist/otel.d.ts.map +1 -0
  55. package/dist/otel.js +38 -0
  56. package/dist/otel.js.map +1 -0
  57. package/dist/runtime/guard.d.cts +7 -0
  58. package/dist/runtime/guard.d.ts +8 -0
  59. package/dist/runtime/guard.d.ts.map +1 -0
  60. package/dist/runtime/initRenderPulse.d.cts +44 -0
  61. package/dist/runtime/initRenderPulse.d.ts +45 -0
  62. package/dist/runtime/initRenderPulse.d.ts.map +1 -0
  63. package/dist/runtime/runtime.d.cts +13 -0
  64. package/dist/runtime/runtime.d.ts +14 -0
  65. package/dist/runtime/runtime.d.ts.map +1 -0
  66. package/dist/runtime/useIsomorphicLayoutEffect.d.cts +2 -0
  67. package/dist/runtime/useIsomorphicLayoutEffect.d.ts +3 -0
  68. package/dist/runtime/useIsomorphicLayoutEffect.d.ts.map +1 -0
  69. package/dist/sample/sample.d.cts +7 -0
  70. package/dist/sample/sample.d.ts +8 -0
  71. package/dist/sample/sample.d.ts.map +1 -0
  72. package/dist/transport/payload.d.cts +8 -0
  73. package/dist/transport/payload.d.ts +9 -0
  74. package/dist/transport/payload.d.ts.map +1 -0
  75. package/dist/transport/transport.d.cts +18 -0
  76. package/dist/transport/transport.d.ts +19 -0
  77. package/dist/transport/transport.d.ts.map +1 -0
  78. package/dist/types.d.cts +120 -0
  79. package/dist/types.d.ts +121 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/package.json +128 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amal M
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 CHANGED
@@ -1,3 +1,98 @@
1
- # Temporary Holding Version
1
+ # react-render-pulse
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/react-render-pulse?color=cb3837&logo=npm)](https://www.npmjs.com/package/react-render-pulse)
4
+ [![CI](https://github.com/Amalmktk/react-render-pulse/actions/workflows/ci.yml/badge.svg)](https://github.com/Amalmktk/react-render-pulse/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/react-render-pulse)](https://github.com/Amalmktk/react-render-pulse/blob/main/LICENSE)
6
+
7
+ Real-user render performance monitoring for React, at the component level. Wrap the parts of your tree you care about and get render timings from real sessions: render counts, slow renders, p50/p75/p95 and the slowest renders per area and route, plus long tasks attributed to the area that was rendering.
8
+
9
+ Under 3.75 kB brotli. No runtime dependencies besides React 18 or 19.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install react-render-pulse
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Start it once, in the browser, before your app renders:
20
+
21
+ ```tsx
22
+ import { createRoot } from "react-dom/client";
23
+ import { initRenderPulse } from "react-render-pulse";
24
+ import { App } from "./App";
25
+
26
+ initRenderPulse({
27
+ endpoint: "/api/render-pulse", // leave out to log batches to the console
28
+ sampleRate: 0.1, // 10% of page loads
29
+ appVersion: import.meta.env.VITE_APP_VERSION,
30
+ });
31
+
32
+ createRoot(document.getElementById("root")!).render(<App />);
33
+ ```
34
+
35
+ Wrap areas you want timings for:
36
+
37
+ ```tsx
38
+ import { RenderMonitor, useRenderMonitor } from "react-render-pulse";
39
+
40
+ export function OrdersPage() {
41
+ return (
42
+ <RenderMonitor id="orders-table">
43
+ <OrdersTable />
44
+ </RenderMonitor>
45
+ );
46
+ }
47
+
48
+ function PriceChart() {
49
+ useRenderMonitor("price-chart"); // exact render count; duration is an upper bound
50
+ return <Chart />;
51
+ }
52
+ ```
53
+
54
+ `<RenderMonitor>` adds no DOM elements and never remounts its children. Errors inside the package never reach your app.
55
+
56
+ Batches are sent when the page is hidden and every 3 minutes while it's visible. Send them to [`react-render-pulse-collector`](https://www.npmjs.com/package/react-render-pulse-collector) and read them with [`react-render-pulse-viewer`](https://www.npmjs.com/package/react-render-pulse-viewer).
57
+
58
+ ## Key options
59
+
60
+ | Option | Default | Description |
61
+ | --- | --- | --- |
62
+ | `endpoint` | none | Where batches are `POST`ed. Without one, batches are logged to the console. |
63
+ | `sampleRate` | `1` | Fraction of page loads that collect anything. |
64
+ | `mode` | `"auto"` | `"lightweight"`, `"profiling"`, or `"auto"`: lightweight until React's profiling build is detected. |
65
+ | `slowRenderMs` | `16` | Renders slower than this count as slow. |
66
+ | `longTaskMs` | `50` | Shorter long tasks and long animation frames are ignored. |
67
+ | `appVersion` | none | Your app's version, for comparing releases. |
68
+ | `getRoute` | numeric path segments → `:id` | Returns the current route template, e.g. `/orders/:id`. |
69
+ | `exporters` | `[]` | Extra receivers, e.g. `createOtelExporter` from `react-render-pulse/otel`. |
70
+
71
+ All options: [client API reference](https://github.com/Amalmktk/react-render-pulse/blob/main/docs/api/client.md).
72
+
73
+ ## Modes
74
+
75
+ - **Lightweight** (any production build): times each area between two marker components. About 10–15 µs per measured render. Doesn't see updates that start inside the area.
76
+ - **Profiling** (React's profiling build): React's `<Profiler>` timings for every commit. Exact, but the profiling build itself costs more; use it for a sampled group.
77
+
78
+ Enable the profiling build by aliasing `react-dom/client` to `react-dom/profiling` (Vite) or with `next build --profile`. See [Modes](https://github.com/Amalmktk/react-render-pulse/blob/main/docs/guide/modes.md).
79
+
80
+ ## Entry points
81
+
82
+ | Import | Contents |
83
+ | --- | --- |
84
+ | `react-render-pulse` | `initRenderPulse`, `RenderMonitor`, `useRenderMonitor`, `createConsoleExporter`, types. Marked `"use client"`. |
85
+ | `react-render-pulse/core` | Types, `HISTOGRAM_BOUNDS`, `histogramPercentile`. No React; safe on servers. |
86
+ | `react-render-pulse/otel` | `createOtelExporter`, for OpenTelemetry metrics. |
87
+
88
+ ## Privacy
89
+
90
+ No user IDs, IP addresses, page text, input values or query strings. The session id is random per page load and never stored. Routes come from `location.pathname`; numeric segments are replaced by default, but pass a `getRoute` if paths can contain names, slugs or UUIDs. See [What we collect](https://github.com/Amalmktk/react-render-pulse/blob/main/docs/guide/what-we-collect.md).
91
+
92
+ ## Documentation
93
+
94
+ Full docs: https://github.com/Amalmktk/react-render-pulse/tree/main/docs
95
+
96
+ ## License
97
+
98
+ MIT © Amal M
@@ -0,0 +1,27 @@
1
+ import type { AreaReport, LongTaskEvent, LongTaskReport, RenderEvent, SessionSummary } from "../types.cjs";
2
+ export interface AggregatorOptions {
3
+ /** Renders slower than this count as slow. */
4
+ slowRenderMs: number;
5
+ /** Most distinct area/route pairs kept per batch; renders for new ones past this are dropped. */
6
+ maxAreas: number;
7
+ /** How many of the slowest renders to keep per area. */
8
+ worstCases: number;
9
+ /** `performance.now()` at session start, so worst cases report time since then. */
10
+ sessionStart: number;
11
+ }
12
+ export interface AggregatedBatch {
13
+ summary: SessionSummary;
14
+ areas: AreaReport[];
15
+ longTasks: LongTaskReport[];
16
+ longFrames: LongTaskReport[];
17
+ droppedAreas: number;
18
+ }
19
+ export interface Aggregator {
20
+ addRender: (event: RenderEvent) => void;
21
+ addLongTask: (event: LongTaskEvent) => void;
22
+ /** True when nothing was recorded since the last `drain()`. */
23
+ isEmpty: () => boolean;
24
+ /** Returns everything recorded since the last call, and starts a new batch. */
25
+ drain: () => AggregatedBatch;
26
+ }
27
+ export declare function createAggregator(options: AggregatorOptions): Aggregator;
@@ -0,0 +1,28 @@
1
+ import type { AreaReport, LongTaskEvent, LongTaskReport, RenderEvent, SessionSummary } from "../types.js";
2
+ export interface AggregatorOptions {
3
+ /** Renders slower than this count as slow. */
4
+ slowRenderMs: number;
5
+ /** Most distinct area/route pairs kept per batch; renders for new ones past this are dropped. */
6
+ maxAreas: number;
7
+ /** How many of the slowest renders to keep per area. */
8
+ worstCases: number;
9
+ /** `performance.now()` at session start, so worst cases report time since then. */
10
+ sessionStart: number;
11
+ }
12
+ export interface AggregatedBatch {
13
+ summary: SessionSummary;
14
+ areas: AreaReport[];
15
+ longTasks: LongTaskReport[];
16
+ longFrames: LongTaskReport[];
17
+ droppedAreas: number;
18
+ }
19
+ export interface Aggregator {
20
+ addRender: (event: RenderEvent) => void;
21
+ addLongTask: (event: LongTaskEvent) => void;
22
+ /** True when nothing was recorded since the last `drain()`. */
23
+ isEmpty: () => boolean;
24
+ /** Returns everything recorded since the last call, and starts a new batch. */
25
+ drain: () => AggregatedBatch;
26
+ }
27
+ export declare function createAggregator(options: AggregatorOptions): Aggregator;
28
+ //# sourceMappingURL=aggregator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregator.d.ts","sourceRoot":"","sources":["../../src/aggregate/aggregator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAa,MAAM,aAAa,CAAC;AAErH,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,iGAAiG;IACjG,QAAQ,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,WAAW,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC5C,+DAA+D;IAC/D,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,eAAe,CAAC;CAC9B;AAyBD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAoGvE"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Upper bounds (ms, inclusive) of the render-duration histogram buckets, plus
3
+ * an implicit overflow bucket. Part of payload v1: the collector merges
4
+ * histograms by index, so changing these needs a new payload version.
5
+ */
6
+ export declare const HISTOGRAM_BOUNDS: readonly [1, 2, 4, 8, 12, 16, 24, 33, 50, 75, 100, 150, 250, 500, 1000];
7
+ export declare function createHistogram(): number[];
8
+ export declare function bucketIndex(ms: number): number;
9
+ /**
10
+ * Estimates a percentile (0–100) from bucket counts, interpolating linearly
11
+ * inside the bucket. The observed `min` and `max` narrow the bucket's range
12
+ * (every value lies between them): without them, durations clustered at a
13
+ * bucket edge, like many renders of exactly 8 ms, would be estimated from
14
+ * the bucket's middle instead.
15
+ */
16
+ export declare function histogramPercentile(hist: readonly number[], p: number, max: number, min?: number): number;
17
+ /** Rounds to 0.1 ms: finer precision isn't meaningful and costs payload bytes. */
18
+ export declare function round(ms: number): number;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Upper bounds (ms, inclusive) of the render-duration histogram buckets, plus
3
+ * an implicit overflow bucket. Part of payload v1: the collector merges
4
+ * histograms by index, so changing these needs a new payload version.
5
+ */
6
+ export declare const HISTOGRAM_BOUNDS: readonly [1, 2, 4, 8, 12, 16, 24, 33, 50, 75, 100, 150, 250, 500, 1000];
7
+ export declare function createHistogram(): number[];
8
+ export declare function bucketIndex(ms: number): number;
9
+ /**
10
+ * Estimates a percentile (0–100) from bucket counts, interpolating linearly
11
+ * inside the bucket. The observed `min` and `max` narrow the bucket's range
12
+ * (every value lies between them): without them, durations clustered at a
13
+ * bucket edge, like many renders of exactly 8 ms, would be estimated from
14
+ * the bucket's middle instead.
15
+ */
16
+ export declare function histogramPercentile(hist: readonly number[], p: number, max: number, min?: number): number;
17
+ /** Rounds to 0.1 ms: finer precision isn't meaningful and costs payload bytes. */
18
+ export declare function round(ms: number): number;
19
+ //# sourceMappingURL=histogram.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"histogram.d.ts","sourceRoot":"","sources":["../../src/aggregate/histogram.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,YAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAU,CAAC;AAExG,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAK9C;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,MAAM,CAsBpG;AAED,kFAAkF;AAClF,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAExC"}
@@ -0,0 +1,19 @@
1
+ import { type ReactNode } from "react";
2
+ export interface RenderMonitorProps {
3
+ /** Names this area in reports, e.g. `"orders-table"`. Areas with the same id are reported together. */
4
+ id: string;
5
+ children?: ReactNode;
6
+ }
7
+ /**
8
+ * Measures the renders of its children and reports them under `id`.
9
+ *
10
+ * The tree it renders is the same in every mode and whether or not the
11
+ * session is sampled (a `<Profiler>` around two empty marker components), so
12
+ * switching modes or calling `initRenderPulse` late never remounts children.
13
+ * - Profiling mode: React's `<Profiler>` reports each commit's actual render time.
14
+ * - Lightweight mode: the time between the start and end markers rendering,
15
+ * which brackets the children's render. Updates that start below this area
16
+ * (a child's own state change) don't re-render the markers, so they aren't
17
+ * seen; profiling mode catches those.
18
+ */
19
+ export declare function RenderMonitor({ id, children }: RenderMonitorProps): ReactNode;
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from "react";
2
+ export interface RenderMonitorProps {
3
+ /** Names this area in reports, e.g. `"orders-table"`. Areas with the same id are reported together. */
4
+ id: string;
5
+ children?: ReactNode;
6
+ }
7
+ /**
8
+ * Measures the renders of its children and reports them under `id`.
9
+ *
10
+ * The tree it renders is the same in every mode and whether or not the
11
+ * session is sampled (a `<Profiler>` around two empty marker components), so
12
+ * switching modes or calling `initRenderPulse` late never remounts children.
13
+ * - Profiling mode: React's `<Profiler>` reports each commit's actual render time.
14
+ * - Lightweight mode: the time between the start and end markers rendering,
15
+ * which brackets the children's render. Updates that start below this area
16
+ * (a child's own state change) don't re-render the markers, so they aren't
17
+ * seen; profiling mode catches those.
18
+ */
19
+ export declare function RenderMonitor({ id, children }: RenderMonitorProps): ReactNode;
20
+ //# sourceMappingURL=RenderMonitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RenderMonitor.d.ts","sourceRoot":"","sources":["../../src/components/RenderMonitor.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAgE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAKrG,MAAM,WAAW,kBAAkB;IACjC,uGAAuG;IACvG,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAoBD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,kBAAkB,GAAG,SAAS,CAyB7E"}
@@ -0,0 +1,2 @@
1
+ import type { DeviceContext } from "../types.cjs";
2
+ export declare function getDeviceContext(): DeviceContext;
@@ -0,0 +1,3 @@
1
+ import type { DeviceContext } from "../types.js";
2
+ export declare function getDeviceContext(): DeviceContext;
3
+ //# sourceMappingURL=device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../src/context/device.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AASjD,wBAAgB,gBAAgB,IAAI,aAAa,CAShD"}
@@ -0,0 +1,7 @@
1
+ /** Default route reader: the path with numeric segments replaced, so `/orders/42` and `/orders/7` group together. */
2
+ export declare function defaultGetRoute(): string;
3
+ /**
4
+ * Wraps a route reader so it only runs when the URL changes, since it's
5
+ * called on every measured render.
6
+ */
7
+ export declare function createRouteReader(getRoute: () => string): () => string;
@@ -0,0 +1,8 @@
1
+ /** Default route reader: the path with numeric segments replaced, so `/orders/42` and `/orders/7` group together. */
2
+ export declare function defaultGetRoute(): string;
3
+ /**
4
+ * Wraps a route reader so it only runs when the URL changes, since it's
5
+ * called on every measured render.
6
+ */
7
+ export declare function createRouteReader(getRoute: () => string): () => string;
8
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/context/route.ts"],"names":[],"mappings":"AAAA,qHAAqH;AACrH,wBAAgB,eAAe,IAAI,MAAM,CAGxC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,MAAM,GAAG,MAAM,MAAM,CAYtE"}
package/dist/core.cjs ADDED
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/core.ts
21
+ var core_exports = {};
22
+ __export(core_exports, {
23
+ HISTOGRAM_BOUNDS: () => HISTOGRAM_BOUNDS,
24
+ histogramPercentile: () => histogramPercentile
25
+ });
26
+ module.exports = __toCommonJS(core_exports);
27
+
28
+ // src/aggregate/histogram.ts
29
+ var HISTOGRAM_BOUNDS = [1, 2, 4, 8, 12, 16, 24, 33, 50, 75, 100, 150, 250, 500, 1e3];
30
+ function histogramPercentile(hist, p, max, min = 0) {
31
+ const total = hist.reduce((sum, n) => sum + n, 0);
32
+ if (total === 0) return 0;
33
+ const target = Math.max(p / 100 * total, Number.MIN_VALUE);
34
+ let seen = 0;
35
+ for (let i = 0; i < hist.length; i++) {
36
+ const count = hist[i] ?? 0;
37
+ if (count === 0) continue;
38
+ if (seen + count >= target) {
39
+ const upper = Math.min(HISTOGRAM_BOUNDS[i] ?? max, max);
40
+ const lower = Math.min(Math.max(i === 0 ? 0 : HISTOGRAM_BOUNDS[i - 1] ?? 0, min), upper);
41
+ const fraction = (target - seen) / count;
42
+ return round(Math.min(lower + (upper - lower) * fraction, max));
43
+ }
44
+ seen += count;
45
+ }
46
+ return round(max);
47
+ }
48
+ function round(ms) {
49
+ return Math.round(ms * 10) / 10;
50
+ }
51
+ // Annotate the CommonJS export names for ESM import in node:
52
+ 0 && (module.exports = {
53
+ HISTOGRAM_BOUNDS,
54
+ histogramPercentile
55
+ });
56
+ //# sourceMappingURL=core.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core.ts","../src/aggregate/histogram.ts"],"sourcesContent":["// Framework-agnostic entry: no React imports, so it's safe in Server Components,\n// route handlers, and plain Node. Everything here is also exported from the main entry.\nexport type {\n AreaReport,\n DeviceContext,\n Exporter,\n LongTaskEvent,\n LongTaskKind,\n LongTaskReport,\n Mode,\n ModeOption,\n Payload,\n RenderEvent,\n RenderPhase,\n SessionInfo,\n SessionSummary,\n WorstCase,\n} from \"./types.js\";\nexport { HISTOGRAM_BOUNDS, histogramPercentile } from \"./aggregate/histogram.js\";\n","/**\n * Upper bounds (ms, inclusive) of the render-duration histogram buckets, plus\n * an implicit overflow bucket. Part of payload v1: the collector merges\n * histograms by index, so changing these needs a new payload version.\n */\nexport const HISTOGRAM_BOUNDS = [1, 2, 4, 8, 12, 16, 24, 33, 50, 75, 100, 150, 250, 500, 1000] as const;\n\nexport function createHistogram(): number[] {\n return new Array<number>(HISTOGRAM_BOUNDS.length + 1).fill(0);\n}\n\nexport function bucketIndex(ms: number): number {\n for (let i = 0; i < HISTOGRAM_BOUNDS.length; i++) {\n if (ms <= (HISTOGRAM_BOUNDS[i] ?? 0)) return i;\n }\n return HISTOGRAM_BOUNDS.length;\n}\n\n/**\n * Estimates a percentile (0–100) from bucket counts, interpolating linearly\n * inside the bucket. The observed `min` and `max` narrow the bucket's range\n * (every value lies between them): without them, durations clustered at a\n * bucket edge, like many renders of exactly 8 ms, would be estimated from\n * the bucket's middle instead.\n */\nexport function histogramPercentile(hist: readonly number[], p: number, max: number, min = 0): number {\n const total = hist.reduce((sum, n) => sum + n, 0);\n if (total === 0) return 0;\n\n const target = Math.max((p / 100) * total, Number.MIN_VALUE);\n let seen = 0;\n\n for (let i = 0; i < hist.length; i++) {\n const count = hist[i] ?? 0;\n if (count === 0) continue;\n\n if (seen + count >= target) {\n const upper = Math.min(HISTOGRAM_BOUNDS[i] ?? max, max);\n const lower = Math.min(Math.max(i === 0 ? 0 : (HISTOGRAM_BOUNDS[i - 1] ?? 0), min), upper);\n const fraction = (target - seen) / count;\n return round(Math.min(lower + (upper - lower) * fraction, max));\n }\n\n seen += count;\n }\n\n return round(max);\n}\n\n/** Rounds to 0.1 ms: finer precision isn't meaningful and costs payload bytes. */\nexport function round(ms: number): number {\n return Math.round(ms * 10) / 10;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,mBAAmB,CAAC,GAAG,GAAG,GAAG,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,GAAI;AAoBtF,SAAS,oBAAoB,MAAyB,GAAW,KAAa,MAAM,GAAW;AACpG,QAAM,QAAQ,KAAK,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,CAAC;AAChD,MAAI,UAAU,EAAG,QAAO;AAExB,QAAM,SAAS,KAAK,IAAK,IAAI,MAAO,OAAO,OAAO,SAAS;AAC3D,MAAI,OAAO;AAEX,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,QAAQ,KAAK,CAAC,KAAK;AACzB,QAAI,UAAU,EAAG;AAEjB,QAAI,OAAO,SAAS,QAAQ;AAC1B,YAAM,QAAQ,KAAK,IAAI,iBAAiB,CAAC,KAAK,KAAK,GAAG;AACtD,YAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,MAAM,IAAI,IAAK,iBAAiB,IAAI,CAAC,KAAK,GAAI,GAAG,GAAG,KAAK;AACzF,YAAM,YAAY,SAAS,QAAQ;AACnC,aAAO,MAAM,KAAK,IAAI,SAAS,QAAQ,SAAS,UAAU,GAAG,CAAC;AAAA,IAChE;AAEA,YAAQ;AAAA,EACV;AAEA,SAAO,MAAM,GAAG;AAClB;AAGO,SAAS,MAAM,IAAoB;AACxC,SAAO,KAAK,MAAM,KAAK,EAAE,IAAI;AAC/B;","names":[]}
@@ -0,0 +1,2 @@
1
+ export type { AreaReport, DeviceContext, Exporter, LongTaskEvent, LongTaskKind, LongTaskReport, Mode, ModeOption, Payload, RenderEvent, RenderPhase, SessionInfo, SessionSummary, WorstCase, } from "./types.cjs";
2
+ export { HISTOGRAM_BOUNDS, histogramPercentile } from "./aggregate/histogram.cjs";
package/dist/core.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type { AreaReport, DeviceContext, Exporter, LongTaskEvent, LongTaskKind, LongTaskReport, Mode, ModeOption, Payload, RenderEvent, RenderPhase, SessionInfo, SessionSummary, WorstCase, } from "./types.js";
2
+ export { HISTOGRAM_BOUNDS, histogramPercentile } from "./aggregate/histogram.js";
3
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,UAAU,EACV,aAAa,EACb,QAAQ,EACR,aAAa,EACb,YAAY,EACZ,cAAc,EACd,IAAI,EACJ,UAAU,EACV,OAAO,EACP,WAAW,EACX,WAAW,EACX,WAAW,EACX,cAAc,EACd,SAAS,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC"}
package/dist/core.js ADDED
@@ -0,0 +1,28 @@
1
+ // src/aggregate/histogram.ts
2
+ var HISTOGRAM_BOUNDS = [1, 2, 4, 8, 12, 16, 24, 33, 50, 75, 100, 150, 250, 500, 1e3];
3
+ function histogramPercentile(hist, p, max, min = 0) {
4
+ const total = hist.reduce((sum, n) => sum + n, 0);
5
+ if (total === 0) return 0;
6
+ const target = Math.max(p / 100 * total, Number.MIN_VALUE);
7
+ let seen = 0;
8
+ for (let i = 0; i < hist.length; i++) {
9
+ const count = hist[i] ?? 0;
10
+ if (count === 0) continue;
11
+ if (seen + count >= target) {
12
+ const upper = Math.min(HISTOGRAM_BOUNDS[i] ?? max, max);
13
+ const lower = Math.min(Math.max(i === 0 ? 0 : HISTOGRAM_BOUNDS[i - 1] ?? 0, min), upper);
14
+ const fraction = (target - seen) / count;
15
+ return round(Math.min(lower + (upper - lower) * fraction, max));
16
+ }
17
+ seen += count;
18
+ }
19
+ return round(max);
20
+ }
21
+ function round(ms) {
22
+ return Math.round(ms * 10) / 10;
23
+ }
24
+ export {
25
+ HISTOGRAM_BOUNDS,
26
+ histogramPercentile
27
+ };
28
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/aggregate/histogram.ts"],"sourcesContent":["/**\n * Upper bounds (ms, inclusive) of the render-duration histogram buckets, plus\n * an implicit overflow bucket. Part of payload v1: the collector merges\n * histograms by index, so changing these needs a new payload version.\n */\nexport const HISTOGRAM_BOUNDS = [1, 2, 4, 8, 12, 16, 24, 33, 50, 75, 100, 150, 250, 500, 1000] as const;\n\nexport function createHistogram(): number[] {\n return new Array<number>(HISTOGRAM_BOUNDS.length + 1).fill(0);\n}\n\nexport function bucketIndex(ms: number): number {\n for (let i = 0; i < HISTOGRAM_BOUNDS.length; i++) {\n if (ms <= (HISTOGRAM_BOUNDS[i] ?? 0)) return i;\n }\n return HISTOGRAM_BOUNDS.length;\n}\n\n/**\n * Estimates a percentile (0–100) from bucket counts, interpolating linearly\n * inside the bucket. The observed `min` and `max` narrow the bucket's range\n * (every value lies between them): without them, durations clustered at a\n * bucket edge, like many renders of exactly 8 ms, would be estimated from\n * the bucket's middle instead.\n */\nexport function histogramPercentile(hist: readonly number[], p: number, max: number, min = 0): number {\n const total = hist.reduce((sum, n) => sum + n, 0);\n if (total === 0) return 0;\n\n const target = Math.max((p / 100) * total, Number.MIN_VALUE);\n let seen = 0;\n\n for (let i = 0; i < hist.length; i++) {\n const count = hist[i] ?? 0;\n if (count === 0) continue;\n\n if (seen + count >= target) {\n const upper = Math.min(HISTOGRAM_BOUNDS[i] ?? max, max);\n const lower = Math.min(Math.max(i === 0 ? 0 : (HISTOGRAM_BOUNDS[i - 1] ?? 0), min), upper);\n const fraction = (target - seen) / count;\n return round(Math.min(lower + (upper - lower) * fraction, max));\n }\n\n seen += count;\n }\n\n return round(max);\n}\n\n/** Rounds to 0.1 ms: finer precision isn't meaningful and costs payload bytes. */\nexport function round(ms: number): number {\n return Math.round(ms * 10) / 10;\n}\n"],"mappings":";AAKO,IAAM,mBAAmB,CAAC,GAAG,GAAG,GAAG,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,GAAI;AAoBtF,SAAS,oBAAoB,MAAyB,GAAW,KAAa,MAAM,GAAW;AACpG,QAAM,QAAQ,KAAK,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,CAAC;AAChD,MAAI,UAAU,EAAG,QAAO;AAExB,QAAM,SAAS,KAAK,IAAK,IAAI,MAAO,OAAO,OAAO,SAAS;AAC3D,MAAI,OAAO;AAEX,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,QAAQ,KAAK,CAAC,KAAK;AACzB,QAAI,UAAU,EAAG;AAEjB,QAAI,OAAO,SAAS,QAAQ;AAC1B,YAAM,QAAQ,KAAK,IAAI,iBAAiB,CAAC,KAAK,KAAK,GAAG;AACtD,YAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,MAAM,IAAI,IAAK,iBAAiB,IAAI,CAAC,KAAK,GAAI,GAAG,GAAG,KAAK;AACzF,YAAM,YAAY,SAAS,QAAQ;AACnC,aAAO,MAAM,KAAK,IAAI,SAAS,QAAQ,SAAS,UAAU,GAAG,CAAC;AAAA,IAChE;AAEA,YAAQ;AAAA,EACV;AAEA,SAAO,MAAM,GAAG;AAClB;AAGO,SAAS,MAAM,IAAoB;AACxC,SAAO,KAAK,MAAM,KAAK,EAAE,IAAI;AAC/B;","names":[]}
@@ -0,0 +1,3 @@
1
+ import type { Exporter } from "../types.cjs";
2
+ /** Logs each batch as a table, slowest areas first. Used by default when no `endpoint` is set, for local testing. */
3
+ export declare function createConsoleExporter(): Exporter;
@@ -0,0 +1,4 @@
1
+ import type { Exporter } from "../types.js";
2
+ /** Logs each batch as a table, slowest areas first. Used by default when no `endpoint` is set, for local testing. */
3
+ export declare function createConsoleExporter(): Exporter;
4
+ //# sourceMappingURL=console.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../src/export/console.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,qHAAqH;AACrH,wBAAgB,qBAAqB,IAAI,QAAQ,CA2BhD"}
@@ -0,0 +1,34 @@
1
+ import type { Exporter } from "../types.cjs";
2
+ type Attributes = Record<string, string | number | boolean>;
3
+ /**
4
+ * The parts of an OpenTelemetry `Meter` this exporter uses. Structurally
5
+ * compatible with `@opentelemetry/api`'s `Meter`, so there's no dependency on it.
6
+ */
7
+ export interface OtelMeterLike {
8
+ createHistogram: (name: string, options?: {
9
+ description?: string;
10
+ unit?: string;
11
+ }) => {
12
+ record: (value: number, attributes?: Attributes) => void;
13
+ };
14
+ createCounter: (name: string, options?: {
15
+ description?: string;
16
+ unit?: string;
17
+ }) => {
18
+ add: (value: number, attributes?: Attributes) => void;
19
+ };
20
+ }
21
+ export interface OtelExporterOptions {
22
+ /** e.g. `metrics.getMeter("my-app")` from `@opentelemetry/api`. */
23
+ meter: OtelMeterLike;
24
+ /** Renders slower than this also increment `render_pulse.render.slow`. Defaults to 16. */
25
+ slowRenderMs?: number | undefined;
26
+ }
27
+ /**
28
+ * Records every measurement as OpenTelemetry metrics:
29
+ * - `render_pulse.render.duration` (histogram, ms) with `render_pulse.area`, `render_pulse.route`, `render_pulse.phase`, `render_pulse.mode`
30
+ * - `render_pulse.render.slow` (counter) with the same attributes
31
+ * - `render_pulse.longtask.duration` (histogram, ms) with `render_pulse.area`, `render_pulse.route`, `render_pulse.kind`
32
+ */
33
+ export declare function createOtelExporter(options: OtelExporterOptions): Exporter;
34
+ export {};
@@ -0,0 +1,35 @@
1
+ import type { Exporter } from "../types.js";
2
+ type Attributes = Record<string, string | number | boolean>;
3
+ /**
4
+ * The parts of an OpenTelemetry `Meter` this exporter uses. Structurally
5
+ * compatible with `@opentelemetry/api`'s `Meter`, so there's no dependency on it.
6
+ */
7
+ export interface OtelMeterLike {
8
+ createHistogram: (name: string, options?: {
9
+ description?: string;
10
+ unit?: string;
11
+ }) => {
12
+ record: (value: number, attributes?: Attributes) => void;
13
+ };
14
+ createCounter: (name: string, options?: {
15
+ description?: string;
16
+ unit?: string;
17
+ }) => {
18
+ add: (value: number, attributes?: Attributes) => void;
19
+ };
20
+ }
21
+ export interface OtelExporterOptions {
22
+ /** e.g. `metrics.getMeter("my-app")` from `@opentelemetry/api`. */
23
+ meter: OtelMeterLike;
24
+ /** Renders slower than this also increment `render_pulse.render.slow`. Defaults to 16. */
25
+ slowRenderMs?: number | undefined;
26
+ }
27
+ /**
28
+ * Records every measurement as OpenTelemetry metrics:
29
+ * - `render_pulse.render.duration` (histogram, ms) with `render_pulse.area`, `render_pulse.route`, `render_pulse.phase`, `render_pulse.mode`
30
+ * - `render_pulse.render.slow` (counter) with the same attributes
31
+ * - `render_pulse.longtask.duration` (histogram, ms) with `render_pulse.area`, `render_pulse.route`, `render_pulse.kind`
32
+ */
33
+ export declare function createOtelExporter(options: OtelExporterOptions): Exporter;
34
+ export {};
35
+ //# sourceMappingURL=otel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otel.d.ts","sourceRoot":"","sources":["../../src/export/otel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAE5D;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,CACf,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAC9C;QAAE,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,KAAK,IAAI,CAAA;KAAE,CAAC;IAClE,aAAa,EAAE,CACb,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAC9C;QAAE,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,KAAK,IAAI,CAAA;KAAE,CAAC;CAChE;AAED,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,KAAK,EAAE,aAAa,CAAC;IACrB,0FAA0F;IAC1F,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,QAAQ,CAkCzE"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tracks the renders of the component that calls it, reported under `id`.
3
+ *
4
+ * Render counts are exact. The duration runs from the hook's call to the
5
+ * commit, so it also includes anything React renders after this component
6
+ * in the same update: an upper bound. Wrap the component in `<RenderMonitor>`
7
+ * when you need its own render time.
8
+ */
9
+ export declare function useRenderMonitor(id: string): void;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Tracks the renders of the component that calls it, reported under `id`.
3
+ *
4
+ * Render counts are exact. The duration runs from the hook's call to the
5
+ * commit, so it also includes anything React renders after this component
6
+ * in the same update: an upper bound. Wrap the component in `<RenderMonitor>`
7
+ * when you need its own render time.
8
+ */
9
+ export declare function useRenderMonitor(id: string): void;
10
+ //# sourceMappingURL=useRenderMonitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRenderMonitor.d.ts","sourceRoot":"","sources":["../../src/hooks/useRenderMonitor.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAgBjD"}