/** * InfluxDB Version Detector Component * * Helps users identify which InfluxDB product they're using through a * guided questionnaire with URL detection and scoring-based recommendations. * * DECISION TREE LOGIC (from .context/drafts/influxdb-version-detector/influxdb-decision-tree.md): * * ## Primary Detection Flow * * START: User enters URL * | * ├─→ URL matches known cloud patterns? * │ │ * │ ├─→ YES: Contains "influxdb.io" → **InfluxDB Cloud Dedicated** ✓ * │ ├─→ YES: Contains "cloud2.influxdata.com" regions → **InfluxDB Cloud Serverless** ✓ * │ ├─→ YES: Contains "influxcloud.net" → **InfluxDB Cloud 1** ✓ * │ └─→ YES: Contains other cloud2 regions → **InfluxDB Cloud (TSM)** ✓ * │ * └─→ NO: Check port and try /ping endpoint * │ * ├─→ Port 8181 detected? → Strong indicator of v3 (Core/Enterprise) * | | Returns 200 (auth successful or disabled)? * | │ │--> `x-influxdb-build: Enterprise` -> **InfluxDB 3 Enterprise** ✓ (definitive) * | │ │--> `x-influxdb-build: Core` -> **InfluxDB 3 Core** ✓ (definitive) * │ │ * │ ├─→ Returns 401 Unauthorized (default - auth required)? * │ │ * │ └─→ Ask "Paid or Free?" * │ ├─→ Paid → **InfluxDB 3 Enterprise** ✓ (definitive) * │ └─→ Free → **InfluxDB 3 Core** ✓ (definitive) * | * ├─→ Port 8086 detected? → Strong indicator of legacy (OSS/Enterprise) * │ │ ⚠️ NOTE: v1.x ping auth optional (ping-auth-enabled), v2.x always open * │ │ * │ ├─→ Returns 401 Unauthorized? * │ │ │ Could be v1.x with ping-auth-enabled=true OR Enterprise * │ │ │ * │ │ └─→ Ask "Paid or Free?" → Show ranked results * │ │ * │ ├─→ Returns 200/204 (accessible)? * │ │ │ Likely v2.x OSS (always open) or v1.x with ping-auth-enabled=false * │ │ │ * │ │ └─→ Continue to questionnaire * │ * └─→ Blocked/Can't detect? * │ * └─→ Start questionnaire * * ## Questionnaire Flow (No URL or after detection) * * Q1: Which type of license do you have? * ├─→ Paid/Commercial License * ├─→ Free/Open Source (including free cloud tiers) * └─→ I'm not sure * * Q2: Is your InfluxDB hosted by InfluxData (cloud) or self-hosted? * ├─→ Cloud service (hosted by InfluxData) * ├─→ Self-hosted (on your own servers) * └─→ I'm not sure * * Q3: How long has your server been in place? * ├─→ Recently installed (less than 1 year) * ├─→ 1-5 years * ├─→ More than 5 years * └─→ I'm not sure * * Q4: Which query language(s) do you use? * ├─→ SQL * ├─→ InfluxQL * ├─→ Flux * ├─→ Multiple languages * └─→ I'm not sure * * ## Definitive Determinations (Stop immediately, no more questions) * * 1. **401 + Port 8181 + Paid** → InfluxDB 3 Enterprise ✓ * 2. **401 + Port 8181 + Free** → InfluxDB 3 Core ✓ * 3. **URL matches cloud pattern** → Specific cloud product ✓ * 4. **x-influxdb-build header** → Definitive product identification ✓ * * ## Scoring System (When not definitive) * * ### Elimination Rules * - **Free + Self-hosted** → Eliminates all cloud products * - **Free** → Eliminates: 3 Enterprise, Enterprise, Clustered, Cloud Dedicated, Cloud 1 * - **Paid + Self-hosted** → Eliminates all cloud products * - **Paid + Cloud** → Eliminates all self-hosted products * - **Free + Cloud** → Eliminates all self-hosted products, favors Serverless/TSM * * ### Strong Signals (High points) * - **401 Response**: +50 for v3 products, +30 for Clustered * - **Port 8181**: +30 for v3 products * - **Port 8086**: +20 for legacy products * - **SQL Language**: +40 for v3 products, eliminates v1/v2 * - **Flux Language**: +30 for v2 era, eliminates v1 and v3 * - **Server Age 5+ years**: +30 for v1 products, -50 for v3 * * ### Ranking Display Rules * - Only show "Most Likely" if: * - Top score > 30 (not low confidence) * - AND difference between #1 and #2 is ≥ 15 points * - Show manual verification commands only if: * - Confidence is not high (score < 60) * - AND it's a self-hosted product * - AND user didn't say it's cloud */ import { getInfluxDBUrls } from './services/local-storage.js'; interface QueryLanguageConfig { required_params: string[]; optional_params?: string[]; } interface ProductConfig { name?: string; query_languages: Record; characteristics: string[]; placeholder_host?: string; detection?: { url_contains?: string[]; ping_headers?: Record; }; } interface Products { [key: string]: ProductConfig; } interface Answers { context?: string | null; portClue?: string | null; isCloud?: boolean; isDocker?: boolean; paid?: string; hosted?: string; age?: string; language?: string; auth?: string; data?: string; version?: string; [key: string]: string | boolean | null | undefined; } interface ComponentOptions { component: HTMLElement; } interface AnalyticsEventData { detected_product?: string; detection_method?: string; interaction_type: string; section?: string; completion_status?: string; question_id?: string; answer_value?: string; } // Global gtag function type declaration declare global { interface Window { gtag?: ( _event: string, _action: string, _parameters?: Record ) => void; } } class InfluxDBVersionDetector { private container: HTMLElement; private products: Products; private influxdbUrls: Record; private answers: Answers = {}; private initialized: boolean = false; private questionFlow: string[] = []; private currentQuestionIndex = 0; private questionHistory: string[] = []; // Track question history for back navigation private progressBar: HTMLElement | null = null; private resultDiv: HTMLElement | null = null; private restartBtn: HTMLElement | null = null; private currentContext: 'questionnaire' | 'result' = 'questionnaire'; constructor(options: ComponentOptions) { this.container = options.component; // Parse data attributes from the component element const { products, influxdbUrls } = this.parseComponentData(); this.products = products; this.influxdbUrls = influxdbUrls; // Check if component is in a modal const modal = this.container.closest('.modal-content'); if (modal) { // If in modal, wait for modal to be opened before initializing this.initializeForModal(); } else { // If not in modal, initialize immediately this.init(); } } private parseComponentData(): { products: Products; influxdbUrls: Record; } { let products: Products = {}; let influxdbUrls: Record = {}; // Parse products data - Hugo always provides this data const productsData = this.container.getAttribute('data-products'); if (productsData) { try { products = JSON.parse(productsData); } catch (error) { console.warn('Failed to parse products data:', error); } } // Parse influxdb URLs data const influxdbUrlsData = this.container.getAttribute('data-influxdb-urls'); if (influxdbUrlsData && influxdbUrlsData !== '#ZgotmplZ') { try { influxdbUrls = JSON.parse(influxdbUrlsData); } catch (error) { console.warn('Failed to parse influxdb_urls data:', error); influxdbUrls = {}; // Fallback to empty object } } else { console.debug( 'InfluxDB URLs data not available or blocked by template security. ' + 'This is expected when Hugo data is unavailable.' ); influxdbUrls = {}; // Fallback to empty object } return { products, influxdbUrls }; } private init(): void { this.render(); this.setupPlaceholders(); this.attachEventListeners(); this.showQuestion('q-url-known'); this.initialized = true; // Track modal opening this.trackAnalyticsEvent({ interaction_type: 'modal_opened', section: this.getCurrentPageSection(), }); } private setupPlaceholders(): void { // This method is called at init but some placeholders need to be set // when questions are actually displayed since DOM elements don't exist yet } private setupPingHeadersPlaceholder(): void { const pingHeaders = this.container.querySelector('#ping-headers'); if (pingHeaders) { const exampleContent = [ '# Replace this with your actual response headers', '# Example formats:', '', '# InfluxDB 3 Core:', 'HTTP/1.1 200 OK', 'x-influxdb-build: core', 'x-influxdb-version: 3.1.0', '', '# InfluxDB 3 Enterprise:', 'HTTP/1.1 200 OK', 'x-influxdb-build: enterprise', 'x-influxdb-version: 3.1.0', '', '# InfluxDB v2 OSS:', 'HTTP/1.1 204 No Content', 'X-Influxdb-Build: OSS', 'X-Influxdb-Version: 2.7.8', '', '# InfluxDB v1:', 'HTTP/1.1 204 No Content', 'X-Influxdb-Version: 1.8.10', ].join('\n'); (pingHeaders as HTMLTextAreaElement).value = exampleContent; // Select all text when user clicks in the textarea so they can easily replace it pingHeaders.addEventListener('focus', () => { (pingHeaders as HTMLTextAreaElement).select(); }); } } private setupDockerOutputPlaceholder(): void { const dockerOutput = this.container.querySelector('#docker-output'); if (dockerOutput) { const exampleContent = [ '# Replace this with your actual command output', '# Example formats:', '', '# Version command output:', 'InfluxDB 3.1.0 (git: abc123def)', 'or', 'InfluxDB v2.7.8 (git: 407fa622e)', '', '# Ping headers from curl -I:', 'HTTP/1.1 200 OK', 'x-influxdb-build: core', 'x-influxdb-version: 3.1.0', '', '# Startup logs:', '2024-01-01T00:00:00.000Z info InfluxDB starting', '2024-01-01T00:00:00.000Z info InfluxDB 3.1.0 (git: abc123)', ].join('\n'); (dockerOutput as HTMLTextAreaElement).value = exampleContent; // Select all text when user clicks in the textarea so they can easily replace it dockerOutput.addEventListener('focus', () => { (dockerOutput as HTMLTextAreaElement).select(); }); } } private getCurrentPageSection(): string { // Extract meaningful section from current page const path = window.location.pathname; const pathSegments = path.split('/').filter((segment) => segment); // Try to get a meaningful section name if (pathSegments.length >= 3) { return pathSegments.slice(0, 3).join('/'); // e.g., "influxdb3/core/visualize-data" } else if (pathSegments.length >= 2) { return pathSegments.slice(0, 2).join('/'); // e.g., "influxdb3/core" } return path || 'unknown'; } private trackAnalyticsEvent(eventData: AnalyticsEventData): void { // Track Google Analytics events following the pattern from code-controls.js try { // Get current page context const currentUrl = new URL(window.location.href); const path = window.location.pathname; // Determine product context from current page let pageContext = 'other'; if (/\/influxdb\/cloud\//.test(path)) { pageContext = 'cloud'; } else if (/\/influxdb3\/core/.test(path)) { pageContext = 'core'; } else if (/\/influxdb3\/enterprise/.test(path)) { pageContext = 'enterprise'; } else if (/\/influxdb3\/cloud-serverless/.test(path)) { pageContext = 'serverless'; } else if (/\/influxdb3\/cloud-dedicated/.test(path)) { pageContext = 'dedicated'; } else if (/\/influxdb3\/clustered/.test(path)) { pageContext = 'clustered'; } else if (/\/(enterprise_|influxdb).*\/v[1-2]\//.test(path)) { pageContext = 'oss/enterprise'; } // Add tracking parameters to URL (following code-controls.js pattern) if (eventData.detected_product) { switch (eventData.detected_product) { case 'core': currentUrl.searchParams.set('dl', 'oss3'); break; case 'enterprise': currentUrl.searchParams.set('dl', 'enterprise'); break; case 'cloud': case 'cloud-v1': case 'cloud-v2-tsm': currentUrl.searchParams.set('dl', 'cloud'); break; case 'serverless': currentUrl.searchParams.set('dl', 'serverless'); break; case 'dedicated': currentUrl.searchParams.set('dl', 'dedicated'); break; case 'clustered': currentUrl.searchParams.set('dl', 'clustered'); break; case 'oss': case 'oss-v1': case 'oss-v2': currentUrl.searchParams.set('dl', 'oss'); break; } } // Add additional tracking parameters if (eventData.detection_method) { currentUrl.searchParams.set( 'detection_method', eventData.detection_method ); } if (eventData.completion_status) { currentUrl.searchParams.set('completion', eventData.completion_status); } if (eventData.section) { currentUrl.searchParams.set( 'section', encodeURIComponent(eventData.section) ); } // Update browser history without triggering page reload if (window.history && window.history.replaceState) { window.history.replaceState(null, '', currentUrl.toString()); } // Send custom Google Analytics event if gtag is available if (typeof window.gtag !== 'undefined') { window.gtag('event', 'influxdb_version_detector', { interaction_type: eventData.interaction_type, detected_product: eventData.detected_product, detection_method: eventData.detection_method, completion_status: eventData.completion_status, question_id: eventData.question_id, answer_value: eventData.answer_value, section: eventData.section, page_context: pageContext, custom_map: { dimension1: eventData.detected_product, dimension2: eventData.detection_method, dimension3: pageContext, }, }); } } catch (error) { // Silently handle analytics errors to avoid breaking functionality console.debug('Analytics tracking error:', error); } } private initializeForModal(): void { // Set up event listener to initialize when modal opens const modalContent = this.container.closest('.modal-content'); if (!modalContent) return; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if ( mutation.type === 'attributes' && mutation.attributeName === 'style' ) { const target = mutation.target as HTMLElement; const isVisible = target.style.display !== 'none' && target.style.display !== ''; if (isVisible && !this.initialized) { // Modal just opened and component not yet initialized this.init(); observer.disconnect(); } } }); }); // Start observing the modal content for style changes observer.observe(modalContent, { attributes: true, attributeFilter: ['style'], }); // Also check if modal is already visible const computedStyle = window.getComputedStyle(modalContent); if (computedStyle.display !== 'none' && !this.initialized) { this.init(); observer.disconnect(); } } private getBasicUrlSuggestion(): string { // Provide a basic placeholder URL suggestion based on common patterns return 'https://your-influxdb-host.com:8086'; } private getProductDisplayName(product: string): string { const displayNames: Record = { // Simplified product keys (used in detection results) 'oss-v1': 'InfluxDB OSS v1.x', 'oss-v2': 'InfluxDB OSS v2.x', oss: 'InfluxDB OSS (version unknown)', cloud: 'InfluxDB Cloud', 'cloud-v1': 'InfluxDB Cloud v1', 'cloud-v2-tsm': 'InfluxDB Cloud v2 (TSM)', serverless: 'InfluxDB Cloud Serverless', core: 'InfluxDB 3 Core', enterprise: 'InfluxDB 3 Enterprise', dedicated: 'InfluxDB Cloud Dedicated', clustered: 'InfluxDB Clustered', custom: 'Custom URL', // Raw product keys from products.yml (used in scoring) influxdb3_core: 'InfluxDB 3 Core', influxdb3_enterprise: 'InfluxDB 3 Enterprise', influxdb3_cloud_serverless: 'InfluxDB Cloud Serverless', influxdb3_cloud_dedicated: 'InfluxDB Cloud Dedicated', influxdb3_clustered: 'InfluxDB Clustered', influxdb_v1: 'InfluxDB OSS v1.x', influxdb_v2: 'InfluxDB OSS v2.x', enterprise_influxdb: 'InfluxDB Enterprise v1.x', influxdb: 'InfluxDB OSS v2.x', }; displayNames['core or enterprise'] = `${displayNames.core} or ${displayNames.enterprise}`; return displayNames[product] || product; } private generateConfigurationGuidance(productKey: string): string { // Map from result product names to products.yml keys const productMapping: Record = { core: 'influxdb3_core', enterprise: 'influxdb3_enterprise', serverless: 'influxdb3_cloud_serverless', dedicated: 'influxdb3_cloud_dedicated', clustered: 'influxdb3_clustered', 'oss-v1': 'influxdb_v1', 'oss-v2': 'influxdb_v2', }; const dataKey = productMapping[productKey]; if (!dataKey || !this.products[dataKey]) { return ''; } const productConfig = this.products[dataKey]; const productName = this.getProductDisplayName(productKey); if ( !productConfig.query_languages || Object.keys(productConfig.query_languages).length === 0 ) { return ''; } let html = `

