@sentry/junior 0.76.1 → 0.78.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 (37) hide show
  1. package/dist/{agent-hooks-ZOE7RIED.js → agent-hooks-OFDNZJB2.js} +6 -4
  2. package/dist/api-reference.d.ts +2 -2
  3. package/dist/app.d.ts +27 -0
  4. package/dist/app.js +264 -21
  5. package/dist/build/copy-build-content.d.ts +2 -0
  6. package/dist/build/virtual-config.d.ts +3 -0
  7. package/dist/chat/conversations/store.d.ts +1 -1
  8. package/dist/chat/plugins/agent-hooks.d.ts +7 -1
  9. package/dist/chat/slack/dashboard-link.d.ts +9 -0
  10. package/dist/chat/state/adapter.d.ts +0 -1
  11. package/dist/chat/state/locks.d.ts +7 -0
  12. package/dist/chat/task-execution/state.d.ts +21 -1
  13. package/dist/{chunk-NYKJ3KON.js → chunk-237T7XAN.js} +3 -3
  14. package/dist/{chunk-FFGXUXMD.js → chunk-2MSW5BZY.js} +10 -10
  15. package/dist/{chunk-ZQB37HUX.js → chunk-C2YBH4S6.js} +1 -1
  16. package/dist/{chunk-XBBC6W45.js → chunk-KIDP757T.js} +1 -1
  17. package/dist/{chunk-R6Z5XWY3.js → chunk-LUNMJQ7D.js} +49 -3
  18. package/dist/{chunk-Y5OFBCBZ.js → chunk-LXTPBU4K.js} +14 -10
  19. package/dist/{chunk-JBASI5VV.js → chunk-PNGAJ75P.js} +2 -2
  20. package/dist/{chunk-KOIMO7S3.js → chunk-QDQVOMBA.js} +21 -2
  21. package/dist/{chunk-56TBVRJG.js → chunk-RARSKPVT.js} +1 -1
  22. package/dist/{chunk-NFTMTIP3.js → chunk-SSWBYEFH.js} +26 -2
  23. package/dist/{chunk-4SCWV7TJ.js → chunk-Y3EG7S7P.js} +1 -1
  24. package/dist/{chunk-ZLMBNBUG.js → chunk-YLVJRYTD.js} +9 -1
  25. package/dist/cli/chat.js +6 -6
  26. package/dist/cli/check.js +2 -2
  27. package/dist/cli/plugins.js +6 -6
  28. package/dist/cli/snapshot-warmup.js +3 -3
  29. package/dist/cli/upgrade.js +7 -7
  30. package/dist/{db-7A7PFRGL.js → db-NGQ3JCMF.js} +1 -1
  31. package/dist/nitro.d.ts +4 -0
  32. package/dist/nitro.js +59 -3
  33. package/dist/{registry-OIPAJU2O.js → registry-RRIDPJBT.js} +1 -1
  34. package/dist/reporting.js +13 -24
  35. package/dist/{runner-7Z4D6AKV.js → runner-WW4GJFUB.js} +9 -9
  36. package/dist/{validation-SLA6IGF7.js → validation-MDMYBRFB.js} +2 -2
  37. package/package.json +5 -5
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  PluginHookDeniedError,
3
3
  createPluginHookRunner,
4
+ getPluginDashboardRoutes,
4
5
  getPluginOperationalReports,
5
6
  getPluginRoutes,
6
7
  getPluginSlackConversationLink,
@@ -10,11 +11,11 @@ import {
10
11
  getPlugins,
11
12
  setPlugins,
12
13
  validatePlugins
13
- } from "./chunk-NFTMTIP3.js";
14
- import "./chunk-56TBVRJG.js";
15
- import "./chunk-NYKJ3KON.js";
14
+ } from "./chunk-SSWBYEFH.js";
15
+ import "./chunk-RARSKPVT.js";
16
+ import "./chunk-237T7XAN.js";
16
17
  import "./chunk-G3E7SCME.js";
17
- import "./chunk-Y5OFBCBZ.js";
18
+ import "./chunk-LXTPBU4K.js";
18
19
  import "./chunk-Q6XFTRV5.js";
19
20
  import "./chunk-T77LUIX3.js";
20
21
  import "./chunk-VALUBQ7R.js";
