import { isMonolithLogOrigin, MAX_JOURNEY_CORRELATED_REQUESTS, sanitizeRequestCorrelation, } from '../core/request-correlation'; import type { JourneyEvent, JourneyRequestEvent } from '../core/types'; const DISCOVER_URL = 'https://kibana.st.dev/app/discover#/'; // Production monolith logs; deliberately not the user's default data view. const DATA_VIEW_ID = '894110ed-c22d-449c-9dc2-c4816f08712a'; const WINDOW_PADDING_MS = 60_000; /** Fixed destination and validated Ray ID only — no request URL, query string or user data. */ export function requestLogsUrl(value: JourneyRequestEvent): string | undefined { const request = sanitizeRequestCorrelation(value); if (!request) { return undefined; } const from = new Date(request.startedAtEpoch - WINDOW_PADDING_MS).toISOString(); const to = new Date( request.startedAtEpoch + request.durationMs + WINDOW_PADDING_MS ).toISOString(); // Origin/edge datacenter suffixes can differ. Match the hex prefix within the request window. const rayPrefix = request.rayId.slice(0, 16); return discoverUrl(`Request.RequestId: ${rayPrefix}*`, from, to); } interface JourneyLogLink { url: string; scope: 'request' | 'related_requests' | 'related_logs'; } function discoverUrl(query: string, from: string, to: string): string { const globalState = `(refreshInterval:(pause:!t,value:0),time:(from:'${from}',to:'${to}'))`; // The query is a quoted Rison string containing a separately escaped KQL expression. const quotedQuery = query.replace(/!/g, '!!').replace(/'/g, "!'"); const appState = `(dataSource:(type:dataView,dataViewId:'${DATA_VIEW_ID}'),query:(language:kuery,query:'${quotedQuery}'))`; return `${DISCOVER_URL}?_g=${encodeURIComponent(globalState)}&_a=${encodeURIComponent(appState)}`; } function userValue(value: unknown): string | undefined { return typeof value === 'string' && value.length > 0 && value.length <= 254 && // eslint-disable-next-line no-control-regex -- Reject controls in log query values. !/[\u0000-\u001f\u007f]/.test(value) ? value : undefined; } function kqlValue(value: string): string { return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; } function relatedLogsUrl( durationMs: number, user: unknown, pageUrl: string | undefined ): string | undefined { if ( !pageUrl || !isMonolithLogOrigin(pageUrl) || !user || typeof user !== 'object' || !Number.isFinite(durationMs) || durationMs < 0 || durationMs > 86_400_000 ) { return; } const context = user as Record; const tenant = userValue(context.tenant); const users = [...new Set([userValue(context.name), userValue(context.email)])].filter( (value): value is string => value !== undefined ); // Never fall back to all tenants or all users when identity is missing. if (!tenant || !/^[a-z0-9][a-z0-9_-]*$/i.test(tenant) || users.length === 0) { return; } const query = `Context.TenantName: ${kqlValue(tenant)} AND (${users .map(value => `Request.User: ${kqlValue(value)}`) .join(' OR ')})`; const now = Date.now(); return discoverUrl( query, new Date(now - durationMs - WINDOW_PADDING_MS).toISOString(), new Date(now + WINDOW_PADDING_MS).toISOString() ); } /** Keep one visible link without claiming unrelated requests caused a JS/business failure. */ export function journeyLogsLink( journey: JourneyEvent['journey'], getUser?: () => unknown, pageUrl?: string ): JourneyLogLink | undefined { if (journey.failedRequest) { const url = requestLogsUrl(journey.failedRequest); if (url) { return { url, scope: 'request' }; } } /* * With no identified failure request, search this user's logs during the journey. * RUM stubs/older hosts can lack getUser; enrichment must never prevent emission. */ try { const url = relatedLogsUrl(journey.durationMs, getUser?.(), pageUrl); if (url) { return { url, scope: 'related_logs' }; } } catch { // Retain request-only links if session context is unavailable or malformed. } const requests = journey.steps .flatMap(step => step.requests ?? []) .slice(0, MAX_JOURNEY_CORRELATED_REQUESTS) .map(sanitizeRequestCorrelation) .filter((request): request is JourneyRequestEvent => request !== undefined); if (!requests.length) { return; } const prefixes = [...new Set(requests.map(request => request.rayId.slice(0, 16)))]; const query = prefixes.map(prefix => `Request.RequestId: ${prefix}*`).join(' OR '); return { url: discoverUrl( query, new Date( Math.min(...requests.map(request => request.startedAtEpoch)) - WINDOW_PADDING_MS ).toISOString(), new Date( Math.max(...requests.map(request => request.startedAtEpoch + request.durationMs)) + WINDOW_PADDING_MS ).toISOString() ), scope: 'related_requests', }; }