@flareapp/react 2.7.0 → 2.8.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 +52 -0
- package/dist/{flareReactErrorHandler-CLRs3ATb.d.mts → flareReactErrorHandler-Ce0sCyMK.d.mts} +4 -0
- package/dist/{flareReactErrorHandler-DDsrRIsz.d.cts → flareReactErrorHandler-CnFo8otu.d.cts} +4 -0
- package/dist/{flareReactErrorHandler-e8mBB8Lg.cjs → flareReactErrorHandler-DBmb7u7U.cjs} +50 -49
- package/dist/{flareReactErrorHandler-zU02JsUc.mjs → flareReactErrorHandler-sRmFoJAG.mjs} +49 -48
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/inject.cjs +1 -1
- package/dist/inject.d.cts +1 -1
- package/dist/inject.d.mts +1 -1
- package/dist/inject.mjs +1 -1
- package/dist/profiler.cjs +63 -0
- package/dist/profiler.d.cts +19 -0
- package/dist/profiler.d.mts +19 -0
- package/dist/profiler.mjs +60 -0
- package/dist/react-router.cjs +94 -0
- package/dist/react-router.d.cts +54 -0
- package/dist/react-router.d.mts +54 -0
- package/dist/react-router.mjs +91 -0
- package/dist/tanstack-router.cjs +89 -0
- package/dist/tanstack-router.d.cts +45 -0
- package/dist/tanstack-router.d.mts +45 -0
- package/dist/tanstack-router.mjs +86 -0
- package/package.json +47 -5
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.
|
package/dist/{flareReactErrorHandler-CLRs3ATb.d.mts → flareReactErrorHandler-Ce0sCyMK.d.mts}
RENAMED
|
@@ -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 };
|
package/dist/{flareReactErrorHandler-DDsrRIsz.d.cts → flareReactErrorHandler-CnFo8otu.d.cts}
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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.8.0" : "?";
|
|
36
53
|
|
|
37
54
|
//#endregion
|
|
38
55
|
//#region src/identify.ts
|
|
39
|
-
const
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -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 {
|
|
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
|
-
|
|
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.8.0" : "?";
|
|
9
26
|
|
|
10
27
|
//#endregion
|
|
11
28
|
//#region src/identify.ts
|
|
12
|
-
const
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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);
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_flareReactErrorHandler = require('./flareReactErrorHandler-
|
|
2
|
+
const require_flareReactErrorHandler = require('./flareReactErrorHandler-DBmb7u7U.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-
|
|
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-
|
|
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-
|
|
1
|
+
import { i as registerReactSdkIdentity, n as FlareErrorBoundary, r as registerDefaultFlare, t as flareReactErrorHandler } from "./flareReactErrorHandler-sRmFoJAG.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-
|
|
2
|
+
const require_flareReactErrorHandler = require('./flareReactErrorHandler-DBmb7u7U.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-
|
|
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-
|
|
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-
|
|
1
|
+
import { n as FlareErrorBoundary, t as flareReactErrorHandler } from "./flareReactErrorHandler-sRmFoJAG.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-DBmb7u7U.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-DBmb7u7U.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-DBmb7u7U.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.
|
|
3
|
+
"version": "2.8.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.
|
|
95
|
+
"@flareapp/core": "2.8.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.
|
|
83
|
-
"react": "
|
|
114
|
+
"@flareapp/js": "^2.8.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"
|