@sentry/junior 0.77.0 → 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.
@@ -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,9 +11,9 @@ import {
10
11
  getPlugins,
11
12
  setPlugins,
12
13
  validatePlugins
13
- } from "./chunk-WBSGTHNO.js";
14
+ } from "./chunk-SSWBYEFH.js";
14
15
  import "./chunk-RARSKPVT.js";
15
- import "./chunk-NYKJ3KON.js";
16
+ import "./chunk-237T7XAN.js";
16
17
  import "./chunk-G3E7SCME.js";
17
18
  import "./chunk-LXTPBU4K.js";
18
19
  import "./chunk-Q6XFTRV5.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-W36B5PT4.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,7 +90,7 @@ import {
90
90
  requestConversationContinuation,
91
91
  requestConversationWork,
92
92
  startConversationWork
93
- } from "./chunk-KPL4WJWA.js";
93
+ } from "./chunk-LUNMJQ7D.js";
94
94
  import {
95
95
  JUNIOR_THREAD_STATE_TTL_MS,
96
96
  coerceThreadConversationState
@@ -129,7 +129,7 @@ import {
129
129
  recordAuthorizationCompleted,
130
130
  splitSlackReplyText,
131
131
  truncateStatusText
132
- } from "./chunk-TO3UAY2M.js";
132
+ } from "./chunk-QDQVOMBA.js";
133
133
  import {
134
134
  validatePluginEgressCredentialHooks,
135
135
  validatePluginRegistrations
@@ -142,13 +142,14 @@ 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-WBSGTHNO.js";
152
+ } from "./chunk-SSWBYEFH.js";
152
153
  import {
153
154
  createPluginLogger,
154
155
  createPluginState
@@ -156,7 +157,7 @@ import {
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
163
  acquireActiveLock,
@@ -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
  }
@@ -11158,6 +11211,7 @@ function createProductionConversationWorkOptions(options) {
11158
11211
  }
11159
11212
 
11160
11213
  // src/app.ts
11214
+ var DASHBOARD_PACKAGE_NAME = "@sentry/junior-dashboard";
11161
11215
  async function defaultWaitUntil() {
11162
11216
  try {
11163
11217
  const { waitUntil } = await import("@vercel/functions");
@@ -11176,6 +11230,8 @@ async function resolveVirtualConfig() {
11176
11230
  try {
11177
11231
  const mod = await import("#junior/config");
11178
11232
  return {
11233
+ createDashboardApp: mod.createDashboardApp,
11234
+ dashboard: mod.dashboard,
11179
11235
  pluginSet: mod.pluginSet,
11180
11236
  plugins: mod.plugins,
11181
11237
  pluginRuntimeRegistrations: mod.pluginRuntimeRegistrations ?? []
@@ -11238,7 +11294,161 @@ function validateBuildIncludesPluginRuntimeRegistrations(runtimeRegistrations, v
11238
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 }).`
11239
11295
  );
11240
11296
  }
11241
- 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) {
11242
11452
  for (const route of routes) {
11243
11453
  const handler = (c) => route.handler(c.req.raw);
11244
11454
  const methods = Array.isArray(route.method) ? route.method : [route.method ?? "ALL"];
@@ -11254,6 +11464,7 @@ function mountPluginRoutes(app, routes) {
11254
11464
  }
11255
11465
  async function createApp(options) {
11256
11466
  const virtualConfig = await resolveVirtualConfig();
11467
+ const dashboard = options?.dashboard ?? virtualConfig?.dashboard;
11257
11468
  const configuredPlugins = options?.plugins ?? virtualConfig?.pluginSet;
11258
11469
  const plugins = pluginRuntimeRegistrationsFromPluginSet(configuredPlugins);
11259
11470
  const pluginConfig = configuredPlugins ? pluginCatalogConfigFromPluginSet(configuredPlugins) : virtualConfig?.plugins ?? pluginCatalogConfigFromEnv();
@@ -11268,7 +11479,9 @@ async function createApp(options) {
11268
11479
  const previousPlugins = setPlugins(plugins);
11269
11480
  const previousConfigDefaults = getConfigDefaults();
11270
11481
  const previousSlackReactionConfig = getSlackReactionConfig();
11482
+ const previousDashboardLinkOptions = setDashboardConversationLinkOptions(dashboard);
11271
11483
  let pluginRoutes = [];
11484
+ let pluginDashboardRoutes = [];
11272
11485
  let sandboxEgressTracePropagationDomains = [];
11273
11486
  try {
11274
11487
  sandboxEgressTracePropagationDomains = normalizeSandboxEgressTracePropagationDomains(
@@ -11286,11 +11499,16 @@ async function createApp(options) {
11286
11499
  );
11287
11500
  }
11288
11501
  pluginRoutes = getPluginRoutes();
11502
+ validateDashboardRouteOwnership({ dashboard, routes: pluginRoutes });
11503
+ if (dashboard && !dashboard.disabled) {
11504
+ pluginDashboardRoutes = getPluginDashboardRoutes();
11505
+ }
11289
11506
  } catch (error) {
11290
11507
  setPluginCatalogConfig(previousPluginCatalogConfig);
11291
11508
  setPlugins(previousPlugins);
11292
11509
  setConfigDefaults(previousConfigDefaults);
11293
11510
  setSlackReactionConfig(previousSlackReactionConfig);
11511
+ setDashboardConversationLinkOptions(previousDashboardLinkOptions);
11294
11512
  throw error;
11295
11513
  }
11296
11514
  const waitUntil = options?.waitUntil ?? await defaultWaitUntil();
@@ -11318,7 +11536,15 @@ async function createApp(options) {
11318
11536
  next
11319
11537
  );
11320
11538
  });
11321
- 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
+ );
11322
11548
  app.get("/", () => GET());
11323
11549
  app.get("/health", () => GET());
11324
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;
@@ -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;
@@ -449,7 +449,7 @@ function destinationUpsertFromDestination(args) {
449
449
  };
450
450
  }
451
451
  function executionStatusFromValue(value) {
452
- if (value === "awaiting_resume" || value === "idle" || value === "pending" || value === "running") {
452
+ if (value === "awaiting_resume" || value === "failed" || value === "idle" || value === "pending" || value === "running") {
453
453
  return value;
454
454
  }
455
455
  throw new Error("Conversation record execution status is invalid");
@@ -727,8 +727,8 @@ var SqlStore = class {
727
727
  executionUpdatedAt: sql2`case when ${incomingExecutionIsFresh} then excluded.execution_updated_at else ${juniorConversations.executionUpdatedAt} end`,
728
728
  executionStatus: sql2`case when ${incomingExecutionIsFresh} then excluded.execution_status else ${juniorConversations.executionStatus} end`,
729
729
  runId: sql2`case when ${incomingExecutionIsFresh} then excluded.run_id else ${juniorConversations.runId} end`,
730
- lastCheckpointAt: sql2`case when ${incomingExecutionIsFresh} then excluded.last_checkpoint_at else ${juniorConversations.lastCheckpointAt} end`,
731
- lastEnqueuedAt: sql2`case when ${incomingExecutionIsFresh} then excluded.last_enqueued_at else ${juniorConversations.lastEnqueuedAt} end`
730
+ lastCheckpointAt: sql2`case when ${incomingExecutionIsFresh} then coalesce(excluded.last_checkpoint_at, ${juniorConversations.lastCheckpointAt}) else ${juniorConversations.lastCheckpointAt} end`,
731
+ lastEnqueuedAt: sql2`case when ${incomingExecutionIsFresh} then coalesce(excluded.last_enqueued_at, ${juniorConversations.lastEnqueuedAt}) else ${juniorConversations.lastEnqueuedAt} end`
732
732
  }
733
733
  });
734
734
  }
@@ -28,7 +28,7 @@ import {
28
28
  recordAuthorizationRequested,
29
29
  recordMcpProviderConnected,
30
30
  upsertAgentTurnSessionRecord
31
- } from "./chunk-TO3UAY2M.js";
31
+ } from "./chunk-QDQVOMBA.js";
32
32
  import {
33
33
  createPluginEmbedder,
34
34
  createPluginHookRunner,
@@ -39,14 +39,14 @@ import {
39
39
  getPlugins,
40
40
  getSlackToolContext,
41
41
  resolveChannelCapabilities
42
- } from "./chunk-WBSGTHNO.js";
42
+ } from "./chunk-SSWBYEFH.js";
43
43
  import {
44
44
  createPluginLogger,
45
45
  createPluginState
46
46
  } from "./chunk-RARSKPVT.js";
47
47
  import {
48
48
  getDb
49
- } from "./chunk-NYKJ3KON.js";
49
+ } from "./chunk-237T7XAN.js";
50
50
  import {
51
51
  SANDBOX_DATA_ROOT,
52
52
  SANDBOX_SKILLS_ROOT,
@@ -74,7 +74,7 @@ function normalizeSource(value) {
74
74
  return void 0;
75
75
  }
76
76
  function normalizeExecutionStatus(value) {
77
- if (value === "awaiting_resume" || value === "idle" || value === "pending" || value === "running") {
77
+ if (value === "awaiting_resume" || value === "failed" || value === "idle" || value === "pending" || value === "running") {
78
78
  return value;
79
79
  }
80
80
  return void 0;
@@ -231,8 +231,11 @@ function isLeaseActive(lease, nowMs) {
231
231
  function pendingMessages(conversation) {
232
232
  return [...conversation.execution.pendingMessages].sort(compareMessages);
233
233
  }
234
+ function isRunnableStatus(status) {
235
+ return status !== "failed" && status !== "idle";
236
+ }
234
237
  function hasRunnableWork(conversation) {
235
- return conversation.execution.status !== "idle" || pendingMessages(conversation).length > 0;
238
+ return isRunnableStatus(conversation.execution.status) || pendingMessages(conversation).length > 0;
236
239
  }
237
240
  function executionWithPendingMessages(execution, pending) {
238
241
  const pendingMessages2 = [...pending].sort(compareMessages);
@@ -736,6 +739,48 @@ async function recordConversationActivity(args) {
736
739
  });
737
740
  });
738
741
  }
742
+ async function recordConversationExecution(args) {
743
+ const nowMs = args.updatedAtMs;
744
+ await withConversationMutation(args, async (state) => {
745
+ const existing = await readConversation(state, args.conversationId);
746
+ if (existing && args.destination) {
747
+ assertSameConversationDestination({
748
+ conversationId: args.conversationId,
749
+ current: existing.destination,
750
+ next: args.destination
751
+ });
752
+ }
753
+ const current = existing ?? emptyConversation({
754
+ conversationId: args.conversationId,
755
+ destination: args.destination,
756
+ nowMs,
757
+ source: args.source
758
+ });
759
+ await writeConversation(
760
+ state,
761
+ withExecutionUpdate(
762
+ {
763
+ ...current,
764
+ ...current.destination ?? args.destination ? { destination: current.destination ?? args.destination } : {},
765
+ ...current.source ?? args.source ? { source: current.source ?? args.source } : {},
766
+ ...current.channelName ?? args.channelName ? { channelName: current.channelName ?? args.channelName } : {},
767
+ ...current.requester ?? args.requester ? { requester: current.requester ?? args.requester } : {},
768
+ ...current.title ?? args.title ? { title: current.title ?? args.title } : {},
769
+ createdAtMs: Math.min(current.createdAtMs, args.createdAtMs),
770
+ lastActivityAtMs: Math.max(
771
+ current.lastActivityAtMs,
772
+ args.lastActivityAtMs
773
+ )
774
+ },
775
+ {
776
+ ...current.execution,
777
+ ...args.execution
778
+ },
779
+ nowMs
780
+ )
781
+ );
782
+ });
783
+ }
739
784
  async function markConversationWorkEnqueued(args) {
740
785
  const nowMs = args.nowMs ?? now();
741
786
  await withConversationMutation(args, async (state) => {
@@ -1061,6 +1106,7 @@ export {
1061
1106
  appendInboundMessage,
1062
1107
  requestConversationWork,
1063
1108
  recordConversationActivity,
1109
+ recordConversationExecution,
1064
1110
  markConversationWorkEnqueued,
1065
1111
  startConversationWork,
1066
1112
  checkInConversationWork,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getConversationStore
3
- } from "./chunk-NYKJ3KON.js";
3
+ } from "./chunk-237T7XAN.js";
4
4
  import {
5
5
  SANDBOX_DATA_ROOT,
6
6
  SANDBOX_WORKSPACE_ROOT,
@@ -1567,6 +1567,14 @@ function parseAgentTurnSessionStatus(parsed) {
1567
1567
  function parseAgentTurnSurface(value) {
1568
1568
  return value === "slack" || value === "api" || value === "scheduler" || value === "internal" ? value : void 0;
1569
1569
  }
1570
+ function conversationExecutionFromSummary(summary) {
1571
+ const status = summary.state === "completed" || summary.state === "abandoned" ? "idle" : summary.state;
1572
+ return {
1573
+ status,
1574
+ runId: summary.sessionId,
1575
+ updatedAtMs: summary.updatedAtMs
1576
+ };
1577
+ }
1570
1578
  function parseSource(value) {
1571
1579
  const result = sourceSchema.safeParse(value);
1572
1580
  return result.success ? result.data : void 0;
@@ -1690,6 +1698,17 @@ async function recordConversationActivityMetadata(args) {
1690
1698
  requester: sessionLogRequester(args.summary.requester),
1691
1699
  source
1692
1700
  });
1701
+ await conversationStore.recordExecution({
1702
+ channelName: args.summary.channelName,
1703
+ conversationId: args.summary.conversationId,
1704
+ createdAtMs: args.summary.startedAtMs,
1705
+ destination: args.summary.destination,
1706
+ execution: conversationExecutionFromSummary(args.summary),
1707
+ lastActivityAtMs: args.summary.updatedAtMs,
1708
+ requester: sessionLogRequester(args.summary.requester),
1709
+ source,
1710
+ updatedAtMs: args.nowMs
1711
+ });
1693
1712
  } catch (error) {
1694
1713
  logWarn(
1695
1714
  "conversation_activity_metadata_update_failed",
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-RARSKPVT.js";
5
5
  import {
6
6
  getDb
7
- } from "./chunk-NYKJ3KON.js";
7
+ } from "./chunk-237T7XAN.js";
8
8
  import {
9
9
  SANDBOX_WORKSPACE_ROOT
10
10
  } from "./chunk-G3E7SCME.js";
@@ -606,6 +606,29 @@ function getPluginRoutes() {
606
606
  }
607
607
  return routes;
608
608
  }
609
+ function getPluginDashboardRoutes() {
610
+ const routes = [];
611
+ for (const plugin of getPlugins()) {
612
+ const pluginName = plugin.manifest.name;
613
+ const hook = plugin.hooks?.dashboardRoutes;
614
+ if (!hook) {
615
+ continue;
616
+ }
617
+ const app = hook({
618
+ ...basePluginContext(plugin)
619
+ });
620
+ if (app === void 0) {
621
+ continue;
622
+ }
623
+ if (!isRecord(app) || typeof app.fetch !== "function") {
624
+ throw new Error(
625
+ `Plugin dashboardRoutes hook from plugin "${pluginName}" must return a fetch-compatible app`
626
+ );
627
+ }
628
+ routes.push({ app, pluginName });
629
+ }
630
+ return routes;
631
+ }
609
632
  function trustedSlackConversationUrl(pluginName, link) {
610
633
  const url = typeof link?.url === "string" ? link.url.trim() : "";
611
634
  if (!url) {
@@ -958,6 +981,7 @@ export {
958
981
  getPluginUserPromptContributions,
959
982
  getPluginTools,
960
983
  getPluginRoutes,
984
+ getPluginDashboardRoutes,
961
985
  getPluginSlackConversationLink,
962
986
  getPluginOperationalReports,
963
987
  createPluginHookRunner
package/dist/cli/chat.js CHANGED
@@ -130,10 +130,10 @@ async function configureLocalChatPlugins(pluginSet) {
130
130
  databaseModule
131
131
  ] = await Promise.all([
132
132
  import("../plugins-PZMDS7AT.js"),
133
- import("../agent-hooks-7P2WSR4R.js"),
133
+ import("../agent-hooks-OFDNZJB2.js"),
134
134
  import("../registry-RRIDPJBT.js"),
135
135
  import("../validation-MDMYBRFB.js"),
136
- import("../db-7A7PFRGL.js")
136
+ import("../db-NGQ3JCMF.js")
137
137
  ]);
138
138
  const resolvedPluginSet = pluginSet === void 0 ? await loadLocalPluginSet() : pluginSet ?? void 0;
139
139
  const plugins = pluginsModule.pluginRuntimeRegistrationsFromPluginSet(resolvedPluginSet);
@@ -193,7 +193,7 @@ async function runPrompt(options, io, pluginSet) {
193
193
  defaultStateAdapterForLocalChat();
194
194
  await configureLocalChatPlugins(pluginSet);
195
195
  const conversationId = newRunConversationId();
196
- const { runLocalAgentTurn } = await import("../runner-JWLZI3EX.js");
196
+ const { runLocalAgentTurn } = await import("../runner-WW4GJFUB.js");
197
197
  const result = await runLocalAgentTurn(
198
198
  {
199
199
  conversationId,
@@ -217,7 +217,7 @@ async function runInteractive(io, pluginSet) {
217
217
  defaultStateAdapterForLocalChat();
218
218
  await configureLocalChatPlugins(pluginSet);
219
219
  const conversationId = newRunConversationId();
220
- const { runLocalAgentTurn } = await import("../runner-JWLZI3EX.js");
220
+ const { runLocalAgentTurn } = await import("../runner-WW4GJFUB.js");
221
221
  const rl = readline.createInterface({
222
222
  input: io.input,
223
223
  output: io.output,
@@ -10,13 +10,13 @@ import {
10
10
  import {
11
11
  setPlugins,
12
12
  validatePlugins
13
- } from "../chunk-WBSGTHNO.js";
13
+ } from "../chunk-SSWBYEFH.js";
14
14
  import {
15
15
  createPluginLogger
16
16
  } from "../chunk-RARSKPVT.js";
17
17
  import {
18
18
  getDb
19
- } from "../chunk-NYKJ3KON.js";
19
+ } from "../chunk-237T7XAN.js";
20
20
  import "../chunk-G3E7SCME.js";
21
21
  import "../chunk-LXTPBU4K.js";
22
22
  import "../chunk-Q6XFTRV5.js";
@@ -2,8 +2,9 @@ import {
2
2
  getConversation,
3
3
  listConversationsByActivity,
4
4
  recordConversationActivity,
5
+ recordConversationExecution,
5
6
  requestConversationWork
6
- } from "../chunk-KPL4WJWA.js";
7
+ } from "../chunk-LUNMJQ7D.js";
7
8
  import {
8
9
  JUNIOR_THREAD_STATE_TTL_MS,
9
10
  coerceThreadConversationState
@@ -22,7 +23,7 @@ import {
22
23
  createJuniorSqlExecutor,
23
24
  createSqlStore,
24
25
  getDb
25
- } from "../chunk-NYKJ3KON.js";
26
+ } from "../chunk-237T7XAN.js";
26
27
  import {
27
28
  disconnectStateAdapter,
28
29
  getConnectedStateContext
@@ -75,8 +76,7 @@ function createStateConversationStore(state) {
75
76
  return {
76
77
  get: (args) => getConversation({ ...args, state }),
77
78
  recordActivity: (args) => recordConversationActivity({ ...args, state }),
78
- recordExecution: async () => {
79
- },
79
+ recordExecution: (args) => recordConversationExecution({ ...args, state }),
80
80
  listByActivity: (args) => listConversationsByActivity({ ...args, state })
81
81
  };
82
82
  }
@@ -2,7 +2,7 @@ import {
2
2
  closeDb,
3
3
  getConversationStore,
4
4
  getDb
5
- } from "./chunk-NYKJ3KON.js";
5
+ } from "./chunk-237T7XAN.js";
6
6
  import "./chunk-Q6XFTRV5.js";
7
7
  import "./chunk-T77LUIX3.js";
8
8
  import "./chunk-VALUBQ7R.js";
package/dist/nitro.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { type JuniorPluginSet } from "./plugins";
2
+ import type { JuniorDashboardOptions } from "./app";
3
+ export type JuniorNitroDashboardOptions = Omit<JuniorDashboardOptions, "reporting">;
2
4
  export interface JuniorPluginModuleReference {
3
5
  /** Runtime-safe module that exports a `defineJuniorPlugins(...)` set. */
4
6
  module: string;
@@ -8,6 +10,8 @@ export interface JuniorPluginModuleReference {
8
10
  export type JuniorNitroPluginSource = JuniorPluginModuleReference | JuniorPluginSet | string;
9
11
  export interface JuniorNitroOptions {
10
12
  cwd?: string;
13
+ /** Authenticated dashboard configuration injected for createApp(). */
14
+ dashboard?: JuniorNitroDashboardOptions;
11
15
  maxDuration?: number;
12
16
  /** Vercel Queue topic for durable conversation work. Must match the runtime queue producer topic. */
13
17
  conversationWorkQueueTopic?: string;
package/dist/nitro.js CHANGED
@@ -54,6 +54,8 @@ function globToRegex(pattern) {
54
54
  }
55
55
 
56
56
  // src/build/copy-build-content.ts
57
+ var DASHBOARD_PACKAGE_NAME = "@sentry/junior-dashboard";
58
+ var DASHBOARD_ASSET_NAMES = ["client.js", "tailwind.css"];
57
59
  function copyAppAndPluginContent(cwd, serverRoot, packageNames) {
58
60
  copyIfExists(path.join(cwd, "app"), path.join(serverRoot, "app"));
59
61
  const packagedContent = discoverInstalledPluginPackageContent(cwd, {
@@ -83,6 +85,27 @@ function copyAppAndPluginContent(cwd, serverRoot, packageNames) {
83
85
  }
84
86
  }
85
87
  }
88
+ function copyDashboardAssets(cwd, serverRoot) {
89
+ const packageDir = resolvePackageDir(cwd, DASHBOARD_PACKAGE_NAME);
90
+ if (!packageDir) {
91
+ throw new Error(
92
+ `createApp({ dashboard }) requires installing "${DASHBOARD_PACKAGE_NAME}"`
93
+ );
94
+ }
95
+ for (const fileName of DASHBOARD_ASSET_NAMES) {
96
+ const source = dashboardAssetPath(packageDir, fileName);
97
+ copyIfExists(
98
+ source,
99
+ path.join(
100
+ serverRoot,
101
+ "node_modules",
102
+ DASHBOARD_PACKAGE_NAME,
103
+ "dist",
104
+ fileName
105
+ )
106
+ );
107
+ }
108
+ }
86
109
  function copyIncludedFiles(cwd, serverRoot, patterns) {
87
110
  if (patterns === void 0) return;
88
111
  if (!Array.isArray(patterns)) {
@@ -170,6 +193,20 @@ function copyIfExists(source, target) {
170
193
  cpSync(source, target, { recursive: true });
171
194
  return true;
172
195
  }
196
+ function dashboardAssetPath(packageDir, fileName) {
197
+ const candidates = [
198
+ path.join(packageDir, fileName),
199
+ path.join(packageDir, "dist", fileName)
200
+ ];
201
+ for (const candidate of candidates) {
202
+ if (existsSync(candidate)) {
203
+ return candidate;
204
+ }
205
+ }
206
+ throw new Error(
207
+ `Junior dashboard asset ${fileName} was not built; run pnpm --filter @sentry/junior-dashboard build before building Nitro`
208
+ );
209
+ }
173
210
  function copyRootIntoServerOutput(cwd, serverRoot, root) {
174
211
  copyIfExists(root, resolveServerOutputPath(cwd, serverRoot, root));
175
212
  }
@@ -205,14 +242,25 @@ function renderRuntimePluginImport(module) {
205
242
  }
206
243
  return `import { ${module.exportName} as juniorRuntimePluginSet } from ${JSON.stringify(module.specifier)};`;
207
244
  }
245
+ function renderDashboardImport(enabled) {
246
+ return enabled ? [
247
+ 'import { createDashboardApp as juniorCreateDashboardApp } from "@sentry/junior-dashboard";',
248
+ "export const createDashboardApp = juniorCreateDashboardApp;"
249
+ ] : ["export const createDashboardApp = undefined;"];
250
+ }
251
+ function dashboardEnabled(dashboard) {
252
+ return Boolean(dashboard && !dashboard.disabled);
253
+ }
208
254
  function renderVirtualConfig(options) {
209
255
  const lines = [
256
+ ...renderDashboardImport(dashboardEnabled(options.dashboard)),
210
257
  ...options.pluginModule ? [
211
258
  renderRuntimePluginImport(options.pluginModule),
212
259
  "export const pluginSet = juniorRuntimePluginSet;"
213
260
  ] : ["export const pluginSet = undefined;"],
214
261
  `export const plugins = ${JSON.stringify(options.plugins ?? { packages: [] })};`,
215
- `export const pluginRuntimeRegistrations = ${JSON.stringify(options.pluginRuntimeRegistrations ?? [])};`
262
+ `export const pluginRuntimeRegistrations = ${JSON.stringify(options.pluginRuntimeRegistrations ?? [])};`,
263
+ `export const dashboard = ${JSON.stringify(options.dashboard)};`
216
264
  ];
217
265
  return lines.join("\n");
218
266
  }
@@ -227,7 +275,8 @@ function injectVirtualConfig(nitro, options = {}) {
227
275
  plugins: pluginCatalogConfigFromPluginSet(pluginSet),
228
276
  pluginRuntimeRegistrations: pluginRuntimeRegistrationsFromPluginSet(
229
277
  pluginSet
230
- ).map((plugin) => plugin.manifest.name)
278
+ ).map((plugin) => plugin.manifest.name),
279
+ dashboard: options.dashboard
231
280
  });
232
281
  };
233
282
  }
@@ -255,6 +304,9 @@ function assertSerializableDirectPluginSet(pluginSet) {
255
304
  `juniorNitro({ plugins }) cannot receive a direct defineJuniorPlugins(...) set with runtime plugin registration(s): ${pluginRuntimeNames.join(", ")}. Export the set from a runtime-safe plugin module and pass juniorNitro({ plugins: "./plugins" }) so createApp() can import the same registrations at runtime.`
256
305
  );
257
306
  }
307
+ function dashboardEnabled2(dashboard) {
308
+ return Boolean(dashboard && !dashboard.disabled);
309
+ }
258
310
  function runtimeModuleForResolvedPluginModule(moduleRef) {
259
311
  return {
260
312
  exportName: moduleRef.exportName,
@@ -356,7 +408,8 @@ function juniorNitro(options = {}) {
356
408
  pluginModule: runtimeModuleForResolvedPluginModule(pluginModule)
357
409
  } : {},
358
410
  plugins: pluginCatalogConfig,
359
- pluginRuntimeRegistrations
411
+ pluginRuntimeRegistrations,
412
+ dashboard: options.dashboard
360
413
  });
361
414
  const copyBuildContent = async () => {
362
415
  const pluginSet = await loadConfiguredPluginSet();
@@ -366,6 +419,9 @@ function juniorNitro(options = {}) {
366
419
  nitro.options.output.serverDir,
367
420
  compiledPluginCatalogConfig?.packages
368
421
  );
422
+ if (dashboardEnabled2(options.dashboard)) {
423
+ copyDashboardAssets(cwd, nitro.options.output.serverDir);
424
+ }
369
425
  copyIncludedFiles(
370
426
  cwd,
371
427
  nitro.options.output.serverDir,
package/dist/reporting.js CHANGED
@@ -11,14 +11,14 @@ import {
11
11
  buildSystemPrompt,
12
12
  getAgentTurnSessionRecord,
13
13
  listAgentTurnSessionSummariesForConversation
14
- } from "./chunk-TO3UAY2M.js";
14
+ } from "./chunk-QDQVOMBA.js";
15
15
  import {
16
16
  getPluginOperationalReports
17
- } from "./chunk-WBSGTHNO.js";
17
+ } from "./chunk-SSWBYEFH.js";
18
18
  import "./chunk-RARSKPVT.js";
19
19
  import {
20
20
  getConversationStore
21
- } from "./chunk-NYKJ3KON.js";
21
+ } from "./chunk-237T7XAN.js";
22
22
  import "./chunk-G3E7SCME.js";
23
23
  import "./chunk-LXTPBU4K.js";
24
24
  import "./chunk-Q6XFTRV5.js";
@@ -163,6 +163,9 @@ function statusFromConversation(conversation, fallback, nowMs) {
163
163
  if (conversation.execution.status === "idle") {
164
164
  return "completed";
165
165
  }
166
+ if (conversation.execution.status === "failed") {
167
+ return "failed";
168
+ }
166
169
  const updatedAtMs = conversation.execution.updatedAtMs ?? conversation.updatedAtMs;
167
170
  if (conversation.execution.status === "running" && nowMs - updatedAtMs > HUNG_TURN_PROGRESS_MS) {
168
171
  return "hung";
@@ -796,22 +799,8 @@ async function readConversationStatsReport(options = {}) {
796
799
  });
797
800
  const truncated = conversations.length > CONVERSATION_STATS_LIMIT;
798
801
  const sampledConversations = conversations.slice(0, CONVERSATION_STATS_LIMIT);
799
- const detailsByConversationId = await getConversationDetailsForIds(
800
- sampledConversations.map((conversation) => conversation.conversationId)
801
- );
802
- const reportsByConversation = await reportsFromConversations({
803
- conversations: sampledConversations,
804
- detailsByConversationId,
805
- nowMs
806
- });
807
- const sessions = sampledConversations.flatMap(
808
- (conversation) => reportsByConversation.get(conversation.conversationId) ?? [
809
- sessionReportFromConversation(
810
- conversation,
811
- nowMs,
812
- detailsByConversationId.get(conversation.conversationId)
813
- )
814
- ]
802
+ const sessions = sampledConversations.map(
803
+ (conversation) => sessionReportFromConversation(conversation, nowMs)
815
804
  );
816
805
  return buildConversationStatsReport({
817
806
  generatedAt,
@@ -14,7 +14,7 @@ import {
14
14
  startActiveTurn,
15
15
  updateConversationStats,
16
16
  upsertConversationMessage
17
- } from "./chunk-W36B5PT4.js";
17
+ } from "./chunk-2MSW5BZY.js";
18
18
  import {
19
19
  coerceThreadConversationState
20
20
  } from "./chunk-Z4CIQ3EB.js";
@@ -23,10 +23,10 @@ import "./chunk-KNFROR7R.js";
23
23
  import {
24
24
  commitMessages,
25
25
  loadProjection
26
- } from "./chunk-TO3UAY2M.js";
27
- import "./chunk-WBSGTHNO.js";
26
+ } from "./chunk-QDQVOMBA.js";
27
+ import "./chunk-SSWBYEFH.js";
28
28
  import "./chunk-RARSKPVT.js";
29
- import "./chunk-NYKJ3KON.js";
29
+ import "./chunk-237T7XAN.js";
30
30
  import "./chunk-G3E7SCME.js";
31
31
  import "./chunk-LXTPBU4K.js";
32
32
  import "./chunk-Q6XFTRV5.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.77.0",
3
+ "version": "0.78.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -70,7 +70,7 @@
70
70
  "pg": "^8.16.3",
71
71
  "yaml": "^2.9.0",
72
72
  "zod": "^4.4.3",
73
- "@sentry/junior-plugin-api": "0.77.0"
73
+ "@sentry/junior-plugin-api": "0.78.0"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@emnapi/core": "^1.10.0",
@@ -86,9 +86,9 @@
86
86
  "typescript": "^6.0.3",
87
87
  "vercel": "^54.4.0",
88
88
  "vitest": "^4.1.7",
89
- "@sentry/junior-memory": "0.77.0",
89
+ "@sentry/junior-memory": "0.78.0",
90
90
  "@sentry/junior-testing": "0.0.0",
91
- "@sentry/junior-scheduler": "0.77.0"
91
+ "@sentry/junior-scheduler": "0.78.0"
92
92
  },
93
93
  "scripts": {
94
94
  "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",