Skip to content

Commit 7496433

Browse files
samikshya-dbclaude
andcommitted
Add authentication support for REST API calls in telemetry
Implement proper authentication for feature flag fetching and telemetry export by adding getAuthHeaders() method to IClientContext. ## Changes ### Core Authentication Infrastructure - **IClientContext**: Add getAuthHeaders() method to expose auth headers - **DBSQLClient**: Implement getAuthHeaders() using authProvider.authenticate() - Returns empty object gracefully if no auth provider available ### Feature Flag Fetching (COMPLETED) - **FeatureFlagCache**: Implement actual server API call - Endpoint: GET /api/2.0/connector-service/feature-flags/OSS_NODEJS/{version} - Uses context.getAuthHeaders() for authentication - Parses JSON response with flags array - Updates cache duration from server-provided ttl_seconds - Looks for: databricks.partnerplatform.clientConfigsFeatureFlags.enableTelemetryForNodeJs - All exceptions swallowed with debug logging only ### Telemetry Export (FIXED) - **DatabricksTelemetryExporter**: Add authentication to authenticated endpoint - Uses context.getAuthHeaders() when authenticatedExport=true - Properly authenticates POST to /api/2.0/sql/telemetry-ext - Removes TODO comments about missing authentication ## JDBC Driver Pattern Alignment Follows same pattern as JDBC driver: - Endpoint: /api/2.0/connector-service/feature-flags/OSS_JDBC/{version} (JDBC) - Endpoint: /api/2.0/connector-service/feature-flags/OSS_NODEJS/{version} (Node.js) - Auth headers from connection's authenticate() method - Response format: { flags: [{ name, value }], ttl_seconds } ## Testing - Build: ✅ Successful - E2E: ✅ Verified with real credentials - Feature flag fetch now fully functional - Telemetry export now properly authenticated Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent a582489 commit 7496433

4 files changed

Lines changed: 121 additions & 16 deletions

File tree

lib/DBSQLClient.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Int64 from 'node-int64';
33
import os from 'os';
44

55
import { EventEmitter } from 'events';
6+
import { HeadersInit } from 'node-fetch';
67
import TCLIService from '../thrift/TCLIService';
78
import { TProtocolVersion } from '../thrift/TCLIService_types';
89
import IDBSQLClient, { ClientOptions, ConnectionOptions, OpenSessionRequest } from './contracts/IDBSQLClient';
@@ -493,4 +494,21 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I
493494
public async getDriver(): Promise<IDriver> {
494495
return this.driver;
495496
}
497+
498+
/**
499+
* Gets authentication headers for HTTP requests.
500+
* Used by telemetry and feature flag fetching to authenticate REST API calls.
501+
* @returns Promise resolving to headers object with authentication, or empty object if no auth
502+
*/
503+
public async getAuthHeaders(): Promise<HeadersInit> {
504+
if (this.authProvider) {
505+
try {
506+
return await this.authProvider.authenticate();
507+
} catch (error) {
508+
this.logger.log(LogLevel.debug, `Error getting auth headers: ${error}`);
509+
return {};
510+
}
511+
}
512+
return {};
513+
}
496514
}

lib/contracts/IClientContext.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { HeadersInit } from 'node-fetch';
12
import IDBSQLLogger from './IDBSQLLogger';
23
import IDriver from './IDriver';
34
import IConnectionProvider from '../connection/contracts/IConnectionProvider';
@@ -43,4 +44,11 @@ export default interface IClientContext {
4344
getClient(): Promise<IThriftClient>;
4445

4546
getDriver(): Promise<IDriver>;
47+
48+
/**
49+
* Gets authentication headers for HTTP requests.
50+
* Used by telemetry and feature flag fetching to authenticate REST API calls.
51+
* @returns Promise resolving to headers object with authentication, or empty object if no auth
52+
*/
53+
getAuthHeaders(): Promise<HeadersInit>;
4654
}

lib/telemetry/DatabricksTelemetryExporter.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,16 +203,16 @@ export default class DatabricksTelemetryExporter {
203203
`Exporting ${metrics.length} telemetry metrics to ${authenticatedExport ? 'authenticated' : 'unauthenticated'} endpoint`
204204
);
205205

206-
// Make HTTP POST request
207-
// Note: In production, auth headers would be added via connectionProvider
206+
// Get authentication headers if using authenticated endpoint
207+
const authHeaders = authenticatedExport ? await this.context.getAuthHeaders() : {};
208+
209+
// Make HTTP POST request with authentication
208210
const response: Response = await this.fetchFn(endpoint, {
209211
method: 'POST',
210212
headers: {
213+
...authHeaders,
211214
'Content-Type': 'application/json',
212215
'User-Agent': this.userAgent,
213-
// Note: ConnectionProvider may add auth headers automatically
214-
// via getThriftConnection, but for telemetry we use direct fetch
215-
// In production, we'd need to extract auth headers from connectionProvider
216216
},
217217
body: JSON.stringify(payload),
218218
});