@@ -25,6 +26,7 @@ import "./chunk-MLKGABMK.js";
25
26
  export {
26
27
  PluginHookDeniedError,
27
28
  createPluginHookRunner,
29
+ getPluginDashboardRoutes,
28
30
  getPluginOperationalReports,
29
31
  getPluginRoutes,
30
32
  getPluginSlackConversationLink,
@@ -1,8 +1,8 @@
1
1
  export { createApp } from "./app";
2
- export type { JuniorAppOptions } from "./app";
2
+ export type { JuniorAppOptions, JuniorDashboardOptions } from "./app";
3
3
  export { initSentry } from "./instrumentation";
4
4
  export { juniorNitro } from "./nitro";
5
- export type { JuniorNitroOptions } from "./nitro";
5
+ export type { JuniorNitroDashboardOptions, JuniorNitroOptions } from "./nitro";
6
6
  export { defineJuniorPlugins } from "./plugins";
7
7
  export type { JuniorPluginInput, JuniorPluginSet, JuniorPluginSetOptions, } from "./plugins";
8
8
  export type { PluginRunContext, PluginRunTranscriptEntry, PluginTaskContext, PluginTaskDefinition, PluginTasks, } from "@sentry/junior-plugin-api";
package/dist/app.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import { Hono } from "hono";
2
+ import type { JuniorReporting } from "./reporting";
2
3
  import { type JuniorPluginSet } from "./plugins";
3
4
  import { type VercelConversationWorkCallbackOptions } from "@/chat/task-execution/vercel-callback";
4
5
  import type { WaitUntilFn } from "@/handlers/types";
5
6
  export { defineJuniorPlugins } from "./plugins";
6
7
  export type { JuniorPluginInput, JuniorPluginSet, JuniorPluginSetOptions, } from "./plugins";
7
8
  export interface JuniorAppOptions {
9
+ /** Authenticated dashboard mounted by core when configured. */
10
+ dashboard?: JuniorDashboardOptions;
8
11
  /** Slack-specific overrides applied after env parsing. */
9
12
  slack?: {
10
13
  /** Slack emoji shown while Junior is processing. Defaults to `eyes`. */
@@ -29,5 +32,29 @@ export interface JuniorAppOptions {
29
32
  };
30
33
  waitUntil?: WaitUntilFn;
31
34
  }
35
+ export interface JuniorDashboardOptions {
36
+ /** Browser auth route prefix used by Better Auth. */
37
+ authPath?: string;
38
+ /** Require a dashboard browser session before serving dashboard pages and APIs. */
39
+ authRequired?: boolean;
40
+ /** Exact Google account emails allowed to open the dashboard. */
41
+ allowedEmails?: string[];
42
+ /** Google Workspace domains allowed to open the dashboard. */
43
+ allowedGoogleDomains?: string[];
44
+ /** Browser route prefix for the dashboard shell. */
45
+ basePath?: string;
46
+ /** Public deployment origin used for auth callbacks and external links. */
47
+ baseURL?: string;
48
+ /** Disable dashboard route mounting while preserving serializable config shape. */
49
+ disabled?: boolean;
50
+ /** Overlay dashboard visual-QA fixture conversations onto real reporting data. */
51
+ mockConversations?: boolean;
52
+ /** Reporting implementation used by dashboard APIs. Defaults to core reporting. */
53
+ reporting?: JuniorReporting;
54
+ /** Browser session lifetime in seconds. */
55
+ sessionMaxAgeSeconds?: number;
56
+ /** Additional trusted origins accepted by Better Auth. */
57
+ trustedOrigins?: string[];
58
+ }
32
59
  /** Create a Hono app with all Junior routes. */
33
60
  export declare function createApp(options?: JuniorAppOptions): Promise<Hono>;
package/dist/app.js CHANGED
@@ -71,7 +71,7 @@ import {
71
71
  updateConversationStats,
72
72
  uploadFilesToThread,
73
73
  upsertConversationMessage
74
- } from "./chunk-FFGXUXMD.js";
74
+ } from "./chunk-2MSW5BZY.js";
75
75
  import {
76
76
  CONVERSATION_WORK_CHECK_IN_INTERVAL_MS,
77
77
  CONVERSATION_WORK_STALE_ENQUEUE_MS,
@@ -90,12 +90,12 @@ import {
90
90
  requestConversationContinuation,
91
91
  requestConversationWork,
92
92
  startConversationWork
93
- } from "./chunk-R6Z5XWY3.js";
93
+ } from "./chunk-LUNMJQ7D.js";
94
94
  import {
95
95
  JUNIOR_THREAD_STATE_TTL_MS,
96
96
  coerceThreadConversationState
97
97
  } from "./chunk-Z4CIQ3EB.js";
98
- import "./chunk-JBASI5VV.js";
98
+ import "./chunk-PNGAJ75P.js";
99
99
  import {
100
100
  getVercelConversationWorkQueue,
101
101
  resolveConversationWorkQueueTopic,
@@ -113,7 +113,7 @@ import {
113
113
  resolveSlackChannelTypeFromMessage,
114
114
  resolveSlackConversationContext,
115
115
  setConversationTitle
116
- } from "./chunk-ZQB37HUX.js";
116
+ } from "./chunk-C2YBH4S6.js";
117
117
  import {
118
118
  abandonAgentTurnSessionRecord,
119
119
  buildSlackOutputMessage,
@@ -129,11 +129,11 @@ import {
129
129
  recordAuthorizationCompleted,
130
130
  splitSlackReplyText,
131
131
  truncateStatusText
132
- } from "./chunk-KOIMO7S3.js";
132
+ } from "./chunk-QDQVOMBA.js";
133
133
  import {
134
134
  validatePluginEgressCredentialHooks,
135
135
  validatePluginRegistrations
136
- } from "./chunk-XBBC6W45.js";
136
+ } from "./chunk-KIDP757T.js";
137
137
  import {
138
138
  defineJuniorPlugins,
139
139
  pluginCatalogConfigFromEnv,
@@ -142,26 +142,27 @@ import {
142
142
  } from "./chunk-SG5WAA7H.js";
143
143
  import {
144
144
  bindSlackDirectCredentialSubject,
145
+ getPluginDashboardRoutes,
145
146
  getPluginRoutes,
146
147
  getPluginSlackConversationLink,
147
148
  getPlugins,
148
149
  setPlugins,
149
150
  validatePlugins,
150
151
  verifySlackDirectCredentialSubject
151
- } from "./chunk-NFTMTIP3.js";
152
+ } from "./chunk-SSWBYEFH.js";
152
153
  import {
153
154
  createPluginLogger,
154
155
  createPluginState
155
- } from "./chunk-56TBVRJG.js";
156
+ } from "./chunk-RARSKPVT.js";
156
157
  import {
157
158
  getConversationStore,
158
159
  getDb
159
- } from "./chunk-NYKJ3KON.js";
160
+ } from "./chunk-237T7XAN.js";
160
161
  import "./chunk-G3E7SCME.js";
