Skip to content

Commit d674b5e

Browse files
sergicalclaude
andcommitted
fix(log): use 30d default period and show newest logs first
The default statsPeriod of 90d exceeded log retention (30 days), causing the Explore/Events API to return stale/incomplete data. Also changes the default sort to newest-first for one-shot queries (matching typical dashboard behavior) and adds a --sort flag. - Change DEFAULT_PROJECT_PERIOD from 90d to 30d (log/list) - Change listLogs fallback from 7d to 14d (api/logs) - Default to newest-first for one-shot queries (no reverse) - Keep oldest-first (chronological) for --follow mode - Add --sort flag (newest/oldest) to log list and trace logs - Update tests for new period and ordering defaults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3ada0ad commit d674b5e

File tree

5 files changed

+125
-44
lines changed

5 files changed

+125
-44
lines changed

src/commands/log/list.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ import {
4141
} from "../../lib/trace-target.js";
4242
import { getUpdateNotification } from "../../lib/version-check.js";
4343

44+
/** Sort direction for log output */
45+
type SortDirection = "newest" | "oldest";
46+
4447
type ListFlags = {
4548
readonly limit: number;
4649
readonly query?: string;
4750
readonly follow?: number;
4851
readonly period?: string;
52+
readonly sort: SortDirection;
4953
readonly json: boolean;
5054
readonly fresh: boolean;
5155
readonly fields?: string[];
@@ -97,6 +101,20 @@ function parseLimit(value: string): number {
97101
return validateLimit(value, MIN_LIMIT, MAX_LIMIT);
98102
}
99103

104+
/** Valid sort direction values */
105+
const VALID_SORT_DIRECTIONS: readonly SortDirection[] = ["newest", "oldest"];
106+
107+
/**
108+
* Parse --sort flag value.
109+
* @throws Error if value is not "newest" or "oldest"
110+
*/
111+
function parseSort(value: string): SortDirection {
112+
if (!VALID_SORT_DIRECTIONS.includes(value as SortDirection)) {
113+
throw new Error(`--sort must be "newest" or "oldest", got "${value}"`);
114+
}
115+
return value as SortDirection;
116+
}
117+
100118
/**
101119
* Parse --follow flag value.
102120
* Supports: -f (empty string → default interval), -f 10 (explicit interval)
@@ -154,8 +172,10 @@ function parseLogListArgs(
154172
return parseDualModeArgs(args, TRACE_USAGE_HINT);
155173
}
156174

157-
/** Default time period for project-scoped log queries */
158-
const DEFAULT_PROJECT_PERIOD = "90d";
175+
/** Default time period for project-scoped log queries.
176+
* Log retention is 30 days (https://docs.sentry.io/security-legal-pii/security/data-retention-periods/).
177+
* Periods >30d hit a degraded API path that returns stale/incomplete data. */
178+
const DEFAULT_PROJECT_PERIOD = "30d";
159179

160180
/**
161181
* Execute a single fetch of logs (non-streaming mode).
@@ -179,15 +199,16 @@ async function executeSingleFetch(
179199
return { result: { logs: [], hasMore: false }, hint: "No logs found." };
180200
}
181201

182-
// Reverse for chronological order (API returns newest first, tail shows oldest first)
183-
const chronological = [...logs].reverse();
202+
// API returns newest first. Reverse only when user wants oldest-first.
203+
const ordered =
204+
flags.sort === "oldest" ? [...logs].reverse() : logs;
184205

185206
const hasMore = logs.length >= flags.limit;
186207
const countText = `Showing ${logs.length} log${logs.length === 1 ? "" : "s"}.`;
187208
const tip = hasMore ? " Use --limit to show more, or -f to follow." : "";
188209

189210
return {
190-
result: { logs: chronological, hasMore },
211+
result: { logs: ordered, hasMore },
191212
hint: `${countText}${tip}`,
192213
};
193214
}
@@ -444,14 +465,15 @@ async function executeTraceSingleFetch(
444465
};
445466
}
446467

447-
const chronological = [...logs].reverse();
468+
const ordered =
469+
flags.sort === "oldest" ? [...logs].reverse() : logs;
448470

449471
const hasMore = logs.length >= flags.limit;
450472
const countText = `Showing ${logs.length} log${logs.length === 1 ? "" : "s"} for trace ${traceId}.`;
451473
const tip = hasMore ? " Use --limit to show more." : "";
452474

453475
return {
454-
result: { logs: chronological, traceId, hasMore },
476+
result: { logs: ordered, traceId, hasMore },
455477
hint: `${countText}${tip}`,
456478
};
457479
}
@@ -633,15 +655,22 @@ export const listCommand = buildListCommand(
633655
kind: "parsed",
634656
parse: String,
635657
brief:
636-
'Time period (e.g., "90d", "14d", "24h"). Default: 90d (project mode), 14d (trace mode)',
658+
'Time period (e.g., "30d", "14d", "24h"). Default: 30d (project mode), 14d (trace mode)',
637659
optional: true,
638660
},
661+
sort: {
662+
kind: "parsed",
663+
parse: parseSort,
664+
brief: 'Sort order: "newest" (default) or "oldest"',
665+
default: "newest",
666+
},
639667
},
640668
aliases: {
641669
n: "limit",
642670
q: "query",
643671
f: "follow",
644672
t: "period",
673+
s: "sort",
645674
},
646675
},
647676
async *func(this: SentryContext, flags: ListFlags, ...args: string[]) {

src/commands/trace/logs.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ import {
2525
warnIfNormalized,
2626
} from "../../lib/trace-target.js";
2727

28+
/** Sort direction for log output */
29+
type SortDirection = "newest" | "oldest";
30+
2831
type LogsFlags = {
2932
readonly json: boolean;
3033
readonly web: boolean;
3134
readonly period: string;
3235
readonly limit: number;
3336
readonly query?: string;
37+
readonly sort: SortDirection;
3438
readonly fresh: boolean;
3539
readonly fields?: string[];
3640
};
@@ -87,6 +91,20 @@ function parseLimit(value: string): number {
8791
return validateLimit(value, 1, MAX_LIMIT);
8892
}
8993

94+
/** Valid sort direction values */
95+
const VALID_SORT_DIRECTIONS: readonly SortDirection[] = ["newest", "oldest"];
96+
97+
/**
98+
* Parse --sort flag value.
99+
* @throws Error if value is not "newest" or "oldest"
100+
*/
101+
function parseSort(value: string): SortDirection {
102+
if (!VALID_SORT_DIRECTIONS.includes(value as SortDirection)) {
103+
throw new Error(`--sort must be "newest" or "oldest", got "${value}"`);
104+
}
105+
return value as SortDirection;
106+
}
107+
90108
export const logsCommand = buildCommand({
91109
docs: {
92110
brief: "View logs associated with a trace",
@@ -147,6 +165,12 @@ export const logsCommand = buildCommand({
147165
brief: "Additional filter query (Sentry search syntax)",
148166
optional: true,
149167
},
168+
sort: {
169+
kind: "parsed",
170+
parse: parseSort,
171+
brief: 'Sort order: "newest" (default) or "oldest"',
172+
default: "newest",
173+
},
150174
fresh: FRESH_FLAG,
151175
},
152176
aliases: {
@@ -155,6 +179,7 @@ export const logsCommand = buildCommand({
155179
t: "period",
156180
n: "limit",
157181
q: "query",
182+
s: "sort",
158183
},
159184
},
160185
async *func(this: SentryContext, flags: LogsFlags, ...args: string[]) {
@@ -183,16 +208,17 @@ export const logsCommand = buildCommand({
183208
})
184209
);
185210

186-
// Reverse to chronological order (API returns newest-first)
187-
const chronological = [...logs].reverse();
188-
const hasMore = chronological.length >= flags.limit;
211+
// API returns newest-first. Reverse only when user wants oldest-first.
212+
const ordered =
213+
flags.sort === "oldest" ? [...logs].reverse() : logs;
214+
const hasMore = ordered.length >= flags.limit;
189215

190216
const emptyMessage =
191217
`No logs found for trace ${traceId} in the last ${flags.period}.\n\n` +
192218
`Try a longer period: sentry trace logs --period 30d ${traceId}`;
193219

194220
return yield new CommandOutput({
195-
logs: chronological,
221+
logs: ordered,
196222
traceId,
197223
hasMore,
198224
emptyMessage,

src/lib/api/logs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export async function listLogs(
8383
project: isNumericProject ? [Number(projectSlug)] : undefined,
8484
query: fullQuery || undefined,
8585
per_page: options.limit || API_MAX_PER_PAGE,
86-
statsPeriod: options.statsPeriod ?? "7d",
86+
statsPeriod: options.statsPeriod ?? "14d",
8787
sort: "-timestamp",
8888
},
8989
});

test/commands/log/list.test.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,14 @@ function mockWithProgress(
135135
const BATCH_FLAGS = {
136136
json: true,
137137
limit: 100,
138+
sort: "newest",
138139
} as const;
139140

140141
/** Human-mode flags for non-follow batch mode (period omitted = use mode default) */
141142
const HUMAN_FLAGS = {
142143
json: false,
143144
limit: 100,
145+
sort: "newest",
144146
} as const;
145147

146148
/** Sample project-scoped logs (SentryLog) */
@@ -270,7 +272,7 @@ describe("listCommand.func — standard mode", () => {
270272
expect(parsed.data).toHaveLength(3);
271273
});
272274

273-
test("outputs JSON in chronological order (oldest first)", async () => {
275+
test("outputs JSON in newest-first order by default", async () => {
274276
// API returns newest first: item003, item002, item001
275277
const newestFirst = [...sampleLogs].reverse();
276278
listLogsSpy.mockResolvedValue(newestFirst);
@@ -282,7 +284,27 @@ describe("listCommand.func — standard mode", () => {
282284

283285
const output = stdoutWrite.mock.calls.map((c) => c[0]).join("");
284286
const parsed = JSON.parse(output);
285-
// After reversal, oldest should be first
287+
// Default sort=newest preserves API order (newest first)
288+
expect(parsed.data[0]["sentry.item_id"]).toBe("item003");
289+
expect(parsed.data[2]["sentry.item_id"]).toBe("item001");
290+
});
291+
292+
test("outputs JSON in oldest-first order with sort=oldest", async () => {
293+
const newestFirst = [...sampleLogs].reverse();
294+
listLogsSpy.mockResolvedValue(newestFirst);
295+
resolveOrgProjectSpy.mockResolvedValue({ org: ORG, project: PROJECT });
296+
297+
const { context, stdoutWrite } = createMockContext();
298+
const func = await listCommand.loader();
299+
await func.call(
300+
context,
301+
{ ...BATCH_FLAGS, sort: "oldest" },
302+
`${ORG}/${PROJECT}`
303+
);
304+
305+
const output = stdoutWrite.mock.calls.map((c) => c[0]).join("");
306+
const parsed = JSON.parse(output);
307+
// sort=oldest reverses to chronological order
286308
expect(parsed.data[0]["sentry.item_id"]).toBe("item001");
287309
expect(parsed.data[2]["sentry.item_id"]).toBe("item003");
288310
});
@@ -358,14 +380,14 @@ describe("listCommand.func — standard mode", () => {
358380
const func = await listCommand.loader();
359381
await func.call(
360382
context,
361-
{ json: false, limit: 50, query: "level:error" },
383+
{ json: false, limit: 50, query: "level:error", sort: "newest" },
362384
`${ORG}/${PROJECT}`
363385
);
364386

365387
expect(listLogsSpy).toHaveBeenCalledWith(ORG, PROJECT, {
366388
query: "level:error",
367389
limit: 50,
368-
statsPeriod: "90d",
390+
statsPeriod: "30d",
369391
});
370392
});
371393

@@ -441,7 +463,7 @@ describe("listCommand.func — trace mode", () => {
441463
expect(parsed.data).toHaveLength(3);
442464
});
443465

444-
test("outputs JSON in chronological order (oldest first)", async () => {
466+
test("outputs JSON in newest-first order by default (trace mode)", async () => {
445467
const newestFirst = [...sampleTraceLogs].reverse();
446468
listTraceLogsSpy.mockResolvedValue(newestFirst);
447469
resolveTraceOrgSpy.mockResolvedValue({ traceId: TRACE_ID, org: ORG });
@@ -452,8 +474,9 @@ describe("listCommand.func — trace mode", () => {
452474

453475
const output = stdoutWrite.mock.calls.map((c) => c[0]).join("");
454476
const parsed = JSON.parse(output);
455-
expect(parsed.data[0].id).toBe("log001");
456-
expect(parsed.data[2].id).toBe("log003");
477+
// Default sort=newest preserves API order (newest first)
478+
expect(parsed.data[0].id).toBe("log003");
479+
expect(parsed.data[2].id).toBe("log001");
457480
});
458481

459482
test("shows empty-trace message in human mode", async () => {
@@ -667,7 +690,7 @@ describe("listCommand.func — period flag", () => {
667690
test("trace mode uses 14d default when period is omitted", async () => {
668691
const { context } = createMockContext();
669692
const func = await listCommand.loader();
670-
await func.call(context, { json: true, limit: 100 }, TRACE_ID);
693+
await func.call(context, { json: true, limit: 100, sort: "newest" }, TRACE_ID);
671694

672695
expect(listTraceLogsSpy).toHaveBeenCalledWith(ORG, TRACE_ID, {
673696
query: undefined,
@@ -681,7 +704,7 @@ describe("listCommand.func — period flag", () => {
681704
const func = await listCommand.loader();
682705
await func.call(
683706
context,
684-
{ json: true, limit: 100, period: "30d" },
707+
{ json: true, limit: 100, period: "30d", sort: "newest" },
685708
TRACE_ID
686709
);
687710

@@ -817,6 +840,7 @@ describe("listCommand.func — follow mode (standard)", () => {
817840
json: false,
818841
limit: 100,
819842
follow: 1,
843+
sort: "newest",
820844
} as const;
821845

822846
test("writes initial logs then resolves on SIGINT", async () => {
@@ -1108,6 +1132,7 @@ describe("listCommand.func — follow mode (trace)", () => {
11081132
json: false,
11091133
limit: 100,
11101134
follow: 1,
1135+
sort: "newest",
11111136
} as const;
11121137

11131138
test("writes initial trace logs then resolves on SIGINT", async () => {

0 commit comments

Comments
 (0)