diff --git a/apps/local/.oxlintrc.json b/apps/local/.oxlintrc.json index 72cf41a39..8c194a448 100644 --- a/apps/local/.oxlintrc.json +++ b/apps/local/.oxlintrc.json @@ -4,6 +4,7 @@ "rules": { "react/rules-of-hooks": "error", "typescript/return-await": ["error", "always"], + "typescript/no-unnecessary-condition": "error", "react/only-export-components": ["warn", { "allowConstantExport": true }] } } diff --git a/apps/local/scripts/replay.ts b/apps/local/scripts/replay.ts index 95fb198d9..beb6a8996 100644 --- a/apps/local/scripts/replay.ts +++ b/apps/local/scripts/replay.ts @@ -136,7 +136,7 @@ async function main() { repeat: Number(values.repeat), onSent: (file, index) => console.log( - `${index}. ${file.slice(dirname(resolve(values.fixtures ?? positionals[0] ?? defaultFixtures)).length + 1)}`, + `${index}. ${file.slice(dirname(resolve(values.fixtures ?? positionals.at(0) ?? defaultFixtures)).length + 1)}`, ), }); console.log(`Sent ${count} envelope${count === 1 ? "" : "s"}.`); diff --git a/apps/local/src/App.integration.test.tsx b/apps/local/src/App.integration.test.tsx index c80bf000e..eac052ddd 100644 --- a/apps/local/src/App.integration.test.tsx +++ b/apps/local/src/App.integration.test.tsx @@ -771,8 +771,9 @@ describe("local receiver to viewer integration", () => { test("replays bundled envelopes through the receiver into every investigation view", async () => { const { server, port } = await startReceiver(); - const originalCreate = URL.createObjectURL?.bind(URL); - const originalRevoke = URL.revokeObjectURL?.bind(URL); + const urlStatics: Partial = URL; + const originalCreate = urlStatics.createObjectURL?.bind(URL); + const originalRevoke = urlStatics.revokeObjectURL?.bind(URL); URL.createObjectURL = () => "blob:fixture"; URL.revokeObjectURL = () => {}; try { @@ -842,8 +843,8 @@ describe("local receiver to viewer integration", () => { expect(await screen.findByText("sentry.javascript.node")).toBeDefined(); } finally { cleanup(); - URL.createObjectURL = originalCreate; - URL.revokeObjectURL = originalRevoke; + urlStatics.createObjectURL = originalCreate; + urlStatics.revokeObjectURL = originalRevoke; await stopReceiver(server); } }, 15_000); @@ -1366,7 +1367,7 @@ describe("local receiver to viewer integration", () => { const source = ControllableEventSource.instances[0]; expect(source).toBeDefined(); - await act(async () => source?.onerror?.(new Event("error"))); + await act(async () => source.onerror?.(new Event("error"))); await screen.findByRole("alert", { name: "Receiver connection error" }); fireEvent.change(screen.getByLabelText("Receiver endpoint"), { @@ -1377,7 +1378,7 @@ describe("local receiver to viewer integration", () => { "Enter a loopback stream or an HTTPS remote stream", ); - await act(async () => source?.onerror?.(new Event("error"))); + await act(async () => source.onerror?.(new Event("error"))); expect(screen.getByRole("alert").textContent).toContain( "Enter a loopback stream or an HTTPS remote stream", ); @@ -1393,7 +1394,7 @@ describe("local receiver to viewer integration", () => { const initialSource = ControllableEventSource.instances[0]; expect(initialSource).toBeDefined(); - await act(async () => initialSource?.onerror?.(new Event("error"))); + await act(async () => initialSource.onerror?.(new Event("error"))); fireEvent.change(screen.getByLabelText("Receiver endpoint"), { target: { value: "http://127.0.0.1:8970/stream" }, }); @@ -1411,7 +1412,7 @@ describe("local receiver to viewer integration", () => { vi.advanceTimersByTime(10_000); }); - expect(source?.closed).toBe(true); + expect(source.closed).toBe(true); expect( screen.getByRole("alert", { name: "Receiver connection error" }) .textContent, @@ -1433,12 +1434,12 @@ describe("local receiver to viewer integration", () => { const source = ControllableEventSource.instances[0]; expect(source).toBeDefined(); - await act(async () => source?.onopen?.(new Event("open"))); + await act(async () => source.onopen?.(new Event("open"))); await screen.findByText("Connected to local receiver"); fireEvent.click(screen.getByRole("button", { name: "Switch receiver" })); await act(async () => { - source?.dispatchEvent( + source.dispatchEvent( new MessageEvent(SENTRY_CONTENT_TYPE, { data: JSON.stringify([ {}, @@ -1463,7 +1464,7 @@ describe("local receiver to viewer integration", () => { const source = ControllableEventSource.instances[0]; expect(source).toBeDefined(); - await act(async () => source?.onopen?.(new Event("open"))); + await act(async () => source.onopen?.(new Event("open"))); await screen.findByText("Connected to local receiver"); fireEvent.click(screen.getByRole("button", { name: "Switch receiver" })); @@ -1485,7 +1486,7 @@ describe("local receiver to viewer integration", () => { const source = ControllableEventSource.instances[0]; expect(source).toBeDefined(); - await act(async () => source?.onopen?.(new Event("open"))); + await act(async () => source.onopen?.(new Event("open"))); await screen.findByText("Connected to local receiver"); fireEvent.click(screen.getByRole("button", { name: "Switch receiver" })); fireEvent.change(screen.getByLabelText("Receiver endpoint"), { @@ -1493,7 +1494,7 @@ describe("local receiver to viewer integration", () => { }); fireEvent.click(screen.getByRole("button", { name: "Connect" })); - expect(source?.closed).toBe(false); + expect(source.closed).toBe(false); expect( screen.getAllByText("Connected to local receiver", { exact: true })[0], ).not.toBeNull(); @@ -1698,11 +1699,11 @@ describe("local receiver to viewer integration", () => { const source = ControllableEventSource.instances[0]; expect(source).toBeDefined(); - await act(async () => source?.onopen?.(new Event("open"))); + await act(async () => source.onopen?.(new Event("open"))); await screen.findByText("Connected to local receiver"); await act(async () => { - source?.dispatchEvent( + source.dispatchEvent( new MessageEvent(SENTRY_CONTENT_TYPE, { data: JSON.stringify([{}, []]), lastEventId: "raw-only-envelope", @@ -1726,8 +1727,8 @@ describe("local receiver to viewer integration", () => { renderBareViewer(); const source = ControllableEventSource.instances[0]; await act(async () => { - source?.onopen?.(new Event("open")); - source?.dispatchEvent( + source.onopen?.(new Event("open")); + source.dispatchEvent( new MessageEvent(SENTRY_CONTENT_TYPE, { data: "undecodable", lastEventId: "raw-broken", @@ -1846,15 +1847,15 @@ describe("local receiver to viewer integration", () => { vi.stubGlobal("fetch", fetchMock); renderBareViewer(); const source = ControllableEventSource.instances[0]; - await act(async () => source?.onopen?.(new Event("open"))); + await act(async () => source.onopen?.(new Event("open"))); await act(async () => { - source?.dispatchEvent( + source.dispatchEvent( new MessageEvent("sentry.local.retention", { data: JSON.stringify({ maxEnvelopes: 500 }), }), ); for (const id of ["one", "two"]) - source?.dispatchEvent( + source.dispatchEvent( new MessageEvent(SENTRY_CONTENT_TYPE, { data: JSON.stringify([ {}, @@ -1885,7 +1886,7 @@ describe("local receiver to viewer integration", () => { ); await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); await act(async () => - source?.dispatchEvent( + source.dispatchEvent( new MessageEvent("sentry.local.retention", { data: JSON.stringify({ maxEnvelopes: 1000 }), }), @@ -1909,7 +1910,7 @@ describe("local receiver to viewer integration", () => { expect( screen.getAllByRole("button", { name: "View event event" }), ).toHaveLength(2); - expect(source?.closed).toBe(false); + expect(source.closed).toBe(false); expect(ControllableEventSource.instances).toHaveLength(1); }); @@ -1919,13 +1920,13 @@ describe("local receiver to viewer integration", () => { renderBareViewer(); const source = ControllableEventSource.instances[0]; await act(async () => { - source?.onopen?.(new Event("open")); - source?.dispatchEvent( + source.onopen?.(new Event("open")); + source.dispatchEvent( new MessageEvent("sentry.local.retention", { data: '{"maxEnvelopes":1000}', }), ); - source?.dispatchEvent( + source.dispatchEvent( new MessageEvent(SENTRY_CONTENT_TYPE, { data: JSON.stringify([ {}, @@ -1934,7 +1935,7 @@ describe("local receiver to viewer integration", () => { lastEventId: "kept", }), ); - source?.onerror?.(new Event("error")); + source.onerror?.(new Event("error")); }); fireEvent.click( await screen.findByRole("button", { name: "Settings", exact: true }), @@ -2013,8 +2014,8 @@ describe("local receiver to viewer integration", () => { renderBareViewer(); let source = ControllableEventSource.instances[0]; await act(async () => { - source?.onopen?.(new Event("open")); - source?.dispatchEvent( + source.onopen?.(new Event("open")); + source.dispatchEvent( new MessageEvent("sentry.local.retention", { data: '{"maxEnvelopes":500}', }), @@ -2044,8 +2045,8 @@ describe("local receiver to viewer integration", () => { source = await openLatestReceiver(); } await act(async () => { - source?.onopen?.(new Event("open")); - source?.dispatchEvent( + source.onopen?.(new Event("open")); + source.dispatchEvent( new MessageEvent("sentry.local.retention", { data: '{"maxEnvelopes":1000}', }), @@ -2067,7 +2068,7 @@ describe("local receiver to viewer integration", () => { ); await act(async () => { for (const id of ["one", "two"]) - source?.dispatchEvent( + source.dispatchEvent( new MessageEvent(SENTRY_CONTENT_TYPE, { data: JSON.stringify([ {}, @@ -2222,7 +2223,7 @@ describe("local receiver to viewer integration", () => { const initialSource = ControllableEventSource.instances[0]; expect(initialSource).toBeDefined(); - await act(async () => initialSource?.onopen?.(new Event("open"))); + await act(async () => initialSource.onopen?.(new Event("open"))); fireEvent.click(screen.getByRole("button", { name: "Switch receiver" })); const streamUrl = @@ -2234,7 +2235,7 @@ describe("local receiver to viewer integration", () => { const scopedSource = await openLatestReceiver(); await act(async () => { - scopedSource?.dispatchEvent( + scopedSource.dispatchEvent( new MessageEvent(SENTRY_CONTENT_TYPE, { data: JSON.stringify([{}, []]), lastEventId: "scoped-envelope", @@ -2280,7 +2281,7 @@ describe("local receiver to viewer integration", () => { signal: expect.any(AbortSignal), }), ); - await waitFor(() => expect(scopedSource?.closed).toBe(true)); + await waitFor(() => expect(scopedSource.closed).toBe(true)); await screen.findByText("Saved capture", { exact: true }); expect( screen.queryByText( @@ -2313,11 +2314,11 @@ describe("local receiver to viewer integration", () => { const source = ControllableEventSource.instances[0]; expect(source).toBeDefined(); - await act(async () => source?.onopen?.(new Event("open"))); + await act(async () => source.onopen?.(new Event("open"))); await screen.findByText("Connected to local receiver"); await act(async () => { - source?.dispatchEvent( + source.dispatchEvent( new MessageEvent(SENTRY_CONTENT_TYPE, { data: JSON.stringify([{}, []]), lastEventId: "raw-envelope-before-failure", @@ -2327,7 +2328,7 @@ describe("local receiver to viewer integration", () => { fireEvent.click(viewButton("Open envelopes view")); fireEvent.click(await screen.findByLabelText("View envelope event")); - await act(async () => source?.onerror?.(new Event("error"))); + await act(async () => source.onerror?.(new Event("error"))); expect(screen.getByLabelText("Workspace navigation")).not.toBeNull(); expect( @@ -2374,7 +2375,7 @@ describe("local receiver to viewer integration", () => { const source = await openLatestReceiver(); await act(async () => { for (const transaction of ["GET /alpha", "GET /beta"]) { - source?.dispatchEvent( + source.dispatchEvent( new MessageEvent(SENTRY_CONTENT_TYPE, { data: JSON.stringify([ {}, @@ -2403,7 +2404,7 @@ describe("local receiver to viewer integration", () => { ); if (state === "closed") { fireEvent.click(captureAction("Stop capture")); - await waitFor(() => expect(source?.closed).toBe(true)); + await waitFor(() => expect(source.closed).toBe(true)); await screen.findByText("Saved capture", { exact: true }); expect(screen.getAllByLabelText("View transaction event")).toHaveLength( 1, @@ -2426,7 +2427,7 @@ describe("local receiver to viewer integration", () => { 1, ); } else { - await act(async () => source?.onerror?.(new Event("error"))); + await act(async () => source.onerror?.(new Event("error"))); } fireEvent.change( screen.getByRole("searchbox", { name: "Filter Events" }), diff --git a/apps/local/src/App.tsx b/apps/local/src/App.tsx index 196e30e9d..f9b54165a 100644 --- a/apps/local/src/App.tsx +++ b/apps/local/src/App.tsx @@ -454,7 +454,7 @@ export default function App() { }); const selectedItem = searchedItems.find((item) => item.id === workspaceQuery.eventId) ?? - searchedItems[0]; + searchedItems.at(0); const selectedEventTrace = selectedItem?.metadata?.traceId ? traces.find((trace) => trace.id === selectedItem.metadata?.traceId) : undefined; @@ -1300,7 +1300,7 @@ export default function App() { new URL(currentEndpoint.url).pathname === "/stream", )} sessionPicker={ - activeDirectoryUrl && !isDisconnected ? ( + activeDirectoryUrl ? ( { @@ -15,7 +17,7 @@ export function attributeFields(value: unknown): DetailField[] { value: typeof content === "string" ? content - : (JSON.stringify(content) ?? String(content)), + : (stringifyJson(content) ?? String(content)), code: true, }; }); diff --git a/apps/local/src/components/performance-workspace.tsx b/apps/local/src/components/performance-workspace.tsx index bcd0a4053..0adf8b0ca 100644 --- a/apps/local/src/components/performance-workspace.tsx +++ b/apps/local/src/components/performance-workspace.tsx @@ -51,7 +51,8 @@ export function SpanAnalysis({ ), [traces, kind, query, sort, ascending], ); - const selected = groups.find((group) => group.id === selectedId) ?? groups[0]; + const selected = + groups.find((group) => group.id === selectedId) ?? groups.at(0); const label = kind === "queries" ? "Database queries" : "Resources"; if (!selected) return ( @@ -203,7 +204,8 @@ export function WebVitalsWorkspace({ ); const [selectedName, setSelectedName] = useState(); const [chosenMetric, setChosenMetric] = useState("lcp"); - const selected = pages.find((page) => page.name === selectedName) ?? pages[0]; + const selected = + pages.find((page) => page.name === selectedName) ?? pages.at(0); if (!selected) return ( diff --git a/apps/local/src/components/profile-workspace.tsx b/apps/local/src/components/profile-workspace.tsx index 147168087..ea326dc09 100644 --- a/apps/local/src/components/profile-workspace.tsx +++ b/apps/local/src/components/profile-workspace.tsx @@ -468,7 +468,7 @@ export function ProfileWorkspace({ item.text.toLowerCase().includes(query.toLowerCase()), ); const selected = - profiles.find((item) => item.id === selectedId) ?? profiles[0]; + profiles.find((item) => item.id === selectedId) ?? profiles.at(0); if (!selected) return (

diff --git a/apps/local/src/components/retention-settings.tsx b/apps/local/src/components/retention-settings.tsx index 5cbaeeb5a..91cfe6396 100644 --- a/apps/local/src/components/retention-settings.tsx +++ b/apps/local/src/components/retention-settings.tsx @@ -88,7 +88,7 @@ export function RetentionSettings({ }); const error = form.formState.errors.maxEnvelopes?.message ?? - form.formState.errors.root?.server?.message; + form.formState.errors.root?.server.message; return (

diff --git a/apps/local/src/components/span-detail.tsx b/apps/local/src/components/span-detail.tsx index d16fa7d42..7aa7b290f 100644 --- a/apps/local/src/components/span-detail.tsx +++ b/apps/local/src/components/span-detail.tsx @@ -2,7 +2,7 @@ import { AiInspector } from "./ai-inspector.tsx"; import { useCopyToClipboard } from "@uidotdev/usehooks"; import { Check, Copy, ChevronLeft } from "lucide-react"; import { type KeyboardEvent, type ReactNode, useId, useState } from "react"; -import type { LocalFeedItem } from "@/lib/spotlight.ts"; +import { stringifyJson, type LocalFeedItem } from "@/lib/spotlight.ts"; import type { TraceGroup, TraceSpan } from "@/lib/trace-model.ts"; import { isErrorEvent } from "@/lib/workspace.ts"; import { JsonView } from "./json-view.tsx"; @@ -275,7 +275,7 @@ export function SpanDetail({ diff --git a/apps/local/src/components/trace-waterfall.tsx b/apps/local/src/components/trace-waterfall.tsx index 8c3938f63..c11db5186 100644 --- a/apps/local/src/components/trace-waterfall.tsx +++ b/apps/local/src/components/trace-waterfall.tsx @@ -61,13 +61,11 @@ function flattenSpans(spans: TraceSpan[]): PositionedSpan[] { positioned.push(current); const children = current.span.children; for (let index = children.length - 1; index >= 0; index -= 1) { - const child = children[index]; - if (child) - pending.push({ - span: child, - depth: current.depth + 1, - isLastChild: index === children.length - 1, - }); + pending.push({ + span: children[index], + depth: current.depth + 1, + isLastChild: index === children.length - 1, + }); } } return positioned; diff --git a/apps/local/src/components/trace-workspace.tsx b/apps/local/src/components/trace-workspace.tsx index 46daf5a5e..b08be884a 100644 --- a/apps/local/src/components/trace-workspace.tsx +++ b/apps/local/src/components/trace-workspace.tsx @@ -48,7 +48,7 @@ export function TraceWorkspace({ onSelectProfile, }: TraceWorkspaceProps) { const selectedTrace = - traces.find((trace) => trace.id === selectedTraceId) ?? traces[0]; + traces.find((trace) => trace.id === selectedTraceId) ?? traces.at(0); const selectedSpan = selectedTrace?.id === selectedTraceId diff --git a/apps/local/src/lib/exceptions.test.ts b/apps/local/src/lib/exceptions.test.ts index 067556e28..ce6765516 100644 --- a/apps/local/src/lib/exceptions.test.ts +++ b/apps/local/src/lib/exceptions.test.ts @@ -56,7 +56,7 @@ describe("getEventExceptions", () => { ], }, }); - expect(exception?.frames).toEqual([ + expect(exception.frames).toEqual([ { filename: "worker", function: "process", diff --git a/apps/local/src/lib/inspection.ts b/apps/local/src/lib/inspection.ts index 066cad593..26190cf7d 100644 --- a/apps/local/src/lib/inspection.ts +++ b/apps/local/src/lib/inspection.ts @@ -1,4 +1,8 @@ -import { getEventMetadata, type LocalFeedItem } from "./spotlight.ts"; +import { + getEventMetadata, + stringifyJson, + type LocalFeedItem, +} from "./spotlight.ts"; import { getEventExceptions, formatExceptionTitle } from "./exceptions.ts"; export function record(value: unknown): Record { @@ -17,7 +21,7 @@ export function finite(value: unknown): number | undefined { export function display(value: unknown): string { return typeof value === "string" ? value - : (JSON.stringify(value, null, 2) ?? String(value)); + : (stringifyJson(value, 2) ?? String(value)); } export function parsed(value: unknown): unknown { if (typeof value !== "string") return value; diff --git a/apps/local/src/lib/performance.test.ts b/apps/local/src/lib/performance.test.ts index 308bbc898..ac2ebf06e 100644 --- a/apps/local/src/lib/performance.test.ts +++ b/apps/local/src/lib/performance.test.ts @@ -34,7 +34,7 @@ test("query analysis groups embedded and standalone executions without counting "queries", ); expect(group).toMatchObject({ count: 1, averageMs: 100, totalMs: 100 }); - expect(group?.executions[0]).toMatchObject({ + expect(group.executions[0]).toMatchObject({ traceId: "t", span: { id: "q1" }, }); @@ -50,11 +50,11 @@ test("Web Vitals retain per-visit units and classify thresholds instead of inven }, }), ]); - expect(page?.visits[0]?.metrics).toMatchObject({ + expect(page.visits[0]?.metrics).toMatchObject({ lcp: { value: 3200, rating: "Needs improvement" }, cls: { value: 0.05, rating: "Good" }, }); - expect(page?.visits[0]?.metrics.inp).toBeUndefined(); + expect(page.visits[0]?.metrics.inp).toBeUndefined(); }); test("profiles build root-first trees, inclusive/self samples, and interval times per thread", () => { const payload = { diff --git a/apps/local/src/lib/profile-spans.ts b/apps/local/src/lib/profile-spans.ts index 41e4617c7..377dc43ad 100644 --- a/apps/local/src/lib/profile-spans.ts +++ b/apps/local/src/lib/profile-spans.ts @@ -171,7 +171,7 @@ function traceIntervals(trace: TraceGroup): Interval[] { if (!heap.length) break; heap[0] = last; let index = 0; - while (true) { + for (;;) { const left = index * 2 + 1, right = left + 1; if (left >= heap.length) break; diff --git a/apps/local/src/lib/spotlight.test.ts b/apps/local/src/lib/spotlight.test.ts index d7175f4ec..fe5926392 100644 --- a/apps/local/src/lib/spotlight.test.ts +++ b/apps/local/src/lib/spotlight.test.ts @@ -61,9 +61,9 @@ describe("getStreamUrlFromHash", () => { ]), "log-http", ); - expect(log?.metadata?.method).toBeUndefined(); - expect(log?.metadata?.route).toBeUndefined(); - expect(log?.metadata?.statusCode).toBeUndefined(); + expect(log.metadata?.method).toBeUndefined(); + expect(log.metadata?.route).toBeUndefined(); + expect(log.metadata?.statusCode).toBeUndefined(); }); test("accepts a loopback stream URL", () => { @@ -191,7 +191,7 @@ describe("decodeEnvelope", () => { JSON.stringify([{}, [[{ type: "span" }, { version: 2, items: [span] }]]]), "streamed-http", ); - expect(item?.metadata).toMatchObject({ + expect(item.metadata).toMatchObject({ title: "GET /orders/42", method: "GET", route: "/orders/42", @@ -199,8 +199,8 @@ describe("decodeEnvelope", () => { operation: "http.server", durationMs: 1000, }); - expect(item?.timestamp).toBe(1700000001); - expect(item?.payload).toEqual(span); + expect(item.timestamp).toBe(1700000001); + expect(item.payload).toEqual(span); }); test("filters unsupported span containers and incomplete spans while preserving valid siblings and logs", () => { @@ -268,10 +268,10 @@ describe("decodeEnvelope", () => { JSON.stringify([{}, [[{ type: "event" }, sdkException]]]), "sdk-error", ); - expect(item?.metadata?.title).toBe( + expect(item.metadata?.title).toBe( "TypeError: Cannot read properties of undefined (reading 'total')", ); - expect(item?.payload).toEqual(sdkException); + expect(item.payload).toEqual(sdkException); }); test.each([ @@ -300,7 +300,7 @@ describe("decodeEnvelope", () => { ]), "partial-error", ); - expect(item?.metadata?.title).toBe(title); + expect(item.metadata?.title).toBe(title); }, ); @@ -321,7 +321,7 @@ describe("decodeEnvelope", () => { ]), "message-error", ); - expect(item?.metadata?.title).toBe("Checkout rejected"); + expect(item.metadata?.title).toBe("Checkout rejected"); }); test("unpacks batched span and log items from the Local receiver", () => { diff --git a/apps/local/src/lib/spotlight.ts b/apps/local/src/lib/spotlight.ts index 8e7bb1e82..fa04be567 100644 --- a/apps/local/src/lib/spotlight.ts +++ b/apps/local/src/lib/spotlight.ts @@ -44,6 +44,14 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } +// JSON.stringify returns undefined for undefined input, which its lib type omits. +export function stringifyJson( + value: unknown, + space?: number, +): string | undefined { + return JSON.stringify(value, null, space); +} + function getString(value: unknown): string | undefined { return typeof value === "string" && value ? value : undefined; } @@ -341,7 +349,7 @@ export function decodeEnvelope(data: string, eventId: string): LocalFeedItem[] { envelopeHeader: isRecord(parsed[0]) ? parsed[0] : {}, itemHeader: header, timestamp: getItemTimestamp(payload), - text: JSON.stringify(payload, null, 2) ?? String(payload), + text: stringifyJson(payload, 2) ?? String(payload), payload, metadata: { ...getEventMetadata(payload, type), diff --git a/apps/local/src/lib/trace-model.test.ts b/apps/local/src/lib/trace-model.test.ts index 660b96e22..0ab0348b1 100644 --- a/apps/local/src/lib/trace-model.test.ts +++ b/apps/local/src/lib/trace-model.test.ts @@ -65,9 +65,9 @@ describe("buildTraceGroups", () => { itemCount: 3, roots: [{ id: "root", children: [{ id: "query", durationMs: 125 }] }], }); - expect(trace?.spans).toHaveLength(2); - expect(trace?.roots).toHaveLength(1); - expect(trace?.roots[0]?.children).toHaveLength(1); + expect(trace.spans).toHaveLength(2); + expect(trace.roots).toHaveLength(1); + expect(trace.roots[0]?.children).toHaveLength(1); }, ); @@ -209,9 +209,9 @@ describe("buildTraceGroups", () => { durationMs: 70000000, itemCount: 1, }); - expect(trace?.spans).toHaveLength(70001); - expect(trace?.roots).toHaveLength(1); - expect(trace?.roots[0]?.children).toHaveLength(70000); + expect(trace.spans).toHaveLength(70001); + expect(trace.roots).toHaveLength(1); + expect(trace.roots[0]?.children).toHaveLength(70000); }); test("uses a transaction ID as the trace fallback used by routing", () => { @@ -438,7 +438,7 @@ test("child spans retain their own raw payload independently of the transaction" "payload", ), ); - expect(trace?.spans.find((span) => span.id === "query")?.payload).toEqual( + expect(trace.spans.find((span) => span.id === "query")?.payload).toEqual( payload, ); }); @@ -480,9 +480,9 @@ test.each([true, false])( ? [...transaction, ...segment] : [...segment, ...transaction], ); - expect(trace?.title).toBe("GET /orders"); - expect(trace?.spans).toHaveLength(1); - expect(trace?.spans[0]).toMatchObject({ + expect(trace.title).toBe("GET /orders"); + expect(trace.spans).toHaveLength(1); + expect(trace.spans[0]).toMatchObject({ description: "GET /orders", operation: "http.server", startTimestamp: 1, @@ -535,8 +535,8 @@ test("cycles keep every captured span reachable without changing declared parent pending.push(...span.children); } expect([...seen].sort()).toEqual(["a", "b", "child", "self"]); - expect(trace?.spans.find((span) => span.id === "a")?.parentId).toBe("b"); - expect(trace?.spans.find((span) => span.id === "b")?.parentId).toBe("a"); + expect(trace.spans.find((span) => span.id === "a")?.parentId).toBe("b"); + expect(trace.spans.find((span) => span.id === "b")?.parentId).toBe("a"); }); test.each([true, false])( @@ -707,7 +707,7 @@ test.each([true, false])( decode(payload, String(index)), ); const [completed] = buildTraceGroups(entries); - expect(completed?.spans[0]).toMatchObject({ + expect(completed.spans[0]).toMatchObject({ description: "Chat", endTimestamp: 3, metadata: { spanStatus: "internal_error" }, @@ -721,7 +721,7 @@ test.each([true, false])( ...entries, ...decode(partial, "partial"), ]); - expect(updated?.spans[0]).toMatchObject({ + expect(updated.spans[0]).toMatchObject({ description: "Chat", operation: "gen_ai.chat", endTimestamp: 4, diff --git a/apps/local/src/lib/trace-model.ts b/apps/local/src/lib/trace-model.ts index 77083f0ea..1225d4114 100644 --- a/apps/local/src/lib/trace-model.ts +++ b/apps/local/src/lib/trace-model.ts @@ -85,7 +85,7 @@ function getPayloadSpan( getTimestamp(event.end_timestamp) ?? getTimestamp(item.timestamp); const operation = getString(trace.op) ?? item.metadata?.operation; - const title = item.metadata?.title?.trim(); + const title = item.metadata?.title.trim(); const description = getString(event.transaction) ?? (title && title !== item.type ? title : undefined) ?? diff --git a/apps/local/src/test-setup.ts b/apps/local/src/test-setup.ts index f3c3d28b4..bb4493203 100644 --- a/apps/local/src/test-setup.ts +++ b/apps/local/src/test-setup.ts @@ -6,7 +6,8 @@ class TestResizeObserver implements ResizeObserver { unobserve(_target: Element) {} } -if (!globalThis.ResizeObserver) { +const environment: Partial = globalThis; +if (!environment.ResizeObserver) { Object.defineProperty(globalThis, "ResizeObserver", { configurable: true, value: TestResizeObserver, @@ -14,6 +15,7 @@ if (!globalThis.ResizeObserver) { }); } -if (typeof Element !== "undefined" && !Element.prototype.scrollIntoView) { - Element.prototype.scrollIntoView = () => {}; +if (typeof Element !== "undefined") { + const elementPrototype: Partial = Element.prototype; + elementPrototype.scrollIntoView ??= () => {}; }