@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.
Files changed (164) hide show
  1. package/dist/app.js +105 -43
  2. package/dist/client-manifest.json +1 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +105 -43
  5. package/dist/public/chunks/AdminPage-M5RBVZFN.js +1 -0
  6. package/dist/public/chunks/AutomationPage-WALCREDT.js +3 -0
  7. package/dist/public/chunks/AutomationRunsPage-CV47E4UX.js +1 -0
  8. package/dist/public/chunks/AutomationsPage-57L3AQVJ.js +1 -0
  9. package/dist/public/chunks/CodePage-XJUYMFJH.js +1 -0
  10. package/dist/public/chunks/ConnectionsPage-3QQAGYWF.js +1 -0
  11. package/dist/public/chunks/DrawerContent-RYYS57ZH.js +1 -0
  12. package/dist/public/chunks/LocationDetailPage-4IL5HQML.js +1 -0
  13. package/dist/public/chunks/LocationsPage-D2DUQAHJ.js +1 -0
  14. package/dist/public/chunks/McpSettingsPage-3RT7JPEK.js +1 -0
  15. package/dist/public/chunks/PeoplePage-6F5GPQ7C.js +1 -0
  16. package/dist/public/chunks/PersonProfilePage-SPAYQPTD.js +1 -0
  17. package/dist/public/chunks/PersonalTokensPage-SHAUJ74M.js +1 -0
  18. package/dist/public/chunks/PluginUserPage-64FIWD7M.js +1 -0
  19. package/dist/public/chunks/RepositoryPage-L7IHOXSN.js +1 -0
  20. package/dist/public/chunks/SettingsPage-U7WG4FHE.js +1 -0
  21. package/dist/public/chunks/SystemPage-ZBC43OJH.js +1 -0
  22. package/dist/public/chunks/WorkspaceFormPage-3NVIIZ4P.js +2 -0
  23. package/dist/public/chunks/WorkspacesPage-G73HPBDD.js +1 -0
  24. package/dist/public/chunks/chunk-2BCS5AQL.js +1 -0
  25. package/dist/public/chunks/chunk-2J3LGF5Q.js +1 -0
  26. package/dist/public/chunks/chunk-2NNKHSZE.js +1 -0
  27. package/dist/public/chunks/chunk-3Y3ICT6K.js +1 -0
  28. package/dist/public/chunks/chunk-4AKUZW6R.js +1 -0
  29. package/dist/public/chunks/chunk-4TQSTCIN.js +13 -0
  30. package/dist/public/chunks/chunk-4UGZNVJM.js +1 -0
  31. package/dist/public/chunks/chunk-564V7YHG.js +1 -0
  32. package/dist/public/chunks/chunk-5ISD3QSC.js +1 -0
  33. package/dist/public/chunks/chunk-5LNGNJ3P.js +1 -0
  34. package/dist/public/chunks/chunk-5ZA674OU.js +1 -0
  35. package/dist/public/chunks/chunk-64GPEGGE.js +1 -0
  36. package/dist/public/chunks/chunk-6FHY65EQ.js +1 -0
  37. package/dist/public/chunks/chunk-6FSJ3W2W.js +1 -0
  38. package/dist/public/chunks/chunk-6PHJZPWD.js +19 -0
  39. package/dist/public/chunks/chunk-B5ZN3SWQ.js +14 -0
  40. package/dist/public/chunks/chunk-BKOACADP.js +1 -0
  41. package/dist/public/chunks/chunk-CQY6BYUB.js +1 -0
  42. package/dist/public/chunks/chunk-CR2ICLSH.js +1 -0
  43. package/dist/public/chunks/chunk-DEEO4D4N.js +1 -0
  44. package/dist/public/chunks/chunk-DZB4LPI6.js +1 -0
  45. package/dist/public/chunks/chunk-FE522UEA.js +1 -0
  46. package/dist/public/chunks/chunk-FSGVY7HB.js +1 -0
  47. package/dist/public/chunks/chunk-FVX42XAD.js +1 -0
  48. package/dist/public/chunks/chunk-G5NWYJTV.js +1 -0
  49. package/dist/public/chunks/chunk-GMZIELFU.js +1 -0
  50. package/dist/public/chunks/chunk-GOTQ4D6W.js +1 -0
  51. package/dist/public/chunks/chunk-GYRM5ZIV.js +1 -0
  52. package/dist/public/chunks/chunk-GZBMHYYU.js +1 -0
  53. package/dist/public/chunks/chunk-H2ZU34CP.js +1 -0
  54. package/dist/public/chunks/chunk-H76B43OQ.js +1 -0
  55. package/dist/public/chunks/chunk-HCLRG2EC.js +1 -0
  56. package/dist/public/chunks/chunk-II4NXLSQ.js +2 -0
  57. package/dist/public/chunks/chunk-ILH47V7O.js +1 -0
  58. package/dist/public/chunks/chunk-IZWTFLCC.js +1 -0
  59. package/dist/public/chunks/chunk-J37MXUED.js +1 -0
  60. package/dist/public/chunks/chunk-JAZABOBA.js +0 -0
  61. package/dist/public/chunks/chunk-JPL2NRWK.js +1 -0
  62. package/dist/public/chunks/chunk-JVJ5UWIK.js +1 -0
  63. package/dist/public/chunks/chunk-K4TYMAA3.js +63 -0
  64. package/dist/public/chunks/chunk-KDUTK357.js +1 -0
  65. package/dist/public/chunks/chunk-L2H3ZWZN.js +1 -0
  66. package/dist/public/chunks/chunk-LBELRO5P.js +1 -0
  67. package/dist/public/chunks/chunk-LKW7MULF.js +1 -0
  68. package/dist/public/chunks/chunk-LZYAHJVQ.js +1 -0
  69. package/dist/public/chunks/chunk-MEHLQZDG.js +3 -0
  70. package/dist/public/chunks/chunk-MKKFXRYO.js +1 -0
  71. package/dist/public/chunks/chunk-MTBUV2GJ.js +1 -0
  72. package/dist/public/chunks/chunk-NGQRNS3C.js +1 -0
  73. package/dist/public/chunks/chunk-ONDKE72P.js +1 -0
  74. package/dist/public/chunks/chunk-PG7M7L24.js +1 -0
  75. package/dist/public/chunks/chunk-PQPAPPEI.js +1 -0
  76. package/dist/public/chunks/chunk-PXHLJIOT.js +1 -0
  77. package/dist/public/chunks/chunk-QOX5RMLM.js +1 -0
  78. package/dist/public/chunks/chunk-QPBLZSNX.js +1 -0
  79. package/dist/public/chunks/chunk-QQUFROWT.js +1 -0
  80. package/dist/public/chunks/chunk-RB5FBDEY.js +1 -0
  81. package/dist/public/chunks/chunk-RGXXD4DM.js +41 -0
  82. package/dist/public/chunks/chunk-RHMZNAJX.js +1 -0
  83. package/dist/public/chunks/chunk-SIOW7OOW.js +1 -0
  84. package/dist/public/chunks/chunk-SUX4QBVF.js +1 -0
  85. package/dist/public/chunks/chunk-T4M53QQT.js +1 -0
  86. package/dist/public/chunks/chunk-TC7TU4GK.js +1 -0
  87. package/dist/public/chunks/chunk-TFJOEF3Q.js +1 -0
  88. package/dist/public/chunks/chunk-UJD3MBMB.js +1 -0
  89. package/dist/public/chunks/chunk-UK2V4UBT.js +1 -0
  90. package/dist/public/chunks/chunk-UT6HZPGL.js +1 -0
  91. package/dist/public/chunks/chunk-UZ44J2LH.js +1 -0
  92. package/dist/public/chunks/chunk-VC6IS564.js +1 -0
  93. package/dist/public/chunks/chunk-WLIOZO3W.js +1 -0
  94. package/dist/public/chunks/chunk-WRXMQB7X.js +25 -0
  95. package/dist/public/chunks/chunk-XDUSY26N.js +55 -0
  96. package/dist/public/chunks/chunk-XNR4RZEC.js +1 -0
  97. package/dist/public/chunks/chunk-YPMUHXYT.js +147 -0
  98. package/dist/public/chunks/chunk-ZIJ6SJDN.js +1 -0
  99. package/dist/public/chunks/code-highlighter-Q7WC7AEF.js +12 -0
  100. package/dist/public/client-4OAAOHF4.js +68 -0
  101. package/dist/tailwind.css +1 -1
  102. package/package.json +5 -4
  103. package/src/api/schema.ts +1 -1
  104. package/src/assets.ts +4 -1
  105. package/src/client/App.tsx +100 -23
  106. package/src/client/TracedRoutes.tsx +8 -4
  107. package/src/client/code-highlighter.ts +26 -0
  108. package/src/client/code.tsx +2 -22
  109. package/src/client/components/BottomSheet.tsx +1 -1
  110. package/src/client/components/DetailsPanel.tsx +2 -1
  111. package/src/client/components/Drawer.tsx +12 -71
  112. package/src/client/components/DrawerContent.tsx +76 -0
  113. package/src/client/components/ProfileMenu.tsx +12 -3
  114. package/src/client/components/UserInputForm.tsx +28 -5
  115. package/src/client/conversations/ConversationBrief.tsx +129 -65
  116. package/src/client/conversations/ConversationCatchUp.tsx +107 -0
  117. package/src/client/conversations/ConversationContextTags.tsx +110 -0
  118. package/src/client/conversations/ConversationDetails.tsx +13 -14
  119. package/src/client/conversations/ConversationEventLog.tsx +135 -70
  120. package/src/client/conversations/ConversationHeader.tsx +74 -65
  121. package/src/client/conversations/ConversationHeaderActions.tsx +36 -38
  122. package/src/client/conversations/ConversationHomeList.tsx +41 -3
  123. package/src/client/conversations/ConversationLinkedWork.tsx +30 -10
  124. package/src/client/conversations/ConversationMeta.tsx +21 -27
  125. package/src/client/conversations/ConversationOverflowMenu.tsx +15 -4
  126. package/src/client/conversations/ConversationPage.tsx +219 -157
  127. package/src/client/conversations/ConversationTranscript.tsx +234 -186
  128. package/src/client/conversations/ConversationUsageBreakdown.tsx +142 -64
  129. package/src/client/conversations/DetailsList.tsx +7 -1
  130. package/src/client/conversations/PendingMailboxStack.tsx +88 -44
  131. package/src/client/conversations/ShareConversationButton.tsx +5 -50
  132. package/src/client/conversations/SlackMark.tsx +16 -4
  133. package/src/client/conversations/TranscriptActivityGroup.tsx +7 -14
  134. package/src/client/conversations/TranscriptEventView.tsx +7 -3
  135. package/src/client/conversations/TranscriptReasoningView.tsx +4 -2
  136. package/src/client/conversations/TranscriptRows.tsx +61 -16
  137. package/src/client/conversations/TranscriptView.tsx +25 -13
  138. package/src/client/conversations/conversationArchive.ts +22 -0
  139. package/src/client/conversations/conversationCatchUpState.ts +96 -0
  140. package/src/client/conversations/conversationMemory.ts +8 -1
  141. package/src/client/conversations/queries.ts +85 -46
  142. package/src/client/conversations/{transcriptUnreadHistory.ts → transcriptAroundHistory.ts} +48 -42
  143. package/src/client/conversations/transcriptBottomPinning.ts +40 -8
  144. package/src/client/conversations/transcriptListRows.ts +215 -0
  145. package/src/client/conversations/transcriptReadPosition.ts +24 -2
  146. package/src/client/conversations/turnContext.ts +1 -1
  147. package/src/client/conversations/virtualRows.ts +168 -0
  148. package/src/client/http.ts +6 -5
  149. package/src/client/pages/admin/AdminPage.tsx +1 -1
  150. package/src/client/pages/admin/ServiceAccountsSection.tsx +1 -1
  151. package/src/client/pages/memory/memoryDashboard.ts +1 -1
  152. package/src/client/pages/memory/memoryRecord.ts +1 -1
  153. package/src/client/pages/settings/ConnectionsPage.tsx +1 -1
  154. package/src/client/pages/user/PluginUserPage.tsx +0 -6
  155. package/src/client/pages/user/pluginUserPageRoutes.ts +5 -0
  156. package/src/client/primaryNav.ts +1 -1
  157. package/src/client/review-desk/reviewItemFormat.ts +1 -1
  158. package/src/client/sentry.ts +53 -3
  159. package/src/conversation-cache.ts +8 -10
  160. package/src/mock-reporting/fixtures.ts +43 -8
  161. package/src/mock-reporting/routes.ts +40 -3
  162. package/src/public-assets.ts +28 -20
  163. package/src/shell.ts +12 -2
  164. 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