Configuration Parameter Meanings for ${productName}

When configuring Grafana or other tools to connect to your ${productName} instance, these parameters mean:

`; // Add HOST explanation const hostExample = this.getHostExample(dataKey); html += `
HOST/URL: The network address where your ${productName} instance is running
For your setup, this would typically be: ${hostExample}
`; // Add database/bucket terminology explanation const usesDatabase = this.usesDatabaseTerminology(productConfig); if (usesDatabase) { html += `
DATABASE: The named collection where your data is stored
${productName} uses "database" terminology for organizing your time series data
`; } else { html += `
BUCKET: The named collection where your data is stored
${productName} uses "bucket" terminology for organizing your time series data
`; } // Add authentication explanation const authInfo = this.getAuthenticationInfo(productConfig); html += `
AUTHENTICATION: ${authInfo.description}
${authInfo.details}
`; // Add query language explanation const languages = Object.keys(productConfig.query_languages).join(', '); html += `
QUERY LANGUAGE: The syntax used to retrieve your data
${productName} supports: ${languages}
`; html += '
'; return html; } private getHostExample(productDataKey: string): string { // Extract placeholder_host from the products data if available const productData = this.products[productDataKey]; // Use placeholder_host from the product configuration if available if (productData?.placeholder_host) { // Add protocol if not present const host = productData.placeholder_host; if (host.startsWith('http://') || host.startsWith('https://')) { return host; } else { // Default to http for localhost, https for others return host.includes('localhost') ? `http://${host}` : `https://${host}`; } } // Fallback based on product type const hostExamples: Record = { influxdb3_core: 'http://localhost:8181', influxdb3_enterprise: 'http://localhost:8181', influxdb3_cloud_serverless: 'https://cloud2.influxdata.com', influxdb3_cloud_dedicated: 'https://cluster-id.a.influxdb.io', influxdb3_clustered: 'https://cluster-host.com', influxdb_v1: 'http://localhost:8086', influxdb_v2: 'http://localhost:8086', }; return hostExamples[productDataKey] || 'http://localhost:8086'; } private usesDatabaseTerminology(productConfig: ProductConfig): boolean { // Check if any query language uses 'Database' parameter for (const language of Object.values(productConfig.query_languages)) { if (language.required_params.includes('Database')) { return true; } } return false; } private getAuthenticationInfo(productConfig: ProductConfig): { description: string; details: string; } { // Check if any query language requires Token const requiresToken = Object.values(productConfig.query_languages).some( (lang) => lang.required_params.includes('Token') ); // Determine if this product uses "database" or "bucket" terminology const usesDatabaseTerm = this.usesDatabaseTerminology(productConfig); const resourceName = usesDatabaseTerm ? 'database' : 'bucket'; if (requiresToken) { return { description: 'Token-based authentication required', details: `You need a valid API token with appropriate permissions for your ${resourceName}`, }; } else { return { description: 'No authentication required by default', details: 'This instance typically runs without authentication, though it may be optionally configured', }; } } private detectEnterpriseFeatures(): { likelyProduct: string; confidence: number; } | null { // According to the decision tree, we cannot reliably distinguish // Core vs Enterprise from URL alone. The real differentiator is: // - Both Enterprise and Core: /ping requires auth by default (opt-out possible) // - Definitive identification requires x-influxdb-build header from 200 response // // Since this component cannot make HTTP requests to test /ping, // we return null to indicate we cannot distinguish them from URL alone. return null; } private analyzeUrlPatterns(url: string): { likelyProduct: string | null; confidence: number; suggestion?: string; } { if (!url || !this.influxdbUrls) { return { likelyProduct: null, confidence: 0 }; } const urlLower = url.toLowerCase(); // PRIORITY 1: Check for definitive cloud patterns first (per decision tree) // These should be checked before localhost patterns for accuracy // InfluxDB Cloud Dedicated: Contains "influxdb.io" if (urlLower.includes('influxdb.io')) { return { likelyProduct: 'dedicated', confidence: 1.0 }; } // InfluxDB Cloud Serverless: Contains "cloud2.influxdata.com" regions if (urlLower.includes('cloud2.influxdata.com')) { // Check for specific Serverless regions const serverlessRegions = [ 'us-east-1-1.aws.cloud2.influxdata.com', 'eu-central-1-1.aws.cloud2.influxdata.com', ]; for (const region of serverlessRegions) { if (urlLower.includes(region.toLowerCase())) { return { likelyProduct: 'serverless', confidence: 1.0 }; } } // Other cloud2 regions default to InfluxDB Cloud v2 (TSM) return { likelyProduct: 'cloud-v2-tsm', confidence: 0.9 }; } // InfluxDB Cloud v1 (legacy): Contains "influxcloud.net" if (urlLower.includes('influxcloud.net')) { return { likelyProduct: 'cloud-v1', confidence: 1.0 }; } // PRIORITY 2: Check for localhost/port-based patterns (OSS, Core, Enterprise) // Note: localhost URLs cannot be cloud versions - they're always self-hosted if (urlLower.includes('localhost') || urlLower.includes('127.0.0.1')) { // OSS default port if (urlLower.includes(':8086')) { return { likelyProduct: 'oss', confidence: 0.8, suggestion: 'version-check', }; } // Core/Enterprise default port - both use 8181 if (urlLower.includes(':8181')) { // Try to distinguish between Core and Enterprise const enterpriseResult = this.detectEnterpriseFeatures(); if (enterpriseResult) { return enterpriseResult; } // Can't distinguish from URL alone - suggest ping test return { likelyProduct: 'core or enterprise', confidence: 0.7, suggestion: 'ping-test', }; } } // Then check cloud products with provider regions // Skip this check if URL is localhost (cannot be cloud) const isLocalhost = urlLower.includes('localhost') || urlLower.includes('127.0.0.1'); if (!isLocalhost) { for (const [productKey, productData] of Object.entries( this.influxdbUrls )) { if (!productData || typeof productData !== 'object') continue; const providers = (productData as Record).providers; if (!Array.isArray(providers)) continue; for (const provider of providers) { if (!provider.regions) continue; for (const region of provider.regions) { if (region.url) { const patternUrl = region.url.toLowerCase(); // Exact match if (urlLower === patternUrl) { return { likelyProduct: productKey, confidence: 1.0 }; } // Domain match for cloud URLs if ( productKey === 'cloud' && urlLower.includes('cloud2.influxdata.com') ) { return { likelyProduct: 'cloud', confidence: 0.9 }; } } } } } } // Additional heuristics based on common patterns // Special handling for user inputs like "cloud 2", "cloud v2", etc. // Skip cloud heuristics for localhost URLs if (!isLocalhost) { if (urlLower.match(/cloud\s*[v]?2/)) { return { likelyProduct: 'cloud', confidence: 0.8 }; } if ( urlLower.includes('cloud') || urlLower.includes('aws') || urlLower.includes('azure') || urlLower.includes('gcp') ) { return { likelyProduct: 'cloud', confidence: 0.6 }; } } // Port-based suggestions for unknown/invalid URLs if (urlLower.includes(':8086')) { return { likelyProduct: 'oss-port', confidence: 0.4, suggestion: 'multiple-candidates-8086', }; } if (urlLower.includes(':8181')) { return { likelyProduct: 'v3-port', confidence: 0.4, suggestion: 'multiple-candidates-8181', }; } return { likelyProduct: null, confidence: 0 }; } private render(): void { this.container.innerHTML = `

