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();