Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/core/src/code-editor/apmEnrichmentEligibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { isApmEnrichmentEligible } from "./apmEnrichmentEligibility";

describe("isApmEnrichmentEligible", () => {
it("accepts Rust files (the feature-flags service is Rust)", () => {
expect(
isApmEnrichmentEligible("rust/feature-flags/src/flags/flag_matching.rs"),
).toBe(true);
});

it.each(["a/b/handler.go", "svc/main.py", "src/index.ts", "app/Foo.java"])(
"accepts source file %s",
(path) => {
expect(isApmEnrichmentEligible(path)).toBe(true);
},
);

it.each(["README.md", "config.json", "styles.css", "image.png"])(
"rejects non-source file %s",
(path) => {
expect(isApmEnrichmentEligible(path)).toBe(false);
},
);
});
10 changes: 10 additions & 0 deletions packages/core/src/code-editor/apmEnrichmentEligibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { apmLangForFile } from "@posthog/shared";

/**
* Whether a file is eligible for APM line enrichment, decided purely by its
* extension. Derives from {@link APM_LANG_BY_EXT} (in `@posthog/shared`) so the
* editor and agent paths share one supported-language list.
*/
export function isApmEnrichmentEligible(filePath: string): boolean {
return apmLangForFile(filePath) !== null;
}
49 changes: 49 additions & 0 deletions packages/core/src/code-editor/buildApmLineMarkers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { SerializedApmEnrichment, SpanLineStat } from "@posthog/shared";
import { describe, expect, it } from "vitest";
import { buildApmLineMarkers } from "./buildApmLineMarkers";

function stat(overrides: Partial<SpanLineStat>): SpanLineStat {
return {
line: 1,
count: 10,
errorCount: 0,
p50Ms: 1,
p95Ms: 2,
...overrides,
};
}

function enrichment(stats: SpanLineStat[]): SerializedApmEnrichment {
return { filePath: "x.rs", stats, tracingUrl: "https://us.posthog.com/x" };
}

describe("buildApmLineMarkers", () => {
it("returns no markers for null enrichment", () => {
expect(buildApmLineMarkers(null)).toEqual([]);
});

it("produces one marker per line stat", () => {
const markers = buildApmLineMarkers(
enrichment([
stat({ line: 459, p95Ms: 4.8 }),
stat({ line: 900, p95Ms: 1.3 }),
]),
);
expect(markers.map((m) => m.line)).toEqual([459, 900]);
});

it("carries the underlying stat through to each marker", () => {
const [marker] = buildApmLineMarkers(
enrichment([stat({ line: 42, p95Ms: 4, count: 7 })]),
);
expect(marker.stat).toMatchObject({ line: 42, p95Ms: 4, count: 7 });
});

it("summarizes p95/p50 latency and span count for the tooltip", () => {
const [marker] = buildApmLineMarkers(
enrichment([stat({ p95Ms: 4.8, p50Ms: 1.7, count: 1240 })]),
);
expect(marker.summary).toContain("p95 4.8");
expect(marker.summary).toContain("1240 spans");
});
});
42 changes: 42 additions & 0 deletions packages/core/src/code-editor/buildApmLineMarkers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
formatMs,
type SerializedApmEnrichment,
type SpanLineStat,
} from "@posthog/shared";

export interface ApmLineMarker {
/** 1-based line number (span code.lineno is already 1-based — no offset). */
line: number;
/** The underlying per-line stats, surfaced in the popover. */
stat: SpanLineStat;
/** Single-line tooltip summary shown on gutter hover. */
summary: string;
}

function summarize(stat: SpanLineStat): string {
const parts = [
`p95 ${formatMs(stat.p95Ms)}`,
`p50 ${formatMs(stat.p50Ms)}`,
`${stat.count} spans`,
];
if (stat.errorCount > 0) parts.push(`${stat.errorCount} err`);
return parts.join(" · ");
}

/**
* Build per-line gutter markers from APM enrichment. Each instrumented line gets
* one presence marker; the gutter renders a single fixed colour ("PostHog has
* data on this line") rather than a severity gradient — latency has no inherent
* good/bad without a threshold, so the numbers live in the popover instead.
*/
export function buildApmLineMarkers(
enrichment: SerializedApmEnrichment | null,
): ApmLineMarker[] {
if (!enrichment || enrichment.stats.length === 0) return [];

return enrichment.stats.map((stat) => ({
line: stat.line,
stat,
summary: summarize(stat),
}));
}
27 changes: 27 additions & 0 deletions packages/core/src/code-editor/enrichmentPresenters.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { formatPercentDelta } from "./enrichmentPresenters";

describe("formatPercentDelta", () => {
it.each([null, undefined])(
"returns null when there is no delta (absent / no baseline): %s",
(input) => {
expect(formatPercentDelta(input)).toBeNull();
},
);

it.each([
[186.2, "+186%"],
[-5.2, "-5%"],
[1.43, "+1%"],
[-0.9, "-1%"],
] as const)("signs and rounds %s → %s", (input, expected) => {
expect(formatPercentDelta(input)).toBe(expected);
});

it.each([0, 0.4, -0.4])(
"hides sub-1%% noise rather than rendering a meaningless +0%%/-0%%: %s",
(input) => {
expect(formatPercentDelta(input)).toBeNull();
},
);
});
5 changes: 5 additions & 0 deletions packages/core/src/code-editor/enrichmentPresenters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { SerializedFlag } from "@posthog/shared";