InfluxDB product detector

Answer a few questions to identify which InfluxDB product you're using

Do you know the URL of your InfluxDB server?
Please enter your InfluxDB server URL:
For airgapped environments, run this command from a machine that can access your InfluxDB:
curl -I http://your-influxdb-url:8086/ping
Then paste the response headers here:
For Docker/Kubernetes environments, run these commands to identify your InfluxDB version:
First, find your container:
docker ps | grep influx
Then run one of these commands (replace <container> with your container name/ID):
# Get version info: docker exec <container> influxd version # Get ping headers: docker exec <container> curl -I localhost:8086/ping # Or check startup logs: docker logs <container> 2>&1 | head -20
Paste the output here:
Which type of InfluxDB license do you have?
Is your InfluxDB instance hosted by InfluxData (cloud) or self-hosted?
How long has your InfluxDB server been in place?
Which query language(s) do you use with InfluxDB?
`; // Cache DOM elements this.progressBar = this.container.querySelector('#progress-bar'); this.resultDiv = this.container.querySelector('#result'); this.restartBtn = this.container.querySelector('#restart-btn'); } private attachEventListeners(): void { this.container.addEventListener('click', (e) => { const target = e.target as HTMLElement; if ( target.classList.contains('option-button') || target.classList.contains('submit-button') || target.classList.contains('back-button') ) { const action = target.dataset.action; switch (action) { case 'url-known': this.trackAnalyticsEvent({ interaction_type: 'question_answered', question_id: 'url-known', answer_value: target.dataset.value || '', section: this.getCurrentPageSection(), }); this.handleUrlKnown(target.dataset.value); break; case 'go-back': this.trackAnalyticsEvent({ interaction_type: 'navigation', section: this.getCurrentPageSection(), }); this.goBack(); break; case 'detect-url': this.trackAnalyticsEvent({ interaction_type: 'url_detection_attempt', detection_method: 'url_analysis', section: this.getCurrentPageSection(), }); this.detectByUrl(); break; case 'analyze-headers': this.trackAnalyticsEvent({ interaction_type: 'manual_analysis', detection_method: 'ping_headers', section: this.getCurrentPageSection(), }); this.analyzePingHeaders(); break; case 'analyze-docker': this.trackAnalyticsEvent({ interaction_type: 'manual_analysis', detection_method: 'docker_output', section: this.getCurrentPageSection(), }); this.analyzeDockerOutput(); break; case 'answer': this.trackAnalyticsEvent({ interaction_type: 'question_answered', question_id: target.dataset.category || '', answer_value: target.dataset.value || '', section: this.getCurrentPageSection(), }); this.answerQuestion( target.dataset.category!, target.dataset.value! ); break; case 'auth-help-answer': this.trackAnalyticsEvent({ interaction_type: 'auth_help_response', question_id: target.dataset.category || '', answer_value: target.dataset.value || '', section: this.getCurrentPageSection(), }); this.handleAuthorizationHelp( target.dataset.category!, target.dataset.value! ); break; case 'restart': this.trackAnalyticsEvent({ interaction_type: 'restart', section: this.getCurrentPageSection(), }); this.restart(); break; case 'start-questionnaire': { this.trackAnalyticsEvent({ interaction_type: 'start_questionnaire', section: this.getCurrentPageSection(), }); // Hide result and restart button first if (this.resultDiv) { this.resultDiv.classList.remove('show'); } if (this.restartBtn) { this.restartBtn.style.display = 'none'; } // Start questionnaire with the detected context this.startQuestionnaire(target.dataset.context || null); // Focus on the component heading const heading = document.getElementById('detector-title'); if (heading) { heading.focus(); heading.scrollIntoView({ behavior: 'smooth', block: 'start' }); } break; } } } }); } private updateProgress(): void { const totalQuestions = this.questionFlow.length || 5; const progress = ((this.currentQuestionIndex + 1) / totalQuestions) * 100; if (this.progressBar) { this.progressBar.style.width = `${progress}%`; } } private showQuestion(questionId: string, addToHistory: boolean = true): void { const questions = this.container.querySelectorAll('.question'); questions.forEach((q) => q.classList.remove('active')); const activeQuestion = this.container.querySelector(`#${questionId}`); if (activeQuestion) { activeQuestion.classList.add('active'); // Add smart suggestions for URL input question if (questionId === 'q-url-input') { this.enhanceUrlInputWithSuggestions(); } } // Track question history for back navigation if (addToHistory) { this.questionHistory.push(questionId); } this.updateProgress(); } private enhanceUrlInputWithSuggestions(): void { const urlInputQuestion = this.container.querySelector('#q-url-input'); if (!urlInputQuestion) return; const urlInput = urlInputQuestion.querySelector( '#url-input' ) as HTMLInputElement; if (!urlInput) return; // Check for existing URL in localStorage const storedUrls = getInfluxDBUrls(); const currentProduct = this.getCurrentProduct(); const storedUrl = storedUrls[currentProduct] || storedUrls.custom; if (storedUrl && storedUrl !== 'http://localhost:8086') { urlInput.value = storedUrl; // Add indicator that URL was pre-filled (only if one doesn't already exist) const existingIndicator = urlInput.parentElement?.querySelector( '.url-prefilled-indicator' ); if (!existingIndicator) { const indicator = document.createElement('div'); indicator.className = 'url-prefilled-indicator'; indicator.textContent = 'Using previously saved URL'; urlInput.parentElement?.insertBefore(indicator, urlInput); // Hide indicator when user starts typing const originalValue = urlInput.value; urlInput.addEventListener('input', () => { if (urlInput.value !== originalValue) { indicator.style.display = 'none'; } }); } } else { // Set a basic placeholder suggestion const suggestedUrl = this.getBasicUrlSuggestion(); urlInput.placeholder = `for example, ${suggestedUrl}`; } } private getCurrentProduct(): string { // Try to determine current product context from page or default // This could be enhanced to detect from page context return 'core'; // Default to core for now } private handleUrlKnown(value: string | undefined): void { this.currentQuestionIndex++; if (value === 'true') { this.showQuestion('q-url-input'); } else if (value === 'airgapped') { this.showQuestion('q-ping-manual'); // Set up placeholder after question is shown setTimeout(() => this.setupPingHeadersPlaceholder(), 0); } else if (value === 'docker') { this.answers.isDocker = true; this.showQuestion('q-docker-manual'); // Set up placeholder after question is shown setTimeout(() => this.setupDockerOutputPlaceholder(), 0); } else { // Start the questionnaire this.answers = {}; this.questionFlow = ['q-paid', 'q-hosted', 'q-age', 'q-language']; this.currentQuestionIndex = 0; this.showQuestion('q-paid'); } } private goBack(): void { // Remove current question from history if (this.questionHistory.length > 0) { this.questionHistory.pop(); } // Go to previous question if available if (this.questionHistory.length > 0) { const previousQuestion = this.questionHistory[this.questionHistory.length - 1]; // Remove it from history before showing (showQuestion will re-add it) this.questionHistory.pop(); // Decrement question index if (this.currentQuestionIndex > 0) { this.currentQuestionIndex--; } // Show previous question this.showQuestion(previousQuestion); } else { // No history - go to first question this.currentQuestionIndex = 0; this.showQuestion('q-url-known'); } } private async detectByUrl(): Promise { const urlInput = ( this.container.querySelector('#url-input') as HTMLInputElement )?.value.trim(); if (!urlInput) { this.showResult('error', 'Please enter a valid URL'); return; } // Use improved URL pattern analysis const analysisResult = this.analyzeUrlPatterns(urlInput); // Store URL detection results for scoring system if (analysisResult.likelyProduct && analysisResult.likelyProduct !== null) { this.answers.detectedProduct = analysisResult.likelyProduct; this.answers.detectedConfidence = analysisResult.confidence.toString(); } if (analysisResult.likelyProduct && analysisResult.likelyProduct !== null) { if (analysisResult.suggestion === 'ping-test') { // Show ping test suggestion for Core/Enterprise detection this.showPingTestSuggestion(urlInput, analysisResult.likelyProduct); return; } else if (analysisResult.suggestion === 'version-check') { // Show OSS version check suggestion this.showOSSVersionCheckSuggestion(urlInput); return; } else if (analysisResult.suggestion === 'multiple-candidates-8086') { // Show multiple product suggestions for port 8086 this.showMultipleCandidatesSuggestion(urlInput, '8086'); return; } else if (analysisResult.suggestion === 'multiple-candidates-8181') { // Show multiple product suggestions for port 8181 this.showMultipleCandidatesSuggestion(urlInput, '8181'); return; } else { // Direct detection this.showDetectedVersion(analysisResult.likelyProduct); return; } } // URL not recognized - start questionnaire with context this.showResult('info', 'Analyzing your InfluxDB server...'); // Check if this is a cloud context (like "cloud 2") const contextResult = this.detectContext(urlInput); if (contextResult.likelyProduct === 'cloud') { // Start questionnaire with cloud context setTimeout(() => { this.startQuestionnaireWithCloudContext(); }, 2000); } else { // For other URLs, use the regular questionnaire setTimeout(() => { this.startQuestionnaire('manual', this.detectPortFromUrl(urlInput)); }, 2000); } } private detectContext(urlInput: string): { likelyProduct?: string } { const input = urlInput.toLowerCase(); // Check for cloud indicators if (input.includes('cloud') || input.includes('influxdata.com')) { return { likelyProduct: 'cloud' }; } // Check for other patterns like "cloud 2" if (/cloud\s*[v]?2/.test(input)) { return { likelyProduct: 'cloud' }; } return {}; } private detectPortFromUrl(urlString: string): string | null { try { const url = new URL(urlString); const port = url.port || (url.protocol === 'https:' ? '443' : '80'); if (port === '8181') { return 'v3'; // InfluxDB 3 Core/Enterprise typically use 8181 } else if (port === '8086') { return 'legacy'; // OSS v1/v2 or Enterprise v1 typically use 8086 } } catch { // Invalid URL } return null; } private startQuestionnaire( context: string | null = null, portClue: string | null = null ): void { this.answers = {}; this.answers.context = context; this.answers.portClue = portClue; this.answers.isCloud = false; this.questionFlow = ['q-paid', 'q-age', 'q-language']; this.currentQuestionIndex = 0; this.showQuestion('q-paid'); } private startQuestionnaireWithCloudContext(): void { this.answers = {}; this.answers.context = 'cloud'; this.answers.hosted = 'cloud'; // Pre-set cloud hosting this.answers.isCloud = true; this.questionFlow = ['q-paid', 'q-age', 'q-language']; this.currentQuestionIndex = 0; this.showQuestion('q-paid'); } private answerQuestion(category: string, answer: string): void { this.answers[category] = answer; // Determine next question or show results if (category === 'paid') { if (!this.answers.context) { // No URL provided - ask about cloud vs self-hosted this.currentQuestionIndex = 1; this.showQuestion('q-hosted'); } else { // We have context from URL - go to age this.currentQuestionIndex = 1; this.showQuestion('q-age'); } } else if (category === 'hosted') { this.currentQuestionIndex = 2; this.showQuestion('q-age'); } else if (category === 'age') { this.currentQuestionIndex = 3; this.showQuestion('q-language'); } else if (category === 'language') { // All questions answered - show ranked results this.showRankedResults(); } } private handleAuthorizationHelp(category: string, answer: string): void { // Store the answer this.answers[category] = answer; // Check if we're in the context of localhost:8181 detection // If so, we can provide a high-confidence result const currentUrl = ( this.container.querySelector('#url-input') as HTMLInputElement )?.value?.toLowerCase() || ''; const isLocalhost8181 = (currentUrl.includes('localhost') || currentUrl.includes('127.0.0.1')) && currentUrl.includes(':8181'); if (isLocalhost8181) { // For localhost:8181, we can give high-confidence results based on license if (answer === 'free') { // High confidence it's InfluxDB 3 Core const html = ` Based on your localhost:8181 server and free license:

