@sentry/junior-dashboard 0.279.0 → 0.281.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/dist/app.js +105 -43
- package/dist/client-manifest.json +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +105 -43
- package/dist/public/chunks/AdminPage-M5RBVZFN.js +1 -0
- package/dist/public/chunks/AutomationPage-WALCREDT.js +3 -0
- package/dist/public/chunks/AutomationRunsPage-CV47E4UX.js +1 -0
- package/dist/public/chunks/AutomationsPage-57L3AQVJ.js +1 -0
- package/dist/public/chunks/CodePage-XJUYMFJH.js +1 -0
- package/dist/public/chunks/ConnectionsPage-3QQAGYWF.js +1 -0
- package/dist/public/chunks/DrawerContent-RYYS57ZH.js +1 -0
- package/dist/public/chunks/LocationDetailPage-4IL5HQML.js +1 -0
- package/dist/public/chunks/LocationsPage-D2DUQAHJ.js +1 -0
- package/dist/public/chunks/McpSettingsPage-3RT7JPEK.js +1 -0
- package/dist/public/chunks/PeoplePage-6F5GPQ7C.js +1 -0
- package/dist/public/chunks/PersonProfilePage-SPAYQPTD.js +1 -0
- package/dist/public/chunks/PersonalTokensPage-SHAUJ74M.js +1 -0
- package/dist/public/chunks/PluginUserPage-64FIWD7M.js +1 -0
- package/dist/public/chunks/RepositoryPage-L7IHOXSN.js +1 -0
- package/dist/public/chunks/SettingsPage-U7WG4FHE.js +1 -0
- package/dist/public/chunks/SystemPage-ZBC43OJH.js +1 -0
- package/dist/public/chunks/WorkspaceFormPage-3NVIIZ4P.js +2 -0
- package/dist/public/chunks/WorkspacesPage-G73HPBDD.js +1 -0
- package/dist/public/chunks/chunk-2BCS5AQL.js +1 -0
- package/dist/public/chunks/chunk-2J3LGF5Q.js +1 -0
- package/dist/public/chunks/chunk-2NNKHSZE.js +1 -0
- package/dist/public/chunks/chunk-3Y3ICT6K.js +1 -0
- package/dist/public/chunks/chunk-4AKUZW6R.js +1 -0
- package/dist/public/chunks/chunk-4TQSTCIN.js +13 -0
- package/dist/public/chunks/chunk-4UGZNVJM.js +1 -0
- package/dist/public/chunks/chunk-564V7YHG.js +1 -0
- package/dist/public/chunks/chunk-5ISD3QSC.js +1 -0
- package/dist/public/chunks/chunk-5LNGNJ3P.js +1 -0
- package/dist/public/chunks/chunk-5ZA674OU.js +1 -0
- package/dist/public/chunks/chunk-64GPEGGE.js +1 -0
- package/dist/public/chunks/chunk-6FHY65EQ.js +1 -0
- package/dist/public/chunks/chunk-6FSJ3W2W.js +1 -0
- package/dist/public/chunks/chunk-6PHJZPWD.js +19 -0
- package/dist/public/chunks/chunk-B5ZN3SWQ.js +14 -0
- package/dist/public/chunks/chunk-BKOACADP.js +1 -0
- package/dist/public/chunks/chunk-CQY6BYUB.js +1 -0
- package/dist/public/chunks/chunk-CR2ICLSH.js +1 -0
- package/dist/public/chunks/chunk-DEEO4D4N.js +1 -0
- package/dist/public/chunks/chunk-DZB4LPI6.js +1 -0
- package/dist/public/chunks/chunk-FE522UEA.js +1 -0
- package/dist/public/chunks/chunk-FSGVY7HB.js +1 -0
- package/dist/public/chunks/chunk-FVX42XAD.js +1 -0
- package/dist/public/chunks/chunk-G5NWYJTV.js +1 -0
- package/dist/public/chunks/chunk-GMZIELFU.js +1 -0
- package/dist/public/chunks/chunk-GOTQ4D6W.js +1 -0
- package/dist/public/chunks/chunk-GYRM5ZIV.js +1 -0
- package/dist/public/chunks/chunk-GZBMHYYU.js +1 -0
- package/dist/public/chunks/chunk-H2ZU34CP.js +1 -0
- package/dist/public/chunks/chunk-H76B43OQ.js +1 -0
- package/dist/public/chunks/chunk-HCLRG2EC.js +1 -0
- package/dist/public/chunks/chunk-II4NXLSQ.js +2 -0
- package/dist/public/chunks/chunk-ILH47V7O.js +1 -0
- package/dist/public/chunks/chunk-IZWTFLCC.js +1 -0
- package/dist/public/chunks/chunk-J37MXUED.js +1 -0
- package/dist/public/chunks/chunk-JAZABOBA.js +0 -0
- package/dist/public/chunks/chunk-JPL2NRWK.js +1 -0
- package/dist/public/chunks/chunk-JVJ5UWIK.js +1 -0
- package/dist/public/chunks/chunk-K4TYMAA3.js +63 -0
- package/dist/public/chunks/chunk-KDUTK357.js +1 -0
- package/dist/public/chunks/chunk-L2H3ZWZN.js +1 -0
- package/dist/public/chunks/chunk-LBELRO5P.js +1 -0
- package/dist/public/chunks/chunk-LKW7MULF.js +1 -0
- package/dist/public/chunks/chunk-LZYAHJVQ.js +1 -0
- package/dist/public/chunks/chunk-MEHLQZDG.js +3 -0
- package/dist/public/chunks/chunk-MKKFXRYO.js +1 -0
- package/dist/public/chunks/chunk-MTBUV2GJ.js +1 -0
- package/dist/public/chunks/chunk-NGQRNS3C.js +1 -0
- package/dist/public/chunks/chunk-ONDKE72P.js +1 -0
- package/dist/public/chunks/chunk-PG7M7L24.js +1 -0
- package/dist/public/chunks/chunk-PQPAPPEI.js +1 -0
- package/dist/public/chunks/chunk-PXHLJIOT.js +1 -0
- package/dist/public/chunks/chunk-QOX5RMLM.js +1 -0
- package/dist/public/chunks/chunk-QPBLZSNX.js +1 -0
- package/dist/public/chunks/chunk-QQUFROWT.js +1 -0
- package/dist/public/chunks/chunk-RB5FBDEY.js +1 -0
- package/dist/public/chunks/chunk-RGXXD4DM.js +41 -0
- package/dist/public/chunks/chunk-RHMZNAJX.js +1 -0
- package/dist/public/chunks/chunk-SIOW7OOW.js +1 -0
- package/dist/public/chunks/chunk-SUX4QBVF.js +1 -0
- package/dist/public/chunks/chunk-T4M53QQT.js +1 -0
- package/dist/public/chunks/chunk-TC7TU4GK.js +1 -0
- package/dist/public/chunks/chunk-TFJOEF3Q.js +1 -0
- package/dist/public/chunks/chunk-UJD3MBMB.js +1 -0
- package/dist/public/chunks/chunk-UK2V4UBT.js +1 -0
- package/dist/public/chunks/chunk-UT6HZPGL.js +1 -0
- package/dist/public/chunks/chunk-UZ44J2LH.js +1 -0
- package/dist/public/chunks/chunk-VC6IS564.js +1 -0
- package/dist/public/chunks/chunk-WLIOZO3W.js +1 -0
- package/dist/public/chunks/chunk-WRXMQB7X.js +25 -0
- package/dist/public/chunks/chunk-XDUSY26N.js +55 -0
- package/dist/public/chunks/chunk-XNR4RZEC.js +1 -0
- package/dist/public/chunks/chunk-YPMUHXYT.js +147 -0
- package/dist/public/chunks/chunk-ZIJ6SJDN.js +1 -0
- package/dist/public/chunks/code-highlighter-Q7WC7AEF.js +12 -0
- package/dist/public/client-4OAAOHF4.js +68 -0
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +1 -1
- package/src/assets.ts +4 -1
- package/src/client/App.tsx +100 -23
- package/src/client/TracedRoutes.tsx +8 -4
- package/src/client/code-highlighter.ts +26 -0
- package/src/client/code.tsx +2 -22
- package/src/client/components/BottomSheet.tsx +1 -1
- package/src/client/components/DetailsPanel.tsx +2 -1
- package/src/client/components/Drawer.tsx +12 -71
- package/src/client/components/DrawerContent.tsx +76 -0
- package/src/client/components/ProfileMenu.tsx +12 -3
- package/src/client/components/UserInputForm.tsx +28 -5
- package/src/client/conversations/ConversationBrief.tsx +129 -65
- package/src/client/conversations/ConversationCatchUp.tsx +107 -0
- package/src/client/conversations/ConversationContextTags.tsx +110 -0
- package/src/client/conversations/ConversationDetails.tsx +13 -14
- package/src/client/conversations/ConversationEventLog.tsx +135 -70
- package/src/client/conversations/ConversationHeader.tsx +74 -65
- package/src/client/conversations/ConversationHeaderActions.tsx +36 -38
- package/src/client/conversations/ConversationHomeList.tsx +41 -3
- package/src/client/conversations/ConversationLinkedWork.tsx +30 -10
- package/src/client/conversations/ConversationMeta.tsx +21 -27
- package/src/client/conversations/ConversationOverflowMenu.tsx +15 -4
- package/src/client/conversations/ConversationPage.tsx +219 -157
- package/src/client/conversations/ConversationTranscript.tsx +234 -186
- package/src/client/conversations/ConversationUsageBreakdown.tsx +142 -64
- package/src/client/conversations/DetailsList.tsx +7 -1
- package/src/client/conversations/PendingMailboxStack.tsx +88 -44
- package/src/client/conversations/ShareConversationButton.tsx +5 -50
- package/src/client/conversations/SlackMark.tsx +16 -4
- package/src/client/conversations/TranscriptActivityGroup.tsx +7 -14
- package/src/client/conversations/TranscriptEventView.tsx +7 -3
- package/src/client/conversations/TranscriptReasoningView.tsx +4 -2
- package/src/client/conversations/TranscriptRows.tsx +61 -16
- package/src/client/conversations/TranscriptView.tsx +25 -13
- package/src/client/conversations/conversationArchive.ts +22 -0
- package/src/client/conversations/conversationCatchUpState.ts +96 -0
- package/src/client/conversations/conversationMemory.ts +8 -1
- package/src/client/conversations/queries.ts +85 -46
- package/src/client/conversations/{transcriptUnreadHistory.ts → transcriptAroundHistory.ts} +48 -42
- package/src/client/conversations/transcriptBottomPinning.ts +40 -8
- package/src/client/conversations/transcriptListRows.ts +215 -0
- package/src/client/conversations/transcriptReadPosition.ts +24 -2
- package/src/client/conversations/turnContext.ts +1 -1
- package/src/client/conversations/virtualRows.ts +168 -0
- package/src/client/http.ts +6 -5
- package/src/client/pages/admin/AdminPage.tsx +1 -1
- package/src/client/pages/admin/ServiceAccountsSection.tsx +1 -1
- package/src/client/pages/memory/memoryDashboard.ts +1 -1
- package/src/client/pages/memory/memoryRecord.ts +1 -1
- package/src/client/pages/settings/ConnectionsPage.tsx +1 -1
- package/src/client/pages/user/PluginUserPage.tsx +0 -6
- package/src/client/pages/user/pluginUserPageRoutes.ts +5 -0
- package/src/client/primaryNav.ts +1 -1
- package/src/client/review-desk/reviewItemFormat.ts +1 -1
- package/src/client/sentry.ts +53 -3
- package/src/conversation-cache.ts +8 -10
- package/src/mock-reporting/fixtures.ts +43 -8
- package/src/mock-reporting/routes.ts +40 -3
- package/src/public-assets.ts +28 -20
- package/src/shell.ts +12 -2
- package/dist/public/client-ZCZXZBTL.js +0 -460
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultRangeExtractor,
|
|
3
|
+
measureElement,
|
|
4
|
+
useVirtualizer,
|
|
5
|
+
type Range,
|
|
6
|
+
type Virtualizer,
|
|
7
|
+
} from "@tanstack/react-virtual";
|
|
8
|
+
import {
|
|
9
|
+
useCallback,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
type Key,
|
|
14
|
+
type RefObject,
|
|
15
|
+
} from "react";
|
|
16
|
+
|
|
17
|
+
import { scrollRootFor } from "./transcriptBottomPinning";
|
|
18
|
+
|
|
19
|
+
/** Rows mounted beyond the viewport on each side. */
|
|
20
|
+
const OVERSCAN_ROWS = 10;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pick and measure the rows of a list near the viewport of its scroll root.
|
|
24
|
+
* The transcript scroll owner holds the reader's place, so this never moves
|
|
25
|
+
* the scroll position. Rows at `keptIndexes` stay mounted outside the
|
|
26
|
+
* viewport, so code that finds them in the DOM keeps working.
|
|
27
|
+
*
|
|
28
|
+
* A row that mounts above the reader with a size other than its estimate
|
|
29
|
+
* moves what they read, and the scroll owner corrects that with a scroll.
|
|
30
|
+
* Phones show such corrections as jumps, so a row's estimate is the mean
|
|
31
|
+
* size of the measured rows of its `sizeClass`, which is close for rows of
|
|
32
|
+
* one kind. `defaultSize` covers a class before any of its rows measure.
|
|
33
|
+
*
|
|
34
|
+
* Callers place each row at `start - options.scrollMargin` with
|
|
35
|
+
* `measureElement` as its ref, inside a list that is `getTotalSize()` tall.
|
|
36
|
+
*/
|
|
37
|
+
export function useVirtualRows(input: {
|
|
38
|
+
count: number;
|
|
39
|
+
defaultSize: (sizeClass: string) => number;
|
|
40
|
+
gap?: number;
|
|
41
|
+
getItemKey: (index: number) => Key;
|
|
42
|
+
keptIndexes: readonly (number | undefined)[];
|
|
43
|
+
listRef: RefObject<HTMLElement | null>;
|
|
44
|
+
sizeClass: (index: number) => string;
|
|
45
|
+
}) {
|
|
46
|
+
const { listRef } = input;
|
|
47
|
+
// The virtualizer reads estimates only for rows without a measured size.
|
|
48
|
+
const sizes = useRef(new Map<string, { count: number; total: number }>());
|
|
49
|
+
const rowSizes = useRef(new Map<Key, { size: number; sizeClass: string }>());
|
|
50
|
+
// The estimate that each unmeasured row has now, by row key.
|
|
51
|
+
const estimates = useRef(new Map<Key, number>());
|
|
52
|
+
const rowInput = useRef(input);
|
|
53
|
+
rowInput.current = input;
|
|
54
|
+
const estimateSize = useCallback((index: number) => {
|
|
55
|
+
const key = rowInput.current.getItemKey(index);
|
|
56
|
+
const current = estimates.current.get(key);
|
|
57
|
+
if (current !== undefined) return current;
|
|
58
|
+
const sizeClass = rowInput.current.sizeClass(index);
|
|
59
|
+
const measured = sizes.current.get(sizeClass);
|
|
60
|
+
const estimate = measured?.count
|
|
61
|
+
? measured.total / measured.count
|
|
62
|
+
: rowInput.current.defaultSize(sizeClass);
|
|
63
|
+
estimates.current.set(key, estimate);
|
|
64
|
+
return estimate;
|
|
65
|
+
}, []);
|
|
66
|
+
// Each row counts once, with its latest size, so rows that grow while
|
|
67
|
+
// they stream do not outweigh the others.
|
|
68
|
+
const recordSize = (key: Key, sizeClass: string, size: number) => {
|
|
69
|
+
const previous = rowSizes.current.get(key);
|
|
70
|
+
if (previous) {
|
|
71
|
+
const total = sizes.current.get(previous.sizeClass)!;
|
|
72
|
+
total.count -= 1;
|
|
73
|
+
total.total -= previous.size;
|
|
74
|
+
}
|
|
75
|
+
rowSizes.current.set(key, { size, sizeClass });
|
|
76
|
+
const total = sizes.current.get(sizeClass) ?? { count: 0, total: 0 };
|
|
77
|
+
total.count += 1;
|
|
78
|
+
total.total += size;
|
|
79
|
+
sizes.current.set(sizeClass, total);
|
|
80
|
+
};
|
|
81
|
+
// A new estimate for a row moves the rows after it. The scroll owner
|
|
82
|
+
// corrects that from the row that it recorded at the last scroll. So rows
|
|
83
|
+
// take the latest estimates only on the render after a scroll, when that
|
|
84
|
+
// row is current. A scroll by other code, such as a reveal, is then
|
|
85
|
+
// recorded before rows move. Other renders keep each row's estimate.
|
|
86
|
+
const instance = useRef<Virtualizer<Element, HTMLElement>>(null);
|
|
87
|
+
const placement = useRef<{
|
|
88
|
+
getItemKey: (index: number) => Key;
|
|
89
|
+
scrollOffset?: number | null;
|
|
90
|
+
}>(null);
|
|
91
|
+
const scrollOffset = instance.current?.scrollOffset;
|
|
92
|
+
if (!placement.current || placement.current.scrollOffset !== scrollOffset) {
|
|
93
|
+
estimates.current.clear();
|
|
94
|
+
placement.current = {
|
|
95
|
+
getItemKey: (index) => rowInput.current.getItemKey(index),
|
|
96
|
+
scrollOffset,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const [scrollMargin, setScrollMargin] = useState(0);
|
|
100
|
+
const keptKey = input.keptIndexes
|
|
101
|
+
.filter((index) => index !== undefined && index >= 0 && index < input.count)
|
|
102
|
+
.join(",");
|
|
103
|
+
const rangeExtractor = useCallback(
|
|
104
|
+
(range: Range) => {
|
|
105
|
+
const indexes = new Set(defaultRangeExtractor(range));
|
|
106
|
+
if (keptKey) {
|
|
107
|
+
for (const index of keptKey.split(",")) indexes.add(Number(index));
|
|
108
|
+
}
|
|
109
|
+
// Rows stay in document order, so the scroll owner can search them.
|
|
110
|
+
return [...indexes].sort((left, right) => left - right);
|
|
111
|
+
},
|
|
112
|
+
[keptKey],
|
|
113
|
+
);
|
|
114
|
+
// Controls above the list, such as the earlier-page control, move it
|
|
115
|
+
// down from the top of the scroll content. The margin only picks which
|
|
116
|
+
// rows mount, so a reading from the last scroll or resize is close
|
|
117
|
+
// enough: the overscan rows cover a small error.
|
|
118
|
+
const measureScrollMargin = useCallback(() => {
|
|
119
|
+
const list = listRef.current;
|
|
120
|
+
const root = scrollRootFor(list);
|
|
121
|
+
if (!list || !(root instanceof Element)) return;
|
|
122
|
+
const margin =
|
|
123
|
+
list.getBoundingClientRect().top -
|
|
124
|
+
root.getBoundingClientRect().top +
|
|
125
|
+
root.scrollTop;
|
|
126
|
+
setScrollMargin((current) =>
|
|
127
|
+
Math.abs(margin - current) >= 1 ? margin : current,
|
|
128
|
+
);
|
|
129
|
+
}, [listRef]);
|
|
130
|
+
useLayoutEffect(measureScrollMargin, [measureScrollMargin]);
|
|
131
|
+
// The dashboard build does not use React Compiler, so nothing memoizes
|
|
132
|
+
// the values that this hook returns.
|
|
133
|
+
// oxlint-disable-next-line react/incompatible-library
|
|
134
|
+
const virtualizer = useVirtualizer<Element, HTMLElement>({
|
|
135
|
+
count: input.count,
|
|
136
|
+
estimateSize,
|
|
137
|
+
gap: input.gap,
|
|
138
|
+
// A new function makes the virtualizer place every row again.
|
|
139
|
+
getItemKey: placement.current.getItemKey,
|
|
140
|
+
measureElement: (element, entry, instance) => {
|
|
141
|
+
const size = measureElement(element, entry, instance);
|
|
142
|
+
const index = instance.indexFromElement(element);
|
|
143
|
+
recordSize(
|
|
144
|
+
rowInput.current.getItemKey(index),
|
|
145
|
+
rowInput.current.sizeClass(index),
|
|
146
|
+
size,
|
|
147
|
+
);
|
|
148
|
+
return size;
|
|
149
|
+
},
|
|
150
|
+
// A server render has no scroll root, so mount the first rows that
|
|
151
|
+
// fill about one screen.
|
|
152
|
+
initialRect:
|
|
153
|
+
typeof window === "undefined" ? { height: 1_000, width: 0 } : undefined,
|
|
154
|
+
getScrollElement: () => {
|
|
155
|
+
const root = scrollRootFor(listRef.current);
|
|
156
|
+
return root instanceof Element ? root : null;
|
|
157
|
+
},
|
|
158
|
+
onChange: measureScrollMargin,
|
|
159
|
+
overscan: OVERSCAN_ROWS,
|
|
160
|
+
rangeExtractor,
|
|
161
|
+
scrollMargin,
|
|
162
|
+
// The scroll owner moves the scroll position. This only reads it.
|
|
163
|
+
scrollToFn: () => {},
|
|
164
|
+
});
|
|
165
|
+
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = () => false;
|
|
166
|
+
instance.current = virtualizer;
|
|
167
|
+
return virtualizer;
|
|
168
|
+
}
|
package/src/client/http.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ZodType } from "zod";
|
|
2
2
|
import { recordDashboardServerVersion } from "./dashboard-version";
|
|
3
|
+
import { readResponseJson } from "./sentry";
|
|
3
4
|
|
|
4
5
|
/** An authenticated dashboard request rejected by the product API. */
|
|
5
6
|
export class DashboardApiError extends Error {
|
|
@@ -91,7 +92,7 @@ export async function patch<T>(
|
|
|
91
92
|
});
|
|
92
93
|
if (response.status === 401) restartDashboardSignIn();
|
|
93
94
|
if (!response.ok) await throwDashboardApiError(path, response);
|
|
94
|
-
return
|
|
95
|
+
return readResponseJson(response, schema);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
/** Send one authenticated POST request and validate its response. */
|
|
@@ -108,7 +109,7 @@ export async function post<T>(
|
|
|
108
109
|
});
|
|
109
110
|
if (response.status === 401) restartDashboardSignIn();
|
|
110
111
|
if (!response.ok) await throwDashboardApiError(path, response);
|
|
111
|
-
return
|
|
112
|
+
return readResponseJson(response, schema);
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/** Send one authenticated PUT request and validate its response. */
|
|
@@ -125,7 +126,7 @@ export async function put<T>(
|
|
|
125
126
|
});
|
|
126
127
|
if (response.status === 401) restartDashboardSignIn();
|
|
127
128
|
if (!response.ok) await throwDashboardApiError(path, response);
|
|
128
|
-
return
|
|
129
|
+
return readResponseJson(response, schema);
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
/** Delete one authenticated dashboard resource. */
|
|
@@ -152,7 +153,7 @@ export async function del<T>(
|
|
|
152
153
|
});
|
|
153
154
|
if (response.status === 401) restartDashboardSignIn();
|
|
154
155
|
if (!response.ok) await throwDashboardApiError(path, response);
|
|
155
|
-
return
|
|
156
|
+
return readResponseJson(response, schema);
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
async function fetchDashboard(
|
|
@@ -171,7 +172,7 @@ export async function fetchDashboardJson<T>(
|
|
|
171
172
|
signal?: AbortSignal,
|
|
172
173
|
): Promise<T> {
|
|
173
174
|
const response = await readDashboardResponse(path, signal);
|
|
174
|
-
return
|
|
175
|
+
return readResponseJson(response, schema);
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
/** Read an authenticated response, allowing 304 only for explicit validator headers. */
|
|
@@ -5,7 +5,7 @@ import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
|
|
|
5
5
|
import { Card } from "../../components/layout/Card";
|
|
6
6
|
import { PageHeader } from "../../components/layout/PageHeader";
|
|
7
7
|
import { dashboardContainerClass } from "../../styles";
|
|
8
|
-
import { pluginUserPagePath } from "../user/
|
|
8
|
+
import { pluginUserPagePath } from "../user/pluginUserPageRoutes";
|
|
9
9
|
import {
|
|
10
10
|
ServiceAccountsSection,
|
|
11
11
|
useServiceAccounts,
|
|
@@ -27,12 +27,6 @@ import {
|
|
|
27
27
|
type PluginUserPageRecordAction,
|
|
28
28
|
} from "./pluginUserPageData";
|
|
29
29
|
|
|
30
|
-
/** Build the canonical dashboard path for a plugin-owned page. */
|
|
31
|
-
export function pluginUserPagePath(pluginName: string, pageId: string): string {
|
|
32
|
-
if (pluginName === "memory" && pageId === "memories") return "/memories";
|
|
33
|
-
return `/plugins/${encodeURIComponent(pluginName)}/${encodeURIComponent(pageId)}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
30
|
/** Render the memory library for its first-class permalink route. */
|
|
37
31
|
export function MemoryPermalinkRoute(props: { pages: PluginUserPageLink[] }) {
|
|
38
32
|
const page = props.pages.find(
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Build the canonical dashboard path for a plugin-owned page. */
|
|
2
|
+
export function pluginUserPagePath(pluginName: string, pageId: string): string {
|
|
3
|
+
if (pluginName === "memory" && pageId === "memories") return "/memories";
|
|
4
|
+
return `/plugins/${encodeURIComponent(pluginName)}/${encodeURIComponent(pageId)}`;
|
|
5
|
+
}
|
package/src/client/primaryNav.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
|
|
2
2
|
|
|
3
3
|
import type { DashboardHeaderNavItem } from "./components/layout/DashboardHeader";
|
|
4
|
-
import { pluginUserPagePath } from "./pages/user/
|
|
4
|
+
import { pluginUserPagePath } from "./pages/user/pluginUserPageRoutes";
|
|
5
5
|
|
|
6
6
|
const AUTH_PRIMARY_NAV_PREFIXES = [
|
|
7
7
|
"/admin",
|
package/src/client/sentry.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BrowserClient,
|
|
2
3
|
browserTracingIntegration,
|
|
4
|
+
defaultStackParser,
|
|
3
5
|
getActiveSpan,
|
|
4
6
|
getRootSpan,
|
|
5
|
-
|
|
7
|
+
makeFetchTransport,
|
|
8
|
+
setCurrentClient,
|
|
6
9
|
spanToJSON,
|
|
7
10
|
startInactiveSpan,
|
|
8
11
|
updateSpanName,
|
|
@@ -118,6 +121,47 @@ export function recordTranscriptShown(): void {
|
|
|
118
121
|
}).end();
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Read, parse, and validate a dashboard API response body. While a pageload
|
|
126
|
+
* or navigation is open, a span times it from the response headers to the
|
|
127
|
+
* validated value. The `http.client` span ends at the headers, so it holds
|
|
128
|
+
* none of this work.
|
|
129
|
+
*/
|
|
130
|
+
export async function readResponseJson<T>(
|
|
131
|
+
response: Response,
|
|
132
|
+
schema: { parse: (value: unknown) => T },
|
|
133
|
+
): Promise<T> {
|
|
134
|
+
const routeSpan = activeRouteSpan();
|
|
135
|
+
if (!routeSpan) return schema.parse(await response.json());
|
|
136
|
+
const span = startInactiveSpan({
|
|
137
|
+
attributes: {
|
|
138
|
+
"url.path": response.url ? new URL(response.url).pathname : undefined,
|
|
139
|
+
},
|
|
140
|
+
name: "Read API response",
|
|
141
|
+
op: "deserialize",
|
|
142
|
+
parentSpan: routeSpan,
|
|
143
|
+
});
|
|
144
|
+
try {
|
|
145
|
+
const text = await response.text();
|
|
146
|
+
const downloadedAt = performance.now();
|
|
147
|
+
const json: unknown = JSON.parse(text);
|
|
148
|
+
const parsedAt = performance.now();
|
|
149
|
+
const value = schema.parse(json);
|
|
150
|
+
span.setAttributes({
|
|
151
|
+
"app.dashboard.response.chars": text.length,
|
|
152
|
+
"app.dashboard.response.json_parse_ms": Math.round(
|
|
153
|
+
parsedAt - downloadedAt,
|
|
154
|
+
),
|
|
155
|
+
"app.dashboard.response.schema_parse_ms": Math.round(
|
|
156
|
+
performance.now() - parsedAt,
|
|
157
|
+
),
|
|
158
|
+
});
|
|
159
|
+
return value;
|
|
160
|
+
} finally {
|
|
161
|
+
span.end();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
121
165
|
/**
|
|
122
166
|
* Send dashboard performance spans to the server's Sentry project when the
|
|
123
167
|
* server provides a DSN. Only tracing is enabled: error events and
|
|
@@ -126,10 +170,14 @@ export function recordTranscriptShown(): void {
|
|
|
126
170
|
export function initDashboardSentry(): void {
|
|
127
171
|
const config = window.__JUNIOR_DASHBOARD_SENTRY__;
|
|
128
172
|
if (!config) return;
|
|
129
|
-
init(
|
|
173
|
+
// `init()` bundles every default integration (about 24 KB of error,
|
|
174
|
+
// breadcrumb, and session code), even with `defaultIntegrations: false`.
|
|
175
|
+
// A client built directly bundles only browser tracing.
|
|
176
|
+
const client = new BrowserClient({
|
|
130
177
|
...config,
|
|
131
178
|
beforeSendSpan: scrubSpan,
|
|
132
|
-
|
|
179
|
+
stackParser: defaultStackParser,
|
|
180
|
+
transport: makeFetchTransport,
|
|
133
181
|
integrations: [
|
|
134
182
|
browserTracingIntegration({
|
|
135
183
|
beforeStartSpan: (options) =>
|
|
@@ -145,4 +193,6 @@ export function initDashboardSentry(): void {
|
|
|
145
193
|
}),
|
|
146
194
|
],
|
|
147
195
|
});
|
|
196
|
+
setCurrentClient(client);
|
|
197
|
+
client.init();
|
|
148
198
|
}
|
|
@@ -14,20 +14,18 @@ export const revalidateConversation: MiddlewareHandler = async (
|
|
|
14
14
|
// The same route pattern also matches /conversations/stats.
|
|
15
15
|
if (typeof report.conversationId !== "string") return;
|
|
16
16
|
// generatedAt is the read time, not a change to the Conversation. The mailbox
|
|
17
|
-
// has its own live watermark,
|
|
17
|
+
// has its own live watermark, so a poll that asks for it gets it alone.
|
|
18
18
|
const { generatedAt: _generatedAt, mailbox, ...content } = report;
|
|
19
19
|
const version = createHash("sha256")
|
|
20
20
|
.update(JSON.stringify(content))
|
|
21
21
|
.digest("hex");
|
|
22
22
|
context.header("cache-control", "private, no-store");
|
|
23
23
|
context.header(CONVERSATION_DETAIL_VERSION_HEADER, version);
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
}
|
|
24
|
+
if (context.req.header(CONVERSATION_DETAIL_VERSION_HEADER) !== version)
|
|
25
|
+
return;
|
|
26
|
+
context.res = mailbox
|
|
27
|
+
? new Response(JSON.stringify({ mailbox, unchanged: true }), {
|
|
28
|
+
headers: context.res.headers,
|
|
29
|
+
})
|
|
30
|
+
: new Response(null, { status: 304, headers: context.res.headers });
|
|
33
31
|
};
|
|
@@ -316,14 +316,6 @@ function mockBrief(
|
|
|
316
316
|
kind: "confirmed",
|
|
317
317
|
},
|
|
318
318
|
],
|
|
319
|
-
openDecisions: live
|
|
320
|
-
? [
|
|
321
|
-
{
|
|
322
|
-
text: "Roll back payments-v42 or patch the serializer path.",
|
|
323
|
-
owner: "Morgan Lee",
|
|
324
|
-
},
|
|
325
|
-
]
|
|
326
|
-
: [],
|
|
327
319
|
facts: live
|
|
328
320
|
? [
|
|
329
321
|
"Checkout p95 increased from 210ms to 890ms.",
|
|
@@ -393,6 +385,7 @@ function activeConversation(nowMs: number): ConversationDetailReport {
|
|
|
393
385
|
kind: "resource_link",
|
|
394
386
|
key: "getsentry/payments#42",
|
|
395
387
|
label: "getsentry/payments#42",
|
|
388
|
+
objectType: "code_change",
|
|
396
389
|
plugin: "github",
|
|
397
390
|
status: "open",
|
|
398
391
|
url: "https://github.com/getsentry/payments/pull/42",
|
|
@@ -685,6 +678,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
|
|
|
685
678
|
kind: "resource_link",
|
|
686
679
|
key: "getsentry/getsentry#21571",
|
|
687
680
|
label: "getsentry/getsentry#21571",
|
|
681
|
+
objectType: "code_change",
|
|
688
682
|
plugin: "github",
|
|
689
683
|
status: "merged",
|
|
690
684
|
url: "https://github.com/getsentry/getsentry/pull/21571",
|
|
@@ -695,6 +689,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
|
|
|
695
689
|
kind: "resource_link",
|
|
696
690
|
key: "getsentry/getsentry#21572",
|
|
697
691
|
label: "getsentry/getsentry#21572",
|
|
692
|
+
objectType: "code_change",
|
|
698
693
|
plugin: "github",
|
|
699
694
|
status: "open",
|
|
700
695
|
url: "https://github.com/getsentry/getsentry/pull/21572",
|
|
@@ -705,6 +700,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
|
|
|
705
700
|
kind: "resource_link",
|
|
706
701
|
key: "getsentry/getsentry#21569",
|
|
707
702
|
label: "getsentry/getsentry#21569",
|
|
703
|
+
objectType: "code_change",
|
|
708
704
|
plugin: "github",
|
|
709
705
|
status: "draft",
|
|
710
706
|
url: "https://github.com/getsentry/getsentry/pull/21569",
|
|
@@ -715,6 +711,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
|
|
|
715
711
|
kind: "resource_link",
|
|
716
712
|
key: "getsentry/sentry#121727",
|
|
717
713
|
label: "getsentry/sentry#121727",
|
|
714
|
+
objectType: "code_change",
|
|
718
715
|
plugin: "github",
|
|
719
716
|
status: "merged",
|
|
720
717
|
url: "https://github.com/getsentry/sentry/pull/121727",
|
|
@@ -1180,6 +1177,7 @@ function longConversation(nowMs: number): ConversationDetailReport {
|
|
|
1180
1177
|
kind: "resource_link",
|
|
1181
1178
|
key: "getsentry/junior#2201",
|
|
1182
1179
|
label: "getsentry/junior#2201",
|
|
1180
|
+
objectType: "code_change",
|
|
1183
1181
|
plugin: "github",
|
|
1184
1182
|
status: "draft",
|
|
1185
1183
|
url: "https://github.com/getsentry/junior/pull/2201",
|
|
@@ -1190,6 +1188,7 @@ function longConversation(nowMs: number): ConversationDetailReport {
|
|
|
1190
1188
|
kind: "resource_link",
|
|
1191
1189
|
key: "getsentry/payments#91",
|
|
1192
1190
|
label: "getsentry/payments#91",
|
|
1191
|
+
objectType: "code_change",
|
|
1193
1192
|
plugin: "github",
|
|
1194
1193
|
status: "open",
|
|
1195
1194
|
url: "https://github.com/getsentry/payments/pull/91",
|
|
@@ -1200,6 +1199,7 @@ function longConversation(nowMs: number): ConversationDetailReport {
|
|
|
1200
1199
|
kind: "resource_link",
|
|
1201
1200
|
key: "getsentry/relay#44",
|
|
1202
1201
|
label: "getsentry/relay#44",
|
|
1202
|
+
objectType: "code_change",
|
|
1203
1203
|
plugin: "github",
|
|
1204
1204
|
status: "merged",
|
|
1205
1205
|
url: "https://github.com/getsentry/relay/pull/44",
|
|
@@ -1268,6 +1268,7 @@ function incidentConversation(nowMs: number): ConversationDetailReport {
|
|
|
1268
1268
|
kind: "resource_link",
|
|
1269
1269
|
key: "getsentry/payments#77",
|
|
1270
1270
|
label: "getsentry/payments#77",
|
|
1271
|
+
objectType: "code_change",
|
|
1271
1272
|
plugin: "github",
|
|
1272
1273
|
status: "merged",
|
|
1273
1274
|
url: "https://github.com/getsentry/payments/pull/77",
|
|
@@ -1278,6 +1279,7 @@ function incidentConversation(nowMs: number): ConversationDetailReport {
|
|
|
1278
1279
|
kind: "resource_link",
|
|
1279
1280
|
key: "getsentry/payments#61",
|
|
1280
1281
|
label: "getsentry/payments#61",
|
|
1282
|
+
objectType: "task",
|
|
1281
1283
|
plugin: "github",
|
|
1282
1284
|
status: "closed",
|
|
1283
1285
|
url: "https://github.com/getsentry/payments/issues/61",
|
|
@@ -2281,6 +2283,39 @@ export function readMockConversationEvents(
|
|
|
2281
2283
|
};
|
|
2282
2284
|
}
|
|
2283
2285
|
|
|
2286
|
+
/**
|
|
2287
|
+
* Return the page around a linked event or Turn. Mock transcripts fit one
|
|
2288
|
+
* page, so it holds every event of the Conversation.
|
|
2289
|
+
*/
|
|
2290
|
+
export function readMockConversationEventsAround(
|
|
2291
|
+
conversationId: string,
|
|
2292
|
+
link: { event: number } | { turn: string },
|
|
2293
|
+
): ConversationEventPage | "turn-not-found" | undefined {
|
|
2294
|
+
const conversation = mockConversations(NOW_MS).find(
|
|
2295
|
+
(candidate) => candidate.conversationId === conversationId,
|
|
2296
|
+
);
|
|
2297
|
+
if (!conversation) return undefined;
|
|
2298
|
+
if (
|
|
2299
|
+
"turn" in link &&
|
|
2300
|
+
!conversation.events.some(
|
|
2301
|
+
({ data }) =>
|
|
2302
|
+
data.type === "turn_lifecycle" &&
|
|
2303
|
+
data.state === "started" &&
|
|
2304
|
+
data.turnId === link.turn,
|
|
2305
|
+
)
|
|
2306
|
+
) {
|
|
2307
|
+
return "turn-not-found";
|
|
2308
|
+
}
|
|
2309
|
+
return {
|
|
2310
|
+
events: conversation.events,
|
|
2311
|
+
eventHistory: conversation.eventHistory,
|
|
2312
|
+
generatedAt: NOW,
|
|
2313
|
+
...(conversation.previousCursor
|
|
2314
|
+
? { previousCursor: conversation.previousCursor }
|
|
2315
|
+
: undefined),
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2284
2319
|
function mockBeforeCursor(conversationId: string, seq: number): string {
|
|
2285
2320
|
return `mock:before:${encodeURIComponent(conversationId)}:${seq}`;
|
|
2286
2321
|
}
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
automationParamsSchema,
|
|
43
43
|
automationSummarySchema,
|
|
44
44
|
automationRunListSchema,
|
|
45
|
+
conversationCatchUpBodySchema,
|
|
46
|
+
conversationCatchUpSchema,
|
|
45
47
|
} from "@sentry/junior/api/schema";
|
|
46
48
|
import { mockChartPng } from "./chart-png";
|
|
47
49
|
import {
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
readMockConversationChildAgents,
|
|
57
59
|
readMockConversationDetail,
|
|
58
60
|
readMockConversationEvents,
|
|
61
|
+
readMockConversationEventsAround,
|
|
59
62
|
readMockConversationFeed,
|
|
60
63
|
readMockConversationPendingMessages,
|
|
61
64
|
readMockConversationStats,
|
|
@@ -198,14 +201,30 @@ export function createMockReportingApi(): Hono<{
|
|
|
198
201
|
if (!params.success) {
|
|
199
202
|
return errorResponse("Invalid route parameters.", 400);
|
|
200
203
|
}
|
|
201
|
-
|
|
202
|
-
// only pages backward.
|
|
203
|
-
if (!query.success || !("before" in query.data)) {
|
|
204
|
+
if (!query.success) {
|
|
204
205
|
return errorResponse("Invalid query parameters.", 400);
|
|
205
206
|
}
|
|
206
207
|
if (!readMockConversationDetail(params.data.conversationId)) {
|
|
207
208
|
return errorResponse("Conversation not found.", 404);
|
|
208
209
|
}
|
|
210
|
+
if ("event" in query.data || "turn" in query.data) {
|
|
211
|
+
const around = readMockConversationEventsAround(
|
|
212
|
+
params.data.conversationId,
|
|
213
|
+
"event" in query.data
|
|
214
|
+
? { event: query.data.event }
|
|
215
|
+
: { turn: query.data.turn },
|
|
216
|
+
);
|
|
217
|
+
return around === "turn-not-found"
|
|
218
|
+
? errorResponse("Turn not found.", 404)
|
|
219
|
+
: around
|
|
220
|
+
? jsonResponse(conversationEventPageSchema, around)
|
|
221
|
+
: errorResponse("Conversation not found.", 404);
|
|
222
|
+
}
|
|
223
|
+
// Mock details fit one page and have no unread cursor, so mock history
|
|
224
|
+
// pages only backward from cursors.
|
|
225
|
+
if (!("before" in query.data)) {
|
|
226
|
+
return errorResponse("Invalid query parameters.", 400);
|
|
227
|
+
}
|
|
209
228
|
const report = readMockConversationEvents(
|
|
210
229
|
params.data.conversationId,
|
|
211
230
|
query.data.before,
|
|
@@ -287,6 +306,24 @@ export function createMockReportingApi(): Hono<{
|
|
|
287
306
|
? jsonResponse(conversationReadPositionSchema, result)
|
|
288
307
|
: errorResponse("Conversation not found.", 404);
|
|
289
308
|
});
|
|
309
|
+
app.post("/conversations/:conversationId/catch-up", async (c) => {
|
|
310
|
+
const params = conversationParamsSchema.safeParse(c.req.param());
|
|
311
|
+
if (!params.success) {
|
|
312
|
+
return errorResponse("Invalid route parameters.", 400);
|
|
313
|
+
}
|
|
314
|
+
const body = conversationCatchUpBodySchema.safeParse(await c.req.json());
|
|
315
|
+
if (!body.success) {
|
|
316
|
+
return errorResponse("Invalid request body.", 400);
|
|
317
|
+
}
|
|
318
|
+
if (!readMockConversationDetail(params.data.conversationId)) {
|
|
319
|
+
return errorResponse("Conversation not found.", 404);
|
|
320
|
+
}
|
|
321
|
+
return jsonResponse(conversationCatchUpSchema, {
|
|
322
|
+
needsYou: "Review the Safari fix on PR #12.",
|
|
323
|
+
summary:
|
|
324
|
+
"CI failed on Safari after you left. Junior pushed a fix to PR #12, and the checks pass now.",
|
|
325
|
+
});
|
|
326
|
+
});
|
|
290
327
|
app.patch("/conversations/:conversationId/archive", async (c) => {
|
|
291
328
|
const params = conversationParamsSchema.safeParse(c.req.param());
|
|
292
329
|
if (!params.success) {
|