lib/telemetry/FeatureFlagCache.ts

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import IClientContext from '../contracts/IClientContext';
1818
import { LogLevel } from '../contracts/IDBSQLLogger';
19+
import fetch from 'node-fetch';
1920

2021
/**
2122
* Context holding feature flag state for a specific host.
@@ -104,17 +105,95 @@ export default class FeatureFlagCache {
104105
}
105106

106107
/**
107-
* Fetches feature flag from server.
108-
* This is a placeholder implementation that returns false.
109-
* Real implementation would fetch from server using connection provider.
110-
* @param _host The host to fetch feature flag for (unused in placeholder implementation)
108+
* Fetches feature flag from server using connector-service API.
109+
* Calls GET /api/2.0/connector-service/feature-flags/OSS_NODEJS/{version}
110+
*
111+
* @param host The host to fetch feature flag for
112+
* @returns true if feature flag is enabled, false otherwise
111113
*/
112-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
113-
private async fetchFeatureFlag(_host: string): Promise<boolean> {
114-
// Placeholder implementation
115-
// Real implementation would use:
116-
// const connectionProvider = await this.context.getConnectionProvider();
117-
// and make an API call to fetch the feature flag
118-
return false;
114+
private async fetchFeatureFlag(host: string): Promise<boolean> {
115+
const logger = this.context.getLogger();
116+
117+
try {
118+
// Get driver version for endpoint
119+
const driverVersion = this.getDriverVersion();
120+
121+
// Build feature flags endpoint
122+
const endpoint = `https://${host}/api/2.0/connector-service/feature-flags/OSS_NODEJS/${driverVersion}`;
123+
124+
// Get authentication headers
125+
const authHeaders = await this.context.getAuthHeaders();
126+
127+
logger.log(LogLevel.debug, `Fetching feature flags from ${endpoint}`);
128+
129+
// Make HTTP GET request with authentication
130+
const response = await fetch(endpoint, {
131+
method: 'GET',
132+
headers: {
133+
...authHeaders,
134+
'Content-Type': 'application/json',
135+
'User-Agent': `databricks-sql-nodejs/${driverVersion}`,
136+
},
137+
});
138+
139+
if (!response.ok) {
140+
logger.log(
141+
LogLevel.debug,
142+
`Feature flag fetch failed: ${response.status} ${response.statusText}`
143+
);
144+
return false;
145+
}
146+
147+
// Parse response JSON
148+
const data: any = await response.json();
149+
150+
// Response format: { flags: [{ name: string, value: string }], ttl_seconds?: number }
151+
if (data && data.flags && Array.isArray(data.flags)) {
152+
// Update cache duration if TTL provided
153+
const ctx = this.contexts.get(host);
154+
if (ctx && data.ttl_seconds) {
155+
ctx.cacheDuration = data.ttl_seconds * 1000; // Convert to milliseconds
156+
logger.log(LogLevel.debug, `Updated cache duration to ${data.ttl_seconds} seconds`);
157+
}
158+
159+
// Look for our specific feature flag
160+
const flag = data.flags.find((f: any) => f.name === this.FEATURE_FLAG_NAME);
161+
162+
if (flag) {
163+
// Parse boolean value (can be string "true"/"false")
164+
const value = String(flag.value).toLowerCase();
165+
const enabled = value === 'true';
166+
logger.log(
167+
LogLevel.debug,
168+
`Feature flag ${this.FEATURE_FLAG_NAME}: ${enabled}`
169+
);
170+
return enabled;
171+
}
172+
}
173+
174+
// Feature flag not found in response, default to false
175+
logger.log(LogLevel.debug, `Feature flag ${this.FEATURE_FLAG_NAME} not found in response`);
176+
return false;
177+
} catch (error: any) {
178+
// Log at debug level only, never propagate exceptions
179+
logger.log(LogLevel.debug, `Error fetching feature flag from ${host}: ${error.message}`);
180+
return false;
181+
}
182+
}
183+
184+
/**
185+
* Gets the driver version without -oss suffix for API calls.
186+
* Format: "1.12.0" from "1.12.0-oss"
187+
*/
188+
private getDriverVersion(): string {
189+
try {
190+
// Import version from lib/version.ts
191+
const version = require('../version').default;
192+
// Remove -oss suffix if present
193+
return version.replace(/-oss$/, '');
194+
} catch (error) {
195+
// Fallback to a default version if import fails
196+
return '1.0.0';
197+
}
119198
}
120199
}

0 commit comments

Comments
 (0)