${this.generateProductResult('core', true, 'High', false)}
Want to confirm this result?
`; this.showResult('success', html); } else if (answer === 'paid') { // High confidence it's InfluxDB 3 Enterprise const html = ` Based on your localhost:8181 server and paid license:

${this.generateProductResult('enterprise', true, 'High', false)}
Want to confirm this result?
`; this.showResult('success', html); } } else { // Original behavior for non-localhost:8181 cases const resultDiv = this.container.querySelector('#result'); if (resultDiv) { // Add a message about what the license answer means const licenseGuidance = document.createElement('div'); licenseGuidance.className = 'license-guidance'; licenseGuidance.style.marginTop = '1rem'; licenseGuidance.style.padding = '0.75rem'; licenseGuidance.style.backgroundColor = 'rgba(var(--article-link-rgb, 0, 163, 255), 0.1)'; licenseGuidance.style.borderLeft = '4px solid var(--article-link, #00A3FF)'; licenseGuidance.style.borderRadius = '4px'; if (answer === 'free') { licenseGuidance.innerHTML = ` Free/Open Source License:

This suggests you're using InfluxDB 3 Core or InfluxDB OSS.

`; } else if (answer === 'paid') { licenseGuidance.innerHTML = ` Paid/Commercial License:

This suggests you're using InfluxDB 3 Enterprise or a paid cloud service.

