From d756dffbad2e0f37be40d7a6ac34f285ec9bf129 Mon Sep 17 00:00:00 2001 From: Alex Fedotyev Date: Tue, 31 Mar 2026 18:58:03 -0700 Subject: [PATCH] Add AI summarize action to event side panel header Adds a contextual summarize button to the log/trace side panel that generates a brief summary of the event based on available attributes. --- .../app/src/components/AISummarizeButton.tsx | 79 ++++++ .../components/AISummarizePatternButton.tsx | 140 ++++++++++ .../app/src/components/DBRowOverviewPanel.tsx | 1 + .../app/src/components/DBRowSidePanel.tsx | 1 + .../src/components/DBRowSidePanelHeader.tsx | 4 + .../app/src/components/PatternSidePanel.tsx | 6 + .../components/aiSummarize/AISummaryPanel.tsx | 140 ++++++++++ .../aiSummarize/attenboroughTheme.ts | 225 ++++++++++++++++ .../app/src/components/aiSummarize/helpers.ts | 106 ++++++++ .../app/src/components/aiSummarize/index.ts | 5 + .../app/src/components/aiSummarize/logic.ts | 202 ++++++++++++++ .../src/components/aiSummarize/noirTheme.ts | 246 ++++++++++++++++++ .../aiSummarize/shakespeareTheme.ts | 244 +++++++++++++++++ 13 files changed, 1399 insertions(+) create mode 100644 packages/app/src/components/AISummarizeButton.tsx create mode 100644 packages/app/src/components/AISummarizePatternButton.tsx create mode 100644 packages/app/src/components/aiSummarize/AISummaryPanel.tsx create mode 100644 packages/app/src/components/aiSummarize/attenboroughTheme.ts create mode 100644 packages/app/src/components/aiSummarize/helpers.ts create mode 100644 packages/app/src/components/aiSummarize/index.ts create mode 100644 packages/app/src/components/aiSummarize/logic.ts create mode 100644 packages/app/src/components/aiSummarize/noirTheme.ts create mode 100644 packages/app/src/components/aiSummarize/shakespeareTheme.ts diff --git a/packages/app/src/components/AISummarizeButton.tsx b/packages/app/src/components/AISummarizeButton.tsx new file mode 100644 index 0000000000..e77b71cc08 --- /dev/null +++ b/packages/app/src/components/AISummarizeButton.tsx @@ -0,0 +1,79 @@ +// Easter egg: April Fools 2026 — see aiSummarize/ for details. +import { useCallback, useEffect, useRef, useState } from 'react'; + +import AISummaryPanel from './aiSummarize/AISummaryPanel'; +import { + dismissEasterEgg, + generateSummary, + isEasterEggVisible, + RowData, + Theme, +} from './aiSummarize'; + +export default function AISummarizeButton({ + rowData, + severityText, +}: { + rowData?: RowData; + severityText?: string; +}) { + const [result, setResult] = useState<{ + text: string; + theme: Theme; + } | null>(null); + const [isGenerating, setIsGenerating] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [dismissed, setDismissed] = useState(false); + const timerRef = useRef | null>(null); + + // Clean up pending timer on unmount. + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + const handleClick = useCallback(() => { + if (result) { + setIsOpen(prev => !prev); + return; + } + setIsGenerating(true); + setIsOpen(true); + timerRef.current = setTimeout(() => { + setResult(generateSummary(rowData ?? {}, severityText)); + setIsGenerating(false); + timerRef.current = null; + }, 1800); + }, [rowData, severityText, result]); + + const handleRegenerate = useCallback(() => { + setIsGenerating(true); + timerRef.current = setTimeout(() => { + setResult(generateSummary(rowData ?? {}, severityText)); + setIsGenerating(false); + timerRef.current = null; + }, 1200); + }, [rowData, severityText]); + + const handleDismiss = useCallback(() => { + dismissEasterEgg(); + setIsOpen(false); + // Let Collapse animate closed before unmounting. + setTimeout(() => setDismissed(true), 300); + }, []); + + if (dismissed || !isEasterEggVisible()) return null; + + return ( + + ); +} diff --git a/packages/app/src/components/AISummarizePatternButton.tsx b/packages/app/src/components/AISummarizePatternButton.tsx new file mode 100644 index 0000000000..39ed412cca --- /dev/null +++ b/packages/app/src/components/AISummarizePatternButton.tsx @@ -0,0 +1,140 @@ +// Easter egg: April Fools 2026 — see aiSummarize/ for details. +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { + Pattern, + PATTERN_COLUMN_ALIAS, + SEVERITY_TEXT_COLUMN_ALIAS, +} from '@/hooks/usePatterns'; + +import AISummaryPanel from './aiSummarize/AISummaryPanel'; +import { + dismissEasterEgg, + generatePatternSummary, + isEasterEggVisible, + RowData, + Theme, +} from './aiSummarize'; + +/** + * Build a synthetic RowData from the first sample event so the summary + * generators can extract OTel facts (service, severity, body, etc.). + */ +function buildRowDataFromSample( + pattern: Pattern, + serviceNameExpression: string, +): { rowData: RowData; severityText?: string } { + const sample = pattern.samples[0]; + if (!sample) return { rowData: {} }; + return { + rowData: { + __hdx_body: sample[PATTERN_COLUMN_ALIAS], + ServiceName: sample[serviceNameExpression], + __hdx_severity_text: sample[SEVERITY_TEXT_COLUMN_ALIAS], + // Pass through any other fields the sample may have (attributes, etc.) + ...sample, + }, + severityText: sample[SEVERITY_TEXT_COLUMN_ALIAS], + }; +} + +export default function AISummarizePatternButton({ + pattern, + serviceNameExpression, +}: { + pattern: Pattern; + serviceNameExpression: string; +}) { + const [result, setResult] = useState<{ + text: string; + theme: Theme; + } | null>(null); + const [isGenerating, setIsGenerating] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [dismissed, setDismissed] = useState(false); + const timerRef = useRef | null>(null); + + // Clean up pending timer on unmount. + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + // Reset when pattern changes. + useEffect(() => { + setResult(null); + setIsOpen(false); + setIsGenerating(false); + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + }, [pattern]); + + const handleClick = useCallback(() => { + if (result) { + setIsOpen(prev => !prev); + return; + } + setIsGenerating(true); + setIsOpen(true); + const { rowData, severityText } = buildRowDataFromSample( + pattern, + serviceNameExpression, + ); + timerRef.current = setTimeout(() => { + setResult( + generatePatternSummary( + pattern.pattern, + pattern.count, + rowData, + severityText, + ), + ); + setIsGenerating(false); + timerRef.current = null; + }, 1800); + }, [pattern, serviceNameExpression, result]); + + const handleRegenerate = useCallback(() => { + setIsGenerating(true); + const { rowData, severityText } = buildRowDataFromSample( + pattern, + serviceNameExpression, + ); + timerRef.current = setTimeout(() => { + setResult( + generatePatternSummary( + pattern.pattern, + pattern.count, + rowData, + severityText, + ), + ); + setIsGenerating(false); + timerRef.current = null; + }, 1200); + }, [pattern, serviceNameExpression]); + + const handleDismiss = useCallback(() => { + dismissEasterEgg(); + setIsOpen(false); + // Let Collapse animate closed before unmounting. + setTimeout(() => setDismissed(true), 300); + }, []); + + if (dismissed || !isEasterEggVisible()) return null; + + return ( + + ); +} diff --git a/packages/app/src/components/DBRowOverviewPanel.tsx b/packages/app/src/components/DBRowOverviewPanel.tsx index 3571eb02c5..2185cd79ad 100644 --- a/packages/app/src/components/DBRowOverviewPanel.tsx +++ b/packages/app/src/components/DBRowOverviewPanel.tsx @@ -195,6 +195,7 @@ export function RowOverviewPanel({ mainContent={mainContent} mainContentHeader={mainContentColumn} severityText={firstRow?.__hdx_severity_text} + rowData={firstRow} /> )} diff --git a/packages/app/src/components/DBRowSidePanel.tsx b/packages/app/src/components/DBRowSidePanel.tsx index 0f88746c88..7bd184c19e 100644 --- a/packages/app/src/components/DBRowSidePanel.tsx +++ b/packages/app/src/components/DBRowSidePanel.tsx @@ -326,6 +326,7 @@ const DBRowSidePanel = ({ mainContent={mainContent} mainContentHeader={mainContentColumn} severityText={severityText} + rowData={normalizedRow} breadcrumbPath={breadcrumbPath} onBreadcrumbClick={handleBreadcrumbClick} /> diff --git a/packages/app/src/components/DBRowSidePanelHeader.tsx b/packages/app/src/components/DBRowSidePanelHeader.tsx index 29cfc5583a..f5786eb723 100644 --- a/packages/app/src/components/DBRowSidePanelHeader.tsx +++ b/packages/app/src/components/DBRowSidePanelHeader.tsx @@ -25,6 +25,7 @@ import { FormatTime } from '@/useFormatTime'; import { useUserPreferences } from '@/useUserPreferences'; import { formatDistanceToNowStrictShort } from '@/utils'; +import AISummarizeButton from './AISummarizeButton'; import { DBHighlightedAttributesList, HighlightedAttribute, @@ -131,6 +132,7 @@ export default function DBRowSidePanelHeader({ mainContentHeader, date, severityText, + rowData, breadcrumbPath = [], onBreadcrumbClick, }: { @@ -139,6 +141,7 @@ export default function DBRowSidePanelHeader({ mainContentHeader?: string; attributes?: HighlightedAttribute[]; severityText?: string; + rowData?: Record; breadcrumbPath?: BreadcrumbPath; onBreadcrumbClick?: BreadcrumbNavigationCallback; }) { @@ -272,6 +275,7 @@ export default function DBRowSidePanelHeader({ )} + diff --git a/packages/app/src/components/PatternSidePanel.tsx b/packages/app/src/components/PatternSidePanel.tsx index 570cc00b27..d8754ce31c 100644 --- a/packages/app/src/components/PatternSidePanel.tsx +++ b/packages/app/src/components/PatternSidePanel.tsx @@ -3,6 +3,8 @@ import { JSDataType } from '@hyperdx/common-utils/dist/clickhouse'; import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types'; import { Button, Card, Drawer, Stack, Text } from '@mantine/core'; +// Easter egg: April Fools 2026 — see aiSummarize/ for details. +import AISummarizePatternButton from '@/components/AISummarizePatternButton'; import DBRowSidePanel from '@/components/DBRowSidePanel'; import { RawLogTable } from '@/components/DBRowTable'; import { DrawerBody, DrawerHeader } from '@/components/DrawerUtils'; @@ -134,6 +136,10 @@ export default function PatternSidePanel({ {pattern.pattern} + diff --git a/packages/app/src/components/aiSummarize/AISummaryPanel.tsx b/packages/app/src/components/aiSummarize/AISummaryPanel.tsx new file mode 100644 index 0000000000..1de7a3e7d9 --- /dev/null +++ b/packages/app/src/components/aiSummarize/AISummaryPanel.tsx @@ -0,0 +1,140 @@ +// Easter egg: April Fools 2026 — shared presentational component for AI Summarize. +import { useState } from 'react'; +import { + Anchor, + Button, + Collapse, + Flex, + Paper, + Popover, + Text, +} from '@mantine/core'; +import { IconInfoCircle, IconSparkles } from '@tabler/icons-react'; + +import { Theme, THEME_LABELS } from './logic'; + +export default function AISummaryPanel({ + isOpen, + isGenerating, + result, + onToggle, + onRegenerate, + onDismiss, + analyzingLabel = 'Analyzing event data...', +}: { + isOpen: boolean; + isGenerating: boolean; + result: { text: string; theme: Theme } | null; + onToggle: () => void; + onRegenerate: () => void; + onDismiss: () => void; + analyzingLabel?: string; +}) { + const [infoOpen, setInfoOpen] = useState(false); + + return ( +
+ + + {result && isOpen && ( + + )} + + + + {isGenerating ? ( + + {analyzingLabel} + + ) : ( + <> + + + + AI Summary + {result && ( + + {THEME_LABELS[result.theme]} + + )} + + + + setInfoOpen(o => !o)} + style={{ + color: 'var(--mantine-color-dimmed)', + cursor: 'help', + flexShrink: 0, + }} + /> + + + + Happy April Fools! No AI was used. This summary was + generated locally from hand-written phrase templates. Your + data never left the browser. + + { + setInfoOpen(false); + onDismiss(); + }} + style={{ cursor: 'pointer' }} + > + Don't show again + + + + + + {result?.text} + + + )} + + +
+ ); +} diff --git a/packages/app/src/components/aiSummarize/attenboroughTheme.ts b/packages/app/src/components/aiSummarize/attenboroughTheme.ts new file mode 100644 index 0000000000..7e416586a7 --- /dev/null +++ b/packages/app/src/components/aiSummarize/attenboroughTheme.ts @@ -0,0 +1,225 @@ +// Easter egg: April Fools 2026 — David Attenborough Nature Documentary theme. + +import { EventFacts, fmtDuration, Mood, pick, short } from './helpers'; + +export function attenboroughSummary(f: EventFacts, mood: Mood): string { + const lines: string[] = []; + + if (mood === 'error') { + lines.push( + pick([ + 'In the harsh environment of the production cluster, not every request survives to see a 200 OK. This is one such tragic tale.', + 'What we are about to witness is a stark reminder of the brutality of distributed computing.', + 'Life in the cluster is unforgiving. And today, we observe a creature that did not survive the journey.', + 'In the wild expanses of the data center, failure is not the exception -- it is the rule. Today we see this firsthand.', + 'The production environment is a hostile place. Not all who enter these service meshes return.', + 'Observe now a scene that would make even the most hardened SRE look away. Nature, in all her cruelty.', + 'Across the tundra of the server rack, a distress signal echoes. Something has gone terribly wrong.', + 'Here we witness one of the harshest realities of the digital ecosystem. Not every process gets to complete.', + ]), + ); + } else if (mood === 'warn') { + lines.push( + pick([ + 'Here, we observe the early warning signs of distress in the cluster ecosystem. A seasoned observer would know to pay close attention.', + 'The system is showing signs of strain. Like a herd sensing a distant predator, the warnings are subtle but unmistakable.', + 'Notice the subtle change in behavior. The metrics are elevated, the latency slightly higher. The experienced naturalist recognizes these signs immediately.', + 'There is tension in the air. The system is not failing -- not yet -- but one can sense the approaching storm.', + 'Like a reef ecosystem before a bleaching event, the early indicators are there for those trained to see them.', + 'The experienced observer will note the elevated stress hormones -- er, warning logs. A sign that all is not well in the colony.', + ]), + ); + } else { + lines.push( + pick([ + 'And here, in the vast savanna of the distributed system, we observe a remarkable creature going about its daily routine.', + 'Deep within the cluster, a fascinating interaction is about to unfold. Let us observe... quietly.', + 'What we are about to witness is one of the most extraordinary phenomena in modern computing. The humble request, in its natural habitat.', + 'In the great migration of packets across the network, every journey tells a story. This one is no exception.', + "Shh. If we remain very still, we can observe one of nature's most elegant processes: a service-to-service interaction in the wild.", + 'Welcome to the cluster. Population: thousands of containers, each one a world unto itself. Let us peek inside.', + 'Nestled among the nodes of a bustling Kubernetes cluster, a quiet miracle of engineering unfolds.', + 'Today we venture into one of the most biodiverse regions of the modern internet: the microservice archipelago.', + 'Here, at the edge of the load balancer, where the wild requests first make landfall, we observe a most peculiar specimen.', + 'In the coral reef of the container orchestrator, countless tiny services go about their business. Let us observe one now.', + 'At first light in the data center, the cluster awakens. Pods stretch, health checks pass, and the first requests of the day begin their migration.', + 'Few have had the privilege of observing a distributed system this closely. What a time to be alive.', + ]), + ); + } + + if (f.service) { + const lang = f.sdkLanguage + ? ` Its DNA is written in ${f.sdkLanguage} -- a fascinating dialect of the programming kingdom.` + : ''; + const deploy = f.k8sDeployment + ? ` It belongs to the "${f.k8sDeployment}" herd.` + : ''; + lines.push( + pick([ + `The ${f.service} service${f.serviceVersion ? `, generation ${f.serviceVersion},` : ''} stirs to life. It has been waiting patiently for precisely this moment.${lang}`, + `Here we see ${f.service}${f.serviceVersion ? ` (${f.serviceVersion})` : ''} -- a remarkable specimen. It processes thousands of requests daily, yet each one receives individual attention.${deploy}`, + `${f.service}${f.serviceVersion ? `, variant ${f.serviceVersion},` : ''} emerges from its container. A solitary creature, yet vital to the health of the entire ecosystem.${lang}`, + `The ${f.service} species${f.serviceVersion ? `, generation ${f.serviceVersion},` : ''} is perfectly adapted to its niche. Evolution -- or rather, continuous deployment -- has honed it for this precise role.${deploy}`, + `Observe the ${f.service}${f.serviceVersion ? ` (${f.serviceVersion})` : ''} -- a keystone species in this particular microservice biome. Remove it, and the entire food chain collapses.${lang}`, + `${f.service}${f.serviceVersion ? `, build ${f.serviceVersion},` : ''} awakens from its idle state. Like a bear emerging from hibernation, it is hungry for requests.${deploy}`, + `And there it is -- ${f.service}${f.serviceVersion ? `, now in its ${f.serviceVersion} iteration` : ''}. Each version a small adaptation. Each deployment, a leap of faith.${lang}`, + ]), + ); + } + + if (f.httpMethod && f.httpUrl) { + lines.push( + pick([ + `A ${f.httpMethod} request approaches ${short(f.httpUrl, 50)} -- cautiously, as if sensing danger. In the wild, only the fastest requests survive to completion.`, + `Watch as it initiates a ${f.httpMethod} to ${short(f.httpUrl, 50)}. A ritual as old as HTTP itself, performed billions of times daily across the planet.`, + `A ${f.httpMethod} request sets forth toward ${short(f.httpUrl, 50)}, navigating the treacherous waters of the network. Remarkable.`, + ]), + ); + } else if (f.dbSystem) { + lines.push( + pick([ + `It reaches out to ${f.dbSystem} -- the ancient oracle of the ecosystem. ${f.dbStatement ? `"${short(f.dbStatement, 50)}" it whispers.` : 'Every query is a prayer.'} The database considers this... and responds.`, + `Now it approaches the ${f.dbSystem} watering hole. ${f.dbStatement ? `"${short(f.dbStatement, 50)}" -- ` : ''}All creatures in this ecosystem must drink from the database eventually.`, + `It ventures to the ${f.dbSystem} feeding grounds. ${f.dbStatement ? `The query "${short(f.dbStatement, 50)}" is offered. ` : ''}A symbiotic relationship, millions of years -- er, commits -- in the making.`, + ]), + ); + } else if (f.rpcService && f.rpcMethod) { + lines.push( + pick([ + `It performs an intricate signaling dance to ${f.rpcService}.${f.rpcMethod}. In the microservice kingdom, cooperation between species is essential for survival.`, + `Watch now as it performs its mating call -- a gRPC invocation to ${f.rpcService}.${f.rpcMethod}. The ritual is precise, protobuf-encoded, and quite beautiful in its own way.`, + `An intricate chemical signal -- or rather, an RPC -- is exchanged with ${f.rpcService}.${f.rpcMethod}. Communication across service boundaries. Truly one of nature's marvels.`, + ]), + ); + } else if (f.messagingSystem) { + lines.push( + pick([ + `A message is released into ${f.messagingSystem}${f.messagingDestination ? `, destination "${f.messagingDestination}"` : ''}. Like a seed carried by the wind, it may take root -- or it may be lost to the void forever.`, + `It deposits a message in ${f.messagingSystem}${f.messagingDestination ? `, topic "${f.messagingDestination}"` : ''}. An asynchronous act of faith, not unlike a salmon laying eggs and swimming on.`, + `A pheromone -- or rather a message -- is released into ${f.messagingSystem}${f.messagingDestination ? `, channel "${f.messagingDestination}"` : ''}. The colony will know what to do with it.`, + ]), + ); + } else if (f.body) { + lines.push( + pick([ + `It communicates: "${short(f.body, 60)}". A simple signal, yet crucial for the health of the colony.`, + `The message it carries: "${short(f.body, 60)}". In the language of the cluster, these words have profound meaning.`, + `Listen -- "${short(f.body, 60)}". A vocalization that, to the untrained ear, seems unremarkable. But to the colony, it is vital intelligence.`, + ]), + ); + } + + if (f.durationMs != null) { + if (f.durationMs > 60_000) + lines.push( + pick([ + `${fmtDuration(f.durationMs)}. Extraordinary. This request has been alive longer than some mayflies. One can only admire its determination.`, + `${fmtDuration(f.durationMs)}. An astonishing display of endurance. The giant tortoise of the API world, plodding ever onward.`, + `${fmtDuration(f.durationMs)}. One begins to wonder if it has simply decided to stay. Some requests, like hermit crabs, find a timeout and make it home.`, + ]), + ); + else if (f.durationMs > 5000) + lines.push( + pick([ + `${fmtDuration(f.durationMs)}. A remarkable endurance display. The three-toed sloth of the API kingdom, yet it perseveres.`, + `${fmtDuration(f.durationMs)}. In internet years, that's a lifetime. Yet the request carries on, driven by some primal instinct to complete.`, + `${fmtDuration(f.durationMs)}. The elephant of microservice calls -- slow, deliberate, but with a certain grandeur to its pace.`, + ]), + ); + else if (f.durationMs > 100) + lines.push( + pick([ + `${fmtDuration(f.durationMs)}. A respectable pace. Neither the cheetah nor the tortoise -- the steady gazelle of microservice calls.`, + `${fmtDuration(f.durationMs)}. A perfectly adequate speed. The wildebeest of latency -- unremarkable, but reliable.`, + `${fmtDuration(f.durationMs)}. A moderate cruising speed. One might say the golden retriever of response times: enthusiastic, if not the fastest.`, + ]), + ); + else if (f.durationMs > 0) + lines.push( + pick([ + `${fmtDuration(f.durationMs)} -- extraordinarily swift. The peregrine falcon of API calls, diving at breathtaking speed.`, + `${fmtDuration(f.durationMs)}. Blink and you'll miss it. The hummingbird of the request kingdom, wings beating faster than the eye can follow.`, + `${fmtDuration(f.durationMs)}. Astonishing velocity. The cheetah would be envious. The mantis shrimp would applaud.`, + ]), + ); + } + + if (f.httpStatus) { + if (f.httpStatus >= 500) + lines.push( + pick([ + `But nature is cruel. A ${f.httpStatus} response. The request collapses mid-stride, its journey cut tragically short. Only the strong survive in production.`, + `A ${f.httpStatus}. The request has fallen. In the great web of life, not every creature reaches its destination. A moment of silence.`, + `${f.httpStatus}. The server, overwhelmed, lashes out. The request never stood a chance. Such is the brutality of production.`, + ]), + ); + else if (f.httpStatus >= 400) + lines.push( + pick([ + `A ${f.httpStatus}. The request has been rejected by the herd. It will not feed here today. A harsh but necessary lesson.`, + `${f.httpStatus}. Turned away at the gate. Even in the digital world, territorial boundaries are strictly enforced.`, + `A ${f.httpStatus}. The request approaches, but is rebuffed. It does not belong here. It retreats, dejected but wiser.`, + ]), + ); + else if (f.httpStatus >= 200) + lines.push( + pick([ + `A ${f.httpStatus}. Success! The request has found nourishment and can return to its caller, mission accomplished.`, + `${f.httpStatus}. A successful hunt! The response payload is secured. Tonight, the client will feast.`, + `A ${f.httpStatus}. The cycle completes. The request returns home, payload in hand, like a bee laden with pollen.`, + ]), + ); + } + + if (f.exceptionType) { + lines.push( + pick([ + `But tragedy strikes. A ${f.exceptionType}${f.exceptionMessage ? ` -- "${short(f.exceptionMessage, 60)}"` : ''} emerges from the undergrowth. In the unforgiving world of production, exceptions show no mercy.`, + `Suddenly -- a predator. ${f.exceptionType}${f.exceptionMessage ? `: "${short(f.exceptionMessage, 60)}"` : ''}. It strikes without warning. The request never saw it coming.`, + `Oh dear. A ${f.exceptionType}${f.exceptionMessage ? ` -- "${short(f.exceptionMessage, 60)}"` : ''} has appeared. In the food chain of software, exceptions are the apex predator.`, + ]), + ); + } + + if (f.k8sPod) { + lines.push( + pick([ + `All of this unfolds within the protective shell of pod "${f.k8sPod}"${f.k8sNamespace ? `, in the "${f.k8sNamespace}" territory` : ''}. A fragile home, one that the scheduler could evict at any moment. Such is life in Kubernetes.`, + `The habitat: pod "${f.k8sPod}"${f.k8sNamespace ? ` of the "${f.k8sNamespace}" biome` : ''}. A temporary nest -- like a weaver bird's creation, intricate yet disposable.`, + `This all takes place within pod "${f.k8sPod}"${f.k8sNamespace ? `, in the "${f.k8sNamespace}" preserve` : ''}. A container, much like a tidepool -- a small, self-contained world within a vast ocean.`, + `Pod "${f.k8sPod}"${f.k8sNamespace ? `, roaming the "${f.k8sNamespace}" grasslands` : ''} -- an ephemeral creature. Born from a deployment, destined to be recycled. The circle of life in Kubernetes.`, + `Deep in the "${f.k8sNamespace || 'default'}" canopy, pod "${f.k8sPod}" clings to its branch. One resource spike, and the eviction predator strikes.`, + `Pod "${f.k8sPod}"${f.k8sNamespace ? `, a denizen of the "${f.k8sNamespace}" reef` : ''} -- it may look permanent, but in Kubernetes, nothing truly is. Even the coral grows and is replaced.`, + `The nesting ground: pod "${f.k8sPod}"${f.k8sNamespace ? ` in "${f.k8sNamespace}"` : ''}. Like a mayfly, its lifespan is measured not in years, but in rolling updates.`, + ]), + ); + } else if (f.hostName) { + lines.push( + pick([ + `The habitat: host "${f.hostName}". A single node in an ecosystem of thousands.`, + `All of this occurs on host "${f.hostName}" -- a permanent fixture in the landscape, like an ancient baobab tree in the savanna.`, + `The territory: "${f.hostName}". A bare-metal habitat, increasingly rare in this age of containers and cloud.`, + ]), + ); + } + + lines.push( + pick([ + 'And so the cycle continues. Requests arrive, responses depart. The great circle of observability.', + 'Extraordinary. Absolutely extraordinary.', + 'Tomorrow, another request will traverse this very path. Such is life in the cluster.', + 'And as the metrics settle, the system rests -- until the next deployment disturbs the peace.', + 'Remarkable. One could watch these systems for hours and still discover something new.', + 'And so we leave the cluster, as we found it -- humming, processing, endlessly fascinating.', + 'One could spend a lifetime studying these systems and never fully understand them. And that, perhaps, is the most extraordinary thing of all.', + 'As the sun sets on this particular request cycle, the ecosystem prepares for what comes next. It always does.', + 'What a privilege it has been to observe this moment. In the grand tapestry of distributed computing, every span tells a story.', + 'The natural world teaches us patience. The digital world teaches us that patience has a timeout of 30 seconds.', + 'Magnificent. Simply magnificent. Though I suspect the oncall engineer might use a different word.', + 'And with that, we take our leave. The cluster carries on, as it has for uptime, and as it will until the next rolling update.', + ]), + ); + + return lines.join('\n\n'); +} diff --git a/packages/app/src/components/aiSummarize/helpers.ts b/packages/app/src/components/aiSummarize/helpers.ts new file mode 100644 index 0000000000..c8f8547c45 --- /dev/null +++ b/packages/app/src/components/aiSummarize/helpers.ts @@ -0,0 +1,106 @@ +// Easter egg: April Fools 2026 — shared helpers and types for AI Summarize. + +import { renderMs } from '../TimelineChart/utils'; + +// --------------------------------------------------------------------------- +// Visibility gate +// --------------------------------------------------------------------------- +// Apr 1–6: always visible. +// Apr 7–30: only visible with ?smart=true in URL. +// May 1+: off entirely. +// Evaluated once at module load so it's stable across re-renders. + +const ALWAYS_ON_END = new Date('2026-04-07T00:00:00').getTime(); +const HARD_OFF = new Date('2026-05-01T00:00:00').getTime(); +const DISMISS_KEY = 'hdx-ai-summarize-dismissed'; + +// eslint-disable-next-line no-restricted-syntax -- one-time module-level check +const NOW_MS = new Date().getTime(); + +function isDismissed(): boolean { + try { + return ( + typeof window !== 'undefined' && + window.localStorage.getItem(DISMISS_KEY) === '1' + ); + } catch { + return false; + } +} + +export function isEasterEggVisible(): boolean { + if (NOW_MS >= HARD_OFF) return false; + if (isDismissed()) return false; + if (NOW_MS < ALWAYS_ON_END) return true; + // Apr 7–30: require ?smart=true + if (typeof window !== 'undefined') { + const params = new URLSearchParams(window.location.search); + return params.get('smart') === 'true'; + } + return false; +} + +export function dismissEasterEgg(): void { + try { + window.localStorage.setItem(DISMISS_KEY, '1'); + } catch { + // localStorage unavailable — ignore + } +} + +export type RowData = Record; + +export interface EventFacts { + service?: string; + serviceVersion?: string; + spanName?: string; + spanKind?: string; + durationMs?: number; + statusCode?: string; + severity?: string; + httpMethod?: string; + httpUrl?: string; + httpStatus?: number; + dbSystem?: string; + dbStatement?: string; + rpcMethod?: string; + rpcService?: string; + messagingSystem?: string; + messagingDestination?: string; + exceptionType?: string; + exceptionMessage?: string; + k8sPod?: string; + k8sNamespace?: string; + k8sDeployment?: string; + k8sCluster?: string; + sdkLanguage?: string; + hostName?: string; + body?: string; +} + +export type Mood = 'error' | 'warn' | 'slow' | 'normal'; + +export function short(s: string | undefined, max: number): string { + if (!s) return ''; + return s.length > max ? s.slice(0, max - 3) + '...' : s; +} + +export function fmtDuration(ms: number): string { + // Use the same formatter as the trace waterfall for normal ranges + if (ms < 60_000) return renderMs(ms); + // Extended ranges for readability + if (ms < 3_600_000) { + const mins = (ms / 60_000).toFixed(1); + return `~${mins}min`; + } + if (ms < 86_400_000) { + const hrs = (ms / 3_600_000).toFixed(1); + return `~${hrs}h`; + } + const days = Math.round(ms / 86_400_000); + return `~${days} day${days !== 1 ? 's' : ''}`; +} + +export function pick(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} diff --git a/packages/app/src/components/aiSummarize/index.ts b/packages/app/src/components/aiSummarize/index.ts new file mode 100644 index 0000000000..4f7551c02e --- /dev/null +++ b/packages/app/src/components/aiSummarize/index.ts @@ -0,0 +1,5 @@ +// Easter egg: April Fools 2026 — AI Summarize public API. +export type { RowData } from './helpers'; +export { dismissEasterEgg, isEasterEggVisible } from './helpers'; +export type { Theme } from './logic'; +export { generatePatternSummary, generateSummary } from './logic'; diff --git a/packages/app/src/components/aiSummarize/logic.ts b/packages/app/src/components/aiSummarize/logic.ts new file mode 100644 index 0000000000..908095caf5 --- /dev/null +++ b/packages/app/src/components/aiSummarize/logic.ts @@ -0,0 +1,202 @@ +// Easter egg: April Fools 2026 — core logic for AI Summarize. +// No real AI is involved. The summaries are randomly assembled from +// hand-written phrase pools themed as Detective Noir, Shakespearean Drama, +// and David Attenborough Nature Documentary. +// Active until end of April 2026 — see AISummarizeButton.tsx for the gate. + +import { attenboroughSummary } from './attenboroughTheme'; +import { EventFacts, Mood, pick, RowData, short } from './helpers'; +import { noirSummary } from './noirTheme'; +import { shakespeareSummary } from './shakespeareTheme'; + +// --------------------------------------------------------------------------- +// Data extraction -- pull structured facts from OTel / K8s attributes +// --------------------------------------------------------------------------- + +function extractFacts(row: RowData, severityText?: string): EventFacts { + const attrs = row.__hdx_event_attributes ?? {}; + const res = row.__hdx_resource_attributes ?? {}; + const exc = row.__hdx_events_exception_attributes ?? {}; + const rawDuration = row.Duration != null ? Number(row.Duration) : undefined; + // Duration from useRowData comes as the raw column value (not converted). + // OTel default durationPrecision=9 means nanoseconds. Always divide by + // 1e6 to get milliseconds -- this matches the default OTel schema and + // the HyperDX demo. For non-default precisions the timing will be + // slightly off but this is a novelty feature so that's fine. + const durationMs = + rawDuration != null && !isNaN(rawDuration) && rawDuration > 0 + ? rawDuration / 1e6 + : undefined; + + return { + service: row.ServiceName || res['service.name'], + serviceVersion: res['service.version'], + spanName: row.SpanName || row.__hdx_body, + spanKind: row.SpanKind, + durationMs, + statusCode: row.StatusCode, + severity: severityText, + httpMethod: attrs['http.method'] || attrs['http.request.method'], + httpUrl: + attrs['http.url'] || + attrs['url.full'] || + attrs['http.target'] || + attrs['url.path'], + httpStatus: (() => { + const s = attrs['http.status_code'] || attrs['http.response.status_code']; + return s ? Number(s) : undefined; + })(), + dbSystem: attrs['db.system'], + dbStatement: attrs['db.statement'], + rpcMethod: attrs['rpc.method'], + rpcService: attrs['rpc.service'], + messagingSystem: attrs['messaging.system'], + messagingDestination: + attrs['messaging.destination'] || attrs['messaging.destination.name'], + exceptionType: exc['exception.type'], + exceptionMessage: (() => { + const m = exc['exception.message']; + return typeof m === 'string' ? m : m ? JSON.stringify(m) : undefined; + })(), + k8sPod: res['k8s.pod.name'], + k8sNamespace: res['k8s.namespace.name'], + k8sDeployment: res['k8s.deployment.name'], + k8sCluster: res['k8s.cluster.name'], + sdkLanguage: res['telemetry.sdk.language'] || res['telemetry.sdk.name'], + hostName: res['host.name'] || res['host.id'], + body: + typeof row.__hdx_body === 'string' + ? row.__hdx_body + : row.__hdx_body != null + ? JSON.stringify(row.__hdx_body) + : undefined, + }; +} + +// Classify event mood based on severity, status code, exception +function classifyMood(f: EventFacts): Mood { + const sev = f.severity?.toLowerCase(); + if ( + sev === 'error' || + sev === 'fatal' || + sev === 'critical' || + f.statusCode === 'STATUS_CODE_ERROR' || + f.exceptionType || + (f.httpStatus && f.httpStatus >= 500) + ) + return 'error'; + if ( + sev === 'warn' || + sev === 'warning' || + (f.httpStatus && f.httpStatus >= 400) + ) + return 'warn'; + if (f.durationMs != null && f.durationMs > 5000) return 'slow'; + return 'normal'; +} + +// --------------------------------------------------------------------------- +// Theme selection -- deterministic based on event context +// --------------------------------------------------------------------------- + +export type Theme = 'noir' | 'attenborough' | 'shakespeare'; + +export const THEME_LABELS: Record = { + noir: 'Detective Noir', + attenborough: 'Nature Documentary', + shakespeare: 'Shakespearean Drama', +}; + +type ThemeFn = (f: EventFacts, mood: Mood) => string; +const THEME_FNS: Record = { + noir: noirSummary, + attenborough: attenboroughSummary, + shakespeare: shakespeareSummary, +}; + +// Error/exception -> noir (crime scene) +// Slow/performance -> shakespeare (dramatic suffering) +// Warning -> pick noir or shakespeare +// Normal/info -> attenborough (nature observation) +function selectTheme(mood: Mood): Theme { + switch (mood) { + case 'error': + return pick(['noir', 'noir', 'shakespeare'] as Theme[]); + case 'warn': + return pick(['noir', 'shakespeare'] as Theme[]); + case 'slow': + return pick(['shakespeare', 'shakespeare', 'attenborough'] as Theme[]); + case 'normal': + return pick([ + 'attenborough', + 'attenborough', + 'shakespeare', + 'noir', + ] as Theme[]); + } +} + +export function generateSummary( + row: RowData, + severityText?: string, +): { text: string; theme: Theme } { + const facts = extractFacts(row, severityText); + const mood = classifyMood(facts); + const theme = selectTheme(mood); + return { text: THEME_FNS[theme](facts, mood), theme }; +} + +// --------------------------------------------------------------------------- +// Pattern-specific summary — uses pattern name + first sample + count +// --------------------------------------------------------------------------- + +function patternPreamble( + patternName: string, + count: number, + theme: Theme, +): string { + const fmtCount = count.toLocaleString(); + switch (theme) { + case 'noir': + return pick([ + `The same message kept turning up: "${short(patternName, 70)}". Not once, not twice -- ${fmtCount} times. That's not a coincidence. That's a pattern.`, + `I opened the case file and found ${fmtCount} identical reports: "${short(patternName, 70)}". Somebody was being very repetitive. Or very broken.`, + `"${short(patternName, 70)}" -- the logs were full of it. ${fmtCount} occurrences. Like a suspect repeating the same alibi over and over.`, + `${fmtCount} witnesses, all telling the same story: "${short(patternName, 70)}". Either they're all telling the truth, or the system has a stutter.`, + `The evidence was overwhelming. ${fmtCount} instances of "${short(patternName, 70)}". In my experience, when a log repeats that many times, it's either very healthy or very sick.`, + ]); + case 'attenborough': + return pick([ + `What we observe here is a remarkable colonial behavior: the same message -- "${short(patternName, 70)}" -- repeated ${fmtCount} times. In nature, such repetition serves a purpose. In software, it usually means someone forgot to add rate limiting.`, + `"${short(patternName, 70)}" -- this call echoes across the cluster ${fmtCount} times. Like the synchronized chirping of a cricket colony, it is both impressive and slightly concerning.`, + `Here we witness one of the most prolific species in the log ecosystem: "${short(patternName, 70)}". With ${fmtCount} specimens observed, it dominates this particular habitat.`, + `Extraordinary. The pattern "${short(patternName, 70)}" has been recorded ${fmtCount} times. One is reminded of the starling murmuration -- thousands of individuals, one pattern, endlessly repeated.`, + `In the dense undergrowth of the log stream, one pattern rises above the rest: "${short(patternName, 70)}". ${fmtCount} instances. A dominant species, thriving in these conditions.`, + ]); + case 'shakespeare': + return pick([ + `"${short(patternName, 70)}" -- so says the log, not once but ${fmtCount} times! "Methinks the service doth protest too much."`, + `Hark! A refrain most persistent: "${short(patternName, 70)}". ${fmtCount} times it echoes through the cluster, like a chorus that hath forgotten how to stop.`, + `"Once more unto the log, dear friends!" ${fmtCount} times hath this message -- "${short(patternName, 70)}" -- graced the stage. A soliloquy on infinite repeat.`, + `"${short(patternName, 70)}" -- the opening line of a play performed ${fmtCount} times. Even Shakespeare did not demand such repetition from his actors.`, + `Act I, Scene 1. And Scene 2. And Scene ${fmtCount}. The line is always the same: "${short(patternName, 70)}". "Brevity is the soul of wit," but nobody told this pattern.`, + ]); + } +} + +export function generatePatternSummary( + patternName: string, + count: number, + sampleRow: RowData, + severityText?: string, +): { text: string; theme: Theme } { + const facts = extractFacts(sampleRow, severityText); + const mood = classifyMood(facts); + const theme = selectTheme(mood); + const preamble = patternPreamble(patternName, count, theme); + const body = THEME_FNS[theme](facts, mood); + // Replace the first paragraph (the generic opener) with our pattern preamble. + const lines = body.split('\n\n'); + lines[0] = preamble; + return { text: lines.join('\n\n'), theme }; +} diff --git a/packages/app/src/components/aiSummarize/noirTheme.ts b/packages/app/src/components/aiSummarize/noirTheme.ts new file mode 100644 index 0000000000..074a889148 --- /dev/null +++ b/packages/app/src/components/aiSummarize/noirTheme.ts @@ -0,0 +1,246 @@ +// Easter egg: April Fools 2026 — Detective Noir theme for AI Summarize. + +import { EventFacts, fmtDuration, Mood, pick, short } from './helpers'; + +export function noirSummary(f: EventFacts, mood: Mood): string { + const lines: string[] = []; + + // Mood-specific openers + if (mood === 'error') { + lines.push( + pick([ + "The call came in at midnight. Something had died in production, and it wasn't pretty.", + "I'd seen my share of ugly stack traces. This one made the others look like bedtime stories.", + 'The pager screamed like a banshee. I already knew it was going to be a long night.', + 'There was trouble in the cluster. The kind that makes senior engineers update their LinkedIn.', + "It started with a 3am page. They always start at 3am. Like the system knows when you're deepest in REM.", + 'The dashboard turned red. Not the gentle blush of a warning -- the deep crimson of a five-alarm fire.', + "I was two sips into my coffee when the incident channel lit up. Should've stayed in bed.", + "The error rate spiked like a heartbeat on a polygraph. Someone was lying, and it wasn't the metrics.", + 'Production was bleeding out. I grabbed my laptop and started triage. No time for pleasantries.', + 'They found the body in the logs. Cause of death: unhandled exception. Time of death: right now.', + 'Another night, another outage. This city never sleeps, and neither does its infrastructure.', + 'The Slack channel exploded. Fifteen engineers, zero answers. Classic.', + ]), + ); + } else if (mood === 'warn') { + lines.push( + pick([ + 'Something smelled wrong. Not rotten yet, but the kind of wrong that gets worse.', + "The warning lights were blinking. Nobody pays attention to warnings. That's how they get you.", + 'A yellow light in a world that only cares about red. But I know better.', + "I've seen this pattern before. Warnings today, incidents tomorrow. The writing was on the wall.", + 'The metrics were nervous. Twitchy. Like a canary that knows the mine air is going bad.', + "It wasn't an error. Not yet. But I could smell one coming, like rain before a storm.", + "The warning came in quiet, like a snitch in a back alley. Most people would ignore it. I don't ignore warnings.", + 'Degraded, they called it. Like calling a knife wound a scratch. I knew better.', + "The system wasn't broken. It was bending. And I've been around long enough to know what comes next.", + ]), + ); + } else { + lines.push( + pick([ + 'The request came in like they all do -- quiet, routine. But in this town, nothing stays routine for long.', + 'Another day, another span. I poured myself a coffee and started reading.', + "On the surface it looked clean. But I've been doing this too long to trust surfaces.", + 'It was a quiet Tuesday in the cluster. Too quiet. I opened the traces anyway.', + "The span landed on my desk like a manila folder. Routine, they said. I've heard that before.", + 'I picked up the trace and held it to the light. Sometimes the boring ones hide the best secrets.', + "Everything looked normal. That's what worried me. Normal is just chaos that hasn't revealed itself yet.", + 'Rain hammered the datacenter windows. Another request, another story nobody would read. Except me.', + "The trace was unremarkable. That's what they all say before the postmortem.", + ]), + ); + } + + if (f.service) { + const ver = f.serviceVersion ? ` v${f.serviceVersion}` : ''; + const lang = f.sdkLanguage + ? ` Written in ${f.sdkLanguage} -- you can always tell by the stack traces.` + : ''; + const deploy = f.k8sDeployment + ? ` Part of the "${f.k8sDeployment}" outfit.` + : ''; + lines.push( + pick([ + `${f.service}${ver} -- I knew the name. It had a rap sheet longer than a Kafka topic.${lang}`, + `The suspect: ${f.service}${f.serviceVersion ? `, version ${f.serviceVersion}` : ''}. It had been at the scene of every major incident this quarter.${deploy}`, + `${f.service}${f.serviceVersion ? ` (${f.serviceVersion})` : ''} was involved. Of course it was.${lang}`, + `${f.service}${f.serviceVersion ? `, build ${f.serviceVersion}` : ''}. I'd seen this one around. It had connections to every service in town.${deploy}`, + `They called it ${f.service}${f.serviceVersion ? ` -- version ${f.serviceVersion}, fresh off the CI pipeline` : ''}. It had that look. The look of a service with something to hide.${lang}`, + `${f.service}${f.serviceVersion ? ` (${f.serviceVersion})` : ''} -- a repeat offender. Last seen at the previous incident. And the one before that.${deploy}`, + `The name on the span said ${f.service}${f.serviceVersion ? `, running ${f.serviceVersion}` : ''}. In my line of work, you learn to recognize the regulars.${lang}`, + `${f.service}${f.serviceVersion ? `, model year ${f.serviceVersion}` : ''}. New paint job, same old bugs underneath.${deploy}`, + ]), + ); + } + + if (f.httpMethod && f.httpUrl) { + lines.push( + pick([ + `${f.httpMethod} ${short(f.httpUrl, 50)} -- someone was knocking on the door.`, + `A ${f.httpMethod} to ${short(f.httpUrl, 50)}. Direct, no nonsense. I respect that in a request.`, + `The request arrived: ${f.httpMethod} ${short(f.httpUrl, 50)}. Like a stranger walking into a bar and ordering the usual.`, + ]), + ); + } else if (f.dbSystem) { + lines.push( + pick([ + f.dbStatement + ? `It was interrogating ${f.dbSystem}: "${short(f.dbStatement, 60)}". Cold. Efficient. No small talk.` + : `${f.dbSystem} was involved. The database always knows more than it lets on.`, + `${f.dbSystem}${f.dbStatement ? ` -- "${short(f.dbStatement, 50)}"` : ''}. The old filing cabinet in the back room. Everything ends up there eventually.`, + `It went straight to ${f.dbSystem}. ${f.dbStatement ? `"${short(f.dbStatement, 50)}" -- ` : ''}The kind of query that knows exactly what it's looking for.`, + ]), + ); + } else if (f.rpcService && f.rpcMethod) { + lines.push( + pick([ + `An RPC to ${f.rpcService}.${f.rpcMethod}. A handshake in the dark between two services that barely trust each other.`, + `It dialed ${f.rpcService}, asked for ${f.rpcMethod}. A private conversation between two processes. No witnesses.`, + `${f.rpcService}.${f.rpcMethod} -- a coded message between accomplices. In microservices, everyone has a handler.`, + ]), + ); + } else if (f.messagingSystem) { + lines.push( + pick([ + `A message dropped into ${f.messagingSystem}${f.messagingDestination ? ` on "${f.messagingDestination}"` : ''}. Fire and forget. The coward's way out.`, + `It left a note in ${f.messagingSystem}${f.messagingDestination ? `, addressed to "${f.messagingDestination}"` : ''}. Dead drop protocol. Classic.`, + `${f.messagingSystem}${f.messagingDestination ? `, channel "${f.messagingDestination}"` : ''} -- an anonymous tip left at the dead drop. No return address.`, + ]), + ); + } + + if (f.httpStatus) { + if (f.httpStatus >= 500) + lines.push( + pick([ + `The server answered ${f.httpStatus}. Five hundred. The kind of number that makes oncall reach for the bourbon.`, + `${f.httpStatus}. The server had given up, like a detective who's seen too much.`, + `A ${f.httpStatus} came back. The server confessed to everything. It couldn't take the pressure anymore.`, + `${f.httpStatus}. Internal server error. The kind of internal that means something broke inside and nobody wants to talk about it.`, + ]), + ); + else if (f.httpStatus >= 400) + lines.push( + pick([ + `A ${f.httpStatus} came back. Wrong credentials at the wrong bar.`, + `${f.httpStatus}. Denied. The bouncer wasn't impressed with the authentication.`, + `The response: ${f.httpStatus}. Access denied. Someone didn't have the right papers.`, + ]), + ); + else if (f.httpStatus >= 200) + lines.push( + pick([ + `${f.httpStatus} -- it survived. But for how long?`, + `${f.httpStatus}. Success. But in this business, today's 200 is tomorrow's 500.`, + `A ${f.httpStatus}. Clean getaway. No evidence, no trace. Almost.`, + ]), + ); + } + + if (f.durationMs != null) { + if (f.durationMs > 60_000) + lines.push( + pick([ + `${fmtDuration(f.durationMs)}. That's not latency, that's a missing persons case.`, + `${fmtDuration(f.durationMs)}. I've seen cold cases close faster than this request.`, + `${fmtDuration(f.durationMs)}. The request had gone dark. We were about to file a missing report.`, + ]), + ); + else if (f.durationMs > 5000) + lines.push( + pick([ + `${fmtDuration(f.durationMs)}. An eternity. Somewhere, a user was staring at a spinner, losing faith in technology.`, + `${fmtDuration(f.durationMs)}. Long enough to make a sandwich. Long enough to regret your career choices.`, + `${fmtDuration(f.durationMs)}. That's not a response time, that's a hostage situation.`, + ]), + ); + else if (f.durationMs > 1000) + lines.push( + pick([ + `${fmtDuration(f.durationMs)}. Slow enough to notice. Slow enough to worry.`, + `${fmtDuration(f.durationMs)}. Not catastrophic, but the kind of slow that keeps you up at night.`, + `${fmtDuration(f.durationMs)}. The request took its sweet time, like a witness who doesn't want to talk.`, + ]), + ); + else if (f.durationMs > 0) + lines.push( + pick([ + `${fmtDuration(f.durationMs)}. Quick. Maybe too quick. I made a note.`, + `${fmtDuration(f.durationMs)}. Fast and clean. No time for fingerprints.`, + `${fmtDuration(f.durationMs)}. In and out before anyone noticed. Professional.`, + ]), + ); + } + + if (f.exceptionType) { + lines.push( + pick([ + f.exceptionMessage + ? `Then I found the body -- a ${f.exceptionType}: "${short(f.exceptionMessage, 80)}". The kind of exception that ends careers and starts postmortems.` + : `A ${f.exceptionType} was waiting in the shadows. It had been there all along.`, + f.exceptionMessage + ? `The murder weapon: ${f.exceptionType}. "${short(f.exceptionMessage, 60)}". Left right there in the stack trace for anyone to find.` + : `A ${f.exceptionType}. The calling card of a serial offender. No message, no remorse.`, + f.exceptionMessage + ? `There it was -- ${f.exceptionType}: "${short(f.exceptionMessage, 70)}". I'd seen this MO before.` + : `${f.exceptionType}. The prime suspect. It had motive, means, and opportunity.`, + ]), + ); + } + + if (f.k8sPod) { + lines.push( + pick([ + `The trail led to pod "${f.k8sPod}"${f.k8sNamespace ? ` in namespace "${f.k8sNamespace}"` : ''}. A small container in a big city of containers.`, + `Pod "${f.k8sPod}"${f.k8sNamespace ? `, "${f.k8sNamespace}" district` : ''} -- that was our crime scene. Could be restarted any minute. Evidence doesn't last long in Kubernetes.`, + `I tracked it to pod "${f.k8sPod}"${f.k8sNamespace ? ` in the "${f.k8sNamespace}" precinct` : ''}. Ephemeral. Could vanish at any moment. The perfect hiding spot.`, + `Pod "${f.k8sPod}"${f.k8sNamespace ? `, namespace "${f.k8sNamespace}"` : ''} -- a disposable identity in a city full of them. The scheduler could whack it at any time.`, + `The address: pod "${f.k8sPod}"${f.k8sNamespace ? `, "${f.k8sNamespace}" block` : ''}. A rented room in a flophouse. Month-to-month. No questions asked.`, + `I found the hideout: "${f.k8sPod}"${f.k8sNamespace ? ` in the "${f.k8sNamespace}" projects` : ''}. Cheap, temporary, scheduled for demolition. Just the way the suspects like it.`, + `Pod "${f.k8sPod}"${f.k8sNamespace ? `, on the wrong side of "${f.k8sNamespace}"` : ''}. A container with a short lease on life. In this cluster, everybody's just passing through.`, + ]), + ); + } else if (f.hostName) { + lines.push( + pick([ + `Host "${f.hostName}". I wrote down the address.`, + `The location: "${f.hostName}". A fixed address in a world of shifting containers.`, + `"${f.hostName}" -- that's where it all went down. I've got the address on file.`, + ]), + ); + } + + if (mood === 'error') { + lines.push( + pick([ + 'I closed the ticket and stared at the dashboard. Tomorrow there would be another incident. There always is.', + "Another postmortem to write. I lit a cigarette and started typing. 'Contributing factors: everything.'", + "The PagerDuty went silent. But I knew it was only sleeping. It's always only sleeping.", + 'The war room emptied. The incident channel went quiet. But the logs would remember.', + 'I updated the status page and closed my laptop. The system was stable. For now.', + "They'd mark it as resolved in Jira. But some things don't resolve. They just stop being visible.", + "The RCA would say 'cascading failure.' It always says cascading failure. Nobody ever cascades on purpose.", + 'I marked the incident resolved and poured one out for the failed requests. They never had a chance.', + 'The 5-whys meeting was scheduled for Monday. I already knew the answer to all five: this codebase.', + ]), + ); + } else { + lines.push( + pick([ + 'I filed the trace and poured myself another coffee. The system would live to serve another day.', + 'Case closed. But in distributed systems, nothing ever really ends.', + "The alerts went quiet. For now. They'd be back. They always come back.", + 'I stamped the trace "resolved" and moved on. The next one was already waiting.', + 'Another clean trace. I should be happy. But clean traces just make me suspicious.', + 'The dashboard went green. But I knew -- they always go red again.', + 'I closed the tab and sipped my coffee. Somewhere out there, a retry was already in flight. They never stop.', + 'The span completed. The trace was closed. But every closed trace is just the prologue to the next incident.', + "All quiet on the cluster front. I didn't trust it. But I clocked out anyway.", + ]), + ); + } + + return lines.join('\n\n'); +} diff --git a/packages/app/src/components/aiSummarize/shakespeareTheme.ts b/packages/app/src/components/aiSummarize/shakespeareTheme.ts new file mode 100644 index 0000000000..478e56d876 --- /dev/null +++ b/packages/app/src/components/aiSummarize/shakespeareTheme.ts @@ -0,0 +1,244 @@ +// Easter egg: April Fools 2026 — Shakespearean Drama theme for AI Summarize. + +import { EventFacts, fmtDuration, Mood, pick, short } from './helpers'; + +export function shakespeareSummary(f: EventFacts, mood: Mood): string { + const lines: string[] = []; + + if (mood === 'error') { + lines.push( + pick([ + 'Friends, engineers, SREs -- lend me your terminals! I come to debug this trace, not to praise it.', + '"Something is rotten in the state of production." The gravedigger digs through logs.', + '"Double, double, toil and trouble; server burn and database bubble." The witches have been at the config again.', + '"O, what a fall was there, my engineers!" A service that once stood proud now lies in ruin.', + '"Now is the winter of our deployment." And this error hath made it most discontent.', + '"Cry havoc and let slip the bugs of war!" The error hath breached the gates of production.', + '"The evil that services do lives after them in the logs; the good is oft interred with their pods."', + '"If errors be the food of outages, deploy on." The tragedy begins.', + 'Act V. The final act. Where all the retries have been spent and only the stack trace remains.', + '"We few, we happy few, we band of oncall." Tonight we debug.', + ]), + ); + } else if (mood === 'slow') { + lines.push( + pick([ + "To retry, or not to retry -- that is the question. Whether 'tis nobler in the cluster to suffer the slings and arrows of outrageous latency...", + '"How poor are they that have not patience!" The request waits. And waits. And waits still more.', + '"Delays have dangerous ends." And this delay hath tested the patience of every user in the realm.', + '"O time, thou must untangle this, not I!" The request hath been waiting since the age of the previous deployment.', + '"Lord, what fools these timeouts be!" Set too high for the impatient, too low for the database.', + '"The wheel is come full circle." And yet the request still spins, awaiting a response.', + '"How weary, stale, flat, and unprofitable seem to me all the uses of this endpoint." For it is slow. So very slow.', + '"There is nothing either fast or slow, but SLOs make it so." And our SLOs are most unkind.', + '"If it were done when \'tis done, then \'twere well it were done quickly." But alas, this span knows not the meaning of quickly.', + ]), + ); + } else { + lines.push( + pick([ + "Hark! What light through yonder load balancer breaks? 'Tis a request, and it beareth tidings.", + "All the world's a cluster, and all the services merely players. They have their exits and their entrances.", + '"Brevity is the soul of wit." And so, let us be brief about this span.', + '"What a piece of work is a microservice!" How noble in architecture, how infinite in endpoints.', + '"Now is the summer of our uptime," made glorious by this successful span. Long may it last.', + '"There are more things in Grafana and Datadog, Horatio, than are dreamt of in your runbooks."', + '"Some are born distributed, some achieve distribution, and some have microservices thrust upon them."', + '"If metrics be the food of SRE, graph on! Give me excess of it."', + '"The quality of uptime is not strained. It droppeth as the gentle deploy from heaven."', + '"Once more unto the endpoint, dear friends, once more."', + '"O brave new world, that has such services in it!" Let us observe one such wonder.', + '"Shall I compare thee to a summer deploy? Thou art more stable and more temperate."', + ]), + ); + } + + if (f.service) { + const lang = f.sdkLanguage + ? ` Forged in the fires of ${f.sdkLanguage} -- a tongue both powerful and perilous.` + : ''; + const deploy = f.k8sDeployment + ? ` It serves the house of "${f.k8sDeployment}".` + : ''; + lines.push( + pick([ + `Enter ${f.service}${f.serviceVersion ? `, Act ${f.serviceVersion}` : ''} -- a service of noble bearing, yet burdened with a thousand requests upon its shoulders.${lang}`, + `${f.service}${f.serviceVersion ? ` (v${f.serviceVersion})` : ''} takes the stage. "The readiness is all," it declares, though readiness, like uptime, is never guaranteed.${deploy}`, + `${f.service}${f.serviceVersion ? `, revision ${f.serviceVersion},` : ''} makes its entrance. "I am not what I am," it warns in its README, and truer words were never committed.${lang}`, + `The noble ${f.service}${f.serviceVersion ? `, heir to version ${f.serviceVersion}` : ''}, strides upon the stage. Heavy is the head that wears the crown of being a critical service.${deploy}`, + `Enter stage left: ${f.service}${f.serviceVersion ? `, Act ${f.serviceVersion}, Scene 1` : ''}. "All that glitters is not gold" -- and all that passes health checks is not healthy.${lang}`, + `${f.service}${f.serviceVersion ? ` (v${f.serviceVersion})` : ''} awakens. "To thine own SLO be true," it whispers to itself. A fine motto. Rarely achieved.${deploy}`, + `Behold ${f.service}${f.serviceVersion ? `, in its ${f.serviceVersion} incarnation` : ''}! "Though this be madness, yet there is method in it." Or so the architects claim.${lang}`, + ]), + ); + } + + if (f.httpMethod && f.httpUrl) { + lines.push( + pick([ + `"${f.httpMethod} ${short(f.httpUrl, 40)}!" it cries unto the void. A plea most desperate, cast upon the network winds.`, + `A ${f.httpMethod} to ${short(f.httpUrl, 40)}. "Once more unto the endpoint!" quoth the client, steeling itself for the response.`, + `"${f.httpMethod} ${short(f.httpUrl, 40)}" -- the battle cry rings across the network. Whether fortune favors this request remains to be seen.`, + ]), + ); + } else if (f.dbSystem) { + lines.push( + pick([ + `It doth consult the ${f.dbSystem} oracle${f.dbStatement ? `, whispering: "${short(f.dbStatement, 50)}"` : ''}. The ancient keeper of state, who remembers what all others forget.`, + `To the ${f.dbSystem} it turns, ${f.dbStatement ? `beseeching: "${short(f.dbStatement, 50)}"` : 'seeking answers in the depths of persistence'}. "The truth will out," sayeth the query optimizer.`, + `It kneels before ${f.dbSystem}${f.dbStatement ? `, offering this query: "${short(f.dbStatement, 50)}"` : ''}. The database, like the Oracle at Delphi, speaks only in response to those who ask correctly.`, + ]), + ); + } else if (f.rpcService && f.rpcMethod) { + lines.push( + pick([ + `A messenger dispatched to ${f.rpcService}, bearing word of ${f.rpcMethod}. "Haste thee hence," quoth the caller, "and return with good tidings."`, + `"Go, bid the soldiers of ${f.rpcService} shoot!" The call to ${f.rpcMethod} is made. The die is cast.`, + `A herald rides forth to ${f.rpcService}, bearing the scroll of ${f.rpcMethod}. "If it be now, 'tis not to come. If it be not to come, it will timeout now."`, + ]), + ); + } else if (f.messagingSystem) { + lines.push( + pick([ + `A letter, sealed and sent through ${f.messagingSystem}${f.messagingDestination ? ` unto "${f.messagingDestination}"` : ''}. Fire-and-forget -- the way of cowards and event-driven architectures alike.`, + `Into the depths of ${f.messagingSystem}${f.messagingDestination ? `, to the "${f.messagingDestination}" mailbox,` : ''} a message is cast. "The readiness is all" -- and the consumer had better be ready.`, + `A scroll is entrusted to ${f.messagingSystem}${f.messagingDestination ? `, addressed to "${f.messagingDestination}"` : ''}. "Neither a producer nor a consumer be?" Too late for that advice.`, + ]), + ); + } else if (f.body) { + lines.push( + pick([ + `The message reads: "${short(f.body, 50)}". Words most plain, yet they carry the weight of the entire transaction.`, + `"${short(f.body, 50)}" -- thus speaks the span. In these humble words, an entire saga is compressed.`, + `Its dying breath carries these words: "${short(f.body, 50)}". Let the postmortem record them faithfully.`, + ]), + ); + } + + if (f.durationMs != null) { + if (f.durationMs > 60_000) + lines.push( + pick([ + `${fmtDuration(f.durationMs)}! "O, that this too, too slow request would resolve itself!" The user grows old waiting.`, + `${fmtDuration(f.durationMs)}! Kingdoms have risen and fallen in less time. "The patient must minister to themselves," for no SRE cometh.`, + `${fmtDuration(f.durationMs)}. "Age cannot wither it, nor custom stale its infinite... buffering." The loading spinner hath become a permanent fixture.`, + ]), + ); + else if (f.durationMs > 5000) + lines.push( + pick([ + `${fmtDuration(f.durationMs)} -- an age! Methinks the user doth grow weary, staring at the spinning wheel of fortune.`, + `${fmtDuration(f.durationMs)}. "I wasted time, and now doth time waste me." So too speaks the impatient user.`, + `${fmtDuration(f.durationMs)}! "How slow this old moon wanes!" quoth the client, watching the progress bar crawl.`, + ]), + ); + else if (f.durationMs > 100) + lines.push( + pick([ + `${fmtDuration(f.durationMs)}. Neither swift as Mercury nor slow as the court bureaucracy.`, + `${fmtDuration(f.durationMs)}. "The course of true requests never did run smooth," but this one ran... acceptably.`, + `${fmtDuration(f.durationMs)}. A middling pace. "There is a tide in the affairs of latency," and this one is at a comfortable ebb.`, + ]), + ); + else if (f.durationMs > 0) + lines.push( + pick([ + `${fmtDuration(f.durationMs)} -- swift as Puck himself! "I'll put a girdle round about the earth in forty milliseconds."`, + `${fmtDuration(f.durationMs)}! "The swiftest hare hath not such feet as this response!" Truly, a span of noble speed.`, + `${fmtDuration(f.durationMs)}. "Screw your courage to the sticking place!" No courage needed -- it was over before it began.`, + ]), + ); + } + + if (f.httpStatus) { + if (f.httpStatus >= 500) + lines.push( + pick([ + `Alas! ${f.httpStatus}! "The fault, dear Brutus, lies not in our clients, but in our servers, that they are overloaded."`, + `${f.httpStatus}. "Et tu, server?" Even the backend hath betrayed us.`, + `${f.httpStatus}! "Now cracks a noble server's heart. Good night, sweet service, and flights of 503s sing thee to thy rest."`, + ]), + ); + else if (f.httpStatus >= 400) + lines.push( + pick([ + `${f.httpStatus} -- rebuffed! "Get thee to a debugger!" cries the gateway.`, + `A ${f.httpStatus}. "The lady doth protest too much!" The authorization middleware is most unforgiving.`, + `${f.httpStatus}. "Off with their tokens!" The auth layer shows no mercy to the unauthorized.`, + ]), + ); + else if (f.httpStatus >= 200) + lines.push( + pick([ + `${f.httpStatus}. "All's well that ends well," quoth the response, though I trust it not entirely.`, + `${f.httpStatus}! "O happy response!" The request is returned, triumphant, from the field of battle.`, + `A ${f.httpStatus}. "The rest is 200." Well done. A standing ovation from the load balancer.`, + ]), + ); + } + + if (f.exceptionType) { + lines.push( + pick([ + `But soft -- what villainy! A ${f.exceptionType} most foul${f.exceptionMessage ? `: "${short(f.exceptionMessage, 60)}"` : ''}! "O villain, villain, smiling, damned villain!"`, + `"Murder most foul!" A ${f.exceptionType} strikes${f.exceptionMessage ? ` -- "${short(f.exceptionMessage, 60)}"` : ''}. The stack trace tells all.`, + `"By the pricking of my thumbs, something ${f.exceptionType} this way comes."${f.exceptionMessage ? ` "${short(f.exceptionMessage, 60)}" -- a curse most specific.` : ''} The prophecy is fulfilled.`, + ]), + ); + } + + if (f.k8sPod) { + lines.push( + pick([ + `The scene is set: pod "${f.k8sPod}"${f.k8sNamespace ? `, in the realm of "${f.k8sNamespace}"` : ''}. A humble vessel upon the Kubernetes sea, subject to the tempests of the scheduler.`, + `The stage is pod "${f.k8sPod}"${f.k8sNamespace ? `, in the kingdom of "${f.k8sNamespace}"` : ''}. "Uneasy lies the head that wears a crown" -- and uneasier still the pod that exceeds its memory limits.`, + `Act, Scene: Pod "${f.k8sPod}"${f.k8sNamespace ? `, Realm "${f.k8sNamespace}"` : ''}. "This above all: to thine own resource limits be true." Wise words. Rarely heeded.`, + `Pod "${f.k8sPod}"${f.k8sNamespace ? `, of the "${f.k8sNamespace}" court` : ''} -- a player upon the Kubernetes stage. "Exit, pursued by an OOMKiller."`, + `"What's in a name? That which we call pod '${f.k8sPod}' by any other name would consume as many millicores."${f.k8sNamespace ? ` The "${f.k8sNamespace}" registry confirms it.` : ''}`, + `Pod "${f.k8sPod}"${f.k8sNamespace ? `, vassal to the "${f.k8sNamespace}" duchy` : ''} -- born in a ReplicaSet, fated to die in a rolling update. "Cowards die many times before their deaths; pods but once."`, + `The Globe: pod "${f.k8sPod}"${f.k8sNamespace ? `, namespace "${f.k8sNamespace}"` : ''}. "All the cluster's a stage, and all the pods merely players." Some have shorter runs than others.`, + ]), + ); + } else if (f.hostName) { + lines.push( + pick([ + `The stage: host "${f.hostName}". A modest theatre for this drama.`, + `Host "${f.hostName}" -- the Globe Theatre of our tale. All the requests are merely players upon its stage.`, + `The scene unfolds upon "${f.hostName}". A sturdy stage, if somewhat showing its age.`, + ]), + ); + } + + if (mood === 'error') { + lines.push( + pick([ + '"Out, damned error! Out, I say!" Yet still it persists upon the dashboard.', + 'A tale told by an idiot service, full of logs and fury, signifying nothing.', + '"The rest is silence." ...Until the next on-call rotation.', + '"Good night, good night! Parting is such sweet 503." The curtain falls on this error.', + '"Though this be madness, yet there is a JIRA ticket in it." Tomorrow we debug.', + '"To sleep, perchance to dream of green dashboards." For now, the pager waits.', + '"All our yesterdays have lit fools the way to dusty stack traces." And tomorrow promises more of the same.', + '"The fault is not in our stars, but in our deployment pipeline."', + '"What\'s done cannot be undone." But it can be reverted. Hopefully.', + ]), + ); + } else { + lines.push( + pick([ + 'Exeunt all. The curtain falls upon this span. Yet the next act begins already.', + '"Parting is such sweet sorrow" -- said no service to any request, ever.', + 'Thus concludes this act. The monitoring continues, as it must.', + '"We are such stuff as spans are made on, and our little traces are rounded with a timeout."', + 'The players take their bow. The span is done. But like all great theatre, it shall be repeated.', + '"If all the year were playing holidays, to deploy would be as tedious as to work." The cycle continues.', + '"The play\'s the thing!" And in this production, every span is a soliloquy.', + '"All that ends well is 200 OK." And with that, the chorus departs.', + '"Now is the winter of our deployment made glorious summer by this successful response." Fin.', + '"To log, or not to log -- there is no question." We log everything. Always. Exeunt.', + ]), + ); + } + + return lines.join('\n\n'); +}