@ciridae/ui 1.0.2 → 1.0.4
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/dist/app/auth.d.ts +10 -0
- package/dist/app/auth.js +60 -0
- package/dist/app/error-boundary.d.ts +18 -0
- package/dist/app/error-boundary.js +45 -0
- package/dist/app/feedback.d.ts +12 -0
- package/dist/app/feedback.js +128 -0
- package/dist/app/index.d.ts +8 -0
- package/dist/app/index.js +8 -0
- package/dist/app/request-errors.d.ts +63 -0
- package/dist/app/request-errors.js +172 -0
- package/dist/app/sentry.d.ts +18 -0
- package/dist/app/sentry.js +52 -0
- package/dist/app/telemetry.d.ts +7 -0
- package/dist/app/telemetry.js +28 -0
- package/dist/app/trail.d.ts +9 -0
- package/dist/app/trail.js +18 -0
- package/dist/chat/chat-query-keys.d.ts +3 -3
- package/dist/chat/contract.d.ts +15 -15
- package/dist/chat/conversation-items-query-options.d.ts +114 -114
- package/dist/chat/conversation-source.d.ts +89 -0
- package/dist/chat/conversation-source.js +1 -0
- package/dist/chat/describe-tool-call.d.ts +3 -3
- package/dist/chat/describe-tool-call.js +3 -3
- package/dist/chat/message-text.d.ts +1 -1
- package/dist/chat/message-text.js +116 -48
- package/dist/chat/use-conversation-list.d.ts +6 -6
- package/dist/ciridae.css +16 -4
- package/dist/components/activity-feed.d.ts +3 -2
- package/dist/components/activity-feed.js +1 -0
- package/dist/components/agent-work.d.ts +3 -3
- package/dist/components/app-frame.d.ts +91 -0
- package/dist/components/app-frame.js +134 -0
- package/dist/components/app-navigation.d.ts +30 -3
- package/dist/components/app-navigation.js +105 -14
- package/dist/components/badge.d.ts +30 -5
- package/dist/components/badge.js +134 -15
- package/dist/components/brand-icons.d.ts +3 -3
- package/dist/components/bulk-action-bar.d.ts +1 -1
- package/dist/components/bulk-action-bar.js +3 -3
- package/dist/components/button.d.ts +12 -5
- package/dist/components/button.js +77 -44
- package/dist/components/calendar.d.ts +1 -1
- package/dist/components/calendar.js +44 -17
- package/dist/components/centered-page.d.ts +1 -1
- package/dist/components/centered-page.js +2 -2
- package/dist/components/chart.d.ts +22 -2
- package/dist/components/chart.js +251 -51
- package/dist/components/chat-client-source.d.ts +16 -0
- package/dist/components/chat-client-source.js +297 -0
- package/dist/components/chat-page.d.ts +9 -8
- package/dist/components/chat-page.js +7 -11
- package/dist/components/chat-sheet-controller.d.ts +11 -14
- package/dist/components/chat-sheet-controller.js +3 -3
- package/dist/components/chat-sheet.d.ts +18 -11
- package/dist/components/chat-sheet.js +188 -289
- package/dist/components/chat.d.ts +109 -19
- package/dist/components/chat.js +520 -124
- package/dist/components/checkbox-item.d.ts +6 -3
- package/dist/components/checkbox-item.js +9 -5
- package/dist/components/checkbox-list.d.ts +1 -1
- package/dist/components/chip.d.ts +6 -1
- package/dist/components/chip.js +27 -1
- package/dist/components/collapsible.d.ts +1 -1
- package/dist/components/color-theme.d.ts +1 -1
- package/dist/components/column-settings.d.ts +1 -1
- package/dist/components/column-settings.js +1 -1
- package/dist/components/command-menu.d.ts +5 -1
- package/dist/components/command-menu.js +98 -32
- package/dist/components/command-palette.d.ts +7 -3
- package/dist/components/command-palette.js +20 -11
- package/dist/components/copyable-id.d.ts +22 -5
- package/dist/components/copyable-id.js +46 -6
- package/dist/components/data-display.d.ts +39 -8
- package/dist/components/data-display.js +138 -19
- package/dist/components/data-grid.d.ts +23 -11
- package/dist/components/data-grid.js +61 -26
- package/dist/components/date-range-picker.d.ts +1 -1
- package/dist/components/date-range-picker.js +9 -20
- package/dist/components/date-time.d.ts +1 -1
- package/dist/components/dialog-height-context.d.ts +1 -1
- package/dist/components/document-viewer.d.ts +2 -2
- package/dist/components/drawer.d.ts +11 -6
- package/dist/components/drawer.js +91 -54
- package/dist/components/dropzone.d.ts +4 -4
- package/dist/components/editable-field.d.ts +12 -3
- package/dist/components/editable-field.js +57 -27
- package/dist/components/email-input.d.ts +1 -1
- package/dist/components/email-input.js +6 -5
- package/dist/components/environment-frame.d.ts +2 -2
- package/dist/components/environment-frame.js +37 -6
- package/dist/components/expandable-text.d.ts +1 -1
- package/dist/components/feedback-screenshot.d.ts +1 -1
- package/dist/components/feedback-sheet.d.ts +18 -3
- package/dist/components/feedback-sheet.js +216 -55
- package/dist/components/field-context.d.ts +1 -1
- package/dist/components/filter-bar.d.ts +3 -3
- package/dist/components/filter-bar.js +43 -29
- package/dist/components/form-controls.d.ts +12 -3
- package/dist/components/form-controls.js +46 -22
- package/dist/components/form-layout.d.ts +14 -10
- package/dist/components/form-layout.js +68 -23
- package/dist/components/glass-menu.d.ts +3 -0
- package/dist/components/glass-menu.js +63 -0
- package/dist/components/gradient-edge.d.ts +9 -0
- package/dist/components/gradient-edge.js +31 -0
- package/dist/components/hotkeys.d.ts +2 -2
- package/dist/components/icon.d.ts +1 -1
- package/dist/components/input.d.ts +7 -5
- package/dist/components/input.js +29 -17
- package/dist/components/keyboard-shortcuts.d.ts +2 -2
- package/dist/components/keycap.d.ts +10 -6
- package/dist/components/keycap.js +57 -19
- package/dist/components/layout.d.ts +13 -13
- package/dist/components/lightbox.d.ts +9 -3
- package/dist/components/lightbox.js +13 -9
- package/dist/components/link.d.ts +1 -1
- package/dist/components/locale.d.ts +42 -2
- package/dist/components/locale.js +43 -3
- package/dist/components/log-entry.d.ts +40 -0
- package/dist/components/log-entry.js +146 -0
- package/dist/components/log-list.d.ts +3 -3
- package/dist/components/mention-input.d.ts +2 -2
- package/dist/components/mention-input.js +4 -3
- package/dist/components/menu.d.ts +7 -3
- package/dist/components/menu.js +34 -37
- package/dist/components/meter.d.ts +10 -2
- package/dist/components/meter.js +64 -6
- package/dist/components/navigation.d.ts +21 -4
- package/dist/components/navigation.js +84 -19
- package/dist/components/notification-tray.d.ts +1 -1
- package/dist/components/notification-tray.js +5 -4
- package/dist/components/overlays.d.ts +6 -4
- package/dist/components/overlays.js +69 -40
- package/dist/components/page-frame.d.ts +4 -4
- package/dist/components/page-frame.js +1 -1
- package/dist/components/panel.d.ts +4 -4
- package/dist/components/panel.js +18 -12
- package/dist/components/profile-header.d.ts +1 -1
- package/dist/components/progress.d.ts +1 -1
- package/dist/components/project-avatar.d.ts +1 -1
- package/dist/components/radio-card.d.ts +2 -2
- package/dist/components/radio-card.js +10 -9
- package/dist/components/record.d.ts +29 -5
- package/dist/components/record.js +147 -18
- package/dist/components/resolved-file-url.d.ts +19 -0
- package/dist/components/resolved-file-url.js +49 -0
- package/dist/components/scroll-area.d.ts +1 -1
- package/dist/components/secret-dialog.d.ts +1 -1
- package/dist/components/segmented-bar.d.ts +4 -2
- package/dist/components/segmented-bar.js +16 -6
- package/dist/components/select.d.ts +9 -6
- package/dist/components/select.js +103 -127
- package/dist/components/settings.d.ts +4 -4
- package/dist/components/shell-skeleton.d.ts +1 -1
- package/dist/components/shell-skeleton.js +1 -1
- package/dist/components/shell.d.ts +21 -21
- package/dist/components/shell.js +43 -43
- package/dist/components/side-panel.d.ts +3 -3
- package/dist/components/sidebar.d.ts +13 -8
- package/dist/components/sidebar.js +55 -27
- package/dist/components/single-line-text.d.ts +2 -0
- package/dist/components/single-line-text.js +16 -0
- package/dist/components/site-banner.d.ts +5 -2
- package/dist/components/site-banner.js +17 -6
- package/dist/components/skeleton.d.ts +8 -7
- package/dist/components/skeleton.js +28 -16
- package/dist/components/skeletons.d.ts +17 -5
- package/dist/components/skeletons.js +71 -8
- package/dist/components/stages.d.ts +83 -11
- package/dist/components/stages.js +512 -156
- package/dist/components/stat-card.d.ts +12 -4
- package/dist/components/stat-card.js +92 -37
- package/dist/components/status-banner.d.ts +1 -1
- package/dist/components/status-banner.js +1 -1
- package/dist/components/status.d.ts +2 -2
- package/dist/components/steps.d.ts +1 -1
- package/dist/components/switch.d.ts +1 -1
- package/dist/components/tabs.d.ts +8 -8
- package/dist/components/tabs.js +26 -13
- package/dist/components/theme-customizer.d.ts +2 -2
- package/dist/components/theme-toggle.d.ts +4 -0
- package/dist/components/theme-toggle.js +67 -0
- package/dist/components/toast.d.ts +10 -3
- package/dist/components/toast.js +78 -29
- package/dist/components/use-new-version-available.d.ts +6 -0
- package/dist/components/use-new-version-available.js +29 -0
- package/dist/components/user-chip.d.ts +3 -3
- package/dist/components/wizard.d.ts +2 -2
- package/dist/feedback/contract.d.ts +136 -136
- package/dist/feedback/feedback-controller.d.ts +6 -6
- package/dist/feedback/generated/zod.gen.d.ts +28 -28
- package/dist/feedback/operator-contract.d.ts +11 -11
- package/dist/feedback/operator-controller.d.ts +32 -13
- package/dist/feedback/operator-controller.js +44 -11
- package/dist/preset.js +289 -33
- package/dist/styled-system/styles.css +3294 -1079
- package/dist/styled-system/tokens/index.mjs +598 -22
- package/dist/styled-system/tokens/tokens.d.ts +11 -11
- package/dist/styled-system/types/prop-type.d.ts +2 -2
- package/dist/styles.css +16 -4
- package/dist/theme/color.d.ts +3 -0
- package/dist/theme/color.js +5 -0
- package/dist/theme/derive.d.ts +9 -2
- package/dist/theme/derive.js +72 -12
- package/dist/theme/palettes.d.ts +49 -6
- package/dist/theme/palettes.js +84 -15
- package/dist/theme/tokens.d.ts +3 -3
- package/dist/theme/tokens.js +107 -32
- package/package.json +40 -3
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Renders children for a signed-in session and sends everyone else to the auth service. */
|
|
3
|
+
export declare function RequireSignIn({ children, loading, }: {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
loading?: ReactNode;
|
|
6
|
+
}): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
7
|
+
/** Where the auth service returns the reader; finishes sign-in and goes back to where they were. */
|
|
8
|
+
export declare function SignInCallbackPage({ loading }: {
|
|
9
|
+
loading?: ReactNode;
|
|
10
|
+
}): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null;
|
package/dist/app/auth.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { parseSameOriginPath } from "@ciridae/auth/client";
|
|
3
|
+
import { useCiridaeAuth, useCompleteCiridaeSignIn } from "@ciridae/auth/react";
|
|
4
|
+
import { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
6
|
+
import { CiridaeMark } from "../components/brand-icons.js";
|
|
7
|
+
import { icon, Spinner } from "../components/icon.js";
|
|
8
|
+
import { Stack } from "../components/layout.js";
|
|
9
|
+
import { EmptyState } from "../components/navigation.js";
|
|
10
|
+
function SignInLoading() {
|
|
11
|
+
const [visible, setVisible] = useState(false);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const timeout = window.setTimeout(() => setVisible(true), 300);
|
|
14
|
+
return () => window.clearTimeout(timeout);
|
|
15
|
+
}, []);
|
|
16
|
+
if (!visible)
|
|
17
|
+
return null;
|
|
18
|
+
return (_jsxs(Stack, { role: "status", "aria-label": "Signing in", gap: "16", style: { minHeight: "100dvh", alignItems: "center", justifyContent: "center" }, children: [_jsx(CiridaeMark, { className: icon({ size: "display" }) }), _jsx(Spinner, {})] }));
|
|
19
|
+
}
|
|
20
|
+
/** Renders children for a signed-in session and sends everyone else to the auth service. */
|
|
21
|
+
export function RequireSignIn({ children, loading = _jsx(SignInLoading, {}), }) {
|
|
22
|
+
const { auth, session } = useCiridaeAuth();
|
|
23
|
+
const signInUrl = session.status === "signedOut"
|
|
24
|
+
? auth.getSignInUrl(window.location.pathname + window.location.search)
|
|
25
|
+
: null;
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (signInUrl)
|
|
28
|
+
window.location.assign(signInUrl);
|
|
29
|
+
}, [signInUrl]);
|
|
30
|
+
if (session.status === "signedIn")
|
|
31
|
+
return children;
|
|
32
|
+
if (session.status === "unavailable" || (session.status === "signedOut" && !signInUrl)) {
|
|
33
|
+
return _jsx(EmptyState, { title: "Sign-in unavailable" });
|
|
34
|
+
}
|
|
35
|
+
return loading;
|
|
36
|
+
}
|
|
37
|
+
const signInFailures = {
|
|
38
|
+
failed: "Sign-in failed",
|
|
39
|
+
missingToken: "No sign-in session found",
|
|
40
|
+
authUnavailable: "Sign-in unavailable",
|
|
41
|
+
};
|
|
42
|
+
/** Where the auth service returns the reader; finishes sign-in and goes back to where they were. */
|
|
43
|
+
export function SignInCallbackPage({ loading = _jsx(SignInLoading, {}) }) {
|
|
44
|
+
const navigate = useNavigate();
|
|
45
|
+
const parameters = new URLSearchParams(useLocation().search);
|
|
46
|
+
const onComplete = useCallback((path) => void navigate(path, { replace: true }), [navigate]);
|
|
47
|
+
const state = useCompleteCiridaeSignIn({
|
|
48
|
+
customToken: parameters.get("customToken"),
|
|
49
|
+
onComplete,
|
|
50
|
+
redirectPath: parseSameOriginPath(parameters.get("redirect_path")),
|
|
51
|
+
});
|
|
52
|
+
const failure = signInFailures[state.status];
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!failure)
|
|
55
|
+
return;
|
|
56
|
+
const timeout = window.setTimeout(() => void navigate("/", { replace: true }), 3000);
|
|
57
|
+
return () => window.clearTimeout(timeout);
|
|
58
|
+
}, [failure, navigate]);
|
|
59
|
+
return failure ? _jsx(EmptyState, { title: failure }) : loading;
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CiridaeTelemetryContext } from "@ciridae/telemetry/react";
|
|
2
|
+
import { Component, type ContextType, type ErrorInfo, type ReactNode } from "react";
|
|
3
|
+
type ErrorBoundaryState = {
|
|
4
|
+
hasError: boolean;
|
|
5
|
+
errorId: string | null;
|
|
6
|
+
};
|
|
7
|
+
/** Reports a render crash to Sentry and PostHog and offers a reload instead of a blank page. */
|
|
8
|
+
export declare class AppErrorBoundary extends Component<{
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}, ErrorBoundaryState> {
|
|
11
|
+
static contextType: typeof CiridaeTelemetryContext;
|
|
12
|
+
context: ContextType<typeof CiridaeTelemetryContext>;
|
|
13
|
+
state: ErrorBoundaryState;
|
|
14
|
+
static getDerivedStateFromError(): ErrorBoundaryState;
|
|
15
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
16
|
+
render(): ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CiridaeTelemetryContext } from "@ciridae/telemetry/react";
|
|
3
|
+
import { WarningCircleIcon } from "@phosphor-icons/react";
|
|
4
|
+
import { Component } from "react";
|
|
5
|
+
import { Button } from "../components/button.js";
|
|
6
|
+
import { CopyableId } from "../components/copyable-id.js";
|
|
7
|
+
import { icon } from "../components/icon.js";
|
|
8
|
+
import { Cluster, Stack } from "../components/layout.js";
|
|
9
|
+
import { EmptyState } from "../components/navigation.js";
|
|
10
|
+
import { captureExceptionInSentry } from "./sentry.js";
|
|
11
|
+
/** Reports a render crash to Sentry and PostHog and offers a reload instead of a blank page. */
|
|
12
|
+
export class AppErrorBoundary extends Component {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
this.state = { hasError: false, errorId: null };
|
|
16
|
+
}
|
|
17
|
+
static getDerivedStateFromError() {
|
|
18
|
+
return { hasError: true, errorId: null };
|
|
19
|
+
}
|
|
20
|
+
componentDidCatch(error, info) {
|
|
21
|
+
const componentStack = info.componentStack ?? "";
|
|
22
|
+
void captureExceptionInSentry(error, componentStack).then((errorId) => {
|
|
23
|
+
if (errorId)
|
|
24
|
+
this.setState({ errorId });
|
|
25
|
+
});
|
|
26
|
+
const telemetry = this.context;
|
|
27
|
+
if (!telemetry)
|
|
28
|
+
return;
|
|
29
|
+
if (telemetry.isInitialized()) {
|
|
30
|
+
telemetry.captureException(error, { componentStack });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// A crash on first render lands before PostHog starts, so it waits until it has.
|
|
34
|
+
const unsubscribe = telemetry.subscribeToInitialization(() => {
|
|
35
|
+
unsubscribe();
|
|
36
|
+
telemetry.captureException(error, { componentStack });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
render() {
|
|
40
|
+
if (!this.state.hasError)
|
|
41
|
+
return this.props.children;
|
|
42
|
+
return (_jsx(EmptyState, { icon: _jsx(WarningCircleIcon, { className: icon({ size: "large", tone: "danger" }), weight: "fill" }), title: "Something went wrong", action: _jsxs(Stack, { gap: "8", children: [this.state.errorId && _jsx(CopyableId, { label: "Error ID", value: this.state.errorId }), _jsxs(Cluster, { gap: "8", children: [_jsx(Button, { onClick: () => window.location.reload(), children: "Reload" }), _jsx(Button, { variant: "ghost", nativeButton: false, render: _jsx("a", { href: "https://statuspage.incident.io/ciridae", target: "_blank", rel: "noreferrer" }), children: "Status page" })] })] }) }));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
AppErrorBoundary.contextType = CiridaeTelemetryContext;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The Crucible project environment whose feedback inbox receives this app's feedback. */
|
|
2
|
+
export type FeedbackScope = {
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
projectId: string;
|
|
5
|
+
environment: string;
|
|
6
|
+
};
|
|
7
|
+
/** Sends feedback about the app and follows up on earlier submissions. */
|
|
8
|
+
export declare function FeedbackDialog({ scope, open, onOpenChange, }: {
|
|
9
|
+
scope: FeedbackScope;
|
|
10
|
+
open: boolean;
|
|
11
|
+
onOpenChange: (open: boolean) => void;
|
|
12
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCiridaeAuth } from "@ciridae/auth/react";
|
|
3
|
+
import { useCiridaeTelemetry } from "@ciridae/telemetry/react";
|
|
4
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { useMemo, useState } from "react";
|
|
6
|
+
import { CopyableId } from "../components/copyable-id.js";
|
|
7
|
+
import { FeedbackSheet, formatFeedbackDescription, } from "../components/feedback-sheet.js";
|
|
8
|
+
import { useToast } from "../components/toast.js";
|
|
9
|
+
import { createFeedbackClient } from "../feedback/client.js";
|
|
10
|
+
import { feedbackMaxLengths } from "../feedback/contract.js";
|
|
11
|
+
import { errorTraceId, reportedInlineMeta } from "./request-errors.js";
|
|
12
|
+
const feedbackTypeByKind = {
|
|
13
|
+
wrong: "bug",
|
|
14
|
+
idea: "feature",
|
|
15
|
+
question: "general",
|
|
16
|
+
};
|
|
17
|
+
/** Sends feedback about the app and follows up on earlier submissions. */
|
|
18
|
+
export function FeedbackDialog({ scope, open, onOpenChange, }) {
|
|
19
|
+
const { auth, session } = useCiridaeAuth();
|
|
20
|
+
const user = session.status === "signedIn" || session.status === "signingOut" ? session.user : null;
|
|
21
|
+
const { apiUrl, projectId, environment } = scope;
|
|
22
|
+
const client = useMemo(() => createFeedbackClient({
|
|
23
|
+
baseUrl: `${apiUrl.replace(/\/$/, "")}/v1/projects/${encodeURIComponent(projectId)}/environments/${encodeURIComponent(environment)}`,
|
|
24
|
+
getBearerToken: async () => {
|
|
25
|
+
const token = await auth.getAccessToken();
|
|
26
|
+
if (!token)
|
|
27
|
+
throw new Error("Sign in again to send feedback.");
|
|
28
|
+
return token;
|
|
29
|
+
},
|
|
30
|
+
}), [auth, apiUrl, projectId, environment]);
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
const toast = useToast();
|
|
33
|
+
const telemetry = useCiridaeTelemetry();
|
|
34
|
+
const [idempotencyKey, setIdempotencyKey] = useState(() => crypto.randomUUID());
|
|
35
|
+
const [openItemId, setOpenItemId] = useState(null);
|
|
36
|
+
const queryKey = ["feedback", user?.uid ?? ""];
|
|
37
|
+
const items = useQuery({
|
|
38
|
+
meta: reportedInlineMeta,
|
|
39
|
+
queryKey,
|
|
40
|
+
queryFn: ({ signal }) => client.list({ signal }),
|
|
41
|
+
enabled: open && user !== null,
|
|
42
|
+
retry: false,
|
|
43
|
+
});
|
|
44
|
+
const item = useQuery({
|
|
45
|
+
meta: reportedInlineMeta,
|
|
46
|
+
queryKey: [...queryKey, openItemId],
|
|
47
|
+
queryFn: ({ signal }) => client.get(openItemId ?? "", { signal }),
|
|
48
|
+
enabled: openItemId !== null,
|
|
49
|
+
retry: false,
|
|
50
|
+
});
|
|
51
|
+
// Resolves once the refetched list and item carry the change, so the sheet never shows stale.
|
|
52
|
+
const refresh = () => queryClient.invalidateQueries({ queryKey });
|
|
53
|
+
const create = useMutation({
|
|
54
|
+
meta: reportedInlineMeta,
|
|
55
|
+
mutationFn: (values) => {
|
|
56
|
+
const type = feedbackTypeByKind[values.kind];
|
|
57
|
+
return client.create({
|
|
58
|
+
type,
|
|
59
|
+
subject: subjectFromMessage(values.whatHappened),
|
|
60
|
+
description: formatFeedbackDescription(values),
|
|
61
|
+
urgency: "normal",
|
|
62
|
+
}, { idempotencyKey, files: values.attachments });
|
|
63
|
+
},
|
|
64
|
+
onSuccess: (createdFeedback, values) => {
|
|
65
|
+
telemetry.capture("feedback_submitted", {
|
|
66
|
+
source: "app_shell",
|
|
67
|
+
type: feedbackTypeByKind[values.kind],
|
|
68
|
+
urgency: createdFeedback.urgency,
|
|
69
|
+
});
|
|
70
|
+
void refresh();
|
|
71
|
+
toast.add({ variant: "success", title: "Feedback sent" });
|
|
72
|
+
setIdempotencyKey(crypto.randomUUID());
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const reply = useMutation({
|
|
76
|
+
meta: reportedInlineMeta,
|
|
77
|
+
mutationFn: ({ feedbackId, body }) => client.createMessage(feedbackId, body),
|
|
78
|
+
onSuccess: () => {
|
|
79
|
+
toast.add({ variant: "success", title: "Reply sent" });
|
|
80
|
+
return refresh();
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const withdraw = useMutation({
|
|
84
|
+
meta: reportedInlineMeta,
|
|
85
|
+
mutationFn: (feedbackId) => client.withdraw(feedbackId),
|
|
86
|
+
onSuccess: () => refresh(),
|
|
87
|
+
});
|
|
88
|
+
if (!user)
|
|
89
|
+
return null;
|
|
90
|
+
return (_jsx(FeedbackSheet, { open: open, onOpenChange: onOpenChange,
|
|
91
|
+
// The page the sheet is over; it cannot change while the sheet is open.
|
|
92
|
+
where: {
|
|
93
|
+
label: document.title || "This page",
|
|
94
|
+
detail: window.location.href,
|
|
95
|
+
href: window.location.href,
|
|
96
|
+
}, onSubmit: async (values) => {
|
|
97
|
+
const createdFeedback = await create.mutateAsync(values);
|
|
98
|
+
return { reference: createdFeedback.id };
|
|
99
|
+
}, errorDetail: (error) => {
|
|
100
|
+
const traceId = errorTraceId(error);
|
|
101
|
+
return traceId && _jsx(CopyableId, { label: "Error ID", value: traceId });
|
|
102
|
+
}, sent: {
|
|
103
|
+
items: loadState(items),
|
|
104
|
+
onOpenItem: setOpenItemId,
|
|
105
|
+
item: openItemId === null ? undefined : loadState(item),
|
|
106
|
+
onReply: async (feedbackId, body) => {
|
|
107
|
+
await reply.mutateAsync({ feedbackId, body });
|
|
108
|
+
},
|
|
109
|
+
onWithdraw: async (feedbackId) => {
|
|
110
|
+
await withdraw.mutateAsync(feedbackId);
|
|
111
|
+
},
|
|
112
|
+
} }));
|
|
113
|
+
}
|
|
114
|
+
/** The query as the sheet reads it: loading until its first answer, then its data or failure. */
|
|
115
|
+
function loadState(query) {
|
|
116
|
+
if (query.isPending)
|
|
117
|
+
return { status: "loading" };
|
|
118
|
+
if (query.isError)
|
|
119
|
+
return { status: "error", error: query.error, retry: () => void query.refetch() };
|
|
120
|
+
return { status: "loaded", data: query.data };
|
|
121
|
+
}
|
|
122
|
+
/** The message's first line, within the contract's subject length. */
|
|
123
|
+
function subjectFromMessage(message) {
|
|
124
|
+
const firstLine = message.split("\n", 1)[0] ?? message;
|
|
125
|
+
return firstLine.length > feedbackMaxLengths.subject
|
|
126
|
+
? `${firstLine.slice(0, feedbackMaxLengths.subject - 1)}…`
|
|
127
|
+
: firstLine;
|
|
128
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Opt-in integrations for Ciridae apps. The standalone UI/frame does not load these peers. */
|
|
2
|
+
export { RequireSignIn, SignInCallbackPage } from "./auth.js";
|
|
3
|
+
export { AppErrorBoundary } from "./error-boundary.js";
|
|
4
|
+
export { FeedbackDialog, type FeedbackScope } from "./feedback.js";
|
|
5
|
+
export { errorMessage, errorStatus, errorTraceId, QueryContent, reportedInline, reportedInlineMeta, requestErrorDescription, RequestErrorToasts, RequestFieldError, requestErrorToast, withResponseDetails, } from "./request-errors.js";
|
|
6
|
+
export { initializeSentry, setSentryOrganization } from "./sentry.js";
|
|
7
|
+
export { AppTelemetryProvider } from "./telemetry.js";
|
|
8
|
+
export { PageTrail } from "./trail.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Opt-in integrations for Ciridae apps. The standalone UI/frame does not load these peers. */
|
|
2
|
+
export { RequireSignIn, SignInCallbackPage } from "./auth.js";
|
|
3
|
+
export { AppErrorBoundary } from "./error-boundary.js";
|
|
4
|
+
export { FeedbackDialog } from "./feedback.js";
|
|
5
|
+
export { errorMessage, errorStatus, errorTraceId, QueryContent, reportedInline, reportedInlineMeta, requestErrorDescription, RequestErrorToasts, RequestFieldError, requestErrorToast, withResponseDetails, } from "./request-errors.js";
|
|
6
|
+
export { initializeSentry, setSentryOrganization } from "./sentry.js";
|
|
7
|
+
export { AppTelemetryProvider } from "./telemetry.js";
|
|
8
|
+
export { PageTrail } from "./trail.js";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type ToastOptions } from "../components/toast.js";
|
|
3
|
+
/** Normalize an unvalidated generated-client failure at the HTTP boundary. */
|
|
4
|
+
export declare function withResponseDetails(error: unknown, response: Response | undefined): Error;
|
|
5
|
+
/** The failed request's HTTP status, when it got a response. */
|
|
6
|
+
export declare function errorStatus(error: unknown): number | null;
|
|
7
|
+
/** The failed request's trace ID, which support looks the request up by. */
|
|
8
|
+
export declare function errorTraceId(error: unknown): string | null;
|
|
9
|
+
/** Human-readable message from a normalized request failure or another Error. */
|
|
10
|
+
export declare function errorMessage(error: unknown, fallback: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* What a failed request's notice says under its title: the request's message when the title does
|
|
13
|
+
* not already say it, and its error ID to copy for support.
|
|
14
|
+
*/
|
|
15
|
+
export declare function requestErrorDescription(error: unknown, title?: string): ReactNode;
|
|
16
|
+
/** An error toast for a failed request, carrying its error ID to copy for support. */
|
|
17
|
+
export declare function requestErrorToast(error: unknown, title?: string): ToastOptions;
|
|
18
|
+
/**
|
|
19
|
+
* The `meta` of a query or mutation whose failure is reported where it is used: a banner or field
|
|
20
|
+
* error on screen names it, so `RequestErrorToasts` does not toast it as well.
|
|
21
|
+
*/
|
|
22
|
+
export declare const reportedInlineMeta: {
|
|
23
|
+
reportedInline: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** Built options, such as a generated client's, marked with `reportedInlineMeta`. */
|
|
26
|
+
export declare function reportedInline<Options extends {
|
|
27
|
+
meta?: Record<string, unknown>;
|
|
28
|
+
}>(options: Options): Options;
|
|
29
|
+
/**
|
|
30
|
+
* Toasts a failed query or mutation with the request's error ID, unless something on screen
|
|
31
|
+
* reports it: a mounted query or a mutation whose options are wrapped in `reportedInline`. A page
|
|
32
|
+
* shows its own query failures in place and a form its own save failures, so the toast is only for
|
|
33
|
+
* a failure nothing on screen owns. An app can also exempt expected query errors such as an empty
|
|
34
|
+
* metrics state.
|
|
35
|
+
*/
|
|
36
|
+
export declare function RequestErrorToasts({ isExpectedQueryError, }: {
|
|
37
|
+
isExpectedQueryError?: (error: unknown) => boolean;
|
|
38
|
+
}): null;
|
|
39
|
+
/**
|
|
40
|
+
* A query's three states in place: its loading placeholder, an error banner titled
|
|
41
|
+
* `unavailableTitle` with the request's message and error ID, or its loaded data. Build the query
|
|
42
|
+
* with `reportedInline` so the failure is not toasted as well. Data loaded before a failed refetch
|
|
43
|
+
* stays shown under the banner.
|
|
44
|
+
*/
|
|
45
|
+
export declare function QueryContent<Data>({ query, loading, unavailableTitle, children, }: {
|
|
46
|
+
query: {
|
|
47
|
+
data: Data | undefined;
|
|
48
|
+
isError: boolean;
|
|
49
|
+
error: unknown;
|
|
50
|
+
};
|
|
51
|
+
/** Drawn at the loaded content's size, such as a table or list with its `loading` prop. */
|
|
52
|
+
loading: ReactNode;
|
|
53
|
+
unavailableTitle: string;
|
|
54
|
+
children: (data: Data) => ReactNode;
|
|
55
|
+
}): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* A failed request's message, or `fallback` when it has none, as a form's field error, with its
|
|
58
|
+
* error ID to copy for support. For a mutation built with `reportedInline`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function RequestFieldError({ error, fallback }: {
|
|
61
|
+
error: unknown;
|
|
62
|
+
fallback: string;
|
|
63
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { CopyableId } from "../components/copyable-id.js";
|
|
5
|
+
import { FieldError } from "../components/form-layout.js";
|
|
6
|
+
import { Stack } from "../components/layout.js";
|
|
7
|
+
import { StatusBanner } from "../components/status-banner.js";
|
|
8
|
+
import { useToast } from "../components/toast.js";
|
|
9
|
+
class RequestError extends Error {
|
|
10
|
+
constructor(message, status, traceId) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.traceId = traceId;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function messageFromResponse(error) {
|
|
17
|
+
if (error instanceof Error)
|
|
18
|
+
return error.message;
|
|
19
|
+
if (typeof error === "string")
|
|
20
|
+
return error;
|
|
21
|
+
if (!error || typeof error !== "object")
|
|
22
|
+
return "";
|
|
23
|
+
// OAuth endpoints answer with `{error, error_description}` rather than `detail`.
|
|
24
|
+
if ("error_description" in error && typeof error.error_description === "string")
|
|
25
|
+
return error.error_description;
|
|
26
|
+
if (!("detail" in error))
|
|
27
|
+
return "";
|
|
28
|
+
if (typeof error.detail === "string")
|
|
29
|
+
return error.detail;
|
|
30
|
+
// Domain conflicts answer with `detail: {error, message}`.
|
|
31
|
+
if (error.detail &&
|
|
32
|
+
typeof error.detail === "object" &&
|
|
33
|
+
"message" in error.detail &&
|
|
34
|
+
typeof error.detail.message === "string")
|
|
35
|
+
return error.detail.message;
|
|
36
|
+
if (!Array.isArray(error.detail))
|
|
37
|
+
return "";
|
|
38
|
+
return error.detail
|
|
39
|
+
.map((item) => item && typeof item === "object" && "msg" in item && typeof item.msg === "string"
|
|
40
|
+
? item.msg
|
|
41
|
+
: "")
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.join("; ");
|
|
44
|
+
}
|
|
45
|
+
/** Normalize an unvalidated generated-client failure at the HTTP boundary. */
|
|
46
|
+
export function withResponseDetails(error, response) {
|
|
47
|
+
if (!response && error instanceof Error)
|
|
48
|
+
return error;
|
|
49
|
+
return new RequestError(messageFromResponse(error), response?.status ?? null, response?.headers.get("X-Trace-ID") ?? null);
|
|
50
|
+
}
|
|
51
|
+
/** The failed request's HTTP status, when it got a response. */
|
|
52
|
+
export function errorStatus(error) {
|
|
53
|
+
if (error instanceof RequestError)
|
|
54
|
+
return error.status;
|
|
55
|
+
return error instanceof Error && "status" in error && typeof error.status === "number"
|
|
56
|
+
? error.status
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
/** The failed request's trace ID, which support looks the request up by. */
|
|
60
|
+
export function errorTraceId(error) {
|
|
61
|
+
if (error instanceof RequestError)
|
|
62
|
+
return error.traceId;
|
|
63
|
+
return error instanceof Error && "errorId" in error && typeof error.errorId === "string"
|
|
64
|
+
? error.errorId
|
|
65
|
+
: null;
|
|
66
|
+
}
|
|
67
|
+
/** Human-readable message from a normalized request failure or another Error. */
|
|
68
|
+
export function errorMessage(error, fallback) {
|
|
69
|
+
return error instanceof Error && error.message ? error.message : fallback;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* What a failed request's notice says under its title: the request's message when the title does
|
|
73
|
+
* not already say it, and its error ID to copy for support.
|
|
74
|
+
*/
|
|
75
|
+
export function requestErrorDescription(error, title) {
|
|
76
|
+
const traceId = errorTraceId(error);
|
|
77
|
+
const message = errorMessage(error, "");
|
|
78
|
+
const description = message && message !== title ? message : null;
|
|
79
|
+
return description || traceId ? (_jsxs(_Fragment, { children: [description, description && traceId && _jsx("br", {}), traceId && _jsx(CopyableId, { label: "Error ID", value: traceId })] })) : undefined;
|
|
80
|
+
}
|
|
81
|
+
/** An error toast for a failed request, carrying its error ID to copy for support. */
|
|
82
|
+
export function requestErrorToast(error, title) {
|
|
83
|
+
const shownTitle = title ?? errorMessage(error, "Request failed.");
|
|
84
|
+
return {
|
|
85
|
+
variant: "error",
|
|
86
|
+
title: shownTitle,
|
|
87
|
+
description: requestErrorDescription(error, shownTitle),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The `meta` of a query or mutation whose failure is reported where it is used: a banner or field
|
|
92
|
+
* error on screen names it, so `RequestErrorToasts` does not toast it as well.
|
|
93
|
+
*/
|
|
94
|
+
export const reportedInlineMeta = { reportedInline: true };
|
|
95
|
+
/** Built options, such as a generated client's, marked with `reportedInlineMeta`. */
|
|
96
|
+
export function reportedInline(options) {
|
|
97
|
+
return { ...options, meta: { ...options.meta, ...reportedInlineMeta } };
|
|
98
|
+
}
|
|
99
|
+
function isReportedInline(meta) {
|
|
100
|
+
return meta?.reportedInline === true;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Toasts a failed query or mutation with the request's error ID, unless something on screen
|
|
104
|
+
* reports it: a mounted query or a mutation whose options are wrapped in `reportedInline`. A page
|
|
105
|
+
* shows its own query failures in place and a form its own save failures, so the toast is only for
|
|
106
|
+
* a failure nothing on screen owns. An app can also exempt expected query errors such as an empty
|
|
107
|
+
* metrics state.
|
|
108
|
+
*/
|
|
109
|
+
export function RequestErrorToasts({ isExpectedQueryError, }) {
|
|
110
|
+
// useToast returns a new object each render; a ref keeps the subscription from restarting.
|
|
111
|
+
const toastApi = useToast();
|
|
112
|
+
const toast = useRef(toastApi);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
toast.current = toastApi;
|
|
115
|
+
});
|
|
116
|
+
const queryClient = useQueryClient();
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const showError = (error) => {
|
|
119
|
+
toast.current.add(requestErrorToast(error));
|
|
120
|
+
};
|
|
121
|
+
// A polled query that keeps failing toasts once, when it starts failing, not on every poll.
|
|
122
|
+
const failingQueries = new Set();
|
|
123
|
+
const stopQueries = queryClient.getQueryCache().subscribe((event) => {
|
|
124
|
+
if (event.type !== "updated")
|
|
125
|
+
return;
|
|
126
|
+
if (event.action.type === "error" && !failingQueries.has(event.query.queryHash)) {
|
|
127
|
+
failingQueries.add(event.query.queryHash);
|
|
128
|
+
if (isExpectedQueryError?.(event.action.error))
|
|
129
|
+
return;
|
|
130
|
+
// Any mounted reader that reports the failure in place owns it.
|
|
131
|
+
if (event.query.observers.some((observer) => isReportedInline(observer.options.meta)))
|
|
132
|
+
return;
|
|
133
|
+
showError(event.action.error);
|
|
134
|
+
}
|
|
135
|
+
else if (event.action.type === "success") {
|
|
136
|
+
failingQueries.delete(event.query.queryHash);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const stopMutations = queryClient.getMutationCache().subscribe((event) => {
|
|
140
|
+
if (event.type !== "updated" || event.action.type !== "error")
|
|
141
|
+
return;
|
|
142
|
+
if (isReportedInline(event.mutation.options.meta))
|
|
143
|
+
return;
|
|
144
|
+
showError(event.action.error);
|
|
145
|
+
});
|
|
146
|
+
return () => {
|
|
147
|
+
stopQueries();
|
|
148
|
+
stopMutations();
|
|
149
|
+
};
|
|
150
|
+
}, [queryClient, isExpectedQueryError]);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A query's three states in place: its loading placeholder, an error banner titled
|
|
155
|
+
* `unavailableTitle` with the request's message and error ID, or its loaded data. Build the query
|
|
156
|
+
* with `reportedInline` so the failure is not toasted as well. Data loaded before a failed refetch
|
|
157
|
+
* stays shown under the banner.
|
|
158
|
+
*/
|
|
159
|
+
export function QueryContent({ query, loading, unavailableTitle, children, }) {
|
|
160
|
+
const failure = query.isError && (_jsx(StatusBanner, { tone: "danger", title: unavailableTitle, description: requestErrorDescription(query.error, unavailableTitle) }));
|
|
161
|
+
if (query.data === undefined)
|
|
162
|
+
return failure || loading;
|
|
163
|
+
return (_jsxs(_Fragment, { children: [failure, children(query.data)] }));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A failed request's message, or `fallback` when it has none, as a form's field error, with its
|
|
167
|
+
* error ID to copy for support. For a mutation built with `reportedInline`.
|
|
168
|
+
*/
|
|
169
|
+
export function RequestFieldError({ error, fallback }) {
|
|
170
|
+
const traceId = errorTraceId(error);
|
|
171
|
+
return (_jsxs(Stack, { gap: "4", children: [_jsx(FieldError, { children: errorMessage(error, fallback) }), traceId && _jsx(CopyableId, { label: "Error ID", value: traceId })] }));
|
|
172
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TelemetryUser } from "@ciridae/telemetry/client";
|
|
2
|
+
export type SentryConfiguration = {
|
|
3
|
+
/** Reporting is off when the build has no DSN. */
|
|
4
|
+
dsn: string | undefined;
|
|
5
|
+
environment: string | undefined;
|
|
6
|
+
/** The commit the bundle was built from, so an error names the build it came from. */
|
|
7
|
+
release: string | undefined;
|
|
8
|
+
/** The API origin that receives trace headers. */
|
|
9
|
+
backendUrl: string | undefined;
|
|
10
|
+
};
|
|
11
|
+
/** Start Sentry before the first render so a crash during it is still reported. */
|
|
12
|
+
export declare function initializeSentry(configuration: SentryConfiguration): void;
|
|
13
|
+
export declare function setSentryUser(user: TelemetryUser | null): void;
|
|
14
|
+
export declare function setSentryOrganization(organization: {
|
|
15
|
+
id: string;
|
|
16
|
+
displayName: string;
|
|
17
|
+
} | null): void;
|
|
18
|
+
export declare function captureExceptionInSentry(error: unknown, componentStack: string): Promise<string | null>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
let sentryModule = null;
|
|
2
|
+
let enabled = false;
|
|
3
|
+
/** Start Sentry before the first render so a crash during it is still reported. */
|
|
4
|
+
export function initializeSentry(configuration) {
|
|
5
|
+
if (!configuration.dsn)
|
|
6
|
+
return;
|
|
7
|
+
enabled = true;
|
|
8
|
+
const { dsn, environment, release, backendUrl } = configuration;
|
|
9
|
+
withSentry((sentry) => {
|
|
10
|
+
const isProduction = environment === "prod";
|
|
11
|
+
sentry.init({
|
|
12
|
+
dsn,
|
|
13
|
+
environment,
|
|
14
|
+
release,
|
|
15
|
+
integrations: [
|
|
16
|
+
sentry.browserTracingIntegration(),
|
|
17
|
+
sentry.replayIntegration(),
|
|
18
|
+
sentry.httpClientIntegration(),
|
|
19
|
+
],
|
|
20
|
+
replaysOnErrorSampleRate: 1,
|
|
21
|
+
replaysSessionSampleRate: isProduction ? 0 : 1,
|
|
22
|
+
tracesSampleRate: isProduction ? 0.1 : 1,
|
|
23
|
+
tracePropagationTargets: backendUrl
|
|
24
|
+
? [new URL(backendUrl, window.location.origin).origin]
|
|
25
|
+
: [],
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function setSentryUser(user) {
|
|
30
|
+
withSentry((sentry) => sentry.setUser(user
|
|
31
|
+
? { id: user.id, email: user.email ?? undefined, username: user.name ?? undefined }
|
|
32
|
+
: null));
|
|
33
|
+
}
|
|
34
|
+
export function setSentryOrganization(organization) {
|
|
35
|
+
withSentry((sentry) => {
|
|
36
|
+
sentry.setContext("organization", organization);
|
|
37
|
+
sentry.setTag("organization_id", organization?.id);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function captureExceptionInSentry(error, componentStack) {
|
|
41
|
+
return withSentry((sentry) => sentry.captureException(error, { contexts: { react: { componentStack } } }));
|
|
42
|
+
}
|
|
43
|
+
// Sentry loads on first use so it stays out of the first paint.
|
|
44
|
+
function withSentry(use) {
|
|
45
|
+
if (!enabled)
|
|
46
|
+
return Promise.resolve(null);
|
|
47
|
+
sentryModule ?? (sentryModule = import("@sentry/react"));
|
|
48
|
+
return sentryModule.then(use).catch((error) => {
|
|
49
|
+
console.error("Sentry failed to load", error);
|
|
50
|
+
return null;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { CiridaeTelemetry } from "@ciridae/telemetry/client";
|
|
3
|
+
/** Identifies the signed-in reader to PostHog and Sentry, and clears them on sign-out. */
|
|
4
|
+
export declare function AppTelemetryProvider({ telemetry, children, }: {
|
|
5
|
+
telemetry: CiridaeTelemetry;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCiridaeAuth } from "@ciridae/auth/react";
|
|
3
|
+
import { CiridaeTelemetryProvider } from "@ciridae/telemetry/react";
|
|
4
|
+
import { useEffect, useMemo } from "react";
|
|
5
|
+
import { useLocation } from "react-router-dom";
|
|
6
|
+
import { setSentryUser } from "./sentry.js";
|
|
7
|
+
/** Identifies the signed-in reader to PostHog and Sentry, and clears them on sign-out. */
|
|
8
|
+
export function AppTelemetryProvider({ telemetry, children, }) {
|
|
9
|
+
const { session } = useCiridaeAuth();
|
|
10
|
+
const { pathname } = useLocation();
|
|
11
|
+
const user = useMemo(() => {
|
|
12
|
+
if (session.status === "signedIn" || session.status === "signingOut") {
|
|
13
|
+
return {
|
|
14
|
+
id: session.user.uid,
|
|
15
|
+
name: session.user.displayName,
|
|
16
|
+
email: session.user.email,
|
|
17
|
+
emailVerified: session.user.emailVerified,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// Auth that failed to load leaves no one signed in, so an earlier identity is cleared too.
|
|
21
|
+
return session.status === "signedOut" || session.status === "unavailable" ? null : undefined;
|
|
22
|
+
}, [session]);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (user !== undefined)
|
|
25
|
+
setSentryUser(user);
|
|
26
|
+
}, [user]);
|
|
27
|
+
return (_jsx(CiridaeTelemetryProvider, { pathname: pathname, telemetry: telemetry, user: user, children: children }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** A page's breadcrumbs for `AppFrame`'s title: links to where it sits, then the page itself. */
|
|
3
|
+
export declare function PageTrail({ parents, children, }: {
|
|
4
|
+
parents?: readonly {
|
|
5
|
+
label: string;
|
|
6
|
+
to: string;
|
|
7
|
+
}[];
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): import("react").JSX.Element;
|