+ }
@@ -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 schema.parse(await response.json());
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 schema.parse(await response.json());
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 schema.parse(await response.json());
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 schema.parse(await response.json());
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 schema.parse(await response.json());
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/PluginUserPage";
8
+ import { pluginUserPagePath } from "../user/pluginUserPageRoutes";
9
9
  import {
10
10
  ServiceAccountsSection,
11
11
  useServiceAccounts,
@@ -5,7 +5,7 @@ import {
5
5
  type UseQueryResult,
6
6
  } from "@tanstack/react-query";
7
7
  import { KeyRound } from "lucide-react";
8
- import type { z } from "zod";
8
+ import type * as z from "zod";
9
9
  import {
10
10
  disconnectServiceAccountResponseSchema,
11
11
  serviceAccountListSchema,
@@ -1,5 +1,5 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
- import { z } from "zod";
2
+ import * as z from "zod";
3
3
 
4
4
  import { fetchDashboardJson } from "../../http";
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
- import { z } from "zod";
2
+ import * as z from "zod";
3
3
 
4
4
  import { fetchDashboardJson } from "../../http";
5
5
  import type { PluginUserPageRecord } from "../user/pluginUserPageData";
@@ -1,6 +1,6 @@
1
1
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
2
  import { Plug } from "lucide-react";
3
- import type { z } from "zod";
3
+ import type * as z from "zod";
4
4
  import {
5
5
  connectionListSchema,
6
6
  disconnectConnectionResponseSchema,
@@ -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
+ }
@@ -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/PluginUserPage";
4
+ import { pluginUserPagePath } from "./pages/user/pluginUserPageRoutes";
5
5
 
6
6
  const AUTH_PRIMARY_NAV_PREFIXES = [
7
7
  "/admin",
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import type {
3
3
  ConversationReportEvent,
4
4
  ReviewItemPayload,
@@ -1,8 +1,11 @@
1
1
  import {
2
+ BrowserClient,
2
3
  browserTracingIntegration,
4
+ defaultStackParser,
3
5
  getActiveSpan,
4
6
  getRootSpan,
5
- init,
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
- defaultIntegrations: false,
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, and a read that asks for it never gets a 304.
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
- !mailbox &&
26
- context.req.header(CONVERSATION_DETAIL_VERSION_HEADER) === version
27
- ) {
28
- context.res = new Response(null, {
29
- status: 304,
30
- headers: context.res.headers,
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
- // Mock details fit one page and have no unread cursor, so mock history
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) {