Skip to content

Commit 3021223

Browse files
sergicalclaude
andcommitted
fix: address Warden findings and consolidate view formatter
- Forward start/end params for absolute date ranges (GGA-VPD) - Guard against Infinity timestamps on empty spans (5RV-D34) - Consolidate view formatter into formatTranscriptResult - Fix tsc strict null check on span index lookup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6d85d92 commit 3021223

4 files changed

Lines changed: 165 additions & 188 deletions

File tree

src/commands/ai-conversations/list.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ function formatListHuman(result: ConversationListResult): string {
7676

7777
function jsonTransform(
7878
result: ConversationListResult,
79-
fields?: string[],
79+
fields?: string[]
8080
): unknown {
8181
const items =
8282
fields && fields.length > 0
@@ -88,7 +88,9 @@ function jsonTransform(
8888
hasMore: result.hasMore,
8989
hasPrev: !!result.hasPrev,
9090
};
91-
if (result.nextCursor) envelope.nextCursor = result.nextCursor;
91+
if (result.nextCursor) {
92+
envelope.nextCursor = result.nextCursor;
93+
}
9294
return envelope;
9395
}
9496

@@ -106,7 +108,7 @@ export const listCommand = buildListCommand("ai-conversations", {
106108
},
107109
output: {
108110
human: formatListHuman,
109-
jsonTransform: jsonTransform,
111+
jsonTransform,
110112
schema: ConversationListItemSchema,
111113
},
112114
parameters: {
@@ -148,20 +150,19 @@ export const listCommand = buildListCommand("ai-conversations", {
148150
const resolved = await resolveOrg({ org: target, cwd });
149151
if (!resolved) {
150152
throw new Error(
151-
`Could not determine organization. Pass it explicitly: sentry ${COMMAND_NAME} <org>`,
153+
`Could not determine organization. Pass it explicitly: sentry ${COMMAND_NAME} <org>`
152154
);
153155
}
154156
const org = resolved.org;
155157

156-
const contextKey = buildPaginationContextKey(
157-
"ai-conversations",
158-
org,
159-
{ q: flags.query, period: serializeTimeRange(flags.period) },
160-
);
158+
const contextKey = buildPaginationContextKey("ai-conversations", org, {
159+
q: flags.query,
160+
period: serializeTimeRange(flags.period),
161+
});
161162
const { cursor, direction } = resolveCursor(
162163
flags.cursor,
163164
PAGINATION_KEY,
164-
contextKey,
165+
contextKey
165166
);
166167

167168
const timeParams = timeRangeToApiParams(flags.period);
@@ -176,8 +177,8 @@ export const listCommand = buildListCommand("ai-conversations", {
176177
query: flags.query,
177178
limit: flags.limit,
178179
cursor,
179-
statsPeriod: timeParams.statsPeriod,
180-
}),
180+
...timeParams,
181+
})
181182
);
182183

183184
advancePaginationState(PAGINATION_KEY, contextKey, direction, nextCursor);

src/commands/ai-conversations/view.ts

Lines changed: 4 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { getConversationSpans } from "../../lib/api/ai-conversations.js";
99
import { buildCommand } from "../../lib/command.js";
1010
import {
1111
buildTranscriptResult,
12-
formatTranscript,
12+
formatTranscriptResult,
1313
type TranscriptResult,
1414
} from "../../lib/formatters/ai-conversations.js";
1515
import { CommandOutput } from "../../lib/formatters/output.js";
@@ -19,94 +19,12 @@ import {
1919
FRESH_FLAG,
2020
} from "../../lib/list-command.js";
2121
import { withProgress } from "../../lib/polling.js";
22-
import { resolveOrg } from "../../lib/resolve-target.js";
2322

2423
type ViewFlags = {
2524
readonly json: boolean;
2625
readonly fresh: boolean;
2726
};
2827

29-
function formatViewHuman(result: TranscriptResult): string {
30-
if (result.spanCount === 0) {
31-
return `No spans found for conversation ${result.conversationId} in the last 30 days.`;
32-
}
33-
34-
const lines: string[] = [];
35-
lines.push(`AI Conversation: ${result.conversationId}`);
36-
lines.push("");
37-
lines.push(` Org: ${result.org}`);
38-
lines.push(` Projects: ${result.projects.join(", ") || "—"}`);
39-
lines.push(
40-
` Started: ${new Date(result.startTimestamp * 1000).toISOString()}`,
41-
);
42-
lines.push(
43-
` Ended: ${new Date(result.endTimestamp * 1000).toISOString()}`,
44-
);
45-
lines.push(` Turns: ${result.turns.length}`);
46-
lines.push(` Spans: ${result.spanCount}`);
47-
lines.push(` Tokens: ${result.totalTokens}`);
48-
lines.push("");
49-
50-
for (const turn of result.turns) {
51-
const meta = [
52-
turn.model,
53-
turn.agentName,
54-
turn.totalTokens > 0 ? `${turn.totalTokens} tokens` : null,
55-
turn.durationMs < 1000
56-
? `${turn.durationMs}ms`
57-
: `${(turn.durationMs / 1000).toFixed(1)}s`,
58-
]
59-
.filter(Boolean)
60-
.join(" | ");
61-
62-
lines.push(
63-
`── Turn ${turn.turn}${new Date(turn.started * 1000).toISOString()}`,
64-
);
65-
if (meta) lines.push(` ${meta}`);
66-
lines.push("");
67-
68-
if (turn.userContent) {
69-
lines.push(" [user]");
70-
const content =
71-
turn.userContent.length > 600
72-
? `${turn.userContent.slice(0, 599)}…`
73-
: turn.userContent;
74-
for (const line of content.split("\n")) {
75-
lines.push(` ${line}`);
76-
}
77-
lines.push("");
78-
}
79-
80-
if (turn.assistantContent) {
81-
lines.push(" [assistant]");
82-
const content =
83-
turn.assistantContent.length > 600
84-
? `${turn.assistantContent.slice(0, 599)}…`
85-
: turn.assistantContent;
86-
for (const line of content.split("\n")) {
87-
lines.push(` ${line}`);
88-
}
89-
lines.push("");
90-
}
91-
92-
if (turn.toolCalls.length > 0) {
93-
lines.push(" [tools]");
94-
for (const tc of turn.toolCalls) {
95-
const dur =
96-
tc.durationMs < 1000
97-
? `${tc.durationMs}ms`
98-
: `${(tc.durationMs / 1000).toFixed(1)}s`;
99-
const status =
100-
tc.status && tc.status !== "ok" ? ` (${tc.status})` : "";
101-
lines.push(` • ${tc.name}${dur}${status}`);
102-
}
103-
lines.push("");
104-
}
105-
}
106-
107-
return lines.join("\n");
108-
}
109-
11028
export const viewCommand = buildCommand({
11129
docs: {
11230
brief: "View an AI conversation transcript",
@@ -117,7 +35,7 @@ export const viewCommand = buildCommand({
11735
" sentry ai-conversations view my-org conv-123 --json\n",
11836
},
11937
output: {
120-
human: formatViewHuman,
38+
human: formatTranscriptResult,
12139
},
12240
parameters: {
12341
positional: {
@@ -144,7 +62,7 @@ export const viewCommand = buildCommand({
14462
this: SentryContext,
14563
flags: ViewFlags,
14664
org: string,
147-
conversationId: string,
65+
conversationId: string
14866
) {
14967
applyFreshFlag(flags);
15068

@@ -153,7 +71,7 @@ export const viewCommand = buildCommand({
15371
message: "Fetching conversation spans...",
15472
json: flags.json,
15573
},
156-
() => getConversationSpans(org, conversationId),
74+
() => getConversationSpans(org, conversationId)
15775
);
15876

15977
const result = buildTranscriptResult(conversationId, org, spans);

src/lib/api/ai-conversations.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
*/
77

88
import {
9-
AIConversationSpanSchema,
10-
ConversationListItemSchema,
119
type AIConversationSpan,
10+
AIConversationSpanSchema,
1211
type ConversationListItem,
12+
ConversationListItemSchema,
1313
} from "../../types/ai-conversations.js";
1414

1515
import { resolveOrgRegion } from "../region.js";
@@ -27,23 +27,39 @@ export async function listConversations(
2727
limit?: number;
2828
cursor?: string;
2929
statsPeriod?: string;
30+
start?: string;
31+
end?: string;
3032
project?: string;
31-
} = {},
33+
} = {}
3234
): Promise<PaginatedResponse<ConversationListItem[]>> {
3335
const regionUrl = await resolveOrgRegion(orgSlug);
3436

3537
const params: Record<string, string> = {
3638
per_page: String(options.limit ?? 10),
3739
};
38-
if (options.statsPeriod) params.statsPeriod = options.statsPeriod;
39-
if (options.cursor) params.cursor = options.cursor;
40-
if (options.query) params.query = options.query;
41-
if (options.project) params.project = options.project;
40+
if (options.statsPeriod) {
41+
params.statsPeriod = options.statsPeriod;
42+
}
43+
if (options.start) {
44+
params.start = options.start;
45+
}
46+
if (options.end) {
47+
params.end = options.end;
48+
}
49+
if (options.cursor) {
50+
params.cursor = options.cursor;
51+
}
52+
if (options.query) {
53+
params.query = options.query;
54+
}
55+
if (options.project) {
56+
params.project = options.project;
57+
}
4258

4359
const { data, headers } = await apiRequestToRegion<unknown[]>(
4460
regionUrl,
4561
`/organizations/${orgSlug}/ai-conversations/`,
46-
{ params },
62+
{ params }
4763
);
4864

4965
const items = data.map((item) => ConversationListItemSchema.parse(item));
@@ -59,32 +75,38 @@ export async function getConversationSpans(
5975
statsPeriod?: string;
6076
project?: string;
6177
perPage?: number;
62-
} = {},
78+
} = {}
6379
): Promise<AIConversationSpan[]> {
6480
const regionUrl = await resolveOrgRegion(orgSlug);
6581

6682
const params: Record<string, string> = {
6783
per_page: String(options.perPage ?? 1000),
6884
statsPeriod: options.statsPeriod ?? "30d",
6985
};
70-
if (options.project) params.project = options.project;
86+
if (options.project) {
87+
params.project = options.project;
88+
}
7189

7290
const spans: AIConversationSpan[] = [];
7391
let cursor: string | undefined;
7492

7593
for (let page = 0; page < 10; page++) {
76-
if (cursor) params.cursor = cursor;
94+
if (cursor) {
95+
params.cursor = cursor;
96+
}
7797

7898
const { data, headers } = await apiRequestToRegion<unknown[]>(
7999
regionUrl,
80100
`/organizations/${orgSlug}/ai-conversations/${encodeURIComponent(conversationId)}/`,
81-
{ params },
101+
{ params }
82102
);
83103

84104
spans.push(...data.map((s) => AIConversationSpanSchema.parse(s)));
85105
const parsed = parseLinkHeader(headers.get("link") ?? null);
86106
cursor = parsed.nextCursor;
87-
if (!cursor) break;
107+
if (!cursor) {
108+
break;
109+
}
88110
}
89111

90112
return spans;

0 commit comments

Comments
 (0)