Skip to content

Commit 10e1dec

Browse files
dcramercodex
andcommitted
fix(mcp): Harden profile detail fallbacks
Infer transaction profile sample timestamp units from the timestamp magnitude instead of the duration delta so missing relative bounds do not explode short nanosecond profiles. Add regressions for both epoch-second samples and sub-millisecond nanosecond samples. Also remove the unused requireSlug branch from resolveProjectContext to keep the profile detail helper aligned with its actual call sites. Co-Authored-By: Codex GPT-5 <noreply@openai.com>
1 parent 768b4a6 commit 10e1dec

3 files changed

Lines changed: 76 additions & 4 deletions

File tree

‎packages/mcp-core/src/tools/get-profile-details.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,13 @@ async function resolveProjectContext(
124124
projectSlugOrId: string | number,
125125
options: {
126126
requireNumericId?: boolean;
127-
requireSlug?: boolean;
128127
} = {},
129128
): Promise<{ projectId: string | number; projectSlug: string }> {
130129
const requireNumericId = options.requireNumericId ?? false;
131-
const requireSlug = options.requireSlug ?? false;
132130
const isNumericProject =
133131
typeof projectSlugOrId === "number" || isNumericId(String(projectSlugOrId));
134132

135-
if (!isNumericProject && !requireNumericId && !requireSlug) {
133+
if (!isNumericProject && !requireNumericId) {
136134
return {
137135
projectId: projectSlugOrId,
138136
projectSlug: String(projectSlugOrId),

‎packages/mcp-core/src/tools/profile/formatter.test.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,54 @@ describe("formatter", () => {
511511
expect(output).toContain("| `app_handler` | app.py:99 | 1 | User Code |");
512512
expect(output).not.toContain("`library_0`");
513513
});
514+
515+
it("falls back to epoch-second sample timestamps when relative bounds are missing", () => {
516+
const profile = createMockTransactionProfile();
517+
518+
if (profile.transaction) {
519+
profile.transaction.relative_start_ns = undefined;
520+
profile.transaction.relative_end_ns = undefined;
521+
}
522+
523+
profile.profile.samples = [
524+
{ stack_id: 0, thread_id: "1", timestamp: 1710958503.629 },
525+
{ stack_id: 1, thread_id: "1", timestamp: 1710958503.679 },
526+
{ stack_id: 1, thread_id: "1", timestamp: 1710958503.729 },
527+
];
528+
529+
const output = formatTransactionProfileAnalysis(profile, {
530+
focusOnUserCode: true,
531+
profileUrl:
532+
"https://sentry-mcp-evals.sentry.io/explore/profiling/profile/backend/cfe78a5c892d4a64a962d837673398d2/flamegraph/",
533+
projectSlug: "backend",
534+
});
535+
536+
expect(output).toContain("- **Duration**: 100ms");
537+
});
538+
539+
it("preserves nanosecond sample durations under 1ms when relative bounds are missing", () => {
540+
const profile = createMockTransactionProfile();
541+
542+
if (profile.transaction) {
543+
profile.transaction.relative_start_ns = undefined;
544+
profile.transaction.relative_end_ns = undefined;
545+
}
546+
547+
profile.profile.samples = [
548+
{ stack_id: 0, thread_id: "1", timestamp: 0 },
549+
{ stack_id: 1, thread_id: "1", timestamp: 250_000 },
550+
{ stack_id: 1, thread_id: "1", timestamp: 500_000 },
551+
];
552+
553+
const output = formatTransactionProfileAnalysis(profile, {
554+
focusOnUserCode: true,
555+
profileUrl:
556+
"https://sentry-mcp-evals.sentry.io/explore/profiling/profile/backend/cfe78a5c892d4a64a962d837673398d2/flamegraph/",
557+
projectSlug: "backend",
558+
});
559+
560+
expect(output).toContain("- **Duration**: 500µs");
561+
});
514562
});
515563

516564
describe("edge cases", () => {

‎packages/mcp-core/src/tools/profile/formatter.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,33 @@ function getTransactionProfileDurationNs(
692692
return null;
693693
}
694694

695-
return duration < 1_000_000 ? Math.round(duration * 1_000_000_000) : duration;
695+
const firstTimestampScale = inferSampleTimestampNsScale(timestamps[0]!);
696+
const lastTimestampScale = inferSampleTimestampNsScale(
697+
timestamps[timestamps.length - 1]!,
698+
);
699+
700+
if (firstTimestampScale && firstTimestampScale === lastTimestampScale) {
701+
return Math.round(duration * firstTimestampScale);
702+
}
703+
704+
return Math.round(duration);
705+
}
706+
707+
function inferSampleTimestampNsScale(timestamp: number): number | null {
708+
const absoluteTimestamp = Math.abs(timestamp);
709+
710+
// Older transaction profile payloads use absolute epoch timestamps.
711+
if (absoluteTimestamp >= 1e8 && absoluteTimestamp < 1e11) {
712+
return 1_000_000_000;
713+
}
714+
if (absoluteTimestamp >= 1e11 && absoluteTimestamp < 1e14) {
715+
return 1_000_000;
716+
}
717+
if (absoluteTimestamp >= 1e14 && absoluteTimestamp < 1e17) {
718+
return 1_000;
719+
}
720+
721+
return null;
696722
}
697723

698724
function formatDeviceSummary(profile: TransactionProfile): string | null {

0 commit comments

Comments
 (0)