diff --git a/web/src/components/rum/PlayerTracesTab.spec.ts b/web/src/components/rum/PlayerTracesTab.spec.ts index 83840b8729..212185a31c 100644 --- a/web/src/components/rum/PlayerTracesTab.spec.ts +++ b/web/src/components/rum/PlayerTracesTab.spec.ts @@ -108,15 +108,27 @@ function createTraceMetadata(overrides: Record = {}) { * the fetchTraceMetadata Promise (setTimeout creates a macrotask that * flushPromises() does not drain). */ -function setupSuccessfulMocks(rumHits?: any[], traceMetadataHits?: any[]) { +function setupSuccessfulMocks(rumHits?: any[], traceMetadataHits?: any[], operationHits?: any[]) { mockSearch.mockReset(); mockFetchQueryDataWithHttpStream.mockReset(); const hits = rumHits ?? [createRumHit()]; const metadata = traceMetadataHits ?? [createTraceMetadata()]; - mockSearch.mockImplementation((_params: any, source: string) => { + mockSearch.mockImplementation((params: any, source: string) => { if (source === "RUM") { + if (params.page_type === "traces") { + return Promise.resolve({ + data: { + hits: + operationHits ?? + metadata.map((hit) => ({ + trace_id: hit.trace_id, + operation_name: hit.first_event?.operation_name, + })), + }, + }); + } return Promise.resolve({ data: { hits } }); } return Promise.resolve({ data: { hits: [] } }); @@ -301,8 +313,8 @@ describe("PlayerTracesTab", () => { expect(wrapper.find('[data-test="rum-player-traces-tab-table"]').exists()).toBe(true); }); - it("should display the route path in the table row", () => { - expect(wrapper.text()).toContain("/products"); + it("should display the HTTP server operation in the table row", () => { + expect(wrapper.text()).toContain("GET /api/products"); }); it("gives each row its own stream and fetches metadata per distinct stream", async () => { @@ -316,17 +328,26 @@ describe("PlayerTracesTab", () => { // RUM hits for both ids; metadata mock answers per queried stream. mockSearch.mockReset(); mockFetchQueryDataWithHttpStream.mockReset(); - mockSearch.mockImplementation((_params: any, source: string) => + mockSearch.mockImplementation((params: any, source: string) => Promise.resolve( source === "RUM" - ? { - data: { - hits: [ - createRumHit({ _trace_id: P1, _view_url: "https://example.com/pay" }), - createRumHit({ _trace_id: C1, _view_url: "https://example.com/checkout" }), - ], - }, - } + ? params.page_type === "traces" + ? { + data: { + hits: [ + { trace_id: P1, operation_name: "POST /api/payments" }, + { trace_id: C1, operation_name: "POST /api/checkout" }, + ], + }, + } + : { + data: { + hits: [ + createRumHit({ _trace_id: P1, _view_url: "https://example.com/pay" }), + createRumHit({ _trace_id: C1, _view_url: "https://example.com/checkout" }), + ], + }, + } : { data: { hits: [] } }, ), ); @@ -350,12 +371,12 @@ describe("PlayerTracesTab", () => { expect(streamsQueried.sort()).toEqual(["checkout_traces", "payments_traces"]); // both rows render, each carrying its own stream context - expect(wrapper.text()).toContain("/pay"); - expect(wrapper.text()).toContain("/checkout"); + expect(wrapper.text()).toContain("POST /api/payments"); + expect(wrapper.text()).toContain("POST /api/checkout"); // clicking the checkout row opens the embedded view against ITS stream const rows = wrapper.findAll('[data-test^="table-row-"]'); - const checkoutRow = rows.find((r: any) => r.text().includes("/checkout")); + const checkoutRow = rows.find((r: any) => r.text().includes("POST /api/checkout")); expect(checkoutRow).toBeDefined(); await checkoutRow!.trigger("click"); await flushPromises(); @@ -443,7 +464,7 @@ describe("PlayerTracesTab", () => { // Row survives the metadata filter (previously dropped: raw 31-char key // never matched the 32-char metadata key) expect(wrapper.find('[data-test="rum-player-traces-tab-table"]').exists()).toBe(true); - expect(wrapper.text()).toContain("/products"); + expect(wrapper.text()).toContain("GET /api/products"); // The traces-stream metadata query was filtered on the padded id const queryReq = mockFetchQueryDataWithHttpStream.mock.calls[0][0].queryReq; @@ -597,11 +618,11 @@ describe("PlayerTracesTab", () => { expect(wrapper.find('[data-test="rum-player-traces-tab-table"]').exists()).toBe(true); }); - it("should show the selected trace route in detail header", async () => { + it("should show the selected HTTP server operation in detail header", async () => { await wrapper.find('[data-test="table-row-0"]').trigger("click"); await nextTick(); - expect(wrapper.text()).toContain("/products"); + expect(wrapper.text()).toContain("GET /api/products"); }); }); @@ -711,6 +732,80 @@ describe("PlayerTracesTab", () => { // ========================================================================= describe("trace metadata", () => { + it("replaces an internal trace summary with the HTTP server operation", async () => { + wrapper.unmount(); + setupSuccessfulMocks( + [createRumHit()], + [ + createTraceMetadata({ + first_event: { + service_name: "product-catalog", + operation_name: "middleware - query", + }, + }), + ], + [ + { + trace_id: "trace-abc123def456", + operation_name: "POST /api/products", + }, + ], + ); + + wrapper = mountComponent(); + await flushPromises(); + + expect(wrapper.text()).toContain("POST /api/products"); + expect(wrapper.text()).not.toContain("middleware - query"); + }); + + it("falls back to the trace summary when operation enrichment fails", async () => { + wrapper.unmount(); + setupSuccessfulMocks(); + mockSearch.mockImplementation((params: any) => { + if (params.page_type === "traces") { + return Promise.reject(new Error("Operation search failed")); + } + return Promise.resolve({ data: { hits: [createRumHit()] } }); + }); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + wrapper = mountComponent(); + await flushPromises(); + + expect(wrapper.text()).toContain("GET /api/products"); + expect(warnSpy).toHaveBeenCalledWith( + "HTTP server operation fetch failed:", + expect.any(Error), + ); + }); + + it("queries the earliest routed server operation for all displayed trace IDs", async () => { + wrapper.unmount(); + setupSuccessfulMocks( + [createRumHit({ _trace_id: "trace-1" }), createRumHit({ _trace_id: "trace-2" })], + [ + createTraceMetadata({ trace_id: "trace-1" }), + createTraceMetadata({ trace_id: "trace-2" }), + ], + ); + + wrapper = mountComponent(); + await flushPromises(); + + const operationSearch = mockSearch.mock.calls.find( + ([params]) => params.page_type === "traces", + ); + const sql = operationSearch?.[0].query.query.sql; + expect(sql).toContain("trace_id IN ('trace-1','trace-2')"); + expect(sql).toContain("first_value(operation_name ORDER BY _timestamp ASC)"); + expect(sql).toContain("span_kind='2'"); + expect(sql).toContain("http_route IS NOT NULL"); + expect( + mockSearch.mock.calls.filter(([params]) => params.page_type === "traces"), + ).toHaveLength(1); + }); + it("should display computed e2e duration in the table cell", async () => { wrapper.unmount(); @@ -824,6 +919,15 @@ describe("PlayerTracesTab", () => { it("should return the original string for an invalid URL", () => { expect((wrapper.vm as any).shortRoute("not-a-url")).toBe("not-a-url"); }); + + it("falls back to the browser route when operation metadata is unavailable", () => { + expect( + (wrapper.vm as any).traceDisplayName({ + route: "https://example.com/products", + metadata: { rootOperation: "unknown" }, + }), + ).toBe("/products"); + }); }); // ========================================================================= diff --git a/web/src/components/rum/PlayerTracesTab.vue b/web/src/components/rum/PlayerTracesTab.vue index c2d963fed0..ab47281d5d 100644 --- a/web/src/components/rum/PlayerTracesTab.vue +++ b/web/src/components/rum/PlayerTracesTab.vue @@ -72,7 +72,7 @@ along with this program. If not, see . {{ - shortRoute(selectedTrace.route) || selectedTrace.label + traceDisplayName(selectedTrace) }}
.