// formatPercentDelta lives in @posthog/shared so the agent inline comments (in
// @posthog/enricher, which can't import core) round deltas identically. Re-
// exported here to keep the editor popover's existing import path stable.
export { formatPercentDelta } from "@posthog/shared";

export function compactNumber(n: number): string {
if (n < 1000) return `${n}`;
if (n < 1_000_000) return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k`;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import {
compactNumber,
formatPercentDelta,
} from "@posthog/core/code-editor/enrichmentPresenters";
import { Badge, Card } from "@posthog/quill";
import { APM_STATS_WINDOW, formatMs, getFileName } from "@posthog/shared";
import { openExternalUrl } from "@posthog/ui/shell/openExternal";
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { useApmPopoverStore } from "../stores/apmPopoverStore";

const POPOVER_WIDTH = 280;
const GAP = 8;

function Metric({
label,
value,
delta,
worseWhenUp,
}: {
label: string;
value: string;
/** % change vs the prior window; null/undefined hides it. */
delta?: number | null;
/** When true, an increase is bad (latency) → red up / green down. */
worseWhenUp?: boolean;
}) {
const deltaText = formatPercentDelta(delta);
const deltaClass = worseWhenUp
? (delta ?? 0) > 0
? "text-(--red-11)"
: "text-(--green-11)"
: "text-muted-foreground";
return (
<div>
<div className="text-muted-foreground">{label}</div>
<div className="font-medium font-mono">
{value}
{deltaText && (
<span className={`ml-1 text-[10px] ${deltaClass}`}>{deltaText}</span>
)}
</div>
</div>
);
}

export function ApmEnrichmentPopover() {
const open = useApmPopoverStore((s) => s.open);
const marker = useApmPopoverStore((s) => s.marker);
const anchorRect = useApmPopoverStore((s) => s.anchorRect);
const filePath = useApmPopoverStore((s) => s.filePath);
const tracingUrl = useApmPopoverStore((s) => s.tracingUrl);
const close = useApmPopoverStore((s) => s.close);
const ref = useRef<HTMLDivElement | null>(null);

useEffect(() => {
if (!open) return;
const onDown = (e: MouseEvent) => {
if (!ref.current) return;
if (!ref.current.contains(e.target as Node)) close();
};
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") close();
};
window.addEventListener("mousedown", onDown);
window.addEventListener("keydown", onKey);
return () => {
window.removeEventListener("mousedown", onDown);
window.removeEventListener("keydown", onKey);
};
}, [open, close]);

if (!open || !marker || !anchorRect) return null;

const { stat } = marker;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const preferredLeft = anchorRect.right + GAP;
const fitsRight = preferredLeft + POPOVER_WIDTH + 8 <= viewportWidth;
const left = fitsRight
? preferredLeft
: Math.max(8, anchorRect.left - POPOVER_WIDTH - GAP);
const top = Math.max(8, Math.min(anchorRect.top, viewportHeight - 260));

const errorRate = stat.count > 0 ? (stat.errorCount / stat.count) * 100 : 0;
const file = filePath ? getFileName(filePath) : null;
const hasDelta = [
stat.p50PctChange,
stat.p95PctChange,
stat.p99PctChange,
stat.countPctChange,
stat.errorRatePctChange,
].some((d) => formatPercentDelta(d) != null);

return createPortal(
<div
ref={ref}
style={{
position: "fixed",
top,
left,
width: POPOVER_WIDTH,
zIndex: 1000,
}}
>
<Card size="sm" className="gap-0 py-0 shadow-lg">
<div className="flex flex-col gap-2 px-3 py-2">
<div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<Badge variant="info">APM</Badge>
<div className="min-w-0">
<div className="font-medium text-sm">Production latency</div>
<div className="truncate text-muted-foreground text-xs">
{APM_STATS_WINDOW.label} · PostHog tracing
</div>
</div>
</div>
{stat.errorCount > 0 && (
<Badge variant="destructive">{stat.errorCount} err</Badge>
)}
</div>

<div className="grid grid-cols-3 gap-2 text-xs">
<Metric
label="p50"
value={formatMs(stat.p50Ms)}
delta={stat.p50PctChange}
worseWhenUp
/>
<Metric
label="p95"
value={formatMs(stat.p95Ms)}
delta={stat.p95PctChange}
worseWhenUp
/>
{stat.p99Ms != null && (
<Metric
label="p99"
value={formatMs(stat.p99Ms)}
delta={stat.p99PctChange}
worseWhenUp
/>
)}
</div>

<div className="grid grid-cols-2 gap-2 text-xs">
<Metric
label="Spans"
value={compactNumber(stat.count)}
delta={stat.countPctChange}
/>
<Metric
label="Error rate"
value={`${errorRate.toFixed(1)}%`}
delta={stat.errorRatePctChange}
worseWhenUp
/>
</div>

{hasDelta && (
<div className="text-[10px] text-muted-foreground">
Δ {APM_STATS_WINDOW.comparisonLabel}
</div>
)}

<div className="flex items-center justify-between gap-2 border-t border-t-(--gray-5) pt-2">
<span className="truncate font-mono text-[11px] text-muted-foreground">
{file ? `${file}:${stat.line}` : `line ${stat.line}`}
</span>
{tracingUrl && (
<button
type="button"
onClick={() => openExternalUrl(tracingUrl)}
className="shrink-0 cursor-pointer whitespace-nowrap text-(--purple-11) text-xs hover:underline"
>
View in PostHog →
</button>
)}
</div>
</div>
</Card>
</div>,
document.body,
);
}
Loading
Loading