161
162
  import {
162
- ACTIVE_LOCK_TTL_MS,
163
+ acquireActiveLock,
163
164
  getStateAdapter
164
- } from "./chunk-Y5OFBCBZ.js";
165
+ } from "./chunk-LXTPBU4K.js";
165
166
  import {
166
167
  createSlackDestination,
167
168
  destinationKey,
@@ -198,7 +199,7 @@ import {
198
199
  } from "./chunk-T77LUIX3.js";
199
200
  import {
200
201
  discoverSkills
201
- } from "./chunk-4SCWV7TJ.js";
202
+ } from "./chunk-Y3EG7S7P.js";
202
203
  import {
203
204
  CredentialUnavailableError,
204
205
  buildOAuthTokenRequest,
@@ -209,7 +210,7 @@ import {
209
210
  isPluginProvider,
210
211
  parseOAuthTokenResponse,
211
212
  setPluginCatalogConfig
212
- } from "./chunk-ZLMBNBUG.js";
213
+ } from "./chunk-YLVJRYTD.js";
213
214
  import {
214
215
  createRequester,
215
216
  createSlackRequester,
@@ -247,8 +248,60 @@ import {
247
248
  import "./chunk-MLKGABMK.js";
248
249
 
249
250
  // src/app.ts
251
+ import { createRequire } from "module";
252
+ import { pathToFileURL } from "url";
250
253
  import { Hono } from "hono";
251
254
 
255
+ // src/chat/slack/dashboard-link.ts
256
+ var dashboardConversationLinkOptions;
257
+ function withHttps(host) {
258
+ return /^https?:\/\//.test(host) ? host : `https://${host}`;
259
+ }
260
+ function stripTrailingSlashes(value) {
261
+ let end = value.length;
262
+ while (end > 1 && value.charCodeAt(end - 1) === 47) {
263
+ end -= 1;
264
+ }
265
+ return end === value.length ? value : value.slice(0, end);
266
+ }
267
+ function normalizeDashboardPath(path2, fallback) {
268
+ const value = path2?.trim() || fallback;
269
+ const withSlash = value.startsWith("/") ? value : `/${value}`;
270
+ return stripTrailingSlashes(withSlash);
271
+ }
272
+ function resolveDashboardBaseURL(config) {
273
+ const explicit = config.baseURL ?? process.env.BETTER_AUTH_URL ?? process.env.JUNIOR_BASE_URL;
274
+ if (explicit?.trim()) {
275
+ return stripTrailingSlashes(withHttps(explicit.trim()));
276
+ }
277
+ const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
278
+ if (vercelProd) {
279
+ return stripTrailingSlashes(withHttps(vercelProd));
280
+ }
281
+ const vercelUrl = process.env.VERCEL_URL?.trim();
282
+ if (vercelUrl) {
283
+ return stripTrailingSlashes(withHttps(vercelUrl));
284
+ }
285
+ return "http://localhost:3000";
286
+ }
287
+ function setDashboardConversationLinkOptions(options) {
288
+ const previous = dashboardConversationLinkOptions;
289
+ dashboardConversationLinkOptions = options?.disabled ? void 0 : options;
290
+ return previous;
291
+ }
292
+ function getDashboardConversationLink(conversationId) {
293
+ if (!dashboardConversationLinkOptions) {
294
+ return void 0;
295
+ }
296
+ const baseURL = resolveDashboardBaseURL(dashboardConversationLinkOptions);
297
+ const basePath = normalizeDashboardPath(
298
+ dashboardConversationLinkOptions.basePath,
299
+ "/"
300
+ );
301
+ const path2 = basePath === "/" ? `/conversations/${encodeURIComponent(conversationId)}` : `${basePath}/conversations/${encodeURIComponent(conversationId)}`;
302
+ return `${baseURL}${path2}`;
303
+ }
304
+
252
305
  // src/chat/slack/reply.ts
253
306
  import { Buffer as Buffer2 } from "buffer";
254
307
 
@@ -267,7 +320,7 @@ function buildSlackReplyFooter(args) {
267
320
  label: "ID",
268
321
  value: conversationId
269
322
  };
270
- const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? buildSentryConversationUrl(conversationId);
323
+ const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
271
324
  if (conversationUrl) {
272
325
  idItem.url = conversationUrl;
273
326
  }
@@ -2790,7 +2843,7 @@ async function resumeSlackTurn(args) {
2790
2843
  const stateAdapter = getStateAdapter();
2791
2844
  await stateAdapter.connect();
2792
2845
  const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
2793
- const lock = await stateAdapter.acquireLock(lockKey, ACTIVE_LOCK_TTL_MS);
2846
+ const lock = await acquireActiveLock(stateAdapter, lockKey);
2794
2847
  if (!lock) {
2795
2848
  throw new ResumeTurnBusyError(lockKey);
2796
2849
  }
@@ -6241,6 +6294,14 @@ var IGNORED_MESSAGE_SUBTYPES = /* @__PURE__ */ new Set([
6241
6294
  "ekm_access_denied",
6242
6295
  "tombstone"
6243
6296
  ]);
6297
+ var SlackEventPersistenceError = class extends Error {
6298
+ cause;
6299
+ constructor(cause) {
6300
+ super("Slack event durable persistence failed");
6301
+ this.name = "SlackEventPersistenceError";
6302
+ this.cause = cause;
6303
+ }
6304
+ };
6244
6305
  function enqueue(waitUntil, task) {
6245
6306
  waitUntil(task);
6246
6307
  }
@@ -6292,7 +6353,7 @@ async function buildThread(args) {
6292
6353
  });
6293
6354
  }
6294
6355
  function shouldIgnoreMessage(message) {
6295
- return message.author.isMe === true || isExternalSlackUser(message.raw);
6356
+ return message.author.isMe === true || !parseActorUserId(message.author.userId) || isExternalSlackUser(message.raw);
6296
6357
  }
6297
6358
  function shouldPersistBeforeAck(body) {
6298
6359
  return body.event?.type === "app_mention" || body.event?.type === "message";
@@ -6312,6 +6373,8 @@ async function persistSlackMessage(args) {
6312
6373
  conversationStore: args.conversationStore,
6313
6374
  queue: args.queue,
6314
6375
  state: args.state
6376
+ }).catch((error) => {
6377
+ throw new SlackEventPersistenceError(error);
6315
6378
  });
6316
6379
  }
