@maple-dev/browser 0.9.0 → 0.10.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.
@@ -0,0 +1,88 @@
1
+ import { Component, ErrorInfo, ReactNode } from "react";
2
+ //#region src/react.d.ts
3
+ export interface MapleErrorBoundaryFallbackProps {
4
+ readonly error: unknown;
5
+ /** Clear the error and render the children again. */
6
+ readonly reset: () => void;
7
+ }
8
+ export interface MapleErrorBoundaryProps {
9
+ readonly children?: ReactNode;
10
+ /** Rendered instead of the children after an error. Default: nothing. */
11
+ readonly fallback?: ReactNode | ((props: MapleErrorBoundaryFallbackProps) => ReactNode);
12
+ /** Called after the error is reported. */
13
+ readonly onError?: (error: unknown, info: ErrorInfo) => void;
14
+ }
15
+ interface MapleErrorBoundaryState {
16
+ readonly failed: boolean;
17
+ readonly error: unknown;
18
+ }
19
+ /** Reports render errors below it to Maple once, then renders `fallback`. */
20
+ export declare class MapleErrorBoundary extends Component<MapleErrorBoundaryProps, MapleErrorBoundaryState> {
21
+ state: MapleErrorBoundaryState;
22
+ static getDerivedStateFromError(error: unknown): MapleErrorBoundaryState;
23
+ componentDidCatch(error: unknown, info: ErrorInfo): void;
24
+ private readonly reset;
25
+ render(): ReactNode;
26
+ }
27
+ /**
28
+ * For React 19's `createRoot(el, { onCaughtError, onUncaughtError, onRecoverableError })`:
29
+ * reports what React caught, with its component stack. Errors already reported
30
+ * (by a `MapleErrorBoundary`, say) are not reported twice.
31
+ */
32
+ export declare function mapleReactErrorHandler(source?: string): (error: unknown, info: {
33
+ readonly componentStack?: string | null | undefined;
34
+ }) => void;
35
+ /** A React Router data router (`createBrowserRouter` and friends), as far as Maple reads it. */
36
+ export interface ReactRouterLike {
37
+ readonly state: ReactRouterState;
38
+ subscribe(listener: (state: ReactRouterState) => void): () => void;
39
+ }
40
+ export interface ReactRouterState {
41
+ readonly initialized: boolean;
42
+ readonly location: {
43
+ readonly pathname: string;
44
+ };
45
+ readonly navigation: {
46
+ readonly state: string;
47
+ readonly location?: {
48
+ readonly pathname: string;
49
+ } | undefined;
50
+ };
51
+ readonly matches: ReadonlyArray<{
52
+ readonly route: {
53
+ readonly path?: string | undefined;
54
+ };
55
+ }>;
56
+ }
57
+ /**
58
+ * Span each navigation of a React Router data router, from the moment it starts
59
+ * loading until its loaders settle, named by route template. The first is the
60
+ * page load. Returns an unsubscribe.
61
+ */
62
+ export declare function instrumentReactRouter(router: ReactRouterLike): () => void;
63
+ /** A TanStack Router instance, as far as Maple reads it. */
64
+ export interface TanStackRouterLike {
65
+ readonly state: {
66
+ readonly status: string;
67
+ readonly location: {
68
+ readonly pathname: string;
69
+ };
70
+ readonly matches: ReadonlyArray<{
71
+ readonly fullPath?: string | undefined;
72
+ readonly routeId: string;
73
+ }>;
74
+ };
75
+ subscribe(eventType: "onBeforeNavigate" | "onResolved", listener: (event: {
76
+ readonly toLocation: {
77
+ readonly pathname: string;
78
+ };
79
+ readonly pathChanged: boolean;
80
+ }) => void): () => void;
81
+ }
82
+ /**
83
+ * Span each TanStack Router navigation from `onBeforeNavigate` to `onResolved`,
84
+ * named by the leaf route's full path (`/projects/$projectId`). Search-only
85
+ * changes are not navigations. The first is the page load. Returns an unsubscribe.
86
+ */
87
+ export declare function instrumentTanStackRouter(router: TanStackRouterLike): () => void;
88
+ //#endregion
package/dist/react.mjs ADDED
@@ -0,0 +1,130 @@
1
+ import { t as MapleBrowser } from "./src-Bzmp8rVr.mjs";
2
+ import { Component } from "react";
3
+ //#region src/react.ts
4
+ /** Component stacks can run to hundreds of lines; the top of it names the failing tree. */
5
+ const MAX_COMPONENT_STACK = 2e3;
6
+ function reportReactError(error, componentStack, source) {
7
+ MapleBrowser.captureException(error, {
8
+ name: "react.render_error",
9
+ attributes: {
10
+ "maple.exception.source": source,
11
+ ...componentStack ? { "maple.react.component_stack": componentStack.trim().slice(0, MAX_COMPONENT_STACK) } : void 0
12
+ }
13
+ });
14
+ }
15
+ /** Reports render errors below it to Maple once, then renders `fallback`. */
16
+ var MapleErrorBoundary = class extends Component {
17
+ state = {
18
+ failed: false,
19
+ error: void 0
20
+ };
21
+ static getDerivedStateFromError(error) {
22
+ return {
23
+ failed: true,
24
+ error
25
+ };
26
+ }
27
+ componentDidCatch(error, info) {
28
+ reportReactError(error, info.componentStack, "react.error_boundary");
29
+ this.props.onError?.(error, info);
30
+ }
31
+ reset = () => {
32
+ this.setState({
33
+ failed: false,
34
+ error: void 0
35
+ });
36
+ };
37
+ render() {
38
+ if (!this.state.failed) return this.props.children ?? null;
39
+ const { fallback } = this.props;
40
+ return typeof fallback === "function" ? fallback({
41
+ error: this.state.error,
42
+ reset: this.reset
43
+ }) : fallback ?? null;
44
+ }
45
+ };
46
+ /**
47
+ * For React 19's `createRoot(el, { onCaughtError, onUncaughtError, onRecoverableError })`:
48
+ * reports what React caught, with its component stack. Errors already reported
49
+ * (by a `MapleErrorBoundary`, say) are not reported twice.
50
+ */
51
+ function mapleReactErrorHandler(source = "react.root") {
52
+ return (error, info) => reportReactError(error, info.componentStack, source);
53
+ }
54
+ /** `/projects/:id/settings`, from the matched routes' own path segments. */
55
+ function reactRouterTemplate(state) {
56
+ let template = "";
57
+ for (const match of state.matches) {
58
+ const path = match.route.path;
59
+ if (!path) continue;
60
+ template = path.startsWith("/") ? path : `${template.replace(/\/$/, "")}/${path}`;
61
+ }
62
+ return template || "/";
63
+ }
64
+ /**
65
+ * Span each navigation of a React Router data router, from the moment it starts
66
+ * loading until its loaders settle, named by route template. The first is the
67
+ * page load. Returns an unsubscribe.
68
+ */
69
+ function instrumentReactRouter(router) {
70
+ let open = false;
71
+ let pathname = router.state.location.pathname;
72
+ const start = (path) => {
73
+ MapleBrowser.startNavigation(path);
74
+ open = true;
75
+ };
76
+ const end = (state) => {
77
+ if (!open) return;
78
+ MapleBrowser.endNavigation(reactRouterTemplate(state));
79
+ open = false;
80
+ };
81
+ start(pathname);
82
+ if (router.state.initialized && router.state.navigation.state === "idle") end(router.state);
83
+ return router.subscribe((state) => {
84
+ const target = state.navigation.location?.pathname;
85
+ if (state.navigation.state !== "idle" && target !== void 0) {
86
+ if (target !== pathname) start(target);
87
+ pathname = target;
88
+ return;
89
+ }
90
+ if (!state.initialized) return;
91
+ if (!open && state.location.pathname !== pathname) start(state.location.pathname);
92
+ pathname = state.location.pathname;
93
+ end(state);
94
+ });
95
+ }
96
+ /**
97
+ * Span each TanStack Router navigation from `onBeforeNavigate` to `onResolved`,
98
+ * named by the leaf route's full path (`/projects/$projectId`). Search-only
99
+ * changes are not navigations. The first is the page load. Returns an unsubscribe.
100
+ */
101
+ function instrumentTanStackRouter(router) {
102
+ /** The path of the navigation in flight, if any. */
103
+ let open;
104
+ const template = () => {
105
+ const leaf = router.state.matches.at(-1);
106
+ return leaf?.fullPath || leaf?.routeId || "/";
107
+ };
108
+ open = router.state.location.pathname;
109
+ MapleBrowser.startNavigation(open);
110
+ if (router.state.status === "idle" && router.state.matches.length > 0) {
111
+ MapleBrowser.endNavigation(template());
112
+ open = void 0;
113
+ }
114
+ const stopBefore = router.subscribe("onBeforeNavigate", (event) => {
115
+ if (!event.pathChanged || event.toLocation.pathname === open) return;
116
+ open = event.toLocation.pathname;
117
+ MapleBrowser.startNavigation(open);
118
+ });
119
+ const stopResolved = router.subscribe("onResolved", () => {
120
+ if (open === void 0) return;
121
+ MapleBrowser.endNavigation(template());
122
+ open = void 0;
123
+ });
124
+ return () => {
125
+ stopBefore();
126
+ stopResolved();
127
+ };
128
+ }
129
+ //#endregion
130
+ export { MapleErrorBoundary, instrumentReactRouter, instrumentTanStackRouter, mapleReactErrorHandler };