@sentry/junior 0.66.3 → 0.67.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/reporting.js CHANGED
@@ -4,24 +4,25 @@ import {
4
4
  buildSentryTraceUrl,
5
5
  buildSystemPrompt,
6
6
  formatSlackConversationRedactedLabel,
7
+ getAgentPluginOperationalReports,
7
8
  getAgentTurnSessionRecord,
8
9
  listAgentTurnSessionSummaries,
9
10
  listAgentTurnSessionSummariesForConversation,
10
11
  resolveSlackConversationContextFromThreadId
11
- } from "./chunk-DG2I6GXC.js";
12
+ } from "./chunk-5UIDU7XR.js";
12
13
  import {
13
14
  discoverSkills
14
- } from "./chunk-YL5G5YC4.js";
15
+ } from "./chunk-V47RLIO2.js";
15
16
  import {
16
17
  canExposeConversationPayload,
17
18
  parseSlackThreadId,
18
19
  resolveConversationPrivacy
19
- } from "./chunk-SAYCFF7O.js";
20
+ } from "./chunk-MT23VNOH.js";
20
21
  import {
21
22
  getPluginPackageContent,
22
23
  getPluginProviders,
23
24
  isRecord
24
- } from "./chunk-6QWWMZCK.js";
25
+ } from "./chunk-OIIXZOOC.js";
25
26
  import "./chunk-Z3YD6NHK.js";