6317
6380
  async function routeParsedMessage(args) {
@@ -6660,9 +6723,16 @@ async function handleSlackWebhook(args) {
6660
6723
  services: args.services
6661
6724
  });
6662
6725
  if (shouldPersistBeforeAck(parsed)) {
6663
- await eventTask.catch((error) => {
6664
- logException(error, "slack_event_persist_failed");
6665
- });
6726
+ try {
6727
+ await eventTask;
6728
+ } catch (error) {
6729
+ if (!(error instanceof SlackEventPersistenceError)) {
6730
+ logException(error, "slack_event_enqueue_failed");
6731
+ return new Response("ok", { status: 200 });
6732
+ }
6733
+ logException(error.cause, "slack_event_persist_failed");
6734
+ return new Response("Slack event persistence failed", { status: 503 });
6735
+ }
6666
6736
  } else {
6667
6737
  enqueue(
6668
6738
  args.waitUntil,
@@ -11141,6 +11211,7 @@ function createProductionConversationWorkOptions(options) {
11141
11211
  }
11142
11212
 
11143
11213
  // src/app.ts
11214
+ var DASHBOARD_PACKAGE_NAME = "@sentry/junior-dashboard";
11144
11215
  async function defaultWaitUntil() {
11145
11216
  try {
11146
11217
  const { waitUntil } = await import("@vercel/functions");
@@ -11159,6 +11230,8 @@ async function resolveVirtualConfig() {
11159
11230
  try {
11160
11231
  const mod = await import("#junior/config");
11161
11232
  return {
11233
+ createDashboardApp: mod.createDashboardApp,
11234
+ dashboard: mod.dashboard,
11162
11235
  pluginSet: mod.pluginSet,
11163
11236
  plugins: mod.plugins,
11164
11237
  pluginRuntimeRegistrations: mod.pluginRuntimeRegistrations ?? []
@@ -11221,7 +11294,161 @@ function validateBuildIncludesPluginRuntimeRegistrations(runtimeRegistrations, v
11221
11294
  `createApp() is missing plugin registration(s) with runtime code bundled by juniorNitro(): ${missing.join(", ")}. Pass a runtime-safe plugin module to juniorNitro({ plugins: "./plugins" }) or pass the same defineJuniorPlugins(...) set to createApp({ plugins }).`
11222
11295
  );
11223
11296
  }
11224
- function mountPluginRoutes(app, routes) {
11297
+ async function createDashboardRouteRegistrations(args) {
11298
+ if (!args.dashboard || args.dashboard.disabled) {
11299
+ return [];
11300
+ }
11301
+ const createDashboardApp = args.createDashboardApp ?? await loadDashboardAppFactory();
11302
+ return dashboardRouteRegistrations({
11303
+ dashboard: args.dashboard,
11304
+ createDashboardApp,
11305
+ pluginRoutes: args.pluginRoutes
11306
+ });
11307
+ }
11308
+ async function loadDashboardAppFactory() {
11309
+ try {
11310
+ const appRequire = createRequire(`${process.cwd()}/package.json`);
11311
+ const mod = await import(pathToFileURL(appRequire.resolve(DASHBOARD_PACKAGE_NAME)).href);
11312
+ return dashboardAppFactoryFromModule(mod);
11313
+ } catch (error) {
11314
+ if (isMissingDashboardPackage(error)) {
11315
+ throw new Error(
11316
+ 'createApp({ dashboard }) requires installing "@sentry/junior-dashboard"',
11317
+ { cause: error }
11318
+ );
11319
+ }
11320
+ throw error;
11321
+ }
11322
+ }
11323
+ function dashboardAppFactoryFromModule(mod) {
11324
+ if (!mod || typeof mod !== "object" || typeof mod.createDashboardApp !== "function") {
11325
+ throw new Error(
11326
+ '@sentry/junior-dashboard must export a "createDashboardApp" function'
11327
+ );
11328
+ }
11329
+ return mod.createDashboardApp;
11330
+ }
11331
+ function isMissingDashboardPackage(error) {
11332
+ if (!(error instanceof Error)) {
11333
+ return false;
11334
+ }
11335
+ const code = error.code;
11336
+ return (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") && error.message.includes("@sentry/junior-dashboard");
11337
+ }
11338
+ function stripTrailingSlashes2(value) {
11339
+ let end = value.length;
11340
+ while (end > 1 && value.charCodeAt(end - 1) === 47) {
11341
+ end -= 1;
11342
+ }
11343
+ return end === value.length ? value : value.slice(0, end);
11344
+ }
11345
+ function normalizeDashboardPath2(path2, fallback) {
11346
+ const value = path2?.trim() || fallback;
11347
+ const withSlash = value.startsWith("/") ? value : `/${value}`;
11348
+ return stripTrailingSlashes2(withSlash);
11349
+ }
11350
+ function dashboardHostRoutePaths(dashboard) {
11351
+ const basePath = normalizeDashboardPath2(dashboard.basePath, "/");
11352
+ const authPath = normalizeDashboardPath2(dashboard.authPath, "/api/auth");
11353
+ const pagePaths = basePath === "/" ? [
11354
+ "/",
11355
+ "/conversations",
11356
+ "/conversations/*",
11357
+ "/plugins",
11358
+ "/plugins/*",
11359
+ "/sessions",
11360
+ "/sessions/*"
11361
+ ] : [basePath, `${basePath}/*`];
11362
+ return [
11363
+ ...pagePaths,
11364
+ "/favicon.ico",
11365
+ "/api/dashboard",
11366
+ "/api/dashboard/*",
11367
+ authPath,
11368
+ `${authPath}/*`
11369
+ ];
11370
+ }
11371
+ function routePrefixCoversPath(routePrefix, path2) {
11372
+ return routePrefix === "/" || path2 === routePrefix || path2.startsWith(`${routePrefix}/`);
11373
+ }
11374
+ function routeSegments(path2) {
11375
+ return normalizeDashboardPath2(path2, "/").split("/").filter(Boolean);
11376
+ }
11377
+ function routeSegmentMatches(pattern, value) {
11378
+ return pattern === value || pattern === "*" || pattern.startsWith(":");
11379
+ }
11380
+ function routePatternMatchesConcretePath(pattern, concretePath) {
11381
+ const patternSegments = routeSegments(pattern);
11382
+ const pathSegments = routeSegments(concretePath);
11383
+ for (let index = 0; index < patternSegments.length; index += 1) {
11384
+ const segment = patternSegments[index];
11385
+ if (segment === "**" || segment === "*") {
11386
+ return true;
11387
+ }
11388
+ const value = pathSegments[index];
11389
+ if (!value || !routeSegmentMatches(segment, value)) {
11390
+ return false;
11391
+ }
11392
+ }
11393
+ return patternSegments.length === pathSegments.length;
11394
+ }
11395
+ function routePatternExamples(routePath) {
11396
+ const normalized = normalizeDashboardPath2(routePath, "/");
11397
+ if (!normalized.endsWith("/*") && !normalized.endsWith("/**")) {
11398
+ return [normalized];
11399
+ }
11400
+ const prefix = normalizeDashboardPath2(
11401
+ normalized.endsWith("/*") ? normalized.slice(0, -2) : normalized.slice(0, -3),
11402
+ "/"
11403
+ );
11404
+ return [
11405
+ prefix,
11406
+ prefix === "/" ? "/__dashboard__" : `${prefix}/__dashboard__`
11407
+ ];
11408
+ }
11409
+ function routePatternOverlaps(ownedPath, routePath) {
11410
+ if (ownedPath.endsWith("/*") && routePrefixCoversPath(ownedPath.slice(0, -2), routePath)) {
11411
+ return true;
11412
+ }
11413
+ return routePatternExamples(ownedPath).some(
11414
+ (example) => routePatternMatchesConcretePath(routePath, example)
11415
+ );
11416
+ }
11417
+ function dashboardOwnedRoutePath(routePath, dashboard) {
11418
+ return dashboardHostRoutePaths(dashboard).some(
11419
+ (path2) => routePatternOverlaps(path2, routePath)
11420
+ );
11421
+ }
11422
+ function dashboardRouteRegistrations(args) {
11423
+ let app;
11424
+ const fetch2 = (request) => {
11425
+ app ??= args.createDashboardApp({
11426
+ ...args.dashboard,
11427
+ pluginRoutes: args.pluginRoutes
11428
+ });
11429
+ if (!app || typeof app.fetch !== "function") {
11430
+ throw new Error("createDashboardApp() must return an app with fetch()");
11431
+ }
11432
+ return app.fetch(request);
11433
+ };
11434
+ return dashboardHostRoutePaths(args.dashboard).map((path2) => ({
11435
+ handler: fetch2,
11436
+ path: path2
11437
+ }));
11438
+ }
11439
+ function validateDashboardRouteOwnership(args) {
11440
+ if (!args.dashboard || args.dashboard.disabled) {
11441
+ return;
11442
+ }
11443
+ for (const route of args.routes) {
11444
+ if (dashboardOwnedRoutePath(route.path, args.dashboard)) {
11445
+ throw new Error(
11446
+ `Plugin "${route.pluginName}" route "${route.path}" conflicts with core dashboard routes`
11447
+ );
11448
+ }
11449
+ }
11450
+ }
11451
+ function mountRoutes(app, routes) {
11225
11452
  for (const route of routes) {
11226
11453
  const handler = (c) => route.handler(c.req.raw);
11227
11454
  const methods = Array.isArray(route.method) ? route.method : [route.method ?? "ALL"];
@@ -11237,6 +11464,7 @@ function mountPluginRoutes(app, routes) {
11237
11464
  }
11238
11465
  async function createApp(options) {
11239
11466
  const virtualConfig = await resolveVirtualConfig();
11467
+ const dashboard = options?.dashboard ?? virtualConfig?.dashboard;
11240
11468
  const configuredPlugins = options?.plugins ?? virtualConfig?.pluginSet;
11241
11469
  const plugins = pluginRuntimeRegistrationsFromPluginSet(configuredPlugins);
11242
11470
  const pluginConfig = configuredPlugins ? pluginCatalogConfigFromPluginSet(configuredPlugins) : virtualConfig?.plugins ?? pluginCatalogConfigFromEnv();
@@ -11251,7 +11479,9 @@ async function createApp(options) {
11251
11479
  const previousPlugins = setPlugins(plugins);
11252
11480
  const previousConfigDefaults = getConfigDefaults();
11253
11481
  const previousSlackReactionConfig = getSlackReactionConfig();
11482
+ const previousDashboardLinkOptions = setDashboardConversationLinkOptions(dashboard);
11254
11483
  let pluginRoutes = [];
11484
+ let pluginDashboardRoutes = [];
11255
11485
  let sandboxEgressTracePropagationDomains = [];
11256
11486
  try {
11257
11487
  sandboxEgressTracePropagationDomains = normalizeSandboxEgressTracePropagationDomains(
@@ -11269,11 +11499,16 @@ async function createApp(options) {
11269
11499
  );
11270
11500
  }
11271
11501
  pluginRoutes = getPluginRoutes();
11502
+ validateDashboardRouteOwnership({ dashboard, routes: pluginRoutes });
11503
+ if (dashboard && !dashboard.disabled) {
11504
+ pluginDashboardRoutes = getPluginDashboardRoutes();
11505
+ }
11272
11506
  } catch (error) {
11273
11507
  setPluginCatalogConfig(previousPluginCatalogConfig);
11274
11508
  setPlugins(previousPlugins);
11275
11509
  setConfigDefaults(previousConfigDefaults);
11276
11510
  setSlackReactionConfig(previousSlackReactionConfig);
11511
+ setDashboardConversationLinkOptions(previousDashboardLinkOptions);
11277
11512
  throw error;
11278
11513
  }
11279
11514
  const waitUntil = options?.waitUntil ?? await defaultWaitUntil();
@@ -11301,7 +11536,15 @@ async function createApp(options) {
11301
11536
  next
11302
11537
  );
11303
11538
  });
11304
- mountPluginRoutes(app, pluginRoutes);
11539
+ mountRoutes(app, pluginRoutes);
11540
+ mountRoutes(
11541
+ app,
11542
+ await createDashboardRouteRegistrations({
11543
+ dashboard,
11544
+ createDashboardApp: virtualConfig?.createDashboardApp,
11545
+ pluginRoutes: pluginDashboardRoutes
11546
+ })
11547
+ );
11305
11548
  app.get("/", () => GET());
11306
11549
  app.get("/health", () => GET());
11307
11550
  app.get("/api/oauth/callback/mcp/:provider", (c) => {
@@ -1,4 +1,6 @@
1
1
  /** Copy app and declared plugin package content into the server output. */
2
2
  export declare function copyAppAndPluginContent(cwd: string, serverRoot: string, packageNames?: unknown): void;
3
+ /** Copy dashboard browser assets when core dashboard routes are enabled. */
4
+ export declare function copyDashboardAssets(cwd: string, serverRoot: string): void;
3
5
  /** Copy extra file patterns into server output for files the bundler cannot trace. */
4
6
  export declare function copyIncludedFiles(cwd: string, serverRoot: string, patterns?: unknown): void;
@@ -1,5 +1,6 @@
1
1
  import type { Nitro } from "nitro/types";
2
2
  import type { PluginCatalogConfig } from "@/chat/plugins/types";
3
+ import type { JuniorDashboardOptions } from "@/app";
3
4
  import { type JuniorPluginSet } from "@/plugins";
4
5
  export interface RuntimePluginModule {
5
6
  exportName: string;
@@ -7,6 +8,7 @@ export interface RuntimePluginModule {
7
8
  }
8
9
  /** Render the virtual config module consumed by createApp(). */
9
10
  export declare function renderVirtualConfig(options: {
11
+ dashboard?: Omit<JuniorDashboardOptions, "reporting">;
10
12
  plugins?: PluginCatalogConfig;
11
13
  pluginModule?: RuntimePluginModule;
12
14
  pluginRuntimeRegistrations?: string[];
@@ -17,4 +19,5 @@ export declare function injectVirtualConfig(nitro: Nitro, options?: {
17
19
  pluginModule?: RuntimePluginModule;
18
20
  plugins?: PluginCatalogConfig;
19
21
  pluginRuntimeRegistrations?: string[];
22
+ dashboard?: Omit<JuniorDashboardOptions, "reporting">;
20
23
  }): void;
@@ -1,7 +1,7 @@
1
1
  import type { Destination } from "@sentry/junior-plugin-api";
2
2
  import type { StoredSlackRequester } from "@/chat/requester";
3
3
  export type ConversationSource = "api" | "internal" | "local" | "plugin" | "scheduler" | "slack";
4
- export type ConversationStatus = "awaiting_resume" | "idle" | "pending" | "running";
4
+ export type ConversationStatus = "awaiting_resume" | "failed" | "idle" | "pending" | "running";
5
5
  export interface ConversationExecution {
6
6
  lastCheckpointAtMs?: number;
7
7
  lastEnqueuedAtMs?: number;
@@ -1,4 +1,4 @@
1
- import type { PluginConversations, PluginRoute, PluginOperationalReport, SlackConversationLink, PluginRegistration } from "@sentry/junior-plugin-api";
1
+ import type { PluginConversations, PluginRoute, PluginOperationalReport, PluginRouteApp, SlackConversationLink, PluginRegistration } from "@sentry/junior-plugin-api";
2
2
  import type { PluginPromptContributionContext } from "@/chat/plugins/prompt";
3
3
  import type { ToolDefinition } from "@/chat/tools/definition";
4
4
  import type { ToolRuntimeContext } from "@/chat/tools/types";
@@ -19,6 +19,10 @@ export interface ToolHookResult {
19
19
  export interface PluginRouteRegistration extends PluginRoute {
20
20
  pluginName: string;
21
21
  }
22
+ export interface PluginDashboardRouteRegistration {
23
+ app: PluginRouteApp;
24
+ pluginName: string;
25
+ }
22
26
  export interface PluginHookRunner {
23
27
  beforeToolExecute(input: ToolHookInput): Promise<ToolHookResult>;
24
28
  prepareSandbox(sandbox: SandboxInstance): Promise<void>;
@@ -39,6 +43,8 @@ export declare function getPluginUserPromptContributions(args: {
39
43
  export declare function getPluginTools(context: ToolRuntimeContext): Record<string, ToolDefinition<any>>;
40
44
  /** Collect route handlers exposed by plugins for app-level mounting. */
41
45
  export declare function getPluginRoutes(): PluginRouteRegistration[];
46
+ /** Collect dashboard-scoped route apps exposed by plugins. */
47
+ export declare function getPluginDashboardRoutes(): PluginDashboardRouteRegistration[];
42
48
  /** Resolve the first plugin conversation URL for finalized Slack footers. */
43
49
  export declare function getPluginSlackConversationLink(conversationId: string): SlackConversationLink | undefined;
44
50
  /** Collect read-only operational summaries exposed by plugins. */
@@ -0,0 +1,9 @@
1
+ export interface DashboardConversationLinkOptions {
2
+ basePath?: string;
3
+ baseURL?: string;
4
+ disabled?: boolean;
5
+ }
6
+ /** Configure core dashboard links used in Slack footers. */
7
+ export declare function setDashboardConversationLinkOptions(options: DashboardConversationLinkOptions | undefined): DashboardConversationLinkOptions | undefined;
8
+ /** Build the dashboard conversation URL when the core dashboard is enabled. */
9
+ export declare function getDashboardConversationLink(conversationId: string): string | undefined;
@@ -1,6 +1,5 @@
1
1
  import type { RedisStateAdapter } from "@chat-adapter/state-redis";
2
2
  import type { StateAdapter } from "chat";
3
- export declare const ACTIVE_LOCK_TTL_MS = 90000;
4
3
  export declare function getConnectedStateContext(): Promise<{
5
4
  redisStateAdapter?: RedisStateAdapter;
6
5
  stateAdapter: StateAdapter;
@@ -0,0 +1,7 @@
1
+ import type { Lock, StateAdapter } from "chat";
2
+ export declare const ACTIVE_LOCK_TTL_MS = 90000;
3
+ /**
4
+ * Acquire a lock for long-running work that the queued state adapter should
5
+ * keep alive while the owning invocation is still making progress.
6
+ */
7
+ export declare function acquireActiveLock(state: StateAdapter, threadId: string): Promise<Lock | null>;
@@ -7,7 +7,7 @@ export declare const CONVERSATION_WORK_LEASE_TTL_MS = 90000;
7
7
  export declare const CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15000;
8
8
  export declare const CONVERSATION_WORK_STALE_ENQUEUE_MS = 60000;
9
9
  export type Source = "api" | "internal" | "local" | "plugin" | "scheduler" | "slack";
10
- export type ExecutionStatus = "awaiting_resume" | "idle" | "pending" | "running";
10
+ export type ExecutionStatus = "awaiting_resume" | "failed" | "idle" | "pending" | "running";
11
11
  export interface AgentInput {
12
12
  attachments?: unknown[];
13
13
  authorId?: string;
@@ -127,6 +127,26 @@ export declare function recordConversationActivity(args: {
127
127
  state?: StateAdapter;
128
128
  title?: string;
129
129
  }): Promise<void>;
130
+ /** Store task-execution metadata for local/no-SQL reporting. */
131
+ export declare function recordConversationExecution(args: {
132
+ channelName?: string;
133
+ conversationId: string;
134
+ createdAtMs: number;
135
+ destination?: Destination;
136
+ execution: {
137
+ lastCheckpointAtMs?: number;
138
+ lastEnqueuedAtMs?: number;
139
+ runId?: string;
140
+ status: ExecutionStatus;
141
+ updatedAtMs?: number;
142
+ };
143
+ lastActivityAtMs: number;
144
+ requester?: StoredSlackRequester;
145
+ source?: Source;
146
+ state?: StateAdapter;
147
+ title?: string;
148
+ updatedAtMs: number;
149
+ }): Promise<void>;
130
150
  /** Record that a wake-up nudge was accepted for the conversation. */
131
151
  export declare function markConversationWorkEnqueued(args: {
132
152
  conversationId: string;