@flareapp/react 2.7.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.
package/README.md CHANGED
@@ -82,7 +82,59 @@ at [flareapp.io/docs/react/general/installation](https://flareapp.io/docs/react/
82
82
 
83
83
  - React 16, 17, 18, 19
84
84
  - `flareReactErrorHandler` requires React 19+
85
+ - `withFlareProfiler` forwards `ref` on React 19 only
85
86
 
86
87
  ## License
87
88
 
88
89
  The MIT License (MIT). Please see [License File](../../LICENSE.md) for more information.
90
+
91
+ ## Component profiler (`@flareapp/react/profiler`)
92
+
93
+ Opt-in mount profiling: wrap a component to record a `browser_component` span for
94
+ its mount, nested under the active page-load / navigation trace. Requires tracing to be
95
+ enabled (`enableTracing: true`).
96
+
97
+ ```tsx
98
+ import { FlareProfiler, withFlareProfiler } from '@flareapp/react/profiler';
99
+
100
+ // Wrap at the definition:
101
+ export default withFlareProfiler(ProductPage);
102
+
103
+ // Or wrap an inline subtree:
104
+ <FlareProfiler name="Gallery">
105
+ <ProductGallery />
106
+ </FlareProfiler>;
107
+ ```
108
+
109
+ Spans nest into a tree: a profiled child nests under its nearest profiled ancestor;
110
+ unprofiled components in between are transparent. A component with no active trace
111
+ (tracing off, or no page-load/navigation root) records nothing and renders normally.
112
+
113
+ A component that mounts later inside an already-mounted profiled ancestor still nests
114
+ under that ancestor, whose own span closed when it finished mounting. The tree is
115
+ correct, but the waterfall shows the child starting after its parent ended. A page body
116
+ swapped inside a persistent layout is the usual way to see this.
117
+
118
+ **Import the main entry somewhere too.** `@flareapp/react/profiler` is deliberately dependency-free, so
119
+ it does not register React as the framework. That identity (`flare.framework.name`) is what tells Flare a
120
+ component span came from React, and importing `@flareapp/react` anywhere in the app sets it. An app that
121
+ uses only the `/profiler` entry reports `js` instead, and its component spans are attributed to plain
122
+ JavaScript rather than React.
123
+
124
+ **Naming:** the span name is `name` (prop or `withFlareProfiler(Component, { name })`),
125
+ then `Component.displayName`, then `Component.name`. Minified production builds can
126
+ mangle `Component.name`, so pass an explicit `name` or set `displayName` for production.
127
+
128
+ **Suspense (v1 limitation):** a `<Suspense>` boundary inside a profiled subtree can end a
129
+ parent span before a suspended child resumes, so the child may appear outside its parent
130
+ in the waterfall, and its duration includes the data wait. If the wait outlasts the
131
+ trace's idle window the child span is dropped rather than attached to a closed trace.
132
+
133
+ **Render-phase start:** the span starts when the component first renders, not when React commits it. A
134
+ render React defers or discards (`useDeferredValue`, an interrupted transition, an `<Activity mode="hidden">`
135
+ prerender revealed later) bills that whole gap to the component.
136
+
137
+ **Refs and statics:** the wrapper is a plain function component. It forwards no `ref` through `forwardRef`
138
+ and hoists no statics. On React 19 `ref` is a normal prop and passes straight through, so this only affects
139
+ the React 16-18 half of the peer range; there, wrap with `FlareProfiler` inside the component instead of
140
+ applying `withFlareProfiler` to it.
@@ -1,58 +1,49 @@
1
+ import { FrameworkName, convertToError, createIdentityTagger, safeDecode, toCustomContext } from "@flareapp/core";
1
2
  import * as React from "react";
2
3
  import { Component, version } from "react";
3
- import { convertToError } from "@flareapp/core";
4
+ import { createFlareResolver } from "@flareapp/js/browser";
4
5
 
5
6
  //#region src/constants.ts
7
+ /**
8
+ * Chrome: `at ComponentName (http://localhost:5173/src/App.tsx:12:9)`; no source: `at div`.
9
+ */
6
10
  const CHROMIUM_STACK_REGEX = /^at\s+(\S+)(?:\s+\((.+):(\d+):(\d+)\))?$/;
11
+ /**
12
+ * Firefox/Safari: `ComponentName@http://localhost:5173/src/App.tsx:12:9`; no source: `div`.
13
+ */
7
14
  const FIREFOX_SAFARI_STACK_REGEX = /^(\S+?)@(.+):(\d+):(\d+)$/;
8
- const PACKAGE_VERSION = typeof process !== "undefined" && true ? "2.7.0" : "?";
15
+ /**
16
+ * React 16/17/18 synthetic component stack: `in ComponentName (at App.jsx:10)`; with an optional
17
+ * column `in ComponentName (at App.jsx:10:5)`; no source: `in ComponentName`. These versions usually
18
+ * emit a line only, so the column capture group is optional. The file capture is lazy so the trailing
19
+ * `:line(:column)` binds to the numeric tail even when the file path itself contains colons. Without
20
+ * `__source` React names the owner instead (`in App (created by Root)`), so that suffix is matched and
21
+ * discarded; the group is greedy because an owner name can carry brackets of its own (`Connect(Form)`).
22
+ */
23
+ const REACT_LEGACY_STACK_REGEX = /^in\s+(\S+)(?:\s+\(at\s+(.+?):(\d+)(?::(\d+))?\))?(?:\s+\(created by\s+.+\))?$/;
24
+ /** Injected at build time via tsdown --env.PACKAGE_VERSION (reads package.json version). */
25
+ const PACKAGE_VERSION = typeof process !== "undefined" && true ? "2.9.0" : "?";
9
26
 
10
27
  //#endregion
11
28
  //#region src/identify.ts
12
- const sdkTagged = /* @__PURE__ */ new WeakSet();
13
- const frameworkTagged = /* @__PURE__ */ new WeakSet();
29
+ const tagger = createIdentityTagger({
30
+ sdkName: "@flareapp/react",
31
+ sdkVersion: PACKAGE_VERSION,
32
+ frameworkName: FrameworkName.React
33
+ });
34
+ /** Web path: full identity on the default singleton (sdk + framework). */
14
35
  function registerReactSdkIdentity(flare) {
15
- if (!sdkTagged.has(flare)) {
16
- sdkTagged.add(flare);
17
- flare.setSdkInfo({
18
- name: "@flareapp/react",
19
- version: PACKAGE_VERSION
20
- });
21
- }
22
- tagReactFramework(flare);
36
+ tagger.registerSdkIdentity(flare);
37
+ tagger.tagFramework(flare, React.version);
23
38
  }
39
+ /** Injected path: framework tag only, never sdkInfo (would clobber the injected SDK name). */
24
40
  function tagReactFramework(flare) {
25
- if (frameworkTagged.has(flare)) return;
26
- frameworkTagged.add(flare);
27
- flare.setFramework({
28
- name: "React",
29
- version: React.version
30
- });
41
+ tagger.tagFramework(flare, React.version);
31
42
  }
32
43
 
33
44
  //#endregion
34
45
  //#region src/resolveFlare.ts
35
- let defaultProvider = null;
36
- function isDevMode() {
37
- try {
38
- return process.env.NODE_ENV !== "production";
39
- } catch {
40
- return false;
41
- }
42
- }
43
- function registerDefaultFlare(provider) {
44
- if (typeof window !== "undefined" && window.__flare) {
45
- const message = "[flare] @flareapp/react (web root) was imported in a renderer where the Electron bridge is present, pulling the keyed @flareapp/js singleton into the renderer. Import @flareapp/react/inject and pass the @flareapp/electron/renderer instance instead.";
46
- if (isDevMode()) throw new Error(message);
47
- console.warn(message);
48
- }
49
- defaultProvider = provider;
50
- }
51
- function resolveFlare(explicit) {
52
- if (explicit) return explicit;
53
- if (defaultProvider) return defaultProvider();
54
- throw new Error("[flare] No Flare instance available. Pass `flare` (e.g. from @flareapp/electron/renderer), or import @flareapp/react (the package root) to use the @flareapp/js default singleton.");
55
- }
46
+ const { registerDefaultFlare, resolveFlare } = createFlareResolver({ packageName: "@flareapp/react" });
56
47
 
57
48
  //#endregion
58
49
  //#region src/formatComponentStack.ts
@@ -62,6 +53,12 @@ function formatComponentStack(stack) {
62
53
 
63
54
  //#endregion
64
55
  //#region src/parseComponentStack.ts
56
+ /**
57
+ * Parse React's newline-separated `errorInfo.componentStack` into structured frames so the Flare UI
58
+ * can render file/line links. The line format is browser-native in React 19 (Chromium / Firefox /
59
+ * Safari shapes) and React-synthetic in 16-18 (`in X (at File:line)`). Unrecognised lines fall back
60
+ * to component-name-only rather than being dropped.
61
+ */
65
62
  function parseComponentStack(stack) {
66
63
  return stack.split(/\s*\n\s*/g).filter((line) => line.length > 0).map((line) => {
67
64
  const chromeMatch = line.match(CHROMIUM_STACK_REGEX);
@@ -78,6 +75,13 @@ function parseComponentStack(stack) {
78
75
  line: Number(firefoxSafariMatch[3]),
79
76
  column: Number(firefoxSafariMatch[4])
80
77
  };
78
+ const reactLegacyMatch = line.match(REACT_LEGACY_STACK_REGEX);
79
+ if (reactLegacyMatch) return {
80
+ component: reactLegacyMatch[1],
81
+ file: reactLegacyMatch[2] ?? null,
82
+ line: reactLegacyMatch[3] ? Number(reactLegacyMatch[3]) : null,
83
+ column: reactLegacyMatch[4] ? Number(reactLegacyMatch[4]) : null
84
+ };
81
85
  return {
82
86
  component: line.replace(/^at\s+/, ""),
83
87
  file: null,
@@ -101,11 +105,11 @@ function buildReactContext(rawStack) {
101
105
  //#region src/contextToAttributes.ts
102
106
  function contextToAttributes(context, minifiedError) {
103
107
  return {
104
- "context.custom": { react: {
108
+ ...toCustomContext("react", {
105
109
  componentStack: context.react.componentStack,
106
110
  componentStackFrames: context.react.componentStackFrames,
107
111
  ...context.react.version ? { version: context.react.version } : {}
108
- } },
112
+ }),
109
113
  ...minifiedError ? { "flare.exception.react_minified_error": {
110
114
  number: minifiedError.number,
111
115
  args: minifiedError.args,
@@ -120,13 +124,6 @@ function contextToAttributes(context, minifiedError) {
120
124
  const NUMBER_PATTERN = /Minified React error #(\d+)/;
121
125
  const ARG_PATTERN = /args\[\]=([^&\s]*)/g;
122
126
  const URL_PATTERN = /(https?:\/\/\S+)/;
123
- function safeDecode(value) {
124
- try {
125
- return decodeURIComponent(value);
126
- } catch {
127
- return value;
128
- }
129
- }
130
127
  function parseMinifiedReactError(error) {
131
128
  const message = error?.message;
132
129
  if (!message) return null;
@@ -210,6 +207,10 @@ var FlareErrorBoundary = class extends Component {
210
207
 
211
208
  //#endregion
212
209
  //#region src/flareReactErrorHandler.ts
210
+ /**
211
+ * Callback shaped to match react-error-boundary's `onError` prop, so consumers using that library
212
+ * can report to Flare without our own boundary.
213
+ */
213
214
  function flareReactErrorHandler(options) {
214
215
  const flare = resolveFlare(options?.flare);
215
216
  tagReactFramework(flare);
@@ -89,6 +89,10 @@ type FlareReactErrorHandlerOptions = {
89
89
  context: FlareReactContext;
90
90
  }) => void;
91
91
  };
92
+ /**
93
+ * Callback shaped to match react-error-boundary's `onError` prop, so consumers using that library
94
+ * can report to Flare without our own boundary.
95
+ */
92
96
  declare function flareReactErrorHandler(options?: FlareReactErrorHandlerOptions): FlareReactErrorHandlerCallback;
93
97
  //#endregion
94
98
  export { FlareErrorBoundaryFallbackProps as a, FlareReactContext as c, FlareErrorBoundary as i, MinifiedReactError as l, FlareReactErrorHandlerOptions as n, FlareErrorBoundaryProps as o, flareReactErrorHandler as r, ComponentStackFrame as s, FlareReactErrorHandlerCallback as t };
@@ -89,6 +89,10 @@ type FlareReactErrorHandlerOptions = {
89
89
  context: FlareReactContext;
90
90
  }) => void;
91
91
  };
92
+ /**
93
+ * Callback shaped to match react-error-boundary's `onError` prop, so consumers using that library
94
+ * can report to Flare without our own boundary.
95
+ */
92
96
  declare function flareReactErrorHandler(options?: FlareReactErrorHandlerOptions): FlareReactErrorHandlerCallback;
93
97
  //#endregion
94
98
  export { FlareErrorBoundaryFallbackProps as a, FlareReactContext as c, FlareErrorBoundary as i, MinifiedReactError as l, FlareReactErrorHandlerOptions as n, FlareErrorBoundaryProps as o, flareReactErrorHandler as r, ComponentStackFrame as s, FlareReactErrorHandlerCallback as t };
@@ -25,61 +25,52 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  }) : target, mod));
26
26
 
27
27
  //#endregion
28
+ let _flareapp_core = require("@flareapp/core");
28
29
  let react = require("react");
29
30
  react = __toESM(react);
30
- let _flareapp_core = require("@flareapp/core");
31
+ let _flareapp_js_browser = require("@flareapp/js/browser");
31
32
 
32
33
  //#region src/constants.ts
34
+ /**
35
+ * Chrome: `at ComponentName (http://localhost:5173/src/App.tsx:12:9)`; no source: `at div`.
36
+ */
33
37
  const CHROMIUM_STACK_REGEX = /^at\s+(\S+)(?:\s+\((.+):(\d+):(\d+)\))?$/;
38
+ /**
39
+ * Firefox/Safari: `ComponentName@http://localhost:5173/src/App.tsx:12:9`; no source: `div`.
40
+ */
34
41
  const FIREFOX_SAFARI_STACK_REGEX = /^(\S+?)@(.+):(\d+):(\d+)$/;
35
- const PACKAGE_VERSION = typeof process !== "undefined" && true ? "2.7.0" : "?";
42
+ /**
43
+ * React 16/17/18 synthetic component stack: `in ComponentName (at App.jsx:10)`; with an optional
44
+ * column `in ComponentName (at App.jsx:10:5)`; no source: `in ComponentName`. These versions usually
45
+ * emit a line only, so the column capture group is optional. The file capture is lazy so the trailing
46
+ * `:line(:column)` binds to the numeric tail even when the file path itself contains colons. Without
47
+ * `__source` React names the owner instead (`in App (created by Root)`), so that suffix is matched and
48
+ * discarded; the group is greedy because an owner name can carry brackets of its own (`Connect(Form)`).
49
+ */
50
+ const REACT_LEGACY_STACK_REGEX = /^in\s+(\S+)(?:\s+\(at\s+(.+?):(\d+)(?::(\d+))?\))?(?:\s+\(created by\s+.+\))?$/;
51
+ /** Injected at build time via tsdown --env.PACKAGE_VERSION (reads package.json version). */
52
+ const PACKAGE_VERSION = typeof process !== "undefined" && true ? "2.9.0" : "?";
36
53
 
37
54
  //#endregion
38
55
  //#region src/identify.ts
39
- const sdkTagged = /* @__PURE__ */ new WeakSet();
40
- const frameworkTagged = /* @__PURE__ */ new WeakSet();
56
+ const tagger = (0, _flareapp_core.createIdentityTagger)({
57
+ sdkName: "@flareapp/react",
58
+ sdkVersion: PACKAGE_VERSION,
59
+ frameworkName: _flareapp_core.FrameworkName.React
60
+ });
61
+ /** Web path: full identity on the default singleton (sdk + framework). */
41
62
  function registerReactSdkIdentity(flare) {
42
- if (!sdkTagged.has(flare)) {
43
- sdkTagged.add(flare);
44
- flare.setSdkInfo({
45
- name: "@flareapp/react",
46
- version: PACKAGE_VERSION
47
- });
48
- }
49
- tagReactFramework(flare);
63
+ tagger.registerSdkIdentity(flare);
64
+ tagger.tagFramework(flare, react.version);
50
65
  }
66
+ /** Injected path: framework tag only, never sdkInfo (would clobber the injected SDK name). */
51
67
  function tagReactFramework(flare) {
52
- if (frameworkTagged.has(flare)) return;
53
- frameworkTagged.add(flare);
54
- flare.setFramework({
55
- name: "React",
56
- version: react.version
57
- });
68
+ tagger.tagFramework(flare, react.version);
58
69
  }
59
70
 
60
71
  //#endregion
61
72
  //#region src/resolveFlare.ts
62
- let defaultProvider = null;
63
- function isDevMode() {
64
- try {
65
- return process.env.NODE_ENV !== "production";
66
- } catch {
67
- return false;
68
- }
69
- }
70
- function registerDefaultFlare(provider) {
71
- if (typeof window !== "undefined" && window.__flare) {
72
- const message = "[flare] @flareapp/react (web root) was imported in a renderer where the Electron bridge is present, pulling the keyed @flareapp/js singleton into the renderer. Import @flareapp/react/inject and pass the @flareapp/electron/renderer instance instead.";
73
- if (isDevMode()) throw new Error(message);
74
- console.warn(message);
75
- }
76
- defaultProvider = provider;
77
- }
78
- function resolveFlare(explicit) {
79
- if (explicit) return explicit;
80
- if (defaultProvider) return defaultProvider();
81
- throw new Error("[flare] No Flare instance available. Pass `flare` (e.g. from @flareapp/electron/renderer), or import @flareapp/react (the package root) to use the @flareapp/js default singleton.");
82
- }
73
+ const { registerDefaultFlare, resolveFlare } = (0, _flareapp_js_browser.createFlareResolver)({ packageName: "@flareapp/react" });
83
74
 
84
75
  //#endregion
85
76
  //#region src/formatComponentStack.ts
@@ -89,6 +80,12 @@ function formatComponentStack(stack) {
89
80
 
90
81
  //#endregion
91
82
  //#region src/parseComponentStack.ts
83
+ /**
84
+ * Parse React's newline-separated `errorInfo.componentStack` into structured frames so the Flare UI
85
+ * can render file/line links. The line format is browser-native in React 19 (Chromium / Firefox /
86
+ * Safari shapes) and React-synthetic in 16-18 (`in X (at File:line)`). Unrecognised lines fall back
87
+ * to component-name-only rather than being dropped.
88
+ */
92
89
  function parseComponentStack(stack) {
93
90
  return stack.split(/\s*\n\s*/g).filter((line) => line.length > 0).map((line) => {
94
91
  const chromeMatch = line.match(CHROMIUM_STACK_REGEX);
@@ -105,6 +102,13 @@ function parseComponentStack(stack) {
105
102
  line: Number(firefoxSafariMatch[3]),
106
103
  column: Number(firefoxSafariMatch[4])
107
104
  };
105
+ const reactLegacyMatch = line.match(REACT_LEGACY_STACK_REGEX);
106
+ if (reactLegacyMatch) return {
107
+ component: reactLegacyMatch[1],
108
+ file: reactLegacyMatch[2] ?? null,
109
+ line: reactLegacyMatch[3] ? Number(reactLegacyMatch[3]) : null,
110
+ column: reactLegacyMatch[4] ? Number(reactLegacyMatch[4]) : null
111
+ };
108
112
  return {
109
113
  component: line.replace(/^at\s+/, ""),
110
114
  file: null,
@@ -128,11 +132,11 @@ function buildReactContext(rawStack) {
128
132
  //#region src/contextToAttributes.ts
129
133
  function contextToAttributes(context, minifiedError) {
130
134
  return {
131
- "context.custom": { react: {
135
+ ...(0, _flareapp_core.toCustomContext)("react", {
132
136
  componentStack: context.react.componentStack,
133
137
  componentStackFrames: context.react.componentStackFrames,
134
138
  ...context.react.version ? { version: context.react.version } : {}
135
- } },
139
+ }),
136
140
  ...minifiedError ? { "flare.exception.react_minified_error": {
137
141
  number: minifiedError.number,
138
142
  args: minifiedError.args,
@@ -147,20 +151,13 @@ function contextToAttributes(context, minifiedError) {
147
151
  const NUMBER_PATTERN = /Minified React error #(\d+)/;
148
152
  const ARG_PATTERN = /args\[\]=([^&\s]*)/g;
149
153
  const URL_PATTERN = /(https?:\/\/\S+)/;
150
- function safeDecode(value) {
151
- try {
152
- return decodeURIComponent(value);
153
- } catch {
154
- return value;
155
- }
156
- }
157
154
  function parseMinifiedReactError(error) {
158
155
  const message = error?.message;
159
156
  if (!message) return null;
160
157
  const numberMatch = message.match(NUMBER_PATTERN);
161
158
  if (!numberMatch) return null;
162
159
  const args = [];
163
- for (const match of message.matchAll(ARG_PATTERN)) args.push(safeDecode(match[1]));
160
+ for (const match of message.matchAll(ARG_PATTERN)) args.push((0, _flareapp_core.safeDecode)(match[1]));
164
161
  const urlMatch = message.match(URL_PATTERN);
165
162
  return {
166
163
  number: Number(numberMatch[1]),
@@ -237,6 +234,10 @@ var FlareErrorBoundary = class extends react.Component {
237
234
 
238
235
  //#endregion
239
236
  //#region src/flareReactErrorHandler.ts
237
+ /**
238
+ * Callback shaped to match react-error-boundary's `onError` prop, so consumers using that library
239
+ * can report to Flare without our own boundary.
240
+ */
240
241
  function flareReactErrorHandler(options) {
241
242
  const flare = resolveFlare(options?.flare);
242
243
  tagReactFramework(flare);
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_flareReactErrorHandler = require('./flareReactErrorHandler-e8mBB8Lg.cjs');
2
+ const require_flareReactErrorHandler = require('./flareReactErrorHandler-CuYSKJ1t.cjs');
3
3
  let _flareapp_js = require("@flareapp/js");
4
4
 
5
5
  //#region src/index.ts
package/dist/index.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as FlareErrorBoundaryFallbackProps, c as FlareReactContext, i as FlareErrorBoundary, l as MinifiedReactError, n as FlareReactErrorHandlerOptions, o as FlareErrorBoundaryProps, r as flareReactErrorHandler, s as ComponentStackFrame, t as FlareReactErrorHandlerCallback } from "./flareReactErrorHandler-DDsrRIsz.cjs";
1
+ import { a as FlareErrorBoundaryFallbackProps, c as FlareReactContext, i as FlareErrorBoundary, l as MinifiedReactError, n as FlareReactErrorHandlerOptions, o as FlareErrorBoundaryProps, r as flareReactErrorHandler, s as ComponentStackFrame, t as FlareReactErrorHandlerCallback } from "./flareReactErrorHandler-CnFo8otu.cjs";
2
2
  export { type ComponentStackFrame, FlareErrorBoundary, type FlareErrorBoundaryFallbackProps, type FlareErrorBoundaryProps, type FlareReactContext, type FlareReactErrorHandlerCallback, type FlareReactErrorHandlerOptions, type MinifiedReactError, flareReactErrorHandler };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as FlareErrorBoundaryFallbackProps, c as FlareReactContext, i as FlareErrorBoundary, l as MinifiedReactError, n as FlareReactErrorHandlerOptions, o as FlareErrorBoundaryProps, r as flareReactErrorHandler, s as ComponentStackFrame, t as FlareReactErrorHandlerCallback } from "./flareReactErrorHandler-CLRs3ATb.mjs";
1
+ import { a as FlareErrorBoundaryFallbackProps, c as FlareReactContext, i as FlareErrorBoundary, l as MinifiedReactError, n as FlareReactErrorHandlerOptions, o as FlareErrorBoundaryProps, r as flareReactErrorHandler, s as ComponentStackFrame, t as FlareReactErrorHandlerCallback } from "./flareReactErrorHandler-Ce0sCyMK.mjs";
2
2
  export { type ComponentStackFrame, FlareErrorBoundary, type FlareErrorBoundaryFallbackProps, type FlareErrorBoundaryProps, type FlareReactContext, type FlareReactErrorHandlerCallback, type FlareReactErrorHandlerOptions, type MinifiedReactError, flareReactErrorHandler };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as registerReactSdkIdentity, n as FlareErrorBoundary, r as registerDefaultFlare, t as flareReactErrorHandler } from "./flareReactErrorHandler-zU02JsUc.mjs";
1
+ import { i as registerReactSdkIdentity, n as FlareErrorBoundary, r as registerDefaultFlare, t as flareReactErrorHandler } from "./flareReactErrorHandler-Bk1akw21.mjs";
2
2
  import { flare } from "@flareapp/js";
3
3
 
4
4
  //#region src/index.ts
package/dist/inject.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_flareReactErrorHandler = require('./flareReactErrorHandler-e8mBB8Lg.cjs');
2
+ const require_flareReactErrorHandler = require('./flareReactErrorHandler-CuYSKJ1t.cjs');
3
3
 
4
4
  exports.FlareErrorBoundary = require_flareReactErrorHandler.FlareErrorBoundary;
5
5
  exports.flareReactErrorHandler = require_flareReactErrorHandler.flareReactErrorHandler;
package/dist/inject.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as FlareErrorBoundaryFallbackProps, c as FlareReactContext, i as FlareErrorBoundary, l as MinifiedReactError, n as FlareReactErrorHandlerOptions, o as FlareErrorBoundaryProps, r as flareReactErrorHandler, s as ComponentStackFrame, t as FlareReactErrorHandlerCallback } from "./flareReactErrorHandler-DDsrRIsz.cjs";
1
+ import { a as FlareErrorBoundaryFallbackProps, c as FlareReactContext, i as FlareErrorBoundary, l as MinifiedReactError, n as FlareReactErrorHandlerOptions, o as FlareErrorBoundaryProps, r as flareReactErrorHandler, s as ComponentStackFrame, t as FlareReactErrorHandlerCallback } from "./flareReactErrorHandler-CnFo8otu.cjs";
2
2
  export { type ComponentStackFrame, FlareErrorBoundary, type FlareErrorBoundaryFallbackProps, type FlareErrorBoundaryProps, type FlareReactContext, type FlareReactErrorHandlerCallback, type FlareReactErrorHandlerOptions, type MinifiedReactError, flareReactErrorHandler };
package/dist/inject.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as FlareErrorBoundaryFallbackProps, c as FlareReactContext, i as FlareErrorBoundary, l as MinifiedReactError, n as FlareReactErrorHandlerOptions, o as FlareErrorBoundaryProps, r as flareReactErrorHandler, s as ComponentStackFrame, t as FlareReactErrorHandlerCallback } from "./flareReactErrorHandler-CLRs3ATb.mjs";
1
+ import { a as FlareErrorBoundaryFallbackProps, c as FlareReactContext, i as FlareErrorBoundary, l as MinifiedReactError, n as FlareReactErrorHandlerOptions, o as FlareErrorBoundaryProps, r as flareReactErrorHandler, s as ComponentStackFrame, t as FlareReactErrorHandlerCallback } from "./flareReactErrorHandler-Ce0sCyMK.mjs";
2
2
  export { type ComponentStackFrame, FlareErrorBoundary, type FlareErrorBoundaryFallbackProps, type FlareErrorBoundaryProps, type FlareReactContext, type FlareReactErrorHandlerCallback, type FlareReactErrorHandlerOptions, type MinifiedReactError, flareReactErrorHandler };
package/dist/inject.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { n as FlareErrorBoundary, t as flareReactErrorHandler } from "./flareReactErrorHandler-zU02JsUc.mjs";
1
+ import { n as FlareErrorBoundary, t as flareReactErrorHandler } from "./flareReactErrorHandler-Bk1akw21.mjs";
2
2
 
3
3
  export { FlareErrorBoundary, flareReactErrorHandler };
@@ -0,0 +1,63 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_flareReactErrorHandler = require('./flareReactErrorHandler-CuYSKJ1t.cjs');
3
+ let react = require("react");
4
+ let _flareapp_js_browser = require("@flareapp/js/browser");
5
+
6
+ //#region src/profiler.ts
7
+ const useMountEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
8
+ const FlareProfilerContext = (0, react.createContext)(null);
9
+ /** Records one `browser_component` span for its mount, nested under the nearest profiled ancestor
10
+ * or the active `browser_pageload` / `browser_navigation` root. */
11
+ function FlareProfiler({ name, children }) {
12
+ const context = (0, react.useContext)(FlareProfilerContext);
13
+ const parentRef = (0, react.useRef)(void 0);
14
+ if (parentRef.current === void 0) try {
15
+ parentRef.current = (0, _flareapp_js_browser.resolveComponentParent)(context, (0, _flareapp_js_browser.activeComponentRoot)());
16
+ } catch {
17
+ parentRef.current = null;
18
+ }
19
+ const parent = parentRef.current;
20
+ const ownRef = (0, react.useRef)(null);
21
+ if (parent && ownRef.current === null) try {
22
+ const spanId = (0, _flareapp_js_browser.reserveSpanId)(parent.traceId);
23
+ ownRef.current = spanId ? {
24
+ spanId,
25
+ startNano: (0, _flareapp_js_browser.nowNano)()
26
+ } : null;
27
+ } catch {}
28
+ const providedRef = (0, react.useRef)(void 0);
29
+ if (providedRef.current === void 0) providedRef.current = parent && ownRef.current ? {
30
+ traceId: parent.traceId,
31
+ parentSpanId: ownRef.current.spanId
32
+ } : null;
33
+ const hasRecorded = (0, react.useRef)(false);
34
+ useMountEffect(() => {
35
+ const own = ownRef.current;
36
+ if (!parent || !own || hasRecorded.current) return;
37
+ hasRecorded.current = true;
38
+ try {
39
+ (0, _flareapp_js_browser.recordComponentSpan)({
40
+ name,
41
+ spanId: own.spanId,
42
+ parent,
43
+ startTimeUnixNano: own.startNano,
44
+ endTimeUnixNano: (0, _flareapp_js_browser.nowNano)()
45
+ });
46
+ } catch {}
47
+ }, []);
48
+ if (providedRef.current === null) return children ?? null;
49
+ return (0, react.createElement)(FlareProfilerContext.Provider, { value: providedRef.current }, children ?? null);
50
+ }
51
+ /** Wraps `Component` in a `FlareProfiler`. Name it explicitly when the component is anonymous or minified. */
52
+ function withFlareProfiler(Component, options) {
53
+ const name = options?.name || Component.displayName || Component.name || "Unknown";
54
+ function Profiled(props) {
55
+ return (0, react.createElement)(FlareProfiler, { name }, (0, react.createElement)(Component, props));
56
+ }
57
+ Profiled.displayName = `withFlareProfiler(${name})`;
58
+ return Profiled;
59
+ }
60
+
61
+ //#endregion
62
+ exports.FlareProfiler = FlareProfiler;
63
+ exports.withFlareProfiler = withFlareProfiler;
@@ -0,0 +1,19 @@
1
+ import { ComponentType, FunctionComponent, ReactNode } from "react";
2
+
3
+ //#region src/profiler.d.ts
4
+ type FlareProfilerProps = {
5
+ /** The span's name, as it appears in the trace. */name: string;
6
+ children?: ReactNode;
7
+ };
8
+ /** Records one `browser_component` span for its mount, nested under the nearest profiled ancestor
9
+ * or the active `browser_pageload` / `browser_navigation` root. */
10
+ declare function FlareProfiler({
11
+ name,
12
+ children
13
+ }: FlareProfilerProps): ReactNode;
14
+ /** Wraps `Component` in a `FlareProfiler`. Name it explicitly when the component is anonymous or minified. */
15
+ declare function withFlareProfiler<P extends object>(Component: ComponentType<P>, options?: {
16
+ name?: string;
17
+ }): FunctionComponent<P>;
18
+ //#endregion
19
+ export { FlareProfiler, FlareProfilerProps, withFlareProfiler };
@@ -0,0 +1,19 @@
1
+ import { ComponentType, FunctionComponent, ReactNode } from "react";
2
+
3
+ //#region src/profiler.d.ts
4
+ type FlareProfilerProps = {
5
+ /** The span's name, as it appears in the trace. */name: string;
6
+ children?: ReactNode;
7
+ };
8
+ /** Records one `browser_component` span for its mount, nested under the nearest profiled ancestor
9
+ * or the active `browser_pageload` / `browser_navigation` root. */
10
+ declare function FlareProfiler({
11
+ name,
12
+ children
13
+ }: FlareProfilerProps): ReactNode;
14
+ /** Wraps `Component` in a `FlareProfiler`. Name it explicitly when the component is anonymous or minified. */
15
+ declare function withFlareProfiler<P extends object>(Component: ComponentType<P>, options?: {
16
+ name?: string;
17
+ }): FunctionComponent<P>;
18
+ //#endregion
19
+ export { FlareProfiler, FlareProfilerProps, withFlareProfiler };
@@ -0,0 +1,60 @@
1
+ import { createContext, createElement, useContext, useEffect, useLayoutEffect, useRef } from "react";
2
+ import { activeComponentRoot, nowNano, recordComponentSpan, reserveSpanId, resolveComponentParent } from "@flareapp/js/browser";
3
+
4
+ //#region src/profiler.ts
5
+ const useMountEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
6
+ const FlareProfilerContext = createContext(null);
7
+ /** Records one `browser_component` span for its mount, nested under the nearest profiled ancestor
8
+ * or the active `browser_pageload` / `browser_navigation` root. */
9
+ function FlareProfiler({ name, children }) {
10
+ const context = useContext(FlareProfilerContext);
11
+ const parentRef = useRef(void 0);
12
+ if (parentRef.current === void 0) try {
13
+ parentRef.current = resolveComponentParent(context, activeComponentRoot());
14
+ } catch {
15
+ parentRef.current = null;
16
+ }
17
+ const parent = parentRef.current;
18
+ const ownRef = useRef(null);
19
+ if (parent && ownRef.current === null) try {
20
+ const spanId = reserveSpanId(parent.traceId);
21
+ ownRef.current = spanId ? {
22
+ spanId,
23
+ startNano: nowNano()
24
+ } : null;
25
+ } catch {}
26
+ const providedRef = useRef(void 0);
27
+ if (providedRef.current === void 0) providedRef.current = parent && ownRef.current ? {
28
+ traceId: parent.traceId,
29
+ parentSpanId: ownRef.current.spanId
30
+ } : null;
31
+ const hasRecorded = useRef(false);
32
+ useMountEffect(() => {
33
+ const own = ownRef.current;
34
+ if (!parent || !own || hasRecorded.current) return;
35
+ hasRecorded.current = true;
36
+ try {
37
+ recordComponentSpan({
38
+ name,
39
+ spanId: own.spanId,
40
+ parent,
41
+ startTimeUnixNano: own.startNano,
42
+ endTimeUnixNano: nowNano()
43
+ });
44
+ } catch {}
45
+ }, []);
46
+ if (providedRef.current === null) return children ?? null;
47
+ return createElement(FlareProfilerContext.Provider, { value: providedRef.current }, children ?? null);
48
+ }
49
+ /** Wraps `Component` in a `FlareProfiler`. Name it explicitly when the component is anonymous or minified. */
50
+ function withFlareProfiler(Component, options) {
51
+ const name = options?.name || Component.displayName || Component.name || "Unknown";
52
+ function Profiled(props) {
53
+ return createElement(FlareProfiler, { name }, createElement(Component, props));
54
+ }
55
+ Profiled.displayName = `withFlareProfiler(${name})`;
56
+ return Profiled;
57
+ }
58
+
59
+ //#endregion
60
+ export { FlareProfiler, withFlareProfiler };
@@ -0,0 +1,94 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_flareReactErrorHandler = require('./flareReactErrorHandler-CuYSKJ1t.cjs');
3
+ let _flareapp_js_browser = require("@flareapp/js/browser");
4
+
5
+ //#region src/react-router.ts
6
+ /**
7
+ * Rebuild the parameterized template (`/product/:id`) by joining each match's declared `route.path`.
8
+ * Follows Sentry's getNormalizedName, but reads the router's already-resolved `state.matches` rather
9
+ * than matching again.
10
+ */
11
+ function routeNameFromMatches(matches) {
12
+ if (!matches || matches.length === 0) return;
13
+ let path = "";
14
+ for (const match of matches) {
15
+ const routePath = match.route?.path;
16
+ if (!routePath) continue;
17
+ path = routePath[0] === "/" ? routePath : (path.endsWith("/") ? path : path + "/") + routePath;
18
+ }
19
+ if (!path) return;
20
+ if (path[0] !== "/") path = "/" + path;
21
+ return path.replace(/\/{2,}/g, "/");
22
+ }
23
+ /**
24
+ * Trace a React Router v7 data router (createBrowserRouter / createHashRouter / createMemoryRouter):
25
+ * name the browser_pageload root from the initial route, and open a parameterized, held
26
+ * browser_navigation root per route change, named once the router settles. Returns a cleanup that
27
+ * unsubscribes and unregisters. Safe to call before or after tracing is enabled; no-ops when off.
28
+ * Calling it twice on the same router replaces the first instrumentation.
29
+ */
30
+ function traceReactRouter(router) {
31
+ if (typeof router?.subscribe !== "function") return () => {};
32
+ return (0, _flareapp_js_browser.instrumentOnce)(router, (track) => install(router, track));
33
+ }
34
+ function install(router, track) {
35
+ const nav = (0, _flareapp_js_browser.registerNavigationSource)();
36
+ track(() => nav.unregister());
37
+ function routeNameFor(state) {
38
+ return (0, _flareapp_js_browser.routeName)(() => routeNameFromMatches(state.matches), state.location.pathname, hrefOf(state.location));
39
+ }
40
+ function hrefOf(loc) {
41
+ return (0, _flareapp_js_browser.resolveHref)(() => router.createHref?.(loc), keyOf(loc));
42
+ }
43
+ function keyOf(loc) {
44
+ return (loc.pathname || "") + (loc.search || "") + (loc.hash || "");
45
+ }
46
+ let sawInitialSettle = false;
47
+ let inFlight = false;
48
+ let lastLocationKey = keyOf(router.state.location);
49
+ try {
50
+ if (router.state.matches.length > 0) nav.setActiveRouteName(routeNameFor(router.state));
51
+ sawInitialSettle = router.state.initialized === true;
52
+ } catch {}
53
+ function onState(state) {
54
+ if (!sawInitialSettle) {
55
+ lastLocationKey = keyOf(state.location);
56
+ if (state.matches.length > 0) nav.setActiveRouteName(routeNameFor(state));
57
+ if (state.initialized) sawInitialSettle = true;
58
+ return;
59
+ }
60
+ const navState = state.navigation.state;
61
+ if (!inFlight && navState !== "idle") {
62
+ inFlight = true;
63
+ const destination = state.navigation.location ?? state.location;
64
+ nav.startNavigation({
65
+ path: destination.pathname,
66
+ url: hrefOf(destination),
67
+ hold: true
68
+ });
69
+ return;
70
+ }
71
+ if (inFlight && navState === "idle") {
72
+ inFlight = false;
73
+ lastLocationKey = keyOf(state.location);
74
+ nav.settleNavigation(routeNameFor(state));
75
+ return;
76
+ }
77
+ if (!inFlight && navState === "idle") {
78
+ const locationKey = keyOf(state.location);
79
+ if (locationKey !== lastLocationKey) {
80
+ lastLocationKey = locationKey;
81
+ nav.startNavigation({
82
+ path: state.location.pathname,
83
+ url: hrefOf(state.location)
84
+ });
85
+ nav.settleNavigation(routeNameFor(state));
86
+ }
87
+ }
88
+ }
89
+ track(router.subscribe((0, _flareapp_js_browser.insulate)(onState)));
90
+ }
91
+
92
+ //#endregion
93
+ exports.routeNameFromMatches = routeNameFromMatches;
94
+ exports.traceReactRouter = traceReactRouter;
@@ -0,0 +1,54 @@
1
+ //#region src/vendor/reactRouterTypes.d.ts
2
+ type ReactRouterLocationLike = {
3
+ pathname: string;
4
+ search?: string;
5
+ hash?: string;
6
+ state?: unknown;
7
+ };
8
+ type ReactRouterRouteLike = {
9
+ path?: string;
10
+ index?: boolean;
11
+ id?: string;
12
+ };
13
+ type ReactRouterMatchLike = {
14
+ route: ReactRouterRouteLike;
15
+ pathname: string;
16
+ params?: Record<string, string | undefined>;
17
+ };
18
+ type ReactRouterNavigationLike = {
19
+ state: 'idle' | 'loading' | 'submitting';
20
+ location?: ReactRouterLocationLike;
21
+ };
22
+ type ReactRouterStateLike = {
23
+ location: ReactRouterLocationLike;
24
+ matches: ReactRouterMatchLike[];
25
+ navigation: ReactRouterNavigationLike;
26
+ initialized?: boolean;
27
+ };
28
+ type ReactRouterLike = {
29
+ subscribe(cb: (state: ReactRouterStateLike) => void): () => void;
30
+ state: ReactRouterStateLike;
31
+ /**
32
+ * Applies the router's `basename` (and, for a hash router, the `#` prefix) to a location.
33
+ * `state.location.pathname` has both stripped. Optional so a hand-built router still types.
34
+ */
35
+ createHref?(location: ReactRouterLocationLike): string;
36
+ };
37
+ //#endregion
38
+ //#region src/react-router.d.ts
39
+ /**
40
+ * Rebuild the parameterized template (`/product/:id`) by joining each match's declared `route.path`.
41
+ * Follows Sentry's getNormalizedName, but reads the router's already-resolved `state.matches` rather
42
+ * than matching again.
43
+ */
44
+ declare function routeNameFromMatches(matches: ReactRouterMatchLike[] | undefined): string | undefined;
45
+ /**
46
+ * Trace a React Router v7 data router (createBrowserRouter / createHashRouter / createMemoryRouter):
47
+ * name the browser_pageload root from the initial route, and open a parameterized, held
48
+ * browser_navigation root per route change, named once the router settles. Returns a cleanup that
49
+ * unsubscribes and unregisters. Safe to call before or after tracing is enabled; no-ops when off.
50
+ * Calling it twice on the same router replaces the first instrumentation.
51
+ */
52
+ declare function traceReactRouter(router: ReactRouterLike): () => void;
53
+ //#endregion
54
+ export { type ReactRouterLike, type ReactRouterLocationLike, type ReactRouterMatchLike, type ReactRouterNavigationLike, type ReactRouterRouteLike, type ReactRouterStateLike, routeNameFromMatches, traceReactRouter };
@@ -0,0 +1,54 @@
1
+ //#region src/vendor/reactRouterTypes.d.ts
2
+ type ReactRouterLocationLike = {
3
+ pathname: string;
4
+ search?: string;
5
+ hash?: string;
6
+ state?: unknown;
7
+ };
8
+ type ReactRouterRouteLike = {
9
+ path?: string;
10
+ index?: boolean;
11
+ id?: string;
12
+ };
13
+ type ReactRouterMatchLike = {
14
+ route: ReactRouterRouteLike;
15
+ pathname: string;
16
+ params?: Record<string, string | undefined>;
17
+ };
18
+ type ReactRouterNavigationLike = {
19
+ state: 'idle' | 'loading' | 'submitting';
20
+ location?: ReactRouterLocationLike;
21
+ };
22
+ type ReactRouterStateLike = {
23
+ location: ReactRouterLocationLike;
24
+ matches: ReactRouterMatchLike[];
25
+ navigation: ReactRouterNavigationLike;
26
+ initialized?: boolean;
27
+ };
28
+ type ReactRouterLike = {
29
+ subscribe(cb: (state: ReactRouterStateLike) => void): () => void;
30
+ state: ReactRouterStateLike;
31
+ /**
32
+ * Applies the router's `basename` (and, for a hash router, the `#` prefix) to a location.
33
+ * `state.location.pathname` has both stripped. Optional so a hand-built router still types.
34
+ */
35
+ createHref?(location: ReactRouterLocationLike): string;
36
+ };
37
+ //#endregion
38
+ //#region src/react-router.d.ts
39
+ /**
40
+ * Rebuild the parameterized template (`/product/:id`) by joining each match's declared `route.path`.
41
+ * Follows Sentry's getNormalizedName, but reads the router's already-resolved `state.matches` rather
42
+ * than matching again.
43
+ */
44
+ declare function routeNameFromMatches(matches: ReactRouterMatchLike[] | undefined): string | undefined;
45
+ /**
46
+ * Trace a React Router v7 data router (createBrowserRouter / createHashRouter / createMemoryRouter):
47
+ * name the browser_pageload root from the initial route, and open a parameterized, held
48
+ * browser_navigation root per route change, named once the router settles. Returns a cleanup that
49
+ * unsubscribes and unregisters. Safe to call before or after tracing is enabled; no-ops when off.
50
+ * Calling it twice on the same router replaces the first instrumentation.
51
+ */
52
+ declare function traceReactRouter(router: ReactRouterLike): () => void;
53
+ //#endregion
54
+ export { type ReactRouterLike, type ReactRouterLocationLike, type ReactRouterMatchLike, type ReactRouterNavigationLike, type ReactRouterRouteLike, type ReactRouterStateLike, routeNameFromMatches, traceReactRouter };
@@ -0,0 +1,91 @@
1
+ import { instrumentOnce, insulate, registerNavigationSource, resolveHref, routeName } from "@flareapp/js/browser";
2
+
3
+ //#region src/react-router.ts
4
+ /**
5
+ * Rebuild the parameterized template (`/product/:id`) by joining each match's declared `route.path`.
6
+ * Follows Sentry's getNormalizedName, but reads the router's already-resolved `state.matches` rather
7
+ * than matching again.
8
+ */
9
+ function routeNameFromMatches(matches) {
10
+ if (!matches || matches.length === 0) return;
11
+ let path = "";
12
+ for (const match of matches) {
13
+ const routePath = match.route?.path;
14
+ if (!routePath) continue;
15
+ path = routePath[0] === "/" ? routePath : (path.endsWith("/") ? path : path + "/") + routePath;
16
+ }
17
+ if (!path) return;
18
+ if (path[0] !== "/") path = "/" + path;
19
+ return path.replace(/\/{2,}/g, "/");
20
+ }
21
+ /**
22
+ * Trace a React Router v7 data router (createBrowserRouter / createHashRouter / createMemoryRouter):
23
+ * name the browser_pageload root from the initial route, and open a parameterized, held
24
+ * browser_navigation root per route change, named once the router settles. Returns a cleanup that
25
+ * unsubscribes and unregisters. Safe to call before or after tracing is enabled; no-ops when off.
26
+ * Calling it twice on the same router replaces the first instrumentation.
27
+ */
28
+ function traceReactRouter(router) {
29
+ if (typeof router?.subscribe !== "function") return () => {};
30
+ return instrumentOnce(router, (track) => install(router, track));
31
+ }
32
+ function install(router, track) {
33
+ const nav = registerNavigationSource();
34
+ track(() => nav.unregister());
35
+ function routeNameFor(state) {
36
+ return routeName(() => routeNameFromMatches(state.matches), state.location.pathname, hrefOf(state.location));
37
+ }
38
+ function hrefOf(loc) {
39
+ return resolveHref(() => router.createHref?.(loc), keyOf(loc));
40
+ }
41
+ function keyOf(loc) {
42
+ return (loc.pathname || "") + (loc.search || "") + (loc.hash || "");
43
+ }
44
+ let sawInitialSettle = false;
45
+ let inFlight = false;
46
+ let lastLocationKey = keyOf(router.state.location);
47
+ try {
48
+ if (router.state.matches.length > 0) nav.setActiveRouteName(routeNameFor(router.state));
49
+ sawInitialSettle = router.state.initialized === true;
50
+ } catch {}
51
+ function onState(state) {
52
+ if (!sawInitialSettle) {
53
+ lastLocationKey = keyOf(state.location);
54
+ if (state.matches.length > 0) nav.setActiveRouteName(routeNameFor(state));
55
+ if (state.initialized) sawInitialSettle = true;
56
+ return;
57
+ }
58
+ const navState = state.navigation.state;
59
+ if (!inFlight && navState !== "idle") {
60
+ inFlight = true;
61
+ const destination = state.navigation.location ?? state.location;
62
+ nav.startNavigation({
63
+ path: destination.pathname,
64
+ url: hrefOf(destination),
65
+ hold: true
66
+ });
67
+ return;
68
+ }
69
+ if (inFlight && navState === "idle") {
70
+ inFlight = false;
71
+ lastLocationKey = keyOf(state.location);
72
+ nav.settleNavigation(routeNameFor(state));
73
+ return;
74
+ }
75
+ if (!inFlight && navState === "idle") {
76
+ const locationKey = keyOf(state.location);
77
+ if (locationKey !== lastLocationKey) {
78
+ lastLocationKey = locationKey;
79
+ nav.startNavigation({
80
+ path: state.location.pathname,
81
+ url: hrefOf(state.location)
82
+ });
83
+ nav.settleNavigation(routeNameFor(state));
84
+ }
85
+ }
86
+ }
87
+ track(router.subscribe(insulate(onState)));
88
+ }
89
+
90
+ //#endregion
91
+ export { routeNameFromMatches, traceReactRouter };
@@ -0,0 +1,89 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_flareReactErrorHandler = require('./flareReactErrorHandler-CuYSKJ1t.cjs');
3
+ let _flareapp_js_browser = require("@flareapp/js/browser");
4
+
5
+ //#region src/tanstack-router.ts
6
+ /**
7
+ * How long a held navigation root waits for `onResolved` before settling itself. Exported so the suite
8
+ * drives it instead of hardcoding the number; not part of the supported surface.
9
+ */
10
+ const STALE_NAVIGATION_TIMEOUT_MS = 5e3;
11
+ /**
12
+ * Trace a TanStack Router instance: name the `browser_pageload` root from the
13
+ * initial route and open a parameterized `browser_navigation` root per route
14
+ * change. Returns a cleanup that unsubscribes and unregisters. Safe to call
15
+ * before or after tracing is enabled; no-ops when tracing is off. Calling it
16
+ * twice on the same router replaces the first instrumentation rather than
17
+ * stacking a second set of subscriptions.
18
+ */
19
+ function traceTanStackRouter(router) {
20
+ if (typeof router?.subscribe !== "function") return () => {};
21
+ return (0, _flareapp_js_browser.instrumentOnce)(router, (track) => install(router, track));
22
+ }
23
+ function install(router, track) {
24
+ const nav = (0, _flareapp_js_browser.registerNavigationSource)();
25
+ track(() => nav.unregister());
26
+ function hrefOf(loc) {
27
+ return (0, _flareapp_js_browser.resolveHref)(() => loc.publicHref ?? loc.href, loc.pathname);
28
+ }
29
+ function routeNameFor(loc) {
30
+ return (0, _flareapp_js_browser.routeName)(() => {
31
+ const matches = router.matchRoutes(loc.pathname, loc.search, {
32
+ preload: false,
33
+ throwOnError: false
34
+ });
35
+ if (!matches.some((m) => m.routeId !== "__root__")) return;
36
+ const last = matches[matches.length - 1];
37
+ return last?.fullPath || last?.routeId;
38
+ }, loc.pathname, hrefOf(loc));
39
+ }
40
+ try {
41
+ nav.setActiveRouteName(routeNameFor(router.state.location));
42
+ } catch {}
43
+ let inFlight = false;
44
+ let destination = null;
45
+ let staleTimer = null;
46
+ function clearStaleTimer() {
47
+ if (staleTimer !== null) {
48
+ clearTimeout(staleTimer);
49
+ staleTimer = null;
50
+ }
51
+ }
52
+ function settle(location) {
53
+ clearStaleTimer();
54
+ inFlight = false;
55
+ destination = null;
56
+ nav.settleNavigation(routeNameFor(location));
57
+ }
58
+ track(clearStaleTimer);
59
+ track(router.subscribe("onBeforeLoad", (0, _flareapp_js_browser.insulate)((event) => {
60
+ if (event.fromLocation === void 0) return;
61
+ if (event.hrefChanged === false) return;
62
+ if (event.toLocation.state === event.fromLocation.state) return;
63
+ if (!inFlight) {
64
+ inFlight = true;
65
+ nav.startNavigation({
66
+ path: event.toLocation.pathname,
67
+ hold: true
68
+ });
69
+ }
70
+ destination = event.toLocation;
71
+ nav.setActiveRouteName(routeNameFor(event.toLocation));
72
+ clearStaleTimer();
73
+ staleTimer = setTimeout((0, _flareapp_js_browser.insulate)(() => {
74
+ staleTimer = null;
75
+ if (destination) settle(destination);
76
+ }), STALE_NAVIGATION_TIMEOUT_MS);
77
+ })));
78
+ track(router.subscribe("onResolved", (0, _flareapp_js_browser.insulate)((event) => {
79
+ if (event.fromLocation === void 0) {
80
+ nav.setActiveRouteName(routeNameFor(event.toLocation));
81
+ return;
82
+ }
83
+ if (inFlight) settle(event.toLocation);
84
+ })));
85
+ }
86
+
87
+ //#endregion
88
+ exports.STALE_NAVIGATION_TIMEOUT_MS = STALE_NAVIGATION_TIMEOUT_MS;
89
+ exports.traceTanStackRouter = traceTanStackRouter;
@@ -0,0 +1,45 @@
1
+ //#region src/vendor/tanstackRouterTypes.d.ts
2
+ type TanStackLocationLike = {
3
+ pathname: string;
4
+ search: unknown;
5
+ href?: string;
6
+ publicHref?: string;
7
+ state?: unknown;
8
+ };
9
+ type TanStackNavEventLike = {
10
+ fromLocation?: TanStackLocationLike;
11
+ toLocation: TanStackLocationLike;
12
+ hrefChanged?: boolean;
13
+ };
14
+ type TanStackMatchLike = {
15
+ routeId?: string;
16
+ fullPath?: string;
17
+ };
18
+ type TanStackRouterLike = {
19
+ subscribe(eventType: 'onBeforeLoad' | 'onResolved', cb: (event: TanStackNavEventLike) => void): () => void;
20
+ matchRoutes(pathname: string, search: unknown, opts?: {
21
+ preload?: boolean;
22
+ throwOnError?: boolean;
23
+ }): TanStackMatchLike[];
24
+ state: {
25
+ location: TanStackLocationLike;
26
+ };
27
+ };
28
+ //#endregion
29
+ //#region src/tanstack-router.d.ts
30
+ /**
31
+ * How long a held navigation root waits for `onResolved` before settling itself. Exported so the suite
32
+ * drives it instead of hardcoding the number; not part of the supported surface.
33
+ */
34
+ declare const STALE_NAVIGATION_TIMEOUT_MS = 5000;
35
+ /**
36
+ * Trace a TanStack Router instance: name the `browser_pageload` root from the
37
+ * initial route and open a parameterized `browser_navigation` root per route
38
+ * change. Returns a cleanup that unsubscribes and unregisters. Safe to call
39
+ * before or after tracing is enabled; no-ops when tracing is off. Calling it
40
+ * twice on the same router replaces the first instrumentation rather than
41
+ * stacking a second set of subscriptions.
42
+ */
43
+ declare function traceTanStackRouter(router: TanStackRouterLike): () => void;
44
+ //#endregion
45
+ export { STALE_NAVIGATION_TIMEOUT_MS, type TanStackLocationLike, type TanStackMatchLike, type TanStackNavEventLike, type TanStackRouterLike, traceTanStackRouter };
@@ -0,0 +1,45 @@
1
+ //#region src/vendor/tanstackRouterTypes.d.ts
2
+ type TanStackLocationLike = {
3
+ pathname: string;
4
+ search: unknown;
5
+ href?: string;
6
+ publicHref?: string;
7
+ state?: unknown;
8
+ };
9
+ type TanStackNavEventLike = {
10
+ fromLocation?: TanStackLocationLike;
11
+ toLocation: TanStackLocationLike;
12
+ hrefChanged?: boolean;
13
+ };
14
+ type TanStackMatchLike = {
15
+ routeId?: string;
16
+ fullPath?: string;
17
+ };
18
+ type TanStackRouterLike = {
19
+ subscribe(eventType: 'onBeforeLoad' | 'onResolved', cb: (event: TanStackNavEventLike) => void): () => void;
20
+ matchRoutes(pathname: string, search: unknown, opts?: {
21
+ preload?: boolean;
22
+ throwOnError?: boolean;
23
+ }): TanStackMatchLike[];
24
+ state: {
25
+ location: TanStackLocationLike;
26
+ };
27
+ };
28
+ //#endregion
29
+ //#region src/tanstack-router.d.ts
30
+ /**
31
+ * How long a held navigation root waits for `onResolved` before settling itself. Exported so the suite
32
+ * drives it instead of hardcoding the number; not part of the supported surface.
33
+ */
34
+ declare const STALE_NAVIGATION_TIMEOUT_MS = 5000;
35
+ /**
36
+ * Trace a TanStack Router instance: name the `browser_pageload` root from the
37
+ * initial route and open a parameterized `browser_navigation` root per route
38
+ * change. Returns a cleanup that unsubscribes and unregisters. Safe to call
39
+ * before or after tracing is enabled; no-ops when tracing is off. Calling it
40
+ * twice on the same router replaces the first instrumentation rather than
41
+ * stacking a second set of subscriptions.
42
+ */
43
+ declare function traceTanStackRouter(router: TanStackRouterLike): () => void;
44
+ //#endregion
45
+ export { STALE_NAVIGATION_TIMEOUT_MS, type TanStackLocationLike, type TanStackMatchLike, type TanStackNavEventLike, type TanStackRouterLike, traceTanStackRouter };
@@ -0,0 +1,86 @@
1
+ import { instrumentOnce, insulate, registerNavigationSource, resolveHref, routeName } from "@flareapp/js/browser";
2
+
3
+ //#region src/tanstack-router.ts
4
+ /**
5
+ * How long a held navigation root waits for `onResolved` before settling itself. Exported so the suite
6
+ * drives it instead of hardcoding the number; not part of the supported surface.
7
+ */
8
+ const STALE_NAVIGATION_TIMEOUT_MS = 5e3;
9
+ /**
10
+ * Trace a TanStack Router instance: name the `browser_pageload` root from the
11
+ * initial route and open a parameterized `browser_navigation` root per route
12
+ * change. Returns a cleanup that unsubscribes and unregisters. Safe to call
13
+ * before or after tracing is enabled; no-ops when tracing is off. Calling it
14
+ * twice on the same router replaces the first instrumentation rather than
15
+ * stacking a second set of subscriptions.
16
+ */
17
+ function traceTanStackRouter(router) {
18
+ if (typeof router?.subscribe !== "function") return () => {};
19
+ return instrumentOnce(router, (track) => install(router, track));
20
+ }
21
+ function install(router, track) {
22
+ const nav = registerNavigationSource();
23
+ track(() => nav.unregister());
24
+ function hrefOf(loc) {
25
+ return resolveHref(() => loc.publicHref ?? loc.href, loc.pathname);
26
+ }
27
+ function routeNameFor(loc) {
28
+ return routeName(() => {
29
+ const matches = router.matchRoutes(loc.pathname, loc.search, {
30
+ preload: false,
31
+ throwOnError: false
32
+ });
33
+ if (!matches.some((m) => m.routeId !== "__root__")) return;
34
+ const last = matches[matches.length - 1];
35
+ return last?.fullPath || last?.routeId;
36
+ }, loc.pathname, hrefOf(loc));
37
+ }
38
+ try {
39
+ nav.setActiveRouteName(routeNameFor(router.state.location));
40
+ } catch {}
41
+ let inFlight = false;
42
+ let destination = null;
43
+ let staleTimer = null;
44
+ function clearStaleTimer() {
45
+ if (staleTimer !== null) {
46
+ clearTimeout(staleTimer);
47
+ staleTimer = null;
48
+ }
49
+ }
50
+ function settle(location) {
51
+ clearStaleTimer();
52
+ inFlight = false;
53
+ destination = null;
54
+ nav.settleNavigation(routeNameFor(location));
55
+ }
56
+ track(clearStaleTimer);
57
+ track(router.subscribe("onBeforeLoad", insulate((event) => {
58
+ if (event.fromLocation === void 0) return;
59
+ if (event.hrefChanged === false) return;
60
+ if (event.toLocation.state === event.fromLocation.state) return;
61
+ if (!inFlight) {
62
+ inFlight = true;
63
+ nav.startNavigation({
64
+ path: event.toLocation.pathname,
65
+ hold: true
66
+ });
67
+ }
68
+ destination = event.toLocation;
69
+ nav.setActiveRouteName(routeNameFor(event.toLocation));
70
+ clearStaleTimer();
71
+ staleTimer = setTimeout(insulate(() => {
72
+ staleTimer = null;
73
+ if (destination) settle(destination);
74
+ }), STALE_NAVIGATION_TIMEOUT_MS);
75
+ })));
76
+ track(router.subscribe("onResolved", insulate((event) => {
77
+ if (event.fromLocation === void 0) {
78
+ nav.setActiveRouteName(routeNameFor(event.toLocation));
79
+ return;
80
+ }
81
+ if (inFlight) settle(event.toLocation);
82
+ })));
83
+ }
84
+
85
+ //#endregion
86
+ export { STALE_NAVIGATION_TIMEOUT_MS, traceTanStackRouter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flareapp/react",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "React client for flareapp.io",
5
5
  "homepage": "https://flareapp.io",
6
6
  "bugs": "https://github.com/spatie/flare-client-js/issues",
@@ -51,22 +51,53 @@
51
51
  "types": "./dist/inject.d.cts",
52
52
  "default": "./dist/inject.cjs"
53
53
  }
54
+ },
55
+ "./tanstack-router": {
56
+ "import": {
57
+ "types": "./dist/tanstack-router.d.mts",
58
+ "default": "./dist/tanstack-router.mjs"
59
+ },
60
+ "require": {
61
+ "types": "./dist/tanstack-router.d.cts",
62
+ "default": "./dist/tanstack-router.cjs"
63
+ }
64
+ },
65
+ "./react-router": {
66
+ "import": {
67
+ "types": "./dist/react-router.d.mts",
68
+ "default": "./dist/react-router.mjs"
69
+ },
70
+ "require": {
71
+ "types": "./dist/react-router.d.cts",
72
+ "default": "./dist/react-router.cjs"
73
+ }
74
+ },
75
+ "./profiler": {
76
+ "import": {
77
+ "types": "./dist/profiler.d.mts",
78
+ "default": "./dist/profiler.mjs"
79
+ },
80
+ "require": {
81
+ "types": "./dist/profiler.d.cts",
82
+ "default": "./dist/profiler.cjs"
83
+ }
54
84
  }
55
85
  },
56
86
  "scripts": {
57
87
  "prepublishOnly": "npm run build",
58
- "build": "tsdown src/index.ts src/inject.ts --format cjs,esm --dts --env.PACKAGE_VERSION=$(node -p \"require('./package.json').version\") --clean",
88
+ "build": "tsdown src/index.ts src/inject.ts src/tanstack-router.ts src/react-router.ts src/profiler.ts --format cjs,esm --dts --env.PACKAGE_VERSION=$(node -p \"require('./package.json').version\") --clean",
59
89
  "test": "vitest run",
60
90
  "typescript": "tsc --noEmit",
61
91
  "verify:inject": "node scripts/verify-inject-no-root.mjs",
62
92
  "release": "release-it"
63
93
  },
64
94
  "dependencies": {
65
- "@flareapp/core": "2.7.0"
95
+ "@flareapp/core": "2.9.0"
66
96
  },
67
97
  "devDependencies": {
68
98
  "@flareapp/electron": "file:../electron",
69
99
  "@flareapp/js": "file:../js",
100
+ "@flareapp/test-helpers": "*",
70
101
  "@testing-library/jest-dom": "^6.9.1",
71
102
  "@testing-library/react": "^16.0.0",
72
103
  "@types/react": "^19.0.0",
@@ -74,13 +105,24 @@
74
105
  "jsdom": "^26.1.0",
75
106
  "react": "^19.0.0",
76
107
  "react-dom": "^19.0.0",
108
+ "react-router": "^7.6.0",
77
109
  "tsdown": "^0.20.3",
78
110
  "typescript": "^5.7.0",
79
111
  "vitest": "^4.0.0"
80
112
  },
81
113
  "peerDependencies": {
82
- "@flareapp/js": "^2.7.0",
83
- "react": "^16.0.0||^17.0.0||^18.0.0||^19.0.0"
114
+ "@flareapp/js": "^2.9.0",
115
+ "@tanstack/react-router": ">=1.64.0 <2",
116
+ "react": "^16.0.0||^17.0.0||^18.0.0||^19.0.0",
117
+ "react-router": ">=7.0.0 <8"
118
+ },
119
+ "peerDependenciesMeta": {
120
+ "@tanstack/react-router": {
121
+ "optional": true
122
+ },
123
+ "react-router": {
124
+ "optional": true
125
+ }
84
126
  },
85
127
  "publishConfig": {
86
128
  "access": "public"