`; } // Remove any existing guidance const existingGuidance = resultDiv.querySelector('.license-guidance'); if (existingGuidance) { existingGuidance.remove(); } // Add the new guidance resultDiv.appendChild(licenseGuidance); // Focus on the guidance message for accessibility licenseGuidance.focus(); } } } private showRankedResults(): void { const scores: Record = {}; // Initialize all products with base score using their full display names // The scoring logic uses full names like 'InfluxDB 3 Core', not keys like 'influxdb3_core' Object.entries(this.products).forEach(([key, config]) => { const fullName = config.name || key; scores[fullName] = 0; }); // Apply scoring logic based on answers this.applyScoring(scores); // Check if user answered "unknown" to all questions const allUnknown = (!this.answers.paid || this.answers.paid === 'unknown') && (!this.answers.hosted || this.answers.hosted === 'unknown') && (!this.answers.age || this.answers.age === 'unknown') && (!this.answers.language || this.answers.language === 'unknown'); // Sort by score and filter out vague products const ranked = Object.entries(scores) .filter(([product, score]) => { // Filter by score threshold if (score <= -50) return false; // Exclude generic "InfluxDB" product (too vague for results) if (product === 'InfluxDB') return false; return true; }) .sort((a, b) => b[1] - a[1]) .slice(0, 5); // Display results this.displayRankedResults(ranked, allUnknown); } /** * Gets the Grafana documentation link for a given product */ private getGrafanaLink(productName: string): string | null { const GRAFANA_LINKS: Record = { 'InfluxDB 3 Core': '/influxdb3/core/visualize-data/grafana/', 'InfluxDB 3 Enterprise': '/influxdb3/enterprise/visualize-data/grafana/', 'InfluxDB Cloud Dedicated': '/influxdb3/cloud-dedicated/visualize-data/grafana/', 'InfluxDB Cloud Serverless': '/influxdb3/cloud-serverless/visualize-data/grafana/', 'InfluxDB OSS 1.x': '/influxdb/v1/tools/grafana/', 'InfluxDB OSS 2.x': '/influxdb/v2/visualize-data/grafana/', 'InfluxDB Enterprise': '/influxdb/enterprise/visualize-data/grafana/', 'InfluxDB Clustered': '/influxdb3/clustered/visualize-data/grafana/', 'InfluxDB Cloud (TSM)': '/influxdb/cloud/visualize-data/grafana/', 'InfluxDB Cloud v1': '/influxdb/cloud/visualize-data/grafana/', }; return GRAFANA_LINKS[productName] || null; } /** * Generates a unified product result block with characteristics and Grafana link */ private generateProductResult( productName: string, isTopResult: boolean = false, confidence?: string, showRanking?: boolean ): string { const displayName = this.getProductDisplayName(productName) || productName; const grafanaLink = this.getGrafanaLink(displayName); const resultClass = isTopResult ? 'product-ranking top-result' : 'product-ranking'; // Get characteristics from products data const characteristics = this.products[productName]?.characteristics; let html = `
`; if (showRanking) { html += `
${displayName}
`; if (isTopResult) { html += 'Most Likely'; } } else { html += `
${displayName}
`; if (isTopResult) { html += 'Detected'; } } // Add characteristics and confidence const details = []; if (confidence) details.push(`Confidence: ${confidence}`); if (characteristics) { details.push(characteristics.slice(0, 3).join(', ')); } if (details.length > 0) { html += `
${details.join(' • ')}
`; } // Add Grafana link if available if (grafanaLink) { html += ` `; } html += '
'; // Add configuration guidance for top results if (isTopResult) { const configGuidance = this.generateConfigurationGuidance(productName); if (configGuidance) { html += configGuidance; } } return html; } /** * Maps simple product keys (used in URL detection) to full product names (used in scoring) */ private mapProductKeyToFullName(productKey: string): string | null { const KEY_TO_FULL_NAME_MAP: Record = { core: 'InfluxDB 3 Core', enterprise: 'InfluxDB 3 Enterprise', serverless: 'InfluxDB Cloud Serverless', dedicated: 'InfluxDB Cloud Dedicated', clustered: 'InfluxDB Clustered', 'cloud-v2-tsm': 'InfluxDB Cloud (TSM)', 'cloud-v1': 'InfluxDB Cloud v1', oss: 'InfluxDB OSS 2.x', 'oss-1x': 'InfluxDB OSS 1.x', 'enterprise-1x': 'InfluxDB Enterprise', }; return KEY_TO_FULL_NAME_MAP[productKey] || null; } private applyScoring(scores: Record): void { // Product release dates for time-aware scoring const PRODUCT_RELEASE_DATES: Record = { 'InfluxDB 3 Core': new Date('2025-01-01'), 'InfluxDB 3 Enterprise': new Date('2025-01-01'), 'InfluxDB Cloud Serverless': new Date('2024-01-01'), 'InfluxDB Cloud Dedicated': new Date('2024-01-01'), 'InfluxDB Clustered': new Date('2024-01-01'), 'InfluxDB OSS 2.x': new Date('2020-11-01'), 'InfluxDB Cloud (TSM)': new Date('2020-11-01'), 'InfluxDB OSS 1.x': new Date('2016-09-01'), 'InfluxDB Enterprise': new Date('2016-09-01'), }; const currentDate = new Date(); // Apply URL detection boost if available if (this.answers.detectedProduct && this.answers.detectedConfidence) { const detectedProduct = this.answers.detectedProduct as string; const confidence = typeof this.answers.detectedConfidence === 'number' ? this.answers.detectedConfidence : parseFloat(this.answers.detectedConfidence as string); // Determine confidence boost value let boostValue = 0; if (confidence >= 1.0) { boostValue = 100; // Definitive match } else if (confidence >= 0.9) { boostValue = 80; // Very high confidence } else if (confidence >= 0.7) { boostValue = 60; // High confidence } else if (confidence >= 0.5) { boostValue = 40; // Medium confidence } // Handle special case: 'core or enterprise' should boost BOTH products equally if (detectedProduct === 'core or enterprise') { scores['InfluxDB 3 Core'] += boostValue; scores['InfluxDB 3 Enterprise'] += boostValue; } else { // Normal case: boost single detected product const fullProductName = this.mapProductKeyToFullName(detectedProduct); if (fullProductName && scores[fullProductName] !== undefined) { scores[fullProductName] += boostValue; } } } // Cloud vs self-hosted if (this.answers.hosted === 'cloud') { scores['InfluxDB 3 Core'] = -1000; scores['InfluxDB 3 Enterprise'] = -1000; scores['InfluxDB OSS 1.x'] = -1000; scores['InfluxDB OSS 2.x'] = -1000; scores['InfluxDB Enterprise'] = -1000; scores['InfluxDB Clustered'] = -1000; } else if (this.answers.hosted === 'self' || !this.answers.isCloud) { scores['InfluxDB Cloud Dedicated'] = -1000; scores['InfluxDB Cloud Serverless'] = -1000; scores['InfluxDB Cloud (TSM)'] = -1000; } // Paid vs Free if (this.answers.paid === 'free') { scores['InfluxDB 3 Core'] += 25; scores['InfluxDB OSS 1.x'] += 25; scores['InfluxDB OSS 2.x'] += 25; scores['InfluxDB'] += 25; // Generic InfluxDB (OSS v2.x) scores['InfluxDB Cloud Serverless'] += 10; scores['InfluxDB Cloud (TSM)'] += 10; scores['InfluxDB 3 Enterprise'] = -1000; scores['InfluxDB Enterprise'] = -1000; scores['InfluxDB Clustered'] = -1000; scores['InfluxDB Cloud Dedicated'] = -1000; } else if (this.answers.paid === 'paid') { scores['InfluxDB 3 Enterprise'] += 25; scores['InfluxDB Enterprise'] += 20; scores['InfluxDB Clustered'] += 15; scores['InfluxDB Cloud Dedicated'] += 20; scores['InfluxDB Cloud Serverless'] += 15; scores['InfluxDB Cloud (TSM)'] += 15; scores['InfluxDB 3 Core'] = -1000; scores['InfluxDB OSS 1.x'] = -1000; scores['InfluxDB OSS 2.x'] = -1000; scores['InfluxDB'] = -1000; // Generic InfluxDB (OSS v2.x) } // Time-aware age-based scoring Object.entries(scores).forEach(([product]) => { const releaseDate = PRODUCT_RELEASE_DATES[product]; if (!releaseDate) return; const yearsSinceRelease = (currentDate.getTime() - releaseDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000); if (this.answers.age === 'recent') { // Favor products released within last year if (yearsSinceRelease < 1) { scores[product] += 40; // Very new product } else if (yearsSinceRelease < 3) { scores[product] += 25; // Relatively new } } else if (this.answers.age === '1-5') { // Check if product existed in this timeframe if (yearsSinceRelease >= 1 && yearsSinceRelease <= 5) { scores[product] += 25; } else if (yearsSinceRelease < 1) { scores[product] -= 30; // Too new for this age range } } else if (this.answers.age === '5+') { // Only penalize if product didn't exist 5+ years ago if (yearsSinceRelease < 5) { scores[product] -= 100; // Product didn't exist 5 years ago } else { scores[product] += 30; // Product was available 5+ years ago } } }); // Query language scoring if (this.answers.language === 'sql') { scores['InfluxDB 3 Core'] += 40; scores['InfluxDB 3 Enterprise'] += 40; scores['InfluxDB Cloud Dedicated'] += 30; scores['InfluxDB Cloud Serverless'] += 30; scores['InfluxDB Clustered'] += 30; scores['InfluxDB OSS 1.x'] = -1000; scores['InfluxDB OSS 2.x'] = -1000; scores['InfluxDB'] = -1000; // Generic InfluxDB (OSS v2.x) scores['InfluxDB Enterprise'] = -1000; scores['InfluxDB Cloud (TSM)'] = -1000; } else if (this.answers.language === 'flux') { scores['InfluxDB OSS 2.x'] += 30; scores['InfluxDB'] += 30; // Generic InfluxDB (OSS v2.x) scores['InfluxDB Cloud (TSM)'] += 40; scores['InfluxDB Cloud Serverless'] += 20; scores['InfluxDB Enterprise'] += 20; // v1.x Enterprise supports Flux scores['InfluxDB OSS 1.x'] = -1000; scores['InfluxDB 3 Core'] = -1000; scores['InfluxDB 3 Enterprise'] = -1000; scores['InfluxDB Cloud Dedicated'] = -1000; scores['InfluxDB Clustered'] = -1000; } else if (this.answers.language === 'influxql') { // InfluxQL is supported by all products except pure Flux products scores['InfluxDB OSS 1.x'] += 30; scores['InfluxDB Enterprise'] += 30; scores['InfluxDB OSS 2.x'] += 20; scores['InfluxDB'] += 20; // Generic InfluxDB (OSS v2.x) scores['InfluxDB Cloud (TSM)'] += 20; scores['InfluxDB 3 Core'] += 25; scores['InfluxDB 3 Enterprise'] += 25; scores['InfluxDB Cloud Dedicated'] += 25; scores['InfluxDB Cloud Serverless'] += 25; scores['InfluxDB Clustered'] += 25; } } private displayRankedResults( ranked: [string, number][], allUnknown: boolean = false ): void { const topScore = ranked[0]?.[1] || 0; const secondScore = ranked[1]?.[1] || 0; const hasStandout = topScore > 30 && topScore - secondScore >= 15; let html = ''; // If all answers were "I'm not sure", show a helpful message if (allUnknown) { html = 'Unable to determine your InfluxDB product

' + '

Since you answered "I\'m not sure" to all questions, we don\'t have enough information to identify your InfluxDB product.

' + '

Please check the InfluxDB version quick reference table below to identify your product based on its characteristics.


'; } else { html = 'Based on your answers, here are the most likely InfluxDB products:

'; } // Only show ranked products if we have meaningful answers if (!allUnknown) { ranked.forEach(([product, score], index) => { const confidence = score > 60 ? 'High' : score > 30 ? 'Medium' : 'Low'; const isTopResult = index === 0 && hasStandout; // Use unified product result generation with ranking number let productHtml = this.generateProductResult( product, isTopResult, confidence, true ); // Add ranking number to the product title productHtml = productHtml.replace( '
', `
${index + 1}. ` ); html += productHtml; }); } // Add Quick Reference table (open by default if all answers unknown) html += `
InfluxDB version quick reference
Product License Hosting Port Ping requires auth Query languages
InfluxDB 3 Enterprise Paid only Self-hosted 8181 Yes (opt-out) SQL, InfluxQL
InfluxDB 3 Core Free only Self-hosted 8181 Yes (opt-out) SQL, InfluxQL
InfluxDB Enterprise Paid only Self-hosted 8086 Yes (required) InfluxQL, Flux
InfluxDB Clustered Paid only Self-hosted Varies No SQL, InfluxQL
InfluxDB OSS 1.x Free only Self-hosted 8086 No (optional) InfluxQL
InfluxDB OSS 2.x Free only Self-hosted 8086 No InfluxQL, Flux
InfluxDB Cloud Dedicated Paid only Cloud N/A No SQL, InfluxQL
InfluxDB Cloud Serverless Free + Paid Cloud N/A N/A SQL, InfluxQL, Flux
InfluxDB Cloud (TSM) Free + Paid Cloud N/A N/A InfluxQL, Flux
`; this.showResult('success', html); } private analyzePingHeaders(): void { const headersText = ( this.container.querySelector('#ping-headers') as HTMLTextAreaElement )?.value.trim(); if (!headersText) { this.showResult('error', 'Please paste the ping response headers'); return; } // Check if user is trying to analyze the example content if ( headersText.includes( '# Replace this with your actual response headers' ) || headersText.includes('# Example formats:') ) { this.showResult( 'error', 'Please replace the example content with your actual ping response headers' ); return; } // Check for 401/403 unauthorized responses if (headersText.includes('401') || headersText.includes('403')) { this.showResult( 'info', ` Authentication Required Detected

