/** @jest-environment jsdom */ import { sanitizeJourneyEvent } from '../../core/sanitize'; import { isMonolithLogOrigin, MAX_JOURNEY_CORRELATED_REQUESTS, } from '../../core/request-correlation'; import type { JourneyEvent, JourneyRequestEvent } from '../../core/types'; import { journeyLogsLink, requestLogsUrl } from '../../sinks/kibana'; const request: JourneyRequestEvent = { rayId: '0123456789abcdef-IAD', startedAtEpoch: Date.parse('2026-09-17T18:00:00.000Z'), durationMs: 2000, status: 500, }; function event(outcome: JourneyEvent['journey']['outcome'] = 'bad'): JourneyEvent { return { journey: { name: 'test', team: 'test', group: 'test', service: 'test', outcome, durationMs: 2000, steps: [ { name: 'save', startMs: 0, durationMs: 2000, outcome: 'bad', requests: [request] }, ], }, }; } describe('[journey] request logs link', () => { test('uses the verified production view and an absolute request window', () => { const url = new URL(requestLogsUrl(request)!); expect(url.origin).toBe('https://kibana.st.dev'); expect(url.pathname).toBe('/app/discover'); const params = new URLSearchParams(url.hash.slice(3)); expect(params.get('_g')).toBe( "(refreshInterval:(pause:!t,value:0),time:(from:'2026-09-17T17:59:00.000Z',to:'2026-09-17T18:01:02.000Z'))" ); expect(params.get('_a')).toBe( "(dataSource:(type:dataView,dataViewId:'894110ed-c22d-449c-9dc2-c4816f08712a'),query:(language:kuery,query:'Request.RequestId: 0123456789abcdef*'))" ); expect(url.href).not.toContain('IAD'); }); test.each([ { rayId: "0123456789abcdef') OR *" }, { rayId: 'https://attacker.example' }, { rayId: 'a'.repeat(1000) }, { rayId: '0123456789abcde' }, { rayId: '0123456789abcdef-INVALID' }, { startedAtEpoch: NaN }, { startedAtEpoch: Infinity }, { startedAtEpoch: -1 }, { startedAtEpoch: 8.64e15 }, { durationMs: -1 }, { durationMs: Infinity }, { status: 0 }, { status: 600 }, { status: 200.5 }, ])('rejects invalid correlation %j', invalid => { expect(requestLogsUrl({ ...request, ...invalid })).toBeUndefined(); const payload = event(); payload.journey.steps[0].requests = [{ ...request, ...invalid }]; expect(sanitizeJourneyEvent(payload).journey.steps[0].requests).toBeUndefined(); }); test('sanitizes correlation by allowlist, not the metric-tag string normalizer', () => { const payload = event(); payload.journey.steps[0].requests = [ { ...request, url: '/test?password=secret', headers: { authorization: 'secret' }, } as JourneyRequestEvent, ]; expect(sanitizeJourneyEvent(payload).journey.steps[0].requests).toEqual([ { ...request, rayId: request.rayId.toLowerCase() }, ]); }); test.each(['good', 'excluded'] as const)('keeps request links on %s events', outcome => { expect(sanitizeJourneyEvent(event(outcome)).journey.steps[0].requests).toEqual([ { ...request, rayId: request.rayId.toLowerCase() }, ]); }); test('caps the event payload and preserves links on the last failing step', () => { const payload = event(); payload.journey.steps = Array.from({ length: 50 }, (_, index) => ({ ...payload.journey.steps[0], name: `step-${index}`, })); const steps = sanitizeJourneyEvent(payload).journey.steps; expect(steps.reduce((count, step) => count + (step.requests?.length ?? 0), 0)).toBe( MAX_JOURNEY_CORRELATED_REQUESTS ); expect(steps[0].requestsTruncated).toBe(true); expect(steps[49].requests).toHaveLength(1); }); test('validates the top-level failure request and strips it from non-bad events', () => { const payload = event(); payload.journey.failedRequest = { ...request, rayId: "0123456789abcdef') OR *" }; expect(sanitizeJourneyEvent(payload).journey.failedRequest).toBeUndefined(); payload.journey.failedRequest = request; expect(sanitizeJourneyEvent(payload).journey.failedRequest).toEqual({ ...request, rayId: request.rayId.toLowerCase(), }); payload.journey.outcome = 'good'; expect(sanitizeJourneyEvent(payload).journey.failedRequest).toBeUndefined(); }); }); describe('[journey] visible Kibana link', () => { const now = Date.parse('2026-09-17T18:00:02.000Z'); const user = { tenant: 'test_tenant', name: 'test-agent', email: 'agent@example.test' }; const page = 'https://go.servicetitan.com/'; beforeEach(() => { jest.spyOn(Date, 'now').mockReturnValue(now); }); afterEach(() => jest.restoreAllMocks()); function queryState(url: string): string { return new URLSearchParams(new URL(url).hash.slice(3)).get('_a')!; } test.each(['step-error', 'request-error', 'step-timeout', 'journey-timeout', 'custom-failure'])( 'links %s without an HTTP response to scoped related logs', reason => { const journey = { ...event().journey, reason, steps: [] }; const link = journeyLogsLink(journey, () => user, page)!; expect(link.scope).toBe('related_logs'); expect(queryState(link.url)).toContain('Context.TenantName: "test_tenant"'); expect(queryState(link.url)).toContain('Request.User: "test-agent"'); expect(queryState(link.url)).toContain('Request.User: "agent@example.test"'); expect(queryState(link.url)).not.toContain('Request.RequestId'); const params = new URLSearchParams(new URL(link.url).hash.slice(3)); expect(params.get('_g')).toContain("from:'2026-09-17T17:59:00.000Z'"); expect(params.get('_g')).toContain("to:'2026-09-17T18:01:02.000Z'"); } ); test.each(['good', 'excluded'] as const)('includes a visible link for %s journeys', outcome => { const link = journeyLogsLink(event(outcome).journey, () => user, page)!; expect(link.scope).toBe('related_logs'); }); test('keeps the existing precise failure link ahead of the fallback', () => { const getUser = jest.fn(() => user); const journey = { ...event().journey, failedRequest: request }; expect(journeyLogsLink(journey, getUser, page)).toEqual({ url: requestLogsUrl(request), scope: 'request', }); expect(getUser).not.toHaveBeenCalled(); }); test('does not blame an earlier successful request for a later network error', () => { const journey = event().journey; journey.reason = 'request-error'; journey.steps[0].requests![0] = { ...request, status: 200 }; const link = journeyLogsLink(journey, () => user, page)!; expect(link.scope).toBe('related_logs'); expect(queryState(link.url)).not.toContain('Request.RequestId'); }); test('makes nested captured requests visible when host user context is unavailable', () => { const journey = event().journey; const other = { ...request, rayId: 'fedcba9876543210', status: 200 }; journey.steps[0].requests = [request, other, request]; const link = journeyLogsLink(journey)!; expect(link.scope).toBe('related_requests'); expect(queryState(link.url)).toContain( 'Request.RequestId: 0123456789abcdef* OR Request.RequestId: fedcba9876543210*' ); expect(queryState(link.url).match(/0123456789abcdef/g)).toHaveLength(1); }); test.each([ {}, { tenant: 'test_tenant' }, { name: 'test-agent' }, { tenant: 'test_tenant OR *', name: 'test-agent' }, { tenant: 'test_tenant', name: 'agent\nOR *' }, { tenant: 'test_tenant', name: 'x'.repeat(255) }, ])('does not create a broad cross-tenant search for %j', invalid => { const journey = { ...event().journey, steps: [] }; expect(journeyLogsLink(journey, () => invalid, page)).toBeUndefined(); }); test.each([-1, Infinity, NaN, 86_400_001])('rejects invalid duration %s', durationMs => { const journey = { ...event().journey, durationMs, steps: [] }; expect(journeyLogsLink(journey, () => user, page)).toBeUndefined(); }); test('deduplicates identical user and email values', () => { const link = journeyLogsLink(event().journey, () => ({ ...user, email: user.name }), page)!; expect(queryState(link.url).match(/Request.User/g)).toHaveLength(1); }); test('escapes quotes and exclamation marks at both KQL and Rison layers', () => { const name = `O'Neil!" OR *`; const link = journeyLogsLink( event().journey, () => ({ ...user, name, email: undefined }), page )!; expect(queryState(link.url)).toContain(`Request.User: "O!'Neil!!\\" OR *"`); expect(queryState(link.url)).toContain('Context.TenantName: "test_tenant" AND ('); }); test('request caps still apply on good journeys', () => { const payload = event('good'); payload.journey.steps = Array.from({ length: 50 }, (_, index) => ({ ...payload.journey.steps[0], name: `step-${index}`, })); const safe = sanitizeJourneyEvent(payload); expect( safe.journey.steps.reduce((count, step) => count + (step.requests?.length ?? 0), 0) ).toBe(MAX_JOURNEY_CORRELATED_REQUESTS); expect(safe.journey.outcome).toBe('good'); expect(safe.journey.steps[0].requestsTruncated).toBe(true); }); test('does not turn non-production user context into a production log link', () => { const journey = { ...event().journey, steps: [] }; expect( journeyLogsLink(journey, () => user, 'https://qa.servicetitan.com/') ).toBeUndefined(); }); test('a throwing getUser cannot prevent captured request links', () => { expect( journeyLogsLink( event().journey, () => { throw new Error('unavailable'); }, page )?.scope ).toBe('related_requests'); }); }); describe('[journey] verified monolith origins', () => { test.each([ 'https://go.servicetitan.com/path', 'https://testtenant.eh.go.servicetitan.com/path', 'https://test_tenant.eh.go.servicetitan.com/path', ])('accepts %s', url => expect(isMonolithLogOrigin(url)).toBe(true)); test.each([ 'https://go.servicetitan.com.attacker.test', 'https://test.eh.go.servicetitan.com.attacker.test', 'https://qa.servicetitan.com', 'https://foo.go.servicetitan.com', 'https://go.servicetitan.com:8443', 'https://user:secret@go.servicetitan.com', 'http://go.servicetitan.com', 'not-a-url', ])('rejects %s', url => expect(isMonolithLogOrigin(url)).toBe(false)); });