26
27
  import {
27
28
  homeDir
@@ -34,6 +35,9 @@ import path from "path";
34
35
  var HUNG_TURN_PROGRESS_MS = 5 * 60 * 1e3;
35
36
  var SAFE_METADATA_KEY_LIMIT = 20;
36
37
  var PRIVATE_CONVERSATION_LABEL = "Private Conversation";
38
+ var DASHBOARD_SESSION_FEED_LIMIT = 50;
39
+ var DASHBOARD_CONVERSATION_STATS_LIMIT = 5e3;
40
+ var RECENT_CONVERSATION_STATS_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
37
41
  function readDescriptionText() {
38
42
  try {
39
43
  const raw = readFileSync(
@@ -61,9 +65,9 @@ async function readPlugins() {
61
65
  name: plugin.manifest.name
62
66
  }));
63
67
  }
64
- function statusFromCheckpoint(summary) {
68
+ function statusFromCheckpoint(summary, nowMs = Date.now()) {
65
69
  const state = summary.state;
66
- if (state === "running" && Date.now() - summary.lastProgressAtMs > HUNG_TURN_PROGRESS_MS) {
70
+ if (state === "running" && nowMs - summary.lastProgressAtMs > HUNG_TURN_PROGRESS_MS) {
67
71
  return "hung";
68
72
  }
69
73
  if (state === "running" || state === "awaiting_resume") {
@@ -75,7 +79,13 @@ function statusFromCheckpoint(summary) {
75
79
  return state;
76
80
  }
77
81
  function surfaceFromConversationId(conversationId) {
78
- return parseSlackThreadId(conversationId) ? "slack" : "internal";
82
+ if (parseSlackThreadId(conversationId)) return "slack";
83
+ if (conversationId.startsWith("scheduler:")) return "scheduler";
84
+ if (conversationId.startsWith("api:")) return "api";
85
+ return "internal";
86
+ }
87
+ function surfaceFromSummary(summary) {
88
+ return summary.surface ?? surfaceFromConversationId(summary.conversationId);
79
89
  }
80
90
  function titleFromSummary(summary) {
81
91
  if (summary.state === "awaiting_resume" && summary.resumeReason) {
@@ -104,7 +114,7 @@ function turnUsageReport(usage) {
104
114
  };
105
115
  return Object.keys(report).length > 0 ? report : void 0;
106
116
  }
107
- function sessionReportFromSummary(summary) {
117
+ function sessionReportFromSummary(summary, nowMs = Date.now()) {
108
118
  const slackThread = parseSlackThreadId(summary.conversationId);
109
119
  const privacy = resolveConversationPrivacy({
110
120
  conversationId: summary.conversationId
@@ -126,14 +136,14 @@ function sessionReportFromSummary(summary) {
126
136
  conversationId: summary.conversationId,
127
137
  ...conversationTitle ? { conversationTitle } : {},
128
138
  id: summary.sessionId,
129
- status: statusFromCheckpoint(summary),
139
+ status: statusFromCheckpoint(summary, nowMs),
130
140
  startedAt: new Date(summary.startedAtMs).toISOString(),
131
141
  lastProgressAt: new Date(summary.lastProgressAtMs).toISOString(),
132
142
  lastSeenAt: new Date(summary.updatedAtMs).toISOString(),
133
143
  ...summary.state === "completed" ? { completedAt: new Date(summary.updatedAtMs).toISOString() } : {},
134
144
  cumulativeDurationMs: summary.cumulativeDurationMs,
135
145
  ...cumulativeUsage ? { cumulativeUsage } : {},
136
- surface: surfaceFromConversationId(summary.conversationId),
146
+ surface: surfaceFromSummary(summary),
137
147
  title: titleFromSummary(summary),
138
148
  ...requesterIdentity ? { requesterIdentity } : {},
139
149
  ...slackThread ? { channel: slackThread.channelId } : {},
@@ -143,6 +153,245 @@ function sessionReportFromSummary(summary) {
143
153
  ...sentryTraceUrl ? { sentryTraceUrl } : {}
144
154
  };
145
155
  }
156
+ function reportTime(value) {
157
+ const time = Date.parse(value);
158
+ return Number.isFinite(time) ? time : void 0;
159
+ }
160
+ function usageTokenTotal(usage) {
161
+ if (!usage) return void 0;
162
+ const components = [
163
+ usage.inputTokens,
164
+ usage.outputTokens,
165
+ usage.cachedInputTokens,
166
+ usage.cacheCreationTokens
167
+ ].reduce((sum, value) => {
168
+ const count = typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
169
+ return count === void 0 ? sum : (sum ?? 0) + count;
170
+ }, void 0);
171
+ if (components !== void 0) {
172
+ return components;
173
+ }
174
+ return typeof usage.totalTokens === "number" && Number.isFinite(usage.totalTokens) ? Math.max(0, Math.floor(usage.totalTokens)) : void 0;
175
+ }
176
+ function turnDurationSnapshot(turn) {
177
+ return typeof turn.cumulativeDurationMs === "number" && Number.isFinite(turn.cumulativeDurationMs) ? Math.max(0, Math.floor(turn.cumulativeDurationMs)) : void 0;
178
+ }
179
+ function turnContributions(turns) {
180
+ let previousDuration = 0;
181
+ let previousTokens = 0;
182
+ return turns.map((turn) => {
183
+ const duration = turnDurationSnapshot(turn);
184
+ const tokens = usageTokenTotal(turn.cumulativeUsage);
185
+ const contribution = {
186
+ durationMs: duration === void 0 ? 0 : Math.max(0, duration - previousDuration),
187
+ turn
188
+ };
189
+ if (tokens !== void 0) {
190
+ contribution.tokens = Math.max(0, tokens - previousTokens);
191
+ }
192
+ if (duration !== void 0) {
193
+ previousDuration = Math.max(previousDuration, duration);
194
+ }
195
+ if (tokens !== void 0) {
196
+ previousTokens = Math.max(previousTokens, tokens);
197
+ }
198
+ return contribution;
199
+ });
200
+ }
201
+ function contributionDurationTotal(contributions) {
202
+ return contributions.reduce(
203
+ (sum, contribution) => sum + contribution.durationMs,
204
+ 0
205
+ );
206
+ }
207
+ function addTokenTotal(total, tokens) {
208
+ return tokens === void 0 ? total : (total ?? 0) + tokens;
209
+ }
210
+ function contributionTokenTotal(contributions) {
211
+ return contributions.reduce(
212
+ (sum, contribution) => addTokenTotal(sum, contribution.tokens),
213
+ void 0
214
+ );
215
+ }
216
+ function requesterLabel(requester) {
217
+ const email = requester?.email?.trim() || void 0;
218
+ const fullName = requester?.fullName?.trim() || void 0;
219
+ const slackUserName = requester?.slackUserName?.trim() || void 0;
220
+ return email ?? fullName ?? slackUserName ?? requester?.slackUserId;
221
+ }
222
+ function slackStatsLocationLabel(input) {
223
+ const channelId = input.channel;
224
+ if (!channelId) return void 0;
225
+ const name = input.channelName?.replace(/^#/, "");
226
+ if (channelId.startsWith("D")) {
227
+ return "Direct Message";
228
+ }
229
+ if (channelId.startsWith("C")) {
230
+ return name ? `#${name}` : "Public Channel";
231
+ }
232
+ if (channelId.startsWith("G")) {
233
+ if (name?.startsWith("mpdm-")) return "Group DM";
234
+ return "Private Channel";
235
+ }
236
+ return name || channelId;
237
+ }
238
+ function locationLabel(turn) {
239
+ return slackStatsLocationLabel(turn) ?? (turn.surface === "scheduler" ? "Scheduler" : turn.surface === "api" ? "API" : turn.surface === "internal" ? "Internal" : "Unknown");
240
+ }
241
+ function emptyStatsItem(label) {
242
+ return {
243
+ active: 0,
244
+ conversations: 0,
245
+ durationMs: 0,
246
+ failed: 0,
247
+ hung: 0,
248
+ label,
249
+ turns: 0
250
+ };
251
+ }
252
+ function addItemTokens(item, tokens) {
253
+ if (tokens !== void 0) {
254
+ item.tokens = (item.tokens ?? 0) + tokens;
255
+ }
256
+ }
257
+ function statusSignals(turns) {
258
+ return {
259
+ active: turns.some((turn) => turn.status === "active"),
260
+ failed: turns.some((turn) => turn.status === "failed"),
261
+ hung: turns.some((turn) => turn.status === "hung")
262
+ };
263
+ }
264
+ function statsItems(map) {
265
+ return [...map.values()].sort(
266
+ (left, right) => right.conversations - left.conversations || right.turns - left.turns || right.durationMs - left.durationMs || left.label.localeCompare(right.label)
267
+ );
268
+ }
269
+ function newestTurn(turns) {
270
+ return [...turns].sort(
271
+ (left, right) => (reportTime(right.lastSeenAt) ?? 0) - (reportTime(left.lastSeenAt) ?? 0) || right.id.localeCompare(left.id)
272
+ )[0];
273
+ }
274
+ function recentConversationGroups(args) {
275
+ const startMs = args.nowMs - RECENT_CONVERSATION_STATS_WINDOW_MS;
276
+ const groups = /* @__PURE__ */ new Map();
277
+ for (const session of args.sessions) {
278
+ groups.set(session.conversationId, [
279
+ ...groups.get(session.conversationId) ?? [],
280
+ session
281
+ ]);
282
+ }
283
+ return [...groups.values()].map(
284
+ (turns) => [...turns].sort(
285
+ (left, right) => (reportTime(left.startedAt) ?? 0) - (reportTime(right.startedAt) ?? 0) || left.id.localeCompare(right.id)
286
+ )
287
+ ).filter((turns) => {
288
+ const activityAt = reportTime(newestTurn(turns).lastSeenAt);
289
+ return activityAt !== void 0 && activityAt >= startMs && activityAt <= args.nowMs;
290
+ });
291
+ }
292
+ function conversationDurationMs(turns) {
293
+ if (!turns.some((turn) => turnDurationSnapshot(turn) !== void 0)) {
294
+ return 0;
295
+ }
296
+ return contributionDurationTotal(turnContributions(turns));
297
+ }
298
+ function buildConversationStatsReport(args) {
299
+ const conversations = recentConversationGroups(args);
300
+ const requesters = /* @__PURE__ */ new Map();
301
+ const locations = /* @__PURE__ */ new Map();
302
+ let durationMs = 0;
303
+ let tokens;
304
+ let active = 0;
305
+ let failed = 0;
306
+ let hung = 0;
307
+ for (const turns of conversations) {
308
+ const contributions = turnContributions(turns);
309
+ const conversationSignals = statusSignals(turns);
310
+ const conversationTokens = contributionTokenTotal(contributions);
311
+ durationMs += contributionDurationTotal(contributions);
312
+ tokens = addTokenTotal(tokens, conversationTokens);
313
+ active += conversationSignals.active ? 1 : 0;
314
+ failed += conversationSignals.failed ? 1 : 0;
315
+ hung += conversationSignals.hung ? 1 : 0;
316
+ const requesterTurns = /* @__PURE__ */ new Map();
317
+ for (const contribution of contributions) {
318
+ const requester = requesterLabel(contribution.turn.requesterIdentity) ?? "Unknown";
319
+ requesterTurns.set(requester, [
320
+ ...requesterTurns.get(requester) ?? [],
321
+ contribution
322
+ ]);
323
+ }
324
+ for (const [requester, requesterContributions] of requesterTurns) {
325
+ const item = requesters.get(requester) ?? emptyStatsItem(requester);
326
+ const signals = statusSignals(
327
+ requesterContributions.map((contribution) => contribution.turn)
328
+ );
329
+ item.conversations += 1;
330
+ item.turns += requesterContributions.length;
331
+ item.durationMs += contributionDurationTotal(requesterContributions);
332
+ item.active += signals.active ? 1 : 0;
333
+ item.failed += signals.failed ? 1 : 0;
334
+ item.hung += signals.hung ? 1 : 0;
335
+ addItemTokens(item, contributionTokenTotal(requesterContributions));
336
+ requesters.set(requester, item);
337
+ }
338
+ const location = locationLabel(newestTurn(turns));
339
+ const locationItem = locations.get(location) ?? emptyStatsItem(location);
340
+ locationItem.conversations += 1;
341
+ locationItem.turns += turns.length;
342
+ locationItem.durationMs += conversationDurationMs(turns);
343
+ locationItem.active += conversationSignals.active ? 1 : 0;
344
+ locationItem.failed += conversationSignals.failed ? 1 : 0;
345
+ locationItem.hung += conversationSignals.hung ? 1 : 0;
346
+ addItemTokens(locationItem, conversationTokens);
347
+ locations.set(location, locationItem);
348
+ }
349
+ return {
350
+ active,
351
+ conversations: conversations.length,
352
+ durationMs,
353
+ failed,
354
+ generatedAt: args.generatedAt,
355
+ hung,
356
+ locations: statsItems(locations),
357
+ requesters: statsItems(requesters),
358
+ sampleLimit: args.sampleLimit,
359
+ sampleSize: args.sampleSize,
360
+ source: "turn_session_records",
361
+ ...tokens !== void 0 ? { tokens } : {},
362
+ truncated: args.truncated,
363
+ turns: conversations.reduce((sum, turns) => sum + turns.length, 0),
364
+ windowEnd: new Date(args.nowMs).toISOString(),
365
+ windowStart: new Date(
366
+ args.nowMs - RECENT_CONVERSATION_STATS_WINDOW_MS
367
+ ).toISOString()
368
+ };
369
+ }
370
+ async function completeSampledConversationSummaries(args) {
371
+ if (!args.truncated) {
372
+ return args.summaries;
373
+ }
374
+ const conversationIds = [
375
+ ...new Set(args.summaries.map((summary) => summary.conversationId))
376
+ ];
377
+ const groups = await Promise.all(
378
+ conversationIds.map(
379
+ (conversationId) => listAgentTurnSessionSummariesForConversation(conversationId)
380
+ )
381
+ );
382
+ const summariesByTurn = /* @__PURE__ */ new Map();
383
+ for (const group of groups) {
384
+ for (const summary of group) {
385
+ summariesByTurn.set(
386
+ `${summary.conversationId}:${summary.sessionId}`,
387
+ summary
388
+ );
389
+ }
390
+ }
391
+ return [...summariesByTurn.values()].sort(
392
+ (left, right) => right.updatedAtMs - left.updatedAtMs
393
+ );
394
+ }
146
395
  function canExposeConversationTranscript(summary) {
147
396
  return canExposeConversationPayload({
148
397
  conversationId: summary.conversationId
@@ -365,11 +614,50 @@ function traceIdFromTranscript(transcript) {
365
614
  return void 0;
366
615
  }
367
616
  async function readSessions() {
368
- const summaries = await listAgentTurnSessionSummaries(50);
617
+ const nowMs = Date.now();
618
+ const summaries = await listAgentTurnSessionSummaries(
619
+ DASHBOARD_SESSION_FEED_LIMIT
620
+ );
369
621
  return {
370
622
  source: "turn_session_records",
371
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
372
- sessions: summaries.map(sessionReportFromSummary)
623
+ generatedAt: new Date(nowMs).toISOString(),
624
+ sessions: summaries.map(
625
+ (summary) => sessionReportFromSummary(summary, nowMs)
626
+ )
627
+ };
628
+ }
629
+ async function readConversationStats() {
630
+ const nowMs = Date.now();
631
+ const generatedAt = new Date(nowMs).toISOString();
632
+ const summaries = await listAgentTurnSessionSummaries(
633
+ DASHBOARD_CONVERSATION_STATS_LIMIT + 1
634
+ );
635
+ const truncated = summaries.length >= DASHBOARD_CONVERSATION_STATS_LIMIT;
636
+ const sampledSummaries = summaries.slice(
637
+ 0,
638
+ DASHBOARD_CONVERSATION_STATS_LIMIT
639
+ );
640
+ const reportSummaries = await completeSampledConversationSummaries({
641
+ summaries: sampledSummaries,
642
+ truncated
643
+ });
644
+ return buildConversationStatsReport({
645
+ generatedAt,
646
+ nowMs,
647
+ sampleLimit: DASHBOARD_CONVERSATION_STATS_LIMIT,
648
+ sampleSize: sampledSummaries.length,
649
+ sessions: reportSummaries.map(
650
+ (summary) => sessionReportFromSummary(summary, nowMs)
651
+ ),
652
+ truncated
653
+ });
654
+ }
655
+ async function readPluginOperationalReports() {
656
+ const nowMs = Date.now();
657
+ return {
658
+ source: "trusted_plugins",
659
+ generatedAt: new Date(nowMs).toISOString(),
660
+ reports: await getAgentPluginOperationalReports(nowMs)
373
661
  };
374
662
  }
375
663
  async function readConversation(conversationId) {
@@ -436,6 +724,8 @@ function createJuniorReporting() {
436
724
  getPlugins: readPlugins,
437
725
  getSkills: readSkills,
438
726
  getSessions: readSessions,
727
+ getConversationStats: readConversationStats,
728
+ getPluginOperationalReports: readPluginOperationalReports,
439
729
  getConversation: readConversation
440
730
  };
441
731
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.66.3",
3
+ "version": "0.67.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -65,7 +65,7 @@
65
65
  "node-html-markdown": "^2.0.0",
66
66
  "yaml": "^2.9.0",
67
67
  "zod": "^4.4.3",
68
- "@sentry/junior-plugin-api": "0.66.3"
68
+ "@sentry/junior-plugin-api": "0.67.1"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.9.1",
@@ -77,7 +77,7 @@
77
77
  "typescript": "^6.0.3",
78
78
  "vercel": "^54.4.0",
79
79
  "vitest": "^4.1.7",
80
- "@sentry/junior-scheduler": "0.66.3"
80
+ "@sentry/junior-scheduler": "0.67.1"
81
81
  },
82
82
  "scripts": {
83
83
  "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",