The ping endpoint requires authentication, which indicates you're likely using one of:

InfluxDB 3 Enterprise - Requires auth by default (opt-out possible)
InfluxDB 3 Core - Requires auth by default (opt-out possible)
Please use the guided questions to narrow down your specific version. ` ); return; } // Parse headers and check against patterns const headers: Record = {}; headersText.split('\n').forEach((line) => { const colonIndex = line.indexOf(':'); if (colonIndex > -1) { const key = line.substring(0, colonIndex).trim().toLowerCase(); const value = line.substring(colonIndex + 1).trim(); headers[key] = value; } }); // PRIORITY: Check for definitive x-influxdb-build header (per decision tree) const buildHeader = headers['x-influxdb-build']; if (buildHeader) { if (buildHeader.toLowerCase().includes('enterprise')) { this.showDetectedVersion('InfluxDB 3 Enterprise'); return; } else if (buildHeader.toLowerCase().includes('core')) { this.showDetectedVersion('InfluxDB 3 Core'); return; } } // Check against product patterns let detectedProduct: string | null = null; for (const [productName, config] of Object.entries(this.products)) { if (config.detection?.ping_headers) { let matches = true; for (const [header, pattern] of Object.entries( config.detection.ping_headers )) { const regex = new RegExp(pattern); if (!headers[header] || !regex.test(headers[header])) { matches = false; break; } } if (matches) { detectedProduct = productName; break; } } } if (detectedProduct) { this.showDetectedVersion(detectedProduct); } else { this.showResult( 'warning', 'Unable to determine version from headers. Consider using the guided questions instead.' ); } } private showResult(type: string, message: string): void { if (this.resultDiv) { this.resultDiv.className = `result ${type} show`; this.resultDiv.innerHTML = message; } if (this.restartBtn) { this.restartBtn.style.display = 'block'; } } private analyzeDockerOutput(): void { const dockerOutput = ( this.container.querySelector('#docker-output') as HTMLTextAreaElement )?.value.trim(); if (!dockerOutput) { this.showResult('error', 'Please paste the Docker command output'); return; } // Check if user is trying to analyze the example content if ( dockerOutput.includes('# Replace this with your actual command output') || dockerOutput.includes('# Example formats:') ) { this.showResult( 'error', 'Please replace the example content with your actual Docker command output' ); return; } let detectedProduct: string | null = null; // Check for version patterns in the output if (dockerOutput.includes('InfluxDB 3 Core')) { detectedProduct = 'InfluxDB 3 Core'; } else if (dockerOutput.includes('InfluxDB 3 Enterprise')) { detectedProduct = 'InfluxDB 3 Enterprise'; } else if (dockerOutput.includes('InfluxDB v3')) { // Generic v3 detection - need more info detectedProduct = 'InfluxDB 3 Core or Enterprise'; } else if ( dockerOutput.includes('InfluxDB v2') || dockerOutput.includes('InfluxDB 2.') ) { detectedProduct = 'InfluxDB OSS 2.x'; } else if ( dockerOutput.includes('InfluxDB v1') || dockerOutput.includes('InfluxDB 1.') ) { if (dockerOutput.includes('Enterprise')) { detectedProduct = 'InfluxDB Enterprise'; } else { detectedProduct = 'InfluxDB OSS 1.x'; } } // Also check for ping header patterns (case-insensitive) if (!detectedProduct) { // First check for x-influxdb-build header (definitive identification) const buildMatch = dockerOutput.match(/x-influxdb-build:\s*(\w+)/i); if (buildMatch) { const build = buildMatch[1].toLowerCase(); if (build === 'enterprise') { detectedProduct = 'InfluxDB 3 Enterprise'; } else if (build === 'core') { detectedProduct = 'InfluxDB 3 Core'; } } // If no build header, check version headers (case-insensitive) if (!detectedProduct) { const versionMatch = dockerOutput.match( /x-influxdb-version:\s*([\d.]+)/i ); if (versionMatch) { const version = versionMatch[1]; if (version.startsWith('3.')) { detectedProduct = 'InfluxDB 3 Core or InfluxDB 3Enterprise'; } else if (version.startsWith('2.')) { detectedProduct = 'InfluxDB OSS 2.x'; } else if (version.startsWith('1.')) { detectedProduct = dockerOutput.includes('Enterprise') ? 'InfluxDB Enterprise' : 'InfluxDB OSS 1.x'; } } } } if (detectedProduct) { this.showDetectedVersion(detectedProduct); } else { this.showResult( 'warning', 'Unable to determine version from Docker output. Consider using the guided questions instead.' ); } } private showPingTestSuggestion(url: string, productName: string): void { // Convert product key to display name const displayName = this.getProductDisplayName(productName) || productName; const html = ` Port 8181 detected - likely ${displayName}

