Skip to content

Commit c466a19

Browse files
sergicalclaudegithub-actions[bot]BYK
authored
fix(log): use 30d default period and show newest logs first (#568)
## Summary - Fix `sentry log list` returning stale/incomplete data by changing the default `statsPeriod` from `90d` to `30d` (log retention is 30 days — periods >30d hit a degraded API path) - Change default sort order to newest-first for one-shot queries, add `--sort` flag (`newest`/`oldest`) - Keep chronological (oldest-first) ordering for `--follow` mode - Sort is server-side (`-timestamp` / `timestamp` API param), not client-side array reversal **Root cause confirmed via API testing:** | Period | Count | Newest | Status | |--------|-------|--------|--------| | 14d | 19 | 16:42:23 | correct | | 30d | 82 | 16:42:23 | correct | | 60d | 13 | 15:21:42 | **stale** | | 90d | 13 | 15:21:42 | **stale** | ## Design decisions - **Server-side sorting**: The `--sort` flag maps to the API `sort` parameter (`"newest"` → `"-timestamp"`, `"oldest"` → `"timestamp"`) rather than reversing arrays client-side. This matches the pattern used by `trace list` and `span list`. - **`LogSortDirection` type in API layer**: Defined and exported from `src/lib/api/logs.ts`, following the convention of `SpanSortValue` in `api/traces.ts`. Re-exported via `api-client.ts`. - **`parseLogSort` naming**: The shared parser in `arg-parsing.ts` is named `parseLogSort` (not `parseSort`) to avoid collisions with the three existing per-command `parseSort` functions that operate on different sort vocabularies (`date`/`duration`, `date`/`new`/`freq`/`user`). - **Follow mode unaffected**: `--follow` streaming always fetches newest-first and displays in arrival order. The `--sort` flag only affects one-shot fetch mode. ## Test plan - [x] Updated unit tests for new default period (`30d`) and newest-first ordering - [x] Added test for `--sort oldest` flag — verifies API receives `sort: "oldest"` - [x] Tests verify sort param is passed through to API (not just output order) - [x] All 65 unit tests pass across `log/list` and `trace/logs` - [ ] Manual: `sentry logs <org>/<project>` shows recent logs, newest at top - [ ] Manual: `sentry logs <org>/<project> --sort oldest` shows oldest at top - [ ] Manual: `sentry logs <org>/<project> -f` follow mode still chronological --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Burak Yigit Kaya <byk@sentry.io>
1 parent fbda58d commit c466a19

10 files changed

Lines changed: 239 additions & 71 deletions

File tree

docs/src/content/docs/commands/log.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ sentry log list <project>
3636
| `-n, --limit <n>` | Number of log entries to show (1-1000, default: 100) |
3737
| `-q, --query <query>` | Filter query (Sentry search syntax) |
3838
| `-f, --follow [interval]` | Stream logs in real-time (optional: poll interval in seconds, default: 2) |
39+
| `-t, --period <period>` | Time period (e.g., "30d", "14d", "24h"). Default: 30d. Log retention is 30 days. |
40+
| `-s, --sort <order>` | Sort order: "newest" (default) or "oldest" |
3941
| `--json` | Output as JSON |
4042

4143
**Examples:**

plugins/sentry-cli/skills/sentry-cli/references/logs.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ List logs from a project
1919
- `-n, --limit <value> - Number of log entries (1-1000) - (default: "100")`
2020
- `-q, --query <value> - Filter query (Sentry search syntax)`
2121
- `-f, --follow <value> - Stream logs (optionally specify poll interval in seconds)`
22-
- `-t, --period <value> - Time period (e.g., "90d", "14d", "24h"). Default: 90d (project mode), 14d (trace mode)`
22+
- `-t, --period <value> - Time period (e.g., "30d", "14d", "24h"). Default: 30d (project mode), 14d (trace mode)`
23+
- `-s, --sort <value> - Sort order: "newest" (default) or "oldest" - (default: "newest")`
2324
- `--fresh - Bypass cache, re-detect projects, and fetch fresh data`
2425

2526
**JSON Fields** (use `--json --fields` to select specific fields):

plugins/sentry-cli/skills/sentry-cli/references/traces.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ View logs associated with a trace
8888
- `-t, --period <value> - Time period to search (e.g., "14d", "7d", "24h"). Default: 14d - (default: "14d")`
8989
- `-n, --limit <value> - Number of log entries (<=1000) - (default: "100")`
9090
- `-q, --query <value> - Additional filter query (Sentry search syntax)`
91+
- `-s, --sort <value> - Sort order: "newest" (default) or "oldest" - (default: "newest")`
9192
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
9293

9394
All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.

src/commands/log/list.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@
99
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
1010
import * as Sentry from "@sentry/node-core/light";
1111
import type { SentryContext } from "../../context.js";
12-
import { listLogs, listTraceLogs } from "../../lib/api-client.js";
13-
import { validateLimit } from "../../lib/arg-parsing.js";
14-
import { AuthError, stringifyUnknown } from "../../lib/errors.js";
12+
import {
13+
type LogSortDirection,
14+
listLogs,
15+
listTraceLogs,
16+
} from "../../lib/api-client.js";
17+
import { parseLogSort, validateLimit } from "../../lib/arg-parsing.js";
18+
import {
19+
AuthError,
20+
stringifyUnknown,
21+
ValidationError,
22+
} from "../../lib/errors.js";
1523
import {
1624
buildLogRowCells,
1725
createLogStreamingTable,
@@ -47,6 +55,7 @@ type ListFlags = {
4755
readonly query?: string;
4856
readonly follow?: number;
4957
readonly period?: string;
58+
readonly sort: LogSortDirection;
5059
readonly json: boolean;
5160
readonly fresh: boolean;
5261
readonly fields?: string[];
@@ -155,8 +164,10 @@ function parseLogListArgs(
155164
return parseDualModeArgs(args, TRACE_USAGE_HINT);
156165
}
157166

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

161172
/**
162173
* Execute a single fetch of logs (non-streaming mode).
@@ -174,21 +185,19 @@ async function executeSingleFetch(
174185
query: flags.query,
175186
limit: flags.limit,
176187
statsPeriod: period,
188+
sort: flags.sort,
177189
});
178190

179191
if (logs.length === 0) {
180192
return { result: { logs: [], hasMore: false }, hint: "No logs found." };
181193
}
182194

183-
// Reverse for chronological order (API returns newest first, tail shows oldest first)
184-
const chronological = [...logs].reverse();
185-
186195
const hasMore = logs.length >= flags.limit;
187196
const countText = `Showing ${logs.length} log${logs.length === 1 ? "" : "s"}.`;
188197
const tip = hasMore ? " Use --limit to show more, or -f to follow." : "";
189198

190199
return {
191-
result: { logs: chronological, hasMore },
200+
result: { logs, hasMore },
192201
hint: `${countText}${tip}`,
193202
};
194203
}
@@ -434,6 +443,7 @@ async function executeTraceSingleFetch(
434443
query: flags.query,
435444
limit: flags.limit,
436445
statsPeriod: period,
446+
sort: flags.sort,
437447
});
438448

439449
if (logs.length === 0) {
@@ -445,14 +455,12 @@ async function executeTraceSingleFetch(
445455
};
446456
}
447457

448-
const chronological = [...logs].reverse();
449-
450458
const hasMore = logs.length >= flags.limit;
451459
const countText = `Showing ${logs.length} log${logs.length === 1 ? "" : "s"} for trace ${traceId}.`;
452460
const tip = hasMore ? " Use --limit to show more." : "";
453461

454462
return {
455-
result: { logs: chronological, traceId, hasMore },
463+
result: { logs, traceId, hasMore },
456464
hint: `${countText}${tip}`,
457465
};
458466
}
@@ -635,18 +643,32 @@ export const listCommand = buildListCommand(
635643
kind: "parsed",
636644
parse: String,
637645
brief:
638-
'Time period (e.g., "90d", "14d", "24h"). Default: 90d (project mode), 14d (trace mode)',
646+
'Time period (e.g., "30d", "14d", "24h"). Default: 30d (project mode), 14d (trace mode)',
639647
optional: true,
640648
},
649+
sort: {
650+
kind: "parsed",
651+
parse: parseLogSort,
652+
brief: 'Sort order: "newest" (default) or "oldest"',
653+
default: "newest",
654+
},
641655
},
642656
aliases: {
643657
n: "limit",
644658
q: "query",
645659
f: "follow",
646660
t: "period",
661+
s: "sort",
647662
},
648663
},
649664
async *func(this: SentryContext, flags: ListFlags, ...args: string[]) {
665+
if (flags.follow && flags.sort === "oldest") {
666+
throw new ValidationError(
667+
'--sort "oldest" cannot be used with --follow. Follow mode streams new logs as they arrive.',
668+
"sort"
669+
);
670+
}
671+
650672
const { cwd } = this;
651673

652674
const parsed = parseLogListArgs(args);

src/commands/trace/logs.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
*/
66

77
import type { SentryContext } from "../../context.js";
8-
import { listTraceLogs } from "../../lib/api-client.js";
9-
import { validateLimit } from "../../lib/arg-parsing.js";
8+
import { type LogSortDirection, listTraceLogs } from "../../lib/api-client.js";
9+
import { parseLogSort, validateLimit } from "../../lib/arg-parsing.js";
1010
import { openInBrowser } from "../../lib/browser.js";
1111
import { buildCommand } from "../../lib/command.js";
1212
import { filterFields } from "../../lib/formatters/json.js";
@@ -31,6 +31,7 @@ type LogsFlags = {
3131
readonly period: string;
3232
readonly limit: number;
3333
readonly query?: string;
34+
readonly sort: LogSortDirection;
3435
readonly fresh: boolean;
3536
readonly fields?: string[];
3637
};
@@ -147,6 +148,12 @@ export const logsCommand = buildCommand({
147148
brief: "Additional filter query (Sentry search syntax)",
148149
optional: true,
149150
},
151+
sort: {
152+
kind: "parsed",
153+
parse: parseLogSort,
154+
brief: 'Sort order: "newest" (default) or "oldest"',
155+
default: "newest",
156+
},
150157
fresh: FRESH_FLAG,
151158
},
152159
aliases: {
@@ -155,6 +162,7 @@ export const logsCommand = buildCommand({
155162
t: "period",
156163
n: "limit",
157164
q: "query",
165+
s: "sort",
158166
},
159167
},
160168
async *func(this: SentryContext, flags: LogsFlags, ...args: string[]) {
@@ -180,19 +188,18 @@ export const logsCommand = buildCommand({
180188
statsPeriod: flags.period,
181189
limit: flags.limit,
182190
query: flags.query,
191+
sort: flags.sort,
183192
})
184193
);
185194

186-
// Reverse to chronological order (API returns newest-first)
187-
const chronological = [...logs].reverse();
188-
const hasMore = chronological.length >= flags.limit;
195+
const hasMore = logs.length >= flags.limit;
189196

190197
const emptyMessage =
191198
`No logs found for trace ${traceId} in the last ${flags.period}.\n\n` +
192199
`Try a longer period: sentry trace logs --period 30d ${traceId}`;
193200

194201
return yield new CommandOutput({
195-
logs: chronological,
202+
logs,
196203
traceId,
197204
hasMore,
198205
emptyMessage,

src/lib/api-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export {
6060
} from "./api/issues.js";
6161
export {
6262
getLogs,
63+
type LogSortDirection,
6364
listLogs,
6465
listTraceLogs,
6566
} from "./api/logs.js";

src/lib/api/logs.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ import {
2626
unwrapResult,
2727
} from "./infrastructure.js";
2828

29+
/** Sort direction for log queries: newest-first or oldest-first. */
30+
export type LogSortDirection = "newest" | "oldest";
31+
32+
/** Map CLI sort direction to Sentry API sort parameter. */
33+
function toApiSort(sort: LogSortDirection | undefined): string {
34+
return sort === "oldest" ? "timestamp" : "-timestamp";
35+
}
36+
2937
/** Fields to request from the logs API */
3038
const LOG_FIELDS = [
3139
"sentry.item_id",
@@ -41,8 +49,14 @@ type ListLogsOptions = {
4149
query?: string;
4250
/** Maximum number of log entries to return */
4351
limit?: number;
44-
/** Time period for logs (e.g., "90d", "10m") */
52+
/**
53+
* Time period for logs (e.g., "30d", "14d", "10m").
54+
* Defaults to "30d" — the maximum log retention period.
55+
* Periods >30d hit a degraded API path returning stale/incomplete data.
56+
*/
4557
statsPeriod?: string;
58+
/** Sort direction: "newest" (default) or "oldest" */
59+
sort?: LogSortDirection;
4660
/** Only return logs after this timestamp_precise value (for streaming) */
4761
afterTimestamp?: number;
4862
};
@@ -83,8 +97,8 @@ export async function listLogs(
8397
project: isNumericProject ? [Number(projectSlug)] : undefined,
8498
query: fullQuery || undefined,
8599
per_page: options.limit || API_MAX_PER_PAGE,
86-
statsPeriod: options.statsPeriod ?? "7d",
87-
sort: "-timestamp",
100+
statsPeriod: options.statsPeriod ?? "30d",
101+
sort: toApiSort(options.sort),
88102
},
89103
});
90104

@@ -193,6 +207,8 @@ type ListTraceLogsOptions = {
193207
* logs exist for the trace. Defaults to "14d".
194208
*/
195209
statsPeriod?: string;
210+
/** Sort direction: "newest" (default) or "oldest" */
211+
sort?: LogSortDirection;
196212
};
197213

198214
/**
@@ -208,8 +224,8 @@ type ListTraceLogsOptions = {
208224
*
209225
* @param orgSlug - Organization slug
210226
* @param traceId - The 32-character hex trace ID
211-
* @param options - Optional query/limit/statsPeriod overrides
212-
* @returns Array of trace log entries, ordered newest-first
227+
* @param options - Optional query/limit/statsPeriod/sort overrides
228+
* @returns Array of trace log entries
213229
*/
214230
export async function listTraceLogs(
215231
orgSlug: string,
@@ -227,7 +243,7 @@ export async function listTraceLogs(
227243
statsPeriod: options.statsPeriod ?? "14d",
228244
per_page: options.limit ?? API_MAX_PER_PAGE,
229245
query: options.query,
230-
sort: "-timestamp",
246+
sort: toApiSort(options.sort),
231247
},
232248
schema: TraceLogsResponseSchema,
233249
}

src/lib/arg-parsing.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* project list) and single-item commands (issue view, explain, plan).
77
*/
88

9+
import type { LogSortDirection } from "./api/logs.js";
910
import { ContextError, ValidationError } from "./errors.js";
1011
import { validateResourceId } from "./input-validation.js";
1112
import { logger } from "./logger.js";
@@ -261,6 +262,28 @@ export function validateLimit(value: string, min = 1, max = 1000): number {
261262
return num;
262263
}
263264

265+
// ---------------------------------------------------------------------------
266+
// Log sort direction parsing (shared by log list, trace logs)
267+
// ---------------------------------------------------------------------------
268+
269+
const VALID_LOG_SORT_DIRECTIONS: readonly LogSortDirection[] = [
270+
"newest",
271+
"oldest",
272+
];
273+
274+
/**
275+
* Parse --sort flag value for log commands.
276+
* @throws Error if value is not "newest" or "oldest"
277+
*/
278+
export function parseLogSort(value: string): LogSortDirection {
279+
if (!VALID_LOG_SORT_DIRECTIONS.includes(value as LogSortDirection)) {
280+
throw new Error(
281+
`Invalid sort value. Must be one of: ${VALID_LOG_SORT_DIRECTIONS.join(", ")}`
282+
);
283+
}
284+
return value as LogSortDirection;
285+
}
286+
264287
/** Default span depth when no value is provided */
265288
const DEFAULT_SPAN_DEPTH = 3;
266289

0 commit comments

Comments
 (0)