diff --git a/Directory.Packages.props b/Directory.Packages.props index 05320f0cd..0d5d29ab7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -42,7 +42,6 @@ - diff --git a/src/Elastic.Documentation.Site/Assets/custom-elements.ts b/src/Elastic.Documentation.Site/Assets/custom-elements.ts deleted file mode 100644 index e24d57208..000000000 --- a/src/Elastic.Documentation.Site/Assets/custom-elements.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './web-components/SearchOrAskAi/SearchOrAskAi' -import './web-components/VersionDropdown' diff --git a/src/Elastic.Documentation.Site/Assets/image-carousel.ts b/src/Elastic.Documentation.Site/Assets/image-carousel.ts index fe4e33ab0..e70424ea0 100644 --- a/src/Elastic.Documentation.Site/Assets/image-carousel.ts +++ b/src/Elastic.Documentation.Site/Assets/image-carousel.ts @@ -208,11 +208,6 @@ class ImageCarousel { this.prevButton.style.top = `${controlTop}px` this.nextButton.style.top = `${controlTop}px` - - // Debug logging (remove in production) - console.log( - `Carousel controls positioned: minHeight=${minHeight}px, controlTop=${controlTop}px` - ) } } } diff --git a/src/Elastic.Documentation.Site/Assets/main.ts b/src/Elastic.Documentation.Site/Assets/main.ts index f5079d744..38f9fcfd6 100644 --- a/src/Elastic.Documentation.Site/Assets/main.ts +++ b/src/Elastic.Documentation.Site/Assets/main.ts @@ -7,6 +7,7 @@ import { openDetailsWithAnchor } from './open-details-with-anchor' import { initNav } from './pages-nav' import { initSmoothScroll } from './smooth-scroll' import { initTabs } from './tabs' +import { initializeOtel } from './telemetry/instrumentation' import { initTocNav } from './toc-nav' import 'htmx-ext-head-support' import 'htmx-ext-preload' @@ -14,6 +15,25 @@ import * as katex from 'katex' import { $, $$ } from 'select-dom' import { UAParser } from 'ua-parser-js' +// Injected at build time from MinVer +const DOCS_BUILDER_VERSION = + process.env.DOCS_BUILDER_VERSION?.trim() ?? '0.0.0-dev' + +// Initialize OpenTelemetry FIRST, before any other code runs +// This must happen early so all subsequent code is instrumented +initializeOtel({ + serviceName: 'docs-frontend', + serviceVersion: DOCS_BUILDER_VERSION, + baseUrl: '/docs', + debug: false, +}) + +// Dynamically import web components after telemetry is initialized +// This ensures telemetry is available when the components execute +// Parcel will automatically code-split this into a separate chunk +import('./web-components/SearchOrAskAi/SearchOrAskAi') +import('./web-components/VersionDropdown') + const { getOS } = new UAParser() const isLazyLoadNavigationEnabled = $('meta[property="docs:feature:lazy-load-navigation"]')?.content === 'true' diff --git a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts index dd715904e..a1a3ebb06 100644 --- a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts +++ b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts @@ -1,6 +1,7 @@ import { UAParser } from 'ua-parser-js' -const { browser } = UAParser() +const parser = new UAParser() +const browser = parser.getBrowser() // This is a fix for anchors in details elements in non-Chrome browsers. export function openDetailsWithAnchor() { diff --git a/src/Elastic.Documentation.Site/Assets/telemetry/instrumentation.ts b/src/Elastic.Documentation.Site/Assets/telemetry/instrumentation.ts new file mode 100644 index 000000000..e7de8876a --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/telemetry/instrumentation.ts @@ -0,0 +1,306 @@ +/** + * OpenTelemetry configuration for frontend telemetry. + * Sends traces and logs to the backend OTLP proxy endpoint. + * + * This module should be imported once at application startup. + * All web components will automatically be instrumented once initialized. + * + * Inspired by: https://signoz.io/docs/frontend-monitoring/sending-logs-with-opentelemetry/ + */ +import { logs } from '@opentelemetry/api-logs' +import { ZoneContextManager } from '@opentelemetry/context-zone' +import { W3CTraceContextPropagator } from '@opentelemetry/core' +import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http' +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' +import { registerInstrumentations } from '@opentelemetry/instrumentation' +import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch' +import { resourceFromAttributes } from '@opentelemetry/resources' +import { + LoggerProvider, + BatchLogRecordProcessor, +} from '@opentelemetry/sdk-logs' +import { + WebTracerProvider, + BatchSpanProcessor, + SpanProcessor, + Span, +} from '@opentelemetry/sdk-trace-web' +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, +} from '@opentelemetry/semantic-conventions' + +let isInitialized = false +let traceProvider: WebTracerProvider | null = null +let loggerProvider: LoggerProvider | null = null + +export function initializeOtel(options: OtelConfigOptions = {}): boolean { + if (isAlreadyInitialized()) return false + + markAsInitialized() + + const config = resolveConfiguration(options) + logInitializationStart(config) + + try { + const resource = createSharedResource(config) + const commonHeaders = createCommonHeaders() + + initializeTracing(resource, config, commonHeaders) + initializeLogging(resource, config, commonHeaders) + + setupAutoFlush(config.debug) + logInitializationSuccess(config) + + return true + } catch (error) { + logInitializationError(error) + isInitialized = false + return false + } +} + +function isAlreadyInitialized(): boolean { + if (isInitialized) { + console.warn( + 'OpenTelemetry already initialized. Skipping re-initialization.' + ) + return true + } + return false +} + +function markAsInitialized(): void { + isInitialized = true +} + +function resolveConfiguration(options: OtelConfigOptions): ResolvedConfig { + return { + serviceName: options.serviceName ?? 'docs-frontend', + serviceVersion: options.serviceVersion ?? '1.0.0', + baseUrl: options.baseUrl ?? window.location.origin, + debug: options.debug ?? false, + } +} + +function logInitializationStart(config: ResolvedConfig): void { + if (config.debug) { + // eslint-disable-next-line no-console + console.log('[OTEL] Initializing OpenTelemetry with config:', config) + } +} + +function createSharedResource(config: ResolvedConfig) { + const resourceAttributes: Record = { + [ATTR_SERVICE_NAME]: config.serviceName, + [ATTR_SERVICE_VERSION]: config.serviceVersion, + } + return resourceFromAttributes(resourceAttributes) +} + +function createCommonHeaders(): Record { + return { + 'X-Docs-Session': 'active', + } +} + +function initializeTracing( + resource: ReturnType, + config: ResolvedConfig, + commonHeaders: Record +): void { + const traceExporter = new OTLPTraceExporter({ + url: `${config.baseUrl}/_api/v1/o/t`, + headers: { ...commonHeaders }, + }) + + const spanProcessor = new BatchSpanProcessor(traceExporter) + const euidProcessor = new EuidSpanProcessor() + + traceProvider = new WebTracerProvider({ + resource, + spanProcessors: [euidProcessor, spanProcessor], + }) + + traceProvider.register({ + contextManager: new ZoneContextManager(), + propagator: new W3CTraceContextPropagator(), + }) + + registerFetchInstrumentation() +} + +function registerFetchInstrumentation(): void { + registerInstrumentations({ + instrumentations: [ + new FetchInstrumentation({ + propagateTraceHeaderCorsUrls: [ + new RegExp(`${window.location.origin}/.*`), + ], + ignoreUrls: [ + /_api\/v1\/o\/.*/, + /_api\/v1\/?$/, + /__parcel_code_frame$/, + ], + applyCustomAttributesOnSpan: (span, request, result) => { + span.setAttribute('http.method', request.method || 'GET') + if (result instanceof Response) { + span.setAttribute('http.status_code', result.status) + } + }, + }), + ], + }) +} + +function initializeLogging( + resource: ReturnType, + config: ResolvedConfig, + commonHeaders: Record +): void { + const logExporter = new OTLPLogExporter({ + url: `${config.baseUrl}/_api/v1/o/l`, + headers: { ...commonHeaders }, + }) + + const logProcessor = new BatchLogRecordProcessor(logExporter) + + loggerProvider = new LoggerProvider({ + resource, + processors: [logProcessor], + }) + + logs.setGlobalLoggerProvider(loggerProvider) +} + +function setupAutoFlush(debug: boolean = false) { + let isFlushing = false + + const performFlush = async () => { + if (isFlushing || !isInitialized) { + return + } + + isFlushing = true + + if (debug) { + // eslint-disable-next-line no-console + console.log( + '[OTEL] Auto-flushing telemetry (visibilitychange or pagehide)' + ) + } + + try { + await flushTelemetry() + } catch (error) { + if (debug) { + console.warn('[OTEL] Error during auto-flush:', error) + } + } finally { + isFlushing = false + } + } + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + performFlush() + } + }) + + window.addEventListener('pagehide', performFlush) + + if (debug) { + // eslint-disable-next-line no-console + console.log('[OTEL] Auto-flush event listeners registered') + // eslint-disable-next-line no-console + console.log( + '[OTEL] Using OTLP HTTP exporters with keepalive for guaranteed delivery' + ) + } +} + +async function flushTelemetry(timeoutMs: number = 1000): Promise { + if (!isInitialized) { + return + } + + const flushPromises: Promise[] = [] + + if (traceProvider) { + flushPromises.push( + traceProvider.forceFlush().catch((err) => { + console.warn('[OTEL] Failed to flush traces:', err) + }) + ) + } + + if (loggerProvider) { + flushPromises.push( + loggerProvider.forceFlush().catch((err) => { + console.warn('[OTEL] Failed to flush logs:', err) + }) + ) + } + + await Promise.race([ + Promise.all(flushPromises), + new Promise((resolve) => setTimeout(resolve, timeoutMs)), + ]) +} + +function logInitializationSuccess(config: ResolvedConfig): void { + if (config.debug) { + // eslint-disable-next-line no-console + console.log('[OTEL] OpenTelemetry initialized successfully', { + serviceName: config.serviceName, + serviceVersion: config.serviceVersion, + traceEndpoint: `${config.baseUrl}/_api/v1/o/t`, + logEndpoint: `${config.baseUrl}/_api/v1/o/l`, + autoFlushOnUnload: true, + }) + } +} + +function logInitializationError(error: unknown): void { + console.error('[OTEL] Failed to initialize OpenTelemetry:', error) +} + +function getCookie(name: string): string | null { + const value = `; ${document.cookie}` + const parts = value.split(`; ${name}=`) + if (parts.length === 2) return parts.pop()?.split(';').shift() || null + return null +} + +class EuidSpanProcessor implements SpanProcessor { + onStart(span: Span): void { + const euid = getCookie('euid') + if (euid) { + span.setAttribute('user.euid', euid) + } + } + + onEnd(): void {} + + shutdown(): Promise { + return Promise.resolve() + } + + forceFlush(): Promise { + return Promise.resolve() + } +} + +export interface OtelConfigOptions { + serviceName?: string + serviceVersion?: string + baseUrl?: string + debug?: boolean +} + +interface ResolvedConfig { + serviceName: string + serviceVersion: string + baseUrl: string + debug: boolean +} diff --git a/src/Elastic.Documentation.Site/Assets/telemetry/logging.ts b/src/Elastic.Documentation.Site/Assets/telemetry/logging.ts new file mode 100644 index 000000000..4d88f630a --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/telemetry/logging.ts @@ -0,0 +1,102 @@ +/** + * Logging utilities for frontend application. + * Provides structured logging functions that send logs to the backend via OTLP. + * + * Based on: https://signoz.io/docs/frontend-monitoring/sending-logs-with-opentelemetry/ + */ +import { logs, SeverityNumber, type AnyValueMap } from '@opentelemetry/api-logs' + +const logger = logs.getLogger('docs-frontend-logger') + +/** + * Log an informational message. + * + * @param body The log message + * @param attrs Additional attributes to attach to the log + * + * @example + * ```ts + * logInfo('User clicked search button', { + * 'user.action': 'search', + * 'search.query': query + * }) + * ``` + */ +export function logInfo(body: string, attrs: AnyValueMap = {}) { + logger.emit({ + body, + severityNumber: SeverityNumber.INFO, + severityText: 'INFO', + attributes: attrs, + }) +} + +/** + * Log a warning message. + * + * @param body The log message + * @param attrs Additional attributes to attach to the log + * + * @example + * ```ts + * logWarn('Search returned no results', { + * 'search.query': query, + * 'search.duration_ms': duration + * }) + * ``` + */ +export function logWarn(body: string, attrs: AnyValueMap = {}) { + logger.emit({ + body, + severityNumber: SeverityNumber.WARN, + severityText: 'WARN', + attributes: attrs, + }) +} + +/** + * Log an error message. + * + * @param body The log message + * @param attrs Additional attributes to attach to the log + * + * @example + * ```ts + * logError('Failed to fetch search results', { + * 'error.message': error.message, + * 'error.stack': error.stack, + * 'search.query': query + * }) + * ``` + */ +export function logError(body: string, attrs: AnyValueMap = {}) { + logger.emit({ + body, + severityNumber: SeverityNumber.ERROR, + severityText: 'ERROR', + attributes: attrs, + }) +} + +/** + * Log a debug message (only useful in development). + * + * @param body The log message + * @param attrs Additional attributes to attach to the log + * + * @example + * ```ts + * logDebug('Component rendered', { + * 'component.name': 'SearchResults', + * 'render.time_ms': renderTime + * }) + * ``` + */ +export function logDebug(body: string, attrs: AnyValueMap = {}) { + logger.emit({ + body, + severityNumber: SeverityNumber.DEBUG, + severityText: 'DEBUG', + attributes: attrs, + }) +} diff --git a/src/Elastic.Documentation.Site/Assets/telemetry/semconv.ts b/src/Elastic.Documentation.Site/Assets/telemetry/semconv.ts new file mode 100644 index 000000000..18e48edc9 --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/telemetry/semconv.ts @@ -0,0 +1,121 @@ +/** + * Semantic Conventions for Documentation Site Telemetry + * + * This file defines custom attribute names for search telemetry. + * Standard OpenTelemetry semconv attributes are imported from @opentelemetry/semantic-conventions. + * + * References: + * - https://opentelemetry.io/docs/specs/semconv/ + * - https://opentelemetry.io/docs/specs/semconv/attributes-registry/ + */ + +// Re-export standard OpenTelemetry semantic conventions +export { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, + ATTR_HTTP_RESPONSE_STATUS_CODE, + ATTR_ERROR_TYPE, +} from '@opentelemetry/semantic-conventions' + +// ============================================================================ +// SEARCH ATTRIBUTES (Custom) +// ============================================================================ + +/** + * The search query string entered by the user + * @example "elasticsearch aggregations" + */ +export const ATTR_SEARCH_QUERY = 'search.query' + +/** + * Length of the search query string + * @example 25 + */ +export const ATTR_SEARCH_QUERY_LENGTH = 'search.query.length' + +/** + * Current page number in search results (0-based) + * @example 0 + */ +export const ATTR_SEARCH_PAGE = 'search.page' + +/** + * Total number of search results found + * @example 142 + */ +export const ATTR_SEARCH_RESULTS_TOTAL = 'search.results.total' + +/** + * Number of results returned in current page + * @example 10 + */ +export const ATTR_SEARCH_RESULTS_COUNT = 'search.results.count' + +/** + * Total number of pages available + * @example 15 + */ +export const ATTR_SEARCH_PAGE_COUNT = 'search.page.count' + +/** + * Whether the search query was empty + * @example true + */ +export const ATTR_SEARCH_EMPTY_QUERY = 'search.empty_query' + +/** + * Whether the search resulted in an error + * @example false + */ +export const ATTR_SEARCH_ERROR = 'search.error' + +// ============================================================================ +// SEARCH RESULT CLICK ATTRIBUTES (Custom) +// ============================================================================ + +/** + * URL of the clicked search result + * @example "/docs/elasticsearch/reference/current/search-aggregations.html" + */ +export const ATTR_SEARCH_RESULT_URL = 'search.result.url' + +/** + * Title of the clicked search result + * @example "Aggregations" + */ +export const ATTR_SEARCH_RESULT_TITLE = 'search.result.title' + +/** + * Absolute position of the result across all pages (0-based) + * @example 23 + */ +export const ATTR_SEARCH_RESULT_POSITION = 'search.result.position' + +/** + * Position of the result within the current page (0-based) + * @example 3 + */ +export const ATTR_SEARCH_RESULT_POSITION_ON_PAGE = + 'search.result.position_on_page' + +/** + * Relevance score of the search result + * @example 0.85 + */ +export const ATTR_SEARCH_RESULT_SCORE = 'search.result.score' + +// ============================================================================ +// EVENT ATTRIBUTES (Custom) +// ============================================================================ + +/** + * Name of the event being tracked + * @example "search_result_clicked" + */ +export const ATTR_EVENT_NAME = 'event.name' + +/** + * Category of the event + * @example "ui" + */ +export const ATTR_EVENT_CATEGORY = 'event.category' diff --git a/src/Elastic.Documentation.Site/Assets/telemetry/tracing.ts b/src/Elastic.Documentation.Site/Assets/telemetry/tracing.ts new file mode 100644 index 000000000..ea89796f5 --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/telemetry/tracing.ts @@ -0,0 +1,40 @@ +/** + * React utilities for OpenTelemetry tracing in components. + */ +import { trace, context, SpanStatusCode, Span } from '@opentelemetry/api' + +export async function traceSpan( + spanName: string, + fn: (span: Span) => Promise, + attributes?: Record +): Promise { + const tracer = trace.getTracer('docs-frontend') + const span = tracer.startSpan(spanName, undefined, context.active()) + + if (attributes) { + span.setAttributes(attributes) + } + + try { + const result = await fn(span) + span.setStatus({ code: SpanStatusCode.OK }) + return result + } catch (error) { + // Check if this is an AbortError (user cancelled/typed more) + if (error instanceof Error && error.name === 'AbortError') { + // Cancellation is NOT an error - it's expected behavior + span.setAttribute('cancelled', true) + span.setStatus({ code: SpanStatusCode.OK }) + } else { + // Real error - mark as ERROR + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error instanceof Error ? error.message : String(error), + }) + span.recordException(error as Error) + } + throw error + } finally { + span.end() + } +} diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/Chat.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/Chat.tsx index 1c50ff1fc..0d742cade 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/Chat.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/Chat.tsx @@ -97,7 +97,6 @@ export const Chat = () => { // Handle abort function from StreamingAiMessage const handleAbortReady = (abort: () => void) => { - console.log('[Chat] Abort function ready, storing in ref') abortFunctionRef.current = abort } @@ -133,13 +132,8 @@ export const Chat = () => { ) const handleButtonClick = useCallback(() => { - console.log('[Chat] Button clicked', { - isStreaming, - hasAbortFunction: !!abortFunctionRef.current, - }) if (isStreaming && abortFunctionRef.current) { // Interrupt current query - console.log('[Chat] Calling abort function') abortFunctionRef.current() abortFunctionRef.current = null // Update message status from 'streaming' to 'complete' diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/StreamingAiMessage.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/StreamingAiMessage.tsx index c89e53ac7..069e1c4ba 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/StreamingAiMessage.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/StreamingAiMessage.tsx @@ -62,13 +62,6 @@ export const StreamingAiMessage = ({ } }, onError: (error: ApiError | Error | null) => { - console.error('[AI Provider] Error in StreamingAiMessage:', { - messageId: message.id, - errorMessage: error?.message, - errorStack: error?.stack, - errorName: error?.name, - fullError: error, - }) updateAiMessage( message.id, message.content || error?.message || 'Error occurred', @@ -80,16 +73,7 @@ export const StreamingAiMessage = ({ // Expose abort function to parent when this is the last message useEffect(() => { - console.log('[StreamingAiMessage] Effect triggered', { - isLast, - status: message.status, - hasAbort: !!abort, - hasCallback: !!onAbortReady, - }) if (isLast && message.status === 'streaming') { - console.log( - '[StreamingAiMessage] Calling onAbortReady with abort function' - ) onAbortReady?.(abort) } }, [isLast, message.status, abort, onAbortReady]) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useAskAi.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useAskAi.ts index d88e99b05..2005224a9 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useAskAi.ts +++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useAskAi.ts @@ -41,11 +41,6 @@ export const useAskAi = (props: Props): UseAskAiResponse => { // Get AI provider from store (user-controlled via UI) const aiProvider = useAiProvider() - // Log which provider is being used for this conversation - useEffect(() => { - console.log(`[AI Provider] Using ${aiProvider} for this conversation`) - }, [aiProvider]) - // Prepare headers with AI provider const headers = useMemo( () => ({ @@ -186,7 +181,6 @@ export const useAskAi = (props: Props): UseAskAiResponse => { error, sendQuestion, abort: () => { - console.log('[useAskAi] Abort called') abort() clearQueue() }, diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useFetchEventSource.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useFetchEventSource.ts index 3b784c47f..6336840a2 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useFetchEventSource.ts +++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useFetchEventSource.ts @@ -51,11 +51,7 @@ export function useFetchEventSource({ const abortControllerRef = useRef(null) const abort = useCallback(() => { - console.log('[useFetchEventSource] Abort called', { - hasController: !!abortControllerRef.current, - }) if (abortControllerRef.current) { - console.log('[useFetchEventSource] Aborting controller') abortControllerRef.current.abort() abortControllerRef.current = null } diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useMessageThrottling.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useMessageThrottling.ts index 978ff48da..de5dfc45e 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useMessageThrottling.ts +++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useMessageThrottling.ts @@ -18,7 +18,7 @@ export function useMessageThrottling({ onMessage, }: UseMessageThrottlingOptions): UseMessageThrottlingReturn { const messageQueueRef = useRef([]) - const timerRef = useRef(null) + const timerRef = useRef | null>(null) const isProcessingRef = useRef(false) const processNextMessage = useCallback(() => { diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useStatusMinDisplay.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useStatusMinDisplay.ts index ae4f78fb8..236bb5b4d 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useStatusMinDisplay.ts +++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useStatusMinDisplay.ts @@ -36,7 +36,7 @@ export const useStatusMinDisplay = ( ) const lastChangeTimeRef = useRef(Date.now()) const pendingStatusRef = useRef(null) - const timeoutRef = useRef(null) + const timeoutRef = useRef | null>(null) useEffect(() => { // Clear any pending timeout diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResults.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResults.tsx index a53c27c5d..7821029b7 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResults.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResults.tsx @@ -51,6 +51,8 @@ export const SearchResults = ({ item={result} key={result.url} index={index} + pageNumber={data.pageNumber} + pageSize={data.pageSize} onKeyDown={onKeyDown} setRef={setItemRef} /> diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResultsListItem.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResultsListItem.tsx index 28e07a43c..031d189c8 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResultsListItem.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResultsListItem.tsx @@ -1,4 +1,16 @@ /** @jsxImportSource @emotion/react */ +import { + ATTR_SEARCH_QUERY, + ATTR_SEARCH_RESULT_URL, + ATTR_SEARCH_RESULT_TITLE, + ATTR_SEARCH_RESULT_POSITION, + ATTR_SEARCH_RESULT_POSITION_ON_PAGE, + ATTR_SEARCH_RESULT_SCORE, + ATTR_SEARCH_PAGE, + ATTR_EVENT_NAME, + ATTR_EVENT_CATEGORY, +} from '../../../../telemetry/semconv' +import { useSearchTerm } from '../search.store' import { type SearchResultItem } from '../useSearchQuery' import { EuiText, @@ -8,12 +20,42 @@ import { EuiSpacer, } from '@elastic/eui' import { css } from '@emotion/react' +import { trace } from '@opentelemetry/api' import DOMPurify from 'dompurify' import { memo, useMemo } from 'react' +function trackSearchResultClick(params: { + query: string + resultUrl: string + resultTitle: string + absolutePosition: number + positionOnPage: number + pageNumber: number + score: number +}): void { + const tracer = trace.getTracer('docs-frontend') + const span = tracer.startSpan('click search_result') + + span.setAttribute(ATTR_SEARCH_QUERY, params.query) + span.setAttribute(ATTR_SEARCH_RESULT_URL, params.resultUrl) + span.setAttribute(ATTR_SEARCH_RESULT_TITLE, params.resultTitle) + span.setAttribute(ATTR_SEARCH_RESULT_POSITION, params.absolutePosition) + span.setAttribute( + ATTR_SEARCH_RESULT_POSITION_ON_PAGE, + params.positionOnPage + ) + span.setAttribute(ATTR_SEARCH_PAGE, params.pageNumber) + span.setAttribute(ATTR_SEARCH_RESULT_SCORE, params.score) + span.setAttribute(ATTR_EVENT_NAME, 'search_result_clicked') + span.setAttribute(ATTR_EVENT_CATEGORY, 'ui') + span.end() +} + interface SearchResultListItemProps { item: SearchResultItem index: number + pageNumber: number + pageSize: number onKeyDown?: (e: React.KeyboardEvent, index: number) => void setRef?: (element: HTMLAnchorElement | null, index: number) => void } @@ -21,11 +63,31 @@ interface SearchResultListItemProps { export function SearchResultListItem({ item: result, index, + pageNumber, + pageSize, onKeyDown, setRef, }: SearchResultListItemProps) { const { euiTheme } = useEuiTheme() const titleFontSize = useEuiFontSize('s') + const searchQuery = useSearchTerm() + + // Calculate absolute position across all pages + // pageNumber is 0-based, so multiply by pageSize and add the index + const absolutePosition = pageNumber * pageSize + index + + const handleClick = () => { + trackSearchResultClick({ + query: searchQuery, + resultUrl: result.url, + resultTitle: result.title, + absolutePosition, + positionOnPage: index, + pageNumber, + score: result.score, + }) + } + return (
  • setRef?.(el, index)} + onClick={handleClick} onKeyDown={(e) => { if (e.key === 'Enter') { + handleClick() + // Navigate to the result URL window.location.href = result.url } else { // Type mismatch: event is from anchor but handler expects HTMLLIElement diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts index ad302a11c..44ef05335 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts +++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts @@ -1,3 +1,11 @@ +import { + ATTR_SEARCH_QUERY, + ATTR_SEARCH_PAGE, + ATTR_SEARCH_RESULTS_TOTAL, + ATTR_SEARCH_RESULTS_COUNT, + ATTR_SEARCH_PAGE_COUNT, +} from '../../../telemetry/semconv' +import { traceSpan } from '../../../telemetry/tracing' import { createApiErrorFromResponse, shouldRetry } from '../errorHandling' import { ApiError } from '../errorHandling' import { usePageNumber, useSearchTerm } from './search.store' @@ -74,23 +82,50 @@ export const useSearchQuery = () => { { searchTerm: debouncedSearchTerm.toLowerCase(), pageNumber }, ], queryFn: async ({ signal }) => { + // Don't create span for empty searches if (!debouncedSearchTerm || debouncedSearchTerm.length < 1) { - return SearchResponse.parse({ results: [], totalResults: 0 }) + return SearchResponse.parse({ + results: [], + totalResults: 0, + }) } - const params = new URLSearchParams({ - q: debouncedSearchTerm, - page: pageNumber.toString(), - }) - const response = await fetch( - '/docs/_api/v1/search?' + params.toString(), - { signal } - ) - if (!response.ok) { - throw await createApiErrorFromResponse(response) - } - const data = await response.json() - return SearchResponse.parse(data) + return traceSpan('execute search', async (span) => { + // Track frontend search (even if backend response is cached by CloudFront) + span.setAttribute(ATTR_SEARCH_QUERY, debouncedSearchTerm) + span.setAttribute(ATTR_SEARCH_PAGE, pageNumber) + + const params = new URLSearchParams({ + q: debouncedSearchTerm, + page: pageNumber.toString(), + }) + + const response = await fetch( + '/docs/_api/v1/search?' + params.toString(), + { signal } + ) + if (!response.ok) { + throw await createApiErrorFromResponse(response) + } + const data = await response.json() + const searchResponse = SearchResponse.parse(data) + + // Add result metrics to span + span.setAttribute( + ATTR_SEARCH_RESULTS_TOTAL, + searchResponse.totalResults + ) + span.setAttribute( + ATTR_SEARCH_RESULTS_COUNT, + searchResponse.results.length + ) + span.setAttribute( + ATTR_SEARCH_PAGE_COUNT, + searchResponse.pageCount + ) + + return searchResponse + }) }, enabled: shouldEnable, refetchOnWindowFocus: false, diff --git a/src/Elastic.Documentation.Site/Elastic.Documentation.Site.csproj b/src/Elastic.Documentation.Site/Elastic.Documentation.Site.csproj index f7e56843d..1ce6c1365 100644 --- a/src/Elastic.Documentation.Site/Elastic.Documentation.Site.csproj +++ b/src/Elastic.Documentation.Site/Elastic.Documentation.Site.csproj @@ -40,9 +40,14 @@ - - + + + diff --git a/src/Elastic.Documentation.Site/FileProviders/Preloader.cs b/src/Elastic.Documentation.Site/FileProviders/Preloader.cs index 1364995a8..743218dbb 100644 --- a/src/Elastic.Documentation.Site/FileProviders/Preloader.cs +++ b/src/Elastic.Documentation.Site/FileProviders/Preloader.cs @@ -11,6 +11,9 @@ public static partial class FontPreloader { private static IReadOnlyCollection? FontUriCache; + // For development: clear cache when needed + public static void ClearCache() => FontUriCache = null; + public static async Task> GetFontUrisAsync(string? urlPrefix) => FontUriCache ??= await LoadFontUrisAsync(urlPrefix); private static async Task> LoadFontUrisAsync(string? urlPrefix) { diff --git a/src/Elastic.Documentation.Site/Layout/_Head.cshtml b/src/Elastic.Documentation.Site/Layout/_Head.cshtml index 99b793339..bdd5b402f 100644 --- a/src/Elastic.Documentation.Site/Layout/_Head.cshtml +++ b/src/Elastic.Documentation.Site/Layout/_Head.cshtml @@ -8,7 +8,6 @@ } - @if (Model.CanonicalBaseUrl is not null) { diff --git a/src/Elastic.Documentation.Site/eslint.config.mjs b/src/Elastic.Documentation.Site/eslint.config.mjs index 8824c6dad..4c2b82072 100644 --- a/src/Elastic.Documentation.Site/eslint.config.mjs +++ b/src/Elastic.Documentation.Site/eslint.config.mjs @@ -16,4 +16,22 @@ export default defineConfig([ extends: ['js/recommended'], }, tseslint.configs.recommended, + { + files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'], + rules: { + 'no-console': [ + 'error', + { + allow: ['warn', 'error'], + }, + ], + }, + }, + { + // Allow console.log in synthetics config (test configuration file) + files: ['synthetics/**/*.ts'], + rules: { + 'no-console': 'off', + }, + }, ]) diff --git a/src/Elastic.Documentation.Site/package-lock.json b/src/Elastic.Documentation.Site/package-lock.json index ad105ff7f..4558dcc9b 100644 --- a/src/Elastic.Documentation.Site/package-lock.json +++ b/src/Elastic.Documentation.Site/package-lock.json @@ -13,6 +13,19 @@ "@emotion/css": "11.13.5", "@emotion/react": "11.14.0", "@microsoft/fetch-event-source": "2.0.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.208.0", + "@opentelemetry/context-zone": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-fetch": "^0.208.0", + "@opentelemetry/otlp-exporter-base": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-logs": "^0.208.0", + "@opentelemetry/sdk-trace-web": "^2.2.0", + "@opentelemetry/semantic-conventions": "^1.38.0", "@r2wc/react-to-web-component": "2.1.0", "@tanstack/react-query": "^5.90.6", "@uidotdev/usehooks": "2.4.1", @@ -39,6 +52,7 @@ "@elastic/synthetics": "1.19.0", "@eslint/js": "9.39.0", "@parcel/reporter-bundle-analyzer": "2.16.0", + "@parcel/transformer-typescript-tsc": "^2.16.1", "@tailwindcss/postcss": "4.1.16", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.0", @@ -63,6 +77,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "text-diff": "1.0.1", + "typescript": "^5.9.3", "typescript-eslint": "8.46.3", "wait-on": "9.0.1" } @@ -4464,6 +4479,269 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-zone": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-zone/-/context-zone-2.2.0.tgz", + "integrity": "sha512-Wq0nUuRyVBmXIeISO1Sg9yTz+mUypCGjwGHSPR9iaY4f+n+F728+5hh85lko6fnm/oJAiKhmSmvvH/o8PhSUnw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-zone-peer-dep": "2.2.0", + "zone.js": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 || ^0.15.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/@opentelemetry/context-zone-peer-dep": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-zone-peer-dep/-/context-zone-peer-dep-2.2.0.tgz", + "integrity": "sha512-/jSqc9MDpI7abRYNoM77G7xrJL8RhvOoQzmWg4Exj642NN1+ZwsqW0EODgaR99/w06nS2IGgY7AJRt5eZY/6QQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0", + "zone.js": "^0.10.2 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 || ^0.15.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz", + "integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-exporter-base": "0.208.0", + "@opentelemetry/otlp-transformer": "0.208.0", + "@opentelemetry/sdk-logs": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.208.0.tgz", + "integrity": "sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-exporter-base": "0.208.0", + "@opentelemetry/otlp-transformer": "0.208.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz", + "integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fetch": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fetch/-/instrumentation-fetch-0.208.0.tgz", + "integrity": "sha512-zgStoUfNF1xH9bCq539k1aeieKxPiAvBo5gKipQ9fIt+eJsFvqGcSzrrDX+OYgpIPW/IVNgWBoOw6zVmKwgNwQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/instrumentation": "0.208.0", + "@opentelemetry/sdk-trace-web": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", + "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-transformer": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", + "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/sdk-metrics": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", + "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.2.0.tgz", + "integrity": "sha512-x/LHsDBO3kfqaFx5qSzBljJ5QHsRXrvS4MybBDy1k7Svidb8ZyIPudWVzj3s5LpPkYZIgi9e+7tdsNCnptoelw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", + "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@parcel/bundler-default": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.16.0.tgz", @@ -5704,181 +5982,810 @@ "semver": "^7.7.1" }, "engines": { - "node": ">= 16.0.0", - "parcel": "2.16.0" + "node": ">= 16.0.0", + "parcel": "2.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.0" + } + }, + "node_modules/@parcel/transformer-js/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-json": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.16.0.tgz", + "integrity": "sha512-qX6Zg+j7HezY+W2TNjJ+VPUsIviNdTuMn39W9M0YEd0WLKh0x7XD4oprVivvgD0Vbm04FUcTQEN1jAF3CAVeGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.0", + "json5": "^2.2.3" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "2.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-node": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-node/-/transformer-node-2.16.0.tgz", + "integrity": "sha512-Mavmjj6SfP0Lhu751G47EFtExZIJyD+V2C5PzdATTaT+cw0MzQgfLH8s4p0CI27MAuyFesm8WTA0lgUtcfzMSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.0" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "2.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-postcss": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.16.0.tgz", + "integrity": "sha512-h+Qnn49UE5RywpuXMHN8Iufjvc7MMqHQc0sPNvwoLBXJXJcb3ul7WEY+DGXs90KsUY1B6JAqKtz9+pzqXZMwIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.0", + "@parcel/plugin": "2.16.0", + "@parcel/rust": "2.16.0", + "@parcel/utils": "2.16.0", + "clone": "^2.1.2", + "nullthrows": "^1.1.1", + "postcss-value-parser": "^4.2.0", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "2.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-postcss/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-posthtml": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.16.0.tgz", + "integrity": "sha512-mvHQNzFO1xPq+/7McjxF7+Zb2zAgksNbSXKi8/OuMRiNO3eDD/r1jWRWKNQZHWUkSx/vS7JJ5Y1ACI5INLxWww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.0", + "@parcel/utils": "2.16.0" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "2.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-raw": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.16.0.tgz", + "integrity": "sha512-LJXwH2rQAo6mOU6uG0IGQIN7KLC2sS8bl6aqf1YMcKk6ZEvylQkP0hUvRYja2IRzPoxjpdcAP5WC4e/Z8S1Vzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.0" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "2.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-react-refresh-wrap": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.16.0.tgz", + "integrity": "sha512-s6O5oJ0pUtZey6unI0mz2WIOpAVLCn5+hlou4YH7FXOiMvSJ2PU2rakk+EZk6K/R+TStYM0hQKSwJkiiN0m7Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/error-overlay": "2.16.0", + "@parcel/plugin": "2.16.0", + "@parcel/utils": "2.16.0", + "react-refresh": "^0.16.0" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "2.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-svg": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.16.0.tgz", + "integrity": "sha512-c4KpIqqbsvsh/ZxLTo0d7/IEVa/jR/+LZ1kFzBWXKvMBzbvqo63J6s3VGk61gPFV9JkSW3UI5LAMbJn/HDXycw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.0", + "@parcel/plugin": "2.16.0", + "@parcel/rust": "2.16.0" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "2.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/transformer-typescript-tsc/-/transformer-typescript-tsc-2.16.1.tgz", + "integrity": "sha512-aItrrBNXzRcdI+YVQP50eKLe8/zlw8t1x70Fu1fK3GjJvN1/wsR+s957agqUPCESt+1CyyLAJsErKPJPiJMIGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.1", + "@parcel/source-map": "^2.1.1", + "@parcel/ts-utils": "2.16.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "typescript": ">=3.0.0" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/cache": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.16.1.tgz", + "integrity": "sha512-qDlHQQ7RDfSi5MBnuFGCfQYiQQomsA5aZLntO5MCRD62VnMf9qz/RrCqpGFGOooljMoUaeVl0Q8ARvorRJJi8w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@parcel/fs": "2.16.1", + "@parcel/logger": "2.16.1", + "@parcel/utils": "2.16.1", + "lmdb": "2.8.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.1" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/codeframe": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.16.1.tgz", + "integrity": "sha512-KLy9Fvf37SX6/wek2SUPw8A/W0kChcNXPUNeCIYWUFI4USAZ5KvesXS5RHUnrJTaR0XzD0Qia+MFJPgp6kuazQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/core": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.16.1.tgz", + "integrity": "sha512-tza8oKYaPopGBwroGJKv7BrTg1lxTycS7SANIizxMB9FxDsAkq4vPny5/KHpFBcW3UTCGBvvNAG1oaVzeWF5Pg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.1", + "@parcel/cache": "2.16.1", + "@parcel/diagnostic": "2.16.1", + "@parcel/events": "2.16.1", + "@parcel/feature-flags": "2.16.1", + "@parcel/fs": "2.16.1", + "@parcel/graph": "3.6.1", + "@parcel/logger": "2.16.1", + "@parcel/package-manager": "2.16.1", + "@parcel/plugin": "2.16.1", + "@parcel/profiler": "2.16.1", + "@parcel/rust": "2.16.1", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.16.1", + "@parcel/utils": "2.16.1", + "@parcel/workers": "2.16.1", + "base-x": "^3.0.11", + "browserslist": "^4.24.5", + "clone": "^2.1.2", + "dotenv": "^16.5.0", + "dotenv-expand": "^11.0.7", + "json5": "^2.2.3", + "msgpackr": "^1.11.2", + "nullthrows": "^1.1.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/diagnostic": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.16.1.tgz", + "integrity": "sha512-PJl7/QGsPboAMVFZId31iGMMY70AllZNOtYka9rTZRjTiBhZw4VrAG/RdqqKzjVuL6fZhurmfcwWzj+3gx8ccg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/events": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.16.1.tgz", + "integrity": "sha512-+U7Trb2W8fm8w/OjwQpWN/Tepiwim/YNXuyPrhikFnsrg6QDdDTD/8/km4ah8Bzr0u4hIrn1k32InwDMCF5sig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/feature-flags": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/feature-flags/-/feature-flags-2.16.1.tgz", + "integrity": "sha512-MY/z4gKZWk0MKvP+gpU42kiE9W4f9NM1fSCa1OcdqF7IUJDDM41CDJ9rbwSQrroDddIViaNzsLo7aSYVI/C7aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/fs": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.16.1.tgz", + "integrity": "sha512-/akyrCaurd8rfgXuT6tDAK6I1JfW56TFJmzfIwuFSPbRy3YVu4JKN1g2PShpOLPdnqfWZNCcsd+yuuMFVhA2HA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@parcel/feature-flags": "2.16.1", + "@parcel/rust": "2.16.1", + "@parcel/types-internal": "2.16.1", + "@parcel/utils": "2.16.1", + "@parcel/watcher": "^2.0.7", + "@parcel/workers": "2.16.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.1" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/graph": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.6.1.tgz", + "integrity": "sha512-82sjbjrSPK5BXH0tb65tQl/qvo/b2vsyA5F6z3SaQ/c3A5bmv5RxTvse1AgOb0St0lZ7ALaZibj1qZFBUyjdqw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@parcel/feature-flags": "2.16.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/logger": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.16.1.tgz", + "integrity": "sha512-w9Qpp5S79fqn6nh/VqVYG4kCbIeW45zdPvYJMFgE90zhBRLrOnqw06cRZQdKj24C7/kdqOFFbrJ3B5uTsYeS0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.1", + "@parcel/events": "2.16.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/markdown-ansi": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.16.1.tgz", + "integrity": "sha512-4Qww9KkGrVrY/JyD2NtrdUmyufKOqGg3t6hkE4UqQBPb+GZd+TQi6i1mjWvOE6r9AF53x5PAZZ13f/HfllU2qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/node-resolver-core": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.7.1.tgz", + "integrity": "sha512-xY+mzz1a5L22HvwkCHtt1fRZa8pD8znXLB8NLnqdu/xa7FGwWNgA2ukFPSlNGwwI5aw3jQylERP8Mr6/qLsefQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.1", + "@parcel/diagnostic": "2.16.1", + "@parcel/fs": "2.16.1", + "@parcel/rust": "2.16.1", + "@parcel/utils": "2.16.1", + "nullthrows": "^1.1.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/package-manager": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.16.1.tgz", + "integrity": "sha512-HDMT0+L7kMBG+YgkxaNv/1nobFRgygte9e0QuYiSVMngdbYvXw9Yy8tEDeWEAOKWs0rGtPXJD6k9gP8/Aa3VQw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@parcel/diagnostic": "2.16.1", + "@parcel/fs": "2.16.1", + "@parcel/logger": "2.16.1", + "@parcel/node-resolver-core": "3.7.1", + "@parcel/types": "2.16.1", + "@parcel/utils": "2.16.1", + "@parcel/workers": "2.16.1", + "@swc/core": "^1.11.24", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.1" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/plugin": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.16.1.tgz", + "integrity": "sha512-/5hdgMFjd4pRZelfzWVAEWEH51qCHGB6I3z4mV3i8Teh0zsOgoHJrn1t+sVYkhKPDOMs16XAkx2iCMvEcktDrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/types": "2.16.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/profiler": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.16.1.tgz", + "integrity": "sha512-9VKswpixK5CggxqoEoThiusnRbqU48QIWwmGQhaTV9iBYi9m/LhEYUoTa8K/KQ70yJknghMMNc1JfAvt2bfh5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.1", + "@parcel/events": "2.16.1", + "@parcel/types-internal": "2.16.1", + "chrome-trace-event": "^1.0.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/rust": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.16.1.tgz", + "integrity": "sha512-lQkf14MLKZSY/P8j1lrOgFvMCt95dO+VdXIIM2aHjbxnzYSIGgHIt2XDVtKULE+DexaYZbleA0tTnX8AABUIyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/rust-darwin-arm64": "2.16.1", + "@parcel/rust-darwin-x64": "2.16.1", + "@parcel/rust-linux-arm-gnueabihf": "2.16.1", + "@parcel/rust-linux-arm64-gnu": "2.16.1", + "@parcel/rust-linux-arm64-musl": "2.16.1", + "@parcel/rust-linux-x64-gnu": "2.16.1", + "@parcel/rust-linux-x64-musl": "2.16.1", + "@parcel/rust-win32-x64-msvc": "2.16.1" + }, + "peerDependencies": { + "napi-wasm": "^1.1.2" + }, + "peerDependenciesMeta": { + "napi-wasm": { + "optional": true + } + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/rust-darwin-arm64": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/rust-darwin-arm64/-/rust-darwin-arm64-2.16.1.tgz", + "integrity": "sha512-6J1pnznHYzH1TOQbDZmbGa6bXNW+KXbD+XIihvQOid42DLGJNXRmwMmCU3en/759lF/pfmzmR7sm6wPKaKGfbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/rust-darwin-x64": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/rust-darwin-x64/-/rust-darwin-x64-2.16.1.tgz", + "integrity": "sha512-NDZpxleSeJ0yPx4OobDcj+z5x6RzsWmuA1RXBDuCKhf2kyXKP3+kfmrQew/7Q0r9uKA5pqCIw0W4eFqy4IoqIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/rust-linux-arm-gnueabihf": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm-gnueabihf/-/rust-linux-arm-gnueabihf-2.16.1.tgz", + "integrity": "sha512-xLLcbMP38ya8/z5esp3ypN2htxO9AsY4uQqF2rigIUZ2abQwL4MPKxfVZtrExWdcrcWiFUbiwn3+GKu/0M9Yow==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/rust-linux-arm64-gnu": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm64-gnu/-/rust-linux-arm64-gnu-2.16.1.tgz", + "integrity": "sha512-asZlimUq1wBmj2PDcoBSKD1SJvcLf1mXTcYGojOsA3dqkOOz7fGz7oubqZYn6IM+02cUDO4ekH+YBV6Eo7XlTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - }, - "peerDependencies": { - "@parcel/core": "^2.16.0" } }, - "node_modules/@parcel/transformer-js/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/rust-linux-arm64-musl": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm64-musl/-/rust-linux-arm64-musl-2.16.1.tgz", + "integrity": "sha512-japSgrHYDD+uNHQ8TEdEhpiWu0zWMVBE48W3HJ5FKkwUOY51whZa8w0lhYW88ykUDYtEEd1ipvflv0fSDFY1jw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/transformer-json": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.16.0.tgz", - "integrity": "sha512-qX6Zg+j7HezY+W2TNjJ+VPUsIviNdTuMn39W9M0YEd0WLKh0x7XD4oprVivvgD0Vbm04FUcTQEN1jAF3CAVeGw==", + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/rust-linux-x64-gnu": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-x64-gnu/-/rust-linux-x64-gnu-2.16.1.tgz", + "integrity": "sha512-A2LHDou7QDsKn3qlE+DHTBFqnjk0Hy1dhVEJgPgvW4N0XMa4x2JEcnLI9oFZ4KDXyMLGs0H6/smZ88zSdFoF3w==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@parcel/plugin": "2.16.0", - "json5": "^2.2.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 16.0.0", - "parcel": "2.16.0" + "node": ">= 10" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/transformer-node": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@parcel/transformer-node/-/transformer-node-2.16.0.tgz", - "integrity": "sha512-Mavmjj6SfP0Lhu751G47EFtExZIJyD+V2C5PzdATTaT+cw0MzQgfLH8s4p0CI27MAuyFesm8WTA0lgUtcfzMSw==", + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/rust-linux-x64-musl": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-x64-musl/-/rust-linux-x64-musl-2.16.1.tgz", + "integrity": "sha512-C+WgGbmIV1XxXUgNJdXpfZazqizYBvy7aesh8Z74QrlY99an/puQufd4kSbvwySN5iMGPSpN0VlyAUjDZLv9rQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@parcel/plugin": "2.16.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 16.0.0", - "parcel": "2.16.0" + "node": ">= 10" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/transformer-postcss": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.16.0.tgz", - "integrity": "sha512-h+Qnn49UE5RywpuXMHN8Iufjvc7MMqHQc0sPNvwoLBXJXJcb3ul7WEY+DGXs90KsUY1B6JAqKtz9+pzqXZMwIg==", + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/rust-win32-x64-msvc": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/rust-win32-x64-msvc/-/rust-win32-x64-msvc-2.16.1.tgz", + "integrity": "sha512-m8LoaBJfw5nv/4elM/jNNhWL5/HqBHNQnrbnN89e8sxn4L/zv9bPoXqHOuZglXwyB5velw1MGonX9Be/aK00ag==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@parcel/diagnostic": "2.16.0", - "@parcel/plugin": "2.16.0", - "@parcel/rust": "2.16.0", - "@parcel/utils": "2.16.0", - "clone": "^2.1.2", - "nullthrows": "^1.1.1", - "postcss-value-parser": "^4.2.0", - "semver": "^7.7.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 16.0.0", - "parcel": "2.16.0" + "node": ">= 10" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/transformer-postcss/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/types": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.16.1.tgz", + "integrity": "sha512-RFeomuzV/0Ze0jyzzx0u/eB4bXX6ISxrARA3k/3c7MQ+jaoY67+ELd8FwPV6ZmLqvvYIFdGiCZl6ascCABKwgg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "@parcel/types-internal": "2.16.1", + "@parcel/workers": "2.16.1" } }, - "node_modules/@parcel/transformer-posthtml": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.16.0.tgz", - "integrity": "sha512-mvHQNzFO1xPq+/7McjxF7+Zb2zAgksNbSXKi8/OuMRiNO3eDD/r1jWRWKNQZHWUkSx/vS7JJ5Y1ACI5INLxWww==", + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/types-internal": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/types-internal/-/types-internal-2.16.1.tgz", + "integrity": "sha512-HVCHm0uFyJMsu30bAfm/pd0RNsXRWX0mUXaDHzGJRZ2Yer53JA6elRwkgrPz1KosBA+OuNU/G8atXfCxPMXdKw==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.0", - "@parcel/utils": "2.16.0" - }, - "engines": { - "node": ">= 16.0.0", - "parcel": "2.16.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "@parcel/diagnostic": "2.16.1", + "@parcel/feature-flags": "2.16.1", + "@parcel/source-map": "^2.1.1", + "utility-types": "^3.11.0" } }, - "node_modules/@parcel/transformer-raw": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.16.0.tgz", - "integrity": "sha512-LJXwH2rQAo6mOU6uG0IGQIN7KLC2sS8bl6aqf1YMcKk6ZEvylQkP0hUvRYja2IRzPoxjpdcAP5WC4e/Z8S1Vzg==", + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/utils": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.16.1.tgz", + "integrity": "sha512-aoY6SCfAY7X6L39PFOsWNNcAobmJr4AJEgco+PJ2UAPFiHhkBZfUofXCwna5GHH5uqXZx6u3rAHiCUrM3bEDXg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.0" + "@parcel/codeframe": "2.16.1", + "@parcel/diagnostic": "2.16.1", + "@parcel/logger": "2.16.1", + "@parcel/markdown-ansi": "2.16.1", + "@parcel/rust": "2.16.1", + "@parcel/source-map": "^2.1.1", + "chalk": "^4.1.2", + "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 16.0.0", - "parcel": "2.16.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/transformer-react-refresh-wrap": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.16.0.tgz", - "integrity": "sha512-s6O5oJ0pUtZey6unI0mz2WIOpAVLCn5+hlou4YH7FXOiMvSJ2PU2rakk+EZk6K/R+TStYM0hQKSwJkiiN0m7Rg==", + "node_modules/@parcel/transformer-typescript-tsc/node_modules/@parcel/workers": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.16.1.tgz", + "integrity": "sha512-yEUAjBrSgo5MYAAQbncYbw1m9WrNiJs+xKdfdHNUrOHlT7G+v62HJAZJWJsvyGQBE2nchSO+bEPgv+kxAF8mIA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/error-overlay": "2.16.0", - "@parcel/plugin": "2.16.0", - "@parcel/utils": "2.16.0", - "react-refresh": "^0.16.0" + "@parcel/diagnostic": "2.16.1", + "@parcel/logger": "2.16.1", + "@parcel/profiler": "2.16.1", + "@parcel/types-internal": "2.16.1", + "@parcel/utils": "2.16.1", + "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 16.0.0", - "parcel": "2.16.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.1" } }, - "node_modules/@parcel/transformer-svg": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.16.0.tgz", - "integrity": "sha512-c4KpIqqbsvsh/ZxLTo0d7/IEVa/jR/+LZ1kFzBWXKvMBzbvqo63J6s3VGk61gPFV9JkSW3UI5LAMbJn/HDXycw==", + "node_modules/@parcel/transformer-typescript-tsc/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/ts-utils": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@parcel/ts-utils/-/ts-utils-2.16.1.tgz", + "integrity": "sha512-UuH60I/cGOy/b++Zx8h4qI2V8DXlmMyTYcUPi+x5JHT6L1VZBWohsz6qlP+Iek4BTMMs/g52Q57q++3eLD8Rdw==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.0", - "@parcel/plugin": "2.16.0", - "@parcel/rust": "2.16.0" + "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 16.0.0", - "parcel": "2.16.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "typescript": ">=3.0.0" } }, "node_modules/@parcel/types": { @@ -6296,6 +7203,70 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@r2wc/core": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@r2wc/core/-/core-1.3.0.tgz", @@ -7276,7 +8247,6 @@ "version": "24.1.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", - "dev": true, "dependencies": { "undici-types": "~7.8.0" } @@ -7970,7 +8940,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -7978,6 +8947,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -10489,6 +11467,24 @@ "node": ">=4" } }, + "node_modules/import-in-the-middle": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.0.tgz", + "integrity": "sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/import-in-the-middle/node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -12495,6 +13491,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -12757,6 +13759,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -13586,6 +14594,30 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -14217,6 +15249,19 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -15102,10 +16147,10 @@ "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==" }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "peer": true, + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15191,8 +16236,7 @@ "node_modules/undici-types": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==" }, "node_modules/unherit": { "version": "1.1.3", @@ -15938,6 +16982,12 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zone.js": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", + "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", + "license": "MIT" + }, "node_modules/zustand": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", diff --git a/src/Elastic.Documentation.Site/package.json b/src/Elastic.Documentation.Site/package.json index 543c4fa62..df0079a42 100644 --- a/src/Elastic.Documentation.Site/package.json +++ b/src/Elastic.Documentation.Site/package.json @@ -25,10 +25,6 @@ "synthetics:push:edge": "DOCS_ENV=edge npm run synthetics:push -- --tags=\"env:edge\"" }, "targets": { - "customElements": { - "distDir": "_static", - "source": "Assets/custom-elements.ts" - }, "js": { "distDir": "_static", "source": "Assets/main.ts" @@ -38,6 +34,9 @@ "source": "Assets/styles.css" } }, + "alias": { + "@opentelemetry/otlp-exporter-base/browser-http": "@opentelemetry/otlp-exporter-base/build/esm/index-browser-http.js" + }, "repository": { "type": "git", "url": "https://github.com/elastic/docs-builder.git" @@ -75,6 +74,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "text-diff": "1.0.1", + "typescript": "^5.9.3", "typescript-eslint": "8.46.3", "wait-on": "9.0.1" }, @@ -87,6 +87,19 @@ "@emotion/css": "11.13.5", "@emotion/react": "11.14.0", "@microsoft/fetch-event-source": "2.0.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.208.0", + "@opentelemetry/context-zone": "^2.2.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/instrumentation-fetch": "^0.208.0", + "@opentelemetry/otlp-exporter-base": "^0.208.0", + "@opentelemetry/resources": "^2.2.0", + "@opentelemetry/sdk-logs": "^0.208.0", + "@opentelemetry/sdk-trace-web": "^2.2.0", + "@opentelemetry/semantic-conventions": "^1.38.0", "@r2wc/react-to-web-component": "2.1.0", "@tanstack/react-query": "^5.90.6", "@uidotdev/usehooks": "2.4.1", diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs b/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs index 4a31ac2e5..43555d174 100644 --- a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs +++ b/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs @@ -18,7 +18,7 @@ public class AskAiUsecase( public async Task AskAi(AskAiRequest askAiRequest, Cancel ctx) { logger.LogInformation("Starting AskAI chat with {AgentProvider} and {AgentId}", streamTransformer.AgentProvider, streamTransformer.AgentId); - var activity = AskAiActivitySource.StartActivity($"chat ${streamTransformer.AgentProvider}", ActivityKind.Client); + var activity = AskAiActivitySource.StartActivity($"chat {streamTransformer.AgentProvider}", ActivityKind.Client); _ = activity?.SetTag("gen_ai.operation.name", "chat"); _ = activity?.SetTag("gen_ai.provider.name", streamTransformer.AgentProvider); // agent-builder or llm-gateway _ = activity?.SetTag("gen_ai.agent.id", streamTransformer.AgentId); // docs-agent or docs_assistant diff --git a/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs b/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs index 20d6ed6f9..4c696c820 100644 --- a/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs +++ b/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Diagnostics; using Microsoft.Extensions.Logging; namespace Elastic.Documentation.Api.Core.Search; diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/IOtlpGateway.cs b/src/api/Elastic.Documentation.Api.Core/Telemetry/IOtlpGateway.cs index ae2518af2..7cb0a67d8 100644 --- a/src/api/Elastic.Documentation.Api.Core/Telemetry/IOtlpGateway.cs +++ b/src/api/Elastic.Documentation.Api.Core/Telemetry/IOtlpGateway.cs @@ -16,8 +16,8 @@ public interface IOtlpGateway /// The raw OTLP payload stream /// Content-Type of the payload /// Cancellation token - /// HTTP status code and response content - Task<(int StatusCode, string? Content)> ForwardOtlp( + /// Result containing HTTP status code and response content + Task ForwardOtlp( OtlpSignalType signalType, Stream requestBody, string contentType, diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpForwardResult.cs b/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpForwardResult.cs new file mode 100644 index 000000000..7a9b4a1d0 --- /dev/null +++ b/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpForwardResult.cs @@ -0,0 +1,26 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Documentation.Api.Core.Telemetry; + +/// +/// Result of forwarding OTLP telemetry to a collector. +/// +public record OtlpForwardResult +{ + /// + /// HTTP status code from the collector response. + /// + public required int StatusCode { get; init; } + + /// + /// Response content from the collector, if any. + /// + public string? Content { get; init; } + + /// + /// Whether the forward operation was successful (2xx status code). + /// + public bool IsSuccess => StatusCode is >= 200 and < 300; +} diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyOptions.cs b/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyOptions.cs index 4872cb9b8..a14f48a05 100644 --- a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyOptions.cs +++ b/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyOptions.cs @@ -22,27 +22,29 @@ namespace Elastic.Documentation.Api.Core.Telemetry; /// /// The proxy will return 503 if the collector is not available. /// -public class OtlpProxyOptions +public class OtlpProxyOptions(IConfiguration configuration) { /// /// OTLP endpoint URL for the local ADOT collector. /// Defaults to localhost:4318 when running in Lambda with ADOT layer. /// - public string Endpoint { get; } + public string Endpoint { get; } = ResolveEndpoint(configuration); - public OtlpProxyOptions(IConfiguration configuration) + private static string ResolveEndpoint(IConfiguration configuration) { - // Check for explicit configuration override first (for tests or custom deployments) - var configEndpoint = configuration["OtlpProxy:Endpoint"]; - if (!string.IsNullOrEmpty(configEndpoint)) - { - Endpoint = configEndpoint; - return; - } + const string configKey = "OtlpProxy:Endpoint"; + const string envVarKey = "OTEL_EXPORTER_OTLP_ENDPOINT"; + const string defaultEndpoint = "http://localhost:4318"; - // Default to localhost:4318 - this is where ADOT Lambda Layer collector runs - // If ADOT layer is not present, the proxy will fail gracefully and return 503 - Endpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") - ?? "http://localhost:4318"; + // Priority 1: Explicit configuration (for tests or custom deployments) + if (!string.IsNullOrEmpty(configuration[configKey])) + return configuration[configKey]!; + + // Priority 2: Environment variable (ADOT Lambda Layer standard) + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envVarKey))) + return Environment.GetEnvironmentVariable(envVarKey)!; + + // Priority 3: Default (ADOT Lambda Layer collector) + return defaultEndpoint; } } diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyUsecase.cs b/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyUsecase.cs index a0eb1fb9f..9c051a579 100644 --- a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyUsecase.cs +++ b/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyUsecase.cs @@ -21,14 +21,14 @@ public class OtlpProxyUsecase(IOtlpGateway gateway) /// The raw OTLP payload (JSON or protobuf) /// Content-Type header from the original request /// Cancellation token - /// HTTP status code and response content - public async Task<(int StatusCode, string? Content)> ProxyOtlp( + /// Result containing HTTP status code and response content + public async Task ProxyOtlp( OtlpSignalType signalType, Stream requestBody, string contentType, Cancel ctx = default) { - using var activity = ActivitySource.StartActivity("ProxyOtlp", ActivityKind.Client); + using var activity = ActivitySource.StartActivity("forward otlp", ActivityKind.Client); // Forward to gateway return await gateway.ForwardOtlp(signalType, requestBody, contentType, ctx); diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs index 6748a40b1..cdf53b3c9 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs @@ -37,7 +37,7 @@ public async Task AskAi(AskAiRequest askAiRequest, Cancel ctx = default) var kibanaUrl = await parameterProvider.GetParam("docs-kibana-url", false, ctx); var kibanaApiKey = await parameterProvider.GetParam("docs-kibana-apikey", true, ctx); - using var request = new HttpRequestMessage(HttpMethod.Post, + var request = new HttpRequestMessage(HttpMethod.Post, $"{kibanaUrl}/api/agent_builder/converse/async") { Content = new StringContent(requestBody, Encoding.UTF8, "application/json") @@ -45,7 +45,7 @@ public async Task AskAi(AskAiRequest askAiRequest, Cancel ctx = default) request.Headers.Add("kbn-xsrf", "true"); request.Headers.Authorization = new AuthenticationHeaderValue("ApiKey", kibanaApiKey); - using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctx); + var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctx); // Ensure the response is successful before streaming if (!response.IsSuccessStatusCode) diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs index 64e3c72ca..f7d1cdf70 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs @@ -25,7 +25,7 @@ public async Task AskAi(AskAiRequest askAiRequest, Cancel ctx = default) { var llmGatewayRequest = LlmGatewayRequest.CreateFromRequest(askAiRequest); var requestBody = JsonSerializer.Serialize(llmGatewayRequest, LlmGatewayContext.Default.LlmGatewayRequest); - using var request = new HttpRequestMessage(HttpMethod.Post, options.FunctionUrl) + var request = new HttpRequestMessage(HttpMethod.Post, options.FunctionUrl) { Content = new StringContent(requestBody, Encoding.UTF8, "application/json") }; @@ -37,7 +37,7 @@ public async Task AskAi(AskAiRequest askAiRequest, Cancel ctx = default) // Use HttpCompletionOption.ResponseHeadersRead to get headers immediately // This allows us to start streaming as soon as headers are received - using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctx); + var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctx); // Ensure the response is successful before streaming if (!response.IsSuccessStatusCode) diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerBase.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerBase.cs index 5913b3df2..3079d7a77 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerBase.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerBase.cs @@ -89,8 +89,9 @@ private async Task ProcessPipeAsync(PipeReader reader, PipeWriter writer, string _ = parentActivity?.SetTag("error.type", ex.GetType().Name); try { + // Complete writer first, then reader - but don't try to complete reader + // if the exception came from reading (would cause "read operation pending" error) await writer.CompleteAsync(ex); - await reader.CompleteAsync(ex); } catch (Exception completeEx) { @@ -103,7 +104,6 @@ private async Task ProcessPipeAsync(PipeReader reader, PipeWriter writer, string try { await writer.CompleteAsync(); - await reader.CompleteAsync(); } catch (Exception ex) { diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Telemetry/AdotOtlpGateway.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Telemetry/AdotOtlpGateway.cs index 600af78e5..d2a573238 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Telemetry/AdotOtlpGateway.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Telemetry/AdotOtlpGateway.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Net.Sockets; using Elastic.Documentation.Api.Core.Telemetry; using Microsoft.Extensions.Logging; @@ -19,7 +20,7 @@ public class AdotOtlpGateway( private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); /// - public async Task<(int StatusCode, string? Content)> ForwardOtlp( + public async Task ForwardOtlp( OtlpSignalType signalType, Stream requestBody, string contentType, @@ -27,22 +28,13 @@ public class AdotOtlpGateway( { try { - // Build the target URL: http://localhost:4318/v1/{signalType} - // Use ToStringFast(true) from generated enum extensions (returns Display name: "traces", "logs", "metrics") var targetUrl = $"{options.Endpoint.TrimEnd('/')}/v1/{signalType.ToStringFast(true)}"; - logger.LogDebug("Forwarding OTLP {SignalType} to ADOT collector at {TargetUrl}", signalType, targetUrl); using var request = new HttpRequestMessage(HttpMethod.Post, targetUrl); - - // Forward the content with the original content type request.Content = new StreamContent(requestBody); _ = request.Content.Headers.TryAddWithoutValidation("Content-Type", contentType); - // No need to add authentication headers - ADOT layer handles auth to backend - // Just forward the telemetry to the local collector - - // Forward to ADOT collector using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, ctx); var responseContent = response.Content.Headers.ContentLength > 0 ? await response.Content.ReadAsStringAsync(ctx) @@ -58,17 +50,43 @@ public class AdotOtlpGateway( logger.LogDebug("Successfully forwarded OTLP {SignalType} to ADOT collector", signalType); } - return ((int)response.StatusCode, responseContent); - } - catch (HttpRequestException ex) when (ex.Message.Contains("Connection refused") || ex.InnerException?.Message?.Contains("Connection refused") == true) - { - logger.LogError(ex, "Failed to connect to ADOT collector at {Endpoint}. Is ADOT Lambda Layer enabled?", options.Endpoint); - return (503, "ADOT collector not available. Ensure AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument is set"); + return new OtlpForwardResult + { + StatusCode = (int)response.StatusCode, + Content = responseContent + }; } catch (Exception ex) { - logger.LogError(ex, "Error forwarding OTLP {SignalType}", signalType); - return (500, $"Error forwarding OTLP: {ex.Message}"); + var (statusCode, message) = MapExceptionToStatusCode(ex); + logger.LogError(ex, "Error forwarding OTLP {SignalType}: {ErrorMessage}", signalType, message); + return new OtlpForwardResult + { + StatusCode = statusCode, + Content = message + }; } } + + private static (int StatusCode, string Message) MapExceptionToStatusCode(Exception ex) => + ex switch + { + // Connection refused - downstream service not available + HttpRequestException { InnerException: SocketException { SocketErrorCode: SocketError.ConnectionRefused } } + => (503, "Telemetry collector unavailable"), + + // Timeout - gateway timeout + HttpRequestException { InnerException: SocketException { SocketErrorCode: SocketError.TimedOut } } + => (504, "Telemetry collector timeout"), + + TaskCanceledException or OperationCanceledException + => (504, "Request to telemetry collector timed out"), + + // Other HTTP/network errors - bad gateway + HttpRequestException + => (502, "Failed to communicate with telemetry collector"), + + // Unknown errors + _ => (500, $"Internal error: {ex.Message}") + }; } diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExtensions.cs b/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExtensions.cs index 1c605e4a4..e336f23b2 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExtensions.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExtensions.cs @@ -63,37 +63,27 @@ private static void MapOtlpProxyEndpoint(IEndpointRouteBuilder group) // Use /o/* to avoid adblocker detection (common blocklists target /otlp, /telemetry, etc.) var otlpGroup = group.MapGroup("/o"); - // Proxy endpoint for traces - // Frontend: POST /_api/v1/o/t → ADOT: POST localhost:4318/v1/traces - _ = otlpGroup.MapPost("/t", - async (HttpContext context, OtlpProxyUsecase proxyUsecase, Cancel ctx) => - { - var contentType = context.Request.ContentType ?? "application/json"; - var (statusCode, content) = await proxyUsecase.ProxyOtlp(OtlpSignalType.Traces, context.Request.Body, contentType, ctx); - return Results.Content(content ?? string.Empty, contentType, statusCode: statusCode); - }) - .DisableAntiforgery(); // Frontend requests won't have antiforgery tokens - - // Proxy endpoint for logs - // Frontend: POST /_api/v1/o/l → ADOT: POST localhost:4318/v1/logs - _ = otlpGroup.MapPost("/l", - async (HttpContext context, OtlpProxyUsecase proxyUsecase, Cancel ctx) => - { - var contentType = context.Request.ContentType ?? "application/json"; - var (statusCode, content) = await proxyUsecase.ProxyOtlp(OtlpSignalType.Logs, context.Request.Body, contentType, ctx); - return Results.Content(content ?? string.Empty, contentType, statusCode: statusCode); - }) - .DisableAntiforgery(); + MapOtlpSignalEndpoint(otlpGroup, "/t", OtlpSignalType.Traces); + MapOtlpSignalEndpoint(otlpGroup, "/l", OtlpSignalType.Logs); + MapOtlpSignalEndpoint(otlpGroup, "/m", OtlpSignalType.Metrics); + } - // Proxy endpoint for metrics - // Frontend: POST /_api/v1/o/m → ADOT: POST localhost:4318/v1/metrics - _ = otlpGroup.MapPost("/m", + private static void MapOtlpSignalEndpoint( + IEndpointRouteBuilder group, + string path, + OtlpSignalType signalType) => + group.MapPost(path, async (HttpContext context, OtlpProxyUsecase proxyUsecase, Cancel ctx) => { var contentType = context.Request.ContentType ?? "application/json"; - var (statusCode, content) = await proxyUsecase.ProxyOtlp(OtlpSignalType.Metrics, context.Request.Body, contentType, ctx); - return Results.Content(content ?? string.Empty, contentType, statusCode: statusCode); + var result = await proxyUsecase.ProxyOtlp( + signalType, + context.Request.Body, + contentType, + ctx); + return result.IsSuccess + ? Results.NoContent() + : Results.StatusCode(result.StatusCode); }) .DisableAntiforgery(); - } } diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs b/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs index 55ad64151..72860efb8 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs +++ b/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs @@ -39,6 +39,14 @@ public static TracerProviderBuilder AddDocsApiTracing(this TracerProviderBuilder .AddSource(TelemetryConstants.OtlpProxySourceName) .AddAspNetCoreInstrumentation(aspNetCoreOptions => { + // Don't trace root API endpoint (health check) + aspNetCoreOptions.Filter = (httpContext) => + { + var path = httpContext.Request.Path.Value ?? string.Empty; + // Exclude root API path: /docs/_api/v1 + return path != "/docs/_api/v1"; + }; + // Enrich spans with custom attributes from HTTP context aspNetCoreOptions.EnrichWithHttpRequest = (activity, httpRequest) => { diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj b/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj index d319a74c5..32e364eba 100644 --- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj +++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj @@ -14,7 +14,6 @@ - diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs index bcecdab1d..e60c8518c 100644 --- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs +++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs @@ -68,7 +68,7 @@ public async Task OtlpProxyTracesEndpointForwardsToCorrectUrl() throw new Exception($"Test failed with {response.StatusCode}: {errorBody}"); } - response.StatusCode.Should().Be(HttpStatusCode.OK); + response.StatusCode.Should().Be(HttpStatusCode.NoContent); capturedRequest.Should().NotBeNull(); capturedRequest!.RequestUri.Should().NotBeNull(); capturedRequest.RequestUri!.ToString().Should().Be("http://localhost:4318/v1/traces"); @@ -129,7 +129,7 @@ public async Task OtlpProxyLogsEndpointForwardsToCorrectUrl() using var response = await client.PostAsync("/docs/_api/v1/o/l", content, TestContext.Current.CancellationToken); // Assert - verify the enum ToStringFast() generates "logs" (lowercase) - response.StatusCode.Should().Be(HttpStatusCode.OK); + response.StatusCode.Should().Be(HttpStatusCode.NoContent); capturedRequest.Should().NotBeNull(); capturedRequest!.RequestUri!.ToString().Should().Be("http://localhost:4318/v1/logs"); @@ -182,7 +182,7 @@ public async Task OtlpProxyMetricsEndpointForwardsToCorrectUrl() using var response = await client.PostAsync("/docs/_api/v1/o/m", content, TestContext.Current.CancellationToken); // Assert - verify the enum ToStringFast() generates "metrics" (lowercase) - response.StatusCode.Should().Be(HttpStatusCode.OK); + response.StatusCode.Should().Be(HttpStatusCode.NoContent); capturedRequest.Should().NotBeNull(); capturedRequest!.RequestUri!.ToString().Should().Be("http://localhost:4318/v1/metrics"); @@ -221,8 +221,6 @@ public async Task OtlpProxyReturnsCollectorErrorStatusCode() // Assert - verify error responses are properly forwarded response.StatusCode.Should().Be(HttpStatusCode.ServiceUnavailable); - var responseBody = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); - responseBody.Should().Contain("Service unavailable"); // Cleanup mock response mockResponse.Dispose();