To distinguish between InfluxDB 3 Core and Enterprise, run one of these commands:

# Direct API call: curl -I ${url}/ping
View Docker/Container Commands
# With Docker Compose: docker compose exec influxdb3 curl -I http://localhost:8181/ping # With Docker (replace <container> with your container name): docker exec <container> curl -I localhost:8181/ping
Expected results:
X-Influxdb-Build: Enterprise → InfluxDB 3 Enterprise (definitive)
X-Influxdb-Build: Core → InfluxDB 3 Core (definitive)
401 Unauthorized → Use the license information below
If you get 401 Unauthorized:

What type of license do you have?

Can't run the command?
`; this.showResult('success', html); } private showOSSVersionCheckSuggestion(url: string): void { const html = ` Port 8086 detected - likely InfluxDB OSS

To determine if this is InfluxDB OSS v1.x or v2.x, run one of these commands:

# Check version directly: influxd version # Or check via API: curl -I ${url}/ping
Expected version patterns:
v1.x.x → ${this.getProductDisplayName('oss-v1')}
v2.x.x → ${this.getProductDisplayName('oss-v2')}
Docker/Container Commands
# Get version info: docker exec <container> influxd version # Get ping headers: docker exec <container> curl -I localhost:8086/ping # Or check startup logs: docker logs <container> 2>&1 | head -20

Replace <container> with your actual container name or ID.

Can't run these commands?
`; this.showResult('success', html); } private showMultipleCandidatesSuggestion(url: string, port: string): void { let candidates: string[] = []; let portDescription = ''; if (port === '8086') { candidates = [ 'InfluxDB OSS 1.x', 'InfluxDB OSS 2.x', 'InfluxDB Enterprise', ]; portDescription = 'Port 8086 is used by InfluxDB OSS v1.x, OSS v2.x, and Enterprise v1.x'; } else if (port === '8181') { candidates = ['InfluxDB 3 Core', 'InfluxDB 3 Enterprise']; portDescription = 'Port 8181 is used by InfluxDB 3 Core and Enterprise'; } const candidatesList = candidates .map((product) => this.generateProductResult(product, false, 'Medium', false) ) .join(''); const html = ` Based on the port pattern in your URL, here are the possible products:

${portDescription}. Without additional information, we cannot determine which specific version you're using.

Possible products:
${candidatesList}
To narrow this down:
`; this.showResult('info', html); } private showDetectedVersion(productName: string): void { // Track successful detection this.trackAnalyticsEvent({ interaction_type: 'product_detected', detected_product: productName.toLowerCase().replace(/\s+/g, '_'), completion_status: 'success', section: this.getCurrentPageSection(), }); const html = ` Based on your input, we believe the InfluxDB product you are using is most likely:

${this.generateProductResult(productName, true, 'High', false)} `; this.showResult('success', html); } private restart(): void { this.answers = {}; this.questionFlow = []; this.currentQuestionIndex = 0; this.questionHistory = []; // Clear inputs const urlInput = this.container.querySelector( '#url-input' ) as HTMLInputElement; const pingHeaders = this.container.querySelector( '#ping-headers' ) as HTMLTextAreaElement; const dockerOutput = this.container.querySelector( '#docker-output' ) as HTMLTextAreaElement; if (urlInput) urlInput.value = ''; if (pingHeaders) pingHeaders.value = ''; if (dockerOutput) dockerOutput.value = ''; // Remove URL prefilled indicator if present const indicator = this.container.querySelector('.url-prefilled-indicator'); if (indicator) { indicator.remove(); } // Hide result if (this.resultDiv) { this.resultDiv.classList.remove('show'); } if (this.restartBtn) { this.restartBtn.style.display = 'none'; } // Show first question this.showQuestion('q-url-known'); // Reset progress if (this.progressBar) { this.progressBar.style.width = '0%'; } } } // Export as component initializer export default function initInfluxDBVersionDetector( options: ComponentOptions ): InfluxDBVersionDetector { return new InfluxDBVersionDetector(options); }