Skip to content

Commit

Permalink
Slightly Fancier debug tab (#141)
Browse files Browse the repository at this point in the history
* Remove ai summaries

* Factor out hooks for orphan logs waterfall and add timeline to debug

* Make prettier error messages

* Bump api package json

* Fix linting

* Stylize env vars and use keyvalue table for them

* Disable goToCode link for now

* Implement better secret detection in env var table

* Fix timeline styles

* Fix styles on sensitive value cells in keyvalue table

* Format

* Update pnpm lock

* Bump package.json

* Remove dead code
  • Loading branch information
brettimus committed Aug 19, 2024
1 parent 2f7a9f2 commit 66b1baa
Show file tree
Hide file tree
Showing 14 changed files with 374 additions and 483 deletions.
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.7.0-alpha.6",
"version": "0.8.0-alpha.4",
"name": "@fiberplane/studio",
"description": "Local development debugging interface for Hono apps",
"author": "Fiberplane<info@fiberplane.com>",
Expand Down
2 changes: 2 additions & 0 deletions api/src/lib/ai/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ Try strategies like specifying invalid data, missing data, or invalid data types
Try to break the system. But do not break yourself!
Keep your responses to a reasonable length. Including your random data.
Never add the x-fpx-trace-id header to the request.
Use the tool "make_request". Always respond in valid JSON.
***Don't make your responses too long, otherwise we cannot parse your JSON response.***
`);
Expand Down
70 changes: 0 additions & 70 deletions api/src/routes/inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,74 +106,4 @@ app.post(
},
);

/**
* NOT YET IN USE IN THE UI. Originally developed for the AI builders demo.
*
* Takes in an fpx trace and tries to make sense of what happened when a route was invoked.
*/
app.post("/v0/summarize-trace-error/:traceId", cors(), async (ctx) => {
const { handlerSourceCode, trace } = await ctx.req.json();
const traceId = ctx.req.param("traceId");
const db = ctx.get("db");
const inferenceConfig = await getInferenceConfig(db);
if (!inferenceConfig) {
return ctx.json(
{
error: "No inference configuration found",
},
403,
);
}
const { openaiApiKey, openaiModel } = inferenceConfig;
const openaiClient = new OpenAI({
apiKey: openaiApiKey,
});

const response = await openaiClient.chat.completions.create({
model: openaiModel ?? "gpt-4o", // TODO - Update this to use correct model and provider (later problem)
messages: [
{
role: "system",
content: cleanPrompt(`
You are a code debugging assistant for apps that use Hono (web framework),
Neon (serverless postgres), Drizzle (ORM), and run on Cloudflare workers.
You are given a route handler and some trace events that happened when the handler was executed.
Provide a succinct summary/overview of what happened, especially if there was an error.
If you have a suggestion for a fix, give that too. But always be concise!!!
We are rendering your response in a compact UI.
If you don't see any errors, just summarize what happened as briefly as possible.
Format your response as markdown. Do not include a "summary" heading specifically, because that's already in our UI.
`),
},
{
role: "user",
content: cleanPrompt(`
I tried to invoke the following handler in my hono app while making a request:
${handlerSourceCode}
And this is a summary of event data (logs, network requests) that happened inside my app:
${trace.join("\n")}
`),
},
],
temperature: 0,
max_tokens: 4096,
});

const {
choices: [{ message }],
} = response;

return ctx.json({
summary: message.content,
traceId,
});
});

export default app;
1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
"react-highlight-words": "^0.20.0",
"react-hook-form": "^7.52.0",
"react-hotkeys-hook": "^4.5.0",
"react-markdown": "^9.0.1",
"react-resizable": "^3.0.5",
"react-resizable-panels": "^2.0.23",
"react-router-dom": "^6.23.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
import { MizuOrphanLog, isMizuOrphanLog } from "@/queries";
import { useOtelTraces } from "@/queries";
import { OtelEvent, useOtelTrace } from "@/queries/traces-otel";
import { useMemo } from "react";
import { useOtelTrace } from "@/queries/traces-otel";
import { EmptyState } from "../EmptyState";
import { SkeletonLoader } from "../SkeletonLoader";
import { usePagination } from "../hooks";
import { getString } from "../v2/otel-helpers";
import { RequestDetailsPageContentV2 } from "./RequestDetailsPageV2Content";

const safeParseJson = (jsonString: string) => {
try {
const parsed = JSON.parse(jsonString);
return parsed;
} catch (error) {
console.error("Failed to parse JSON:", error);
return jsonString;
}
};
import { useOrphanLogs } from "./useOrphanLogs";

export function Otel({
traceId,
Expand Down Expand Up @@ -46,33 +34,7 @@ export function Otel({
});

// NOTE - Flatten out events into orphan logs to allow the UI to render them
const orphanLogs = useMemo(() => {
const orphans: MizuOrphanLog[] = [];
for (const span of spans ?? []) {
if (span.events) {
for (const event of span.events) {
// TODO - Visualize other types of events on the timeline?
if (event.name === "log") {
let args =
safeParseJson(getString(event.attributes.arguments)) || [];
if (!Array.isArray(args)) {
args = [args];
}
// TODO - Use a more deterministic ID - preferably string that includes the trace+span+event_index
const logId = Math.floor(Math.random() * 1000000);
const orphanLog = convertEventToOrphanLog(traceId, logId, event);
// HACK - We want to be sure that we construct a valid orphan log, otherwise the UI will break
if (isMizuOrphanLog(orphanLog)) {
orphans.push(orphanLog);
} else {
console.error("Constructed invalid orphan log", orphanLog);
}
}
}
}
}
return orphans;
}, [spans, traceId]);
const orphanLogs = useOrphanLogs(traceId, spans ?? []);

if (error) {
console.error("Error!", error);
Expand All @@ -94,26 +56,3 @@ export function Otel({
/>
);
}

/**
* Converts an Otel event to a so-called Orphan Log to maintain backwards compatibility with the old Mizu data format
*/
function convertEventToOrphanLog(
traceId: string,
logId: number,
event: OtelEvent,
) {
const argsAsString = getString(event.attributes.arguments);
const parsedArgs = argsAsString ? safeParseJson(argsAsString) : [];

return {
id: logId,
traceId,
level: getString(event.attributes.level),
args: parsedArgs || [],
timestamp: event.timestamp,
message: getString(event.attributes.message),
createdAt: event.timestamp,
updatedAt: event.timestamp,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import {
import { MizuOrphanLog } from "@/queries";
import { OtelSpan } from "@/queries/traces-otel";
import { cn } from "@/utils";
import { useMemo } from "react";
import { EmptyState } from "../EmptyState";
import { TraceDetailsTimeline, TraceDetailsV2 } from "../v2";
import { HttpSummary, SummaryV2 } from "../v2/SummaryV2";
import { getVendorInfo } from "../v2/vendorify-traces";
import { useRequestWaterfall } from "./useRequestWaterfall";

export type SpanWithVendorInfo = {
span: OtelSpan;
Expand All @@ -44,38 +44,7 @@ export function RequestDetailsPageContentV2({
handleNextTrace: () => void;
};
}) {
const spansWithVendorInfo: Array<SpanWithVendorInfo> = useMemo(
() =>
spans.map((span) => ({
span,
vendorInfo: getVendorInfo(span),
})),
[spans],
);

// HACK - normally we'd look for the root span by trying to find the span with the parent_span_id === null
// but we set a fake parent_span_id for the root span in the middleware for now
const rootSpan = spansWithVendorInfo.find(
// (item) => item.span.parent_span_id === null,
(item) => item.span.name === "request",
);

const waterfall = useMemo((): Waterfall => {
return [...spansWithVendorInfo, ...orphanLogs].sort((a, b) => {
const timeA = "span" in a ? a.span.start_time : a.timestamp;
const timeB = "span" in b ? b.span.start_time : b.timestamp;
if (timeA === timeB) {
// If the times are the same, we need to sort giving the priority to the root span
if ("span" in a && a?.span?.name === "request") {
return -1;
}
if ("span" in b && b?.span?.name === "request") {
return 1;
}
}
return new Date(timeA).getTime() - new Date(timeB).getTime();
});
}, [spansWithVendorInfo, orphanLogs]);
const { rootSpan, waterfall } = useRequestWaterfall(spans, orphanLogs);

if (!rootSpan) {
return <EmptyState />;
Expand Down Expand Up @@ -166,9 +135,9 @@ export function RequestDetailsPageContentV2({
"2xl:min-w-[320px]",
)}
>
<TraceDetailsTimeline waterfall={waterfall} />
<TraceDetailsTimeline waterfall={waterfall} className="lg:pt-0" />
</ResizablePanel>
<ResizableHandle />
<ResizableHandle className="max-lg:hidden" />
<ResizablePanel
className={cn(
"grid items-center gap-4 overflow-x-auto relative",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { MizuOrphanLog, OtelSpan, isMizuOrphanLog } from "@/queries";
import { OtelEvent } from "@/queries/traces-otel";
import { safeParseJson } from "@/utils";
import { useMemo } from "react";
import { getString } from "../v2/otel-helpers";

export function useOrphanLogs(traceId: string, spans: Array<OtelSpan>) {
// NOTE - Flatten out events into orphan logs to allow the UI to render them
const orphanLogs = useMemo(() => {
const orphans: MizuOrphanLog[] = [];
for (const span of spans ?? []) {
if (span.events) {
for (const event of span.events) {
// TODO - Visualize other types of events on the timeline?
if (event.name === "log") {
let args =
safeParseJson(getString(event.attributes.arguments)) || [];
if (!Array.isArray(args)) {
args = [args];
}
// TODO - Use a more deterministic ID - preferably string that includes the trace+span+event_index
const logId = Math.floor(Math.random() * 1000000);
const orphanLog = convertEventToOrphanLog(traceId, logId, event);
// HACK - We want to be sure that we construct a valid orphan log, otherwise the UI will break
if (isMizuOrphanLog(orphanLog)) {
orphans.push(orphanLog);
} else {
console.error("Constructed invalid orphan log", orphanLog);
}
}
}
}
}
return orphans;
}, [spans, traceId]);

return orphanLogs;
}

/**
* Converts an Otel event to a so-called Orphan Log to maintain backwards compatibility with the old Mizu data format
*/
function convertEventToOrphanLog(
traceId: string,
logId: number,
event: OtelEvent,
) {
const argsAsString = getString(event.attributes.arguments);
const parsedArgs = argsAsString ? safeParseJson(argsAsString) : [];

return {
id: logId,
traceId,
level: getString(event.attributes.level),
args: parsedArgs || [],
timestamp: event.timestamp,
message: getString(event.attributes.message),
createdAt: event.timestamp,
updatedAt: event.timestamp,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { MizuOrphanLog, OtelSpan } from "@/queries";
import { useMemo } from "react";
import { getVendorInfo } from "../v2/vendorify-traces";
import { SpanWithVendorInfo, Waterfall } from "./RequestDetailsPageV2Content";

export function useRequestWaterfall(
spans: Array<OtelSpan>,
orphanLogs: Array<MizuOrphanLog>,
) {
const spansWithVendorInfo: Array<SpanWithVendorInfo> = useMemo(
() =>
spans.map((span) => ({
span,
vendorInfo: getVendorInfo(span),
})),
[spans],
);

// HACK - normally we'd look for the root span by trying to find the span with the parent_span_id === null
// but we set a fake parent_span_id for the root span in the middleware for now
const rootSpan = spansWithVendorInfo.find(
// (item) => item.span.parent_span_id === null,
(item) => item.span.name === "request",
);

const waterfall = useMemo((): Waterfall => {
return [...spansWithVendorInfo, ...orphanLogs].sort((a, b) => {
const timeA = "span" in a ? a.span.start_time : a.timestamp;
const timeB = "span" in b ? b.span.start_time : b.timestamp;
if (timeA === timeB) {
// If the times are the same, we need to sort giving the priority to the root span
if ("span" in a && a?.span?.name === "request") {
return -1;
}
if ("span" in b && b?.span?.name === "request") {
return 1;
}
}
return new Date(timeA).getTime() - new Date(timeB).getTime();
});
}, [spansWithVendorInfo, orphanLogs]);

return {
rootSpan,
waterfall,
};
}
Loading

0 comments on commit 66b1baa

